[
  {
    "path": ".babelrc",
    "content": "{\n\t\"plugins\": [\n\t],\n  \"presets\": [\n    [\n      \"@babel/preset-env\",\n      {\n        \"shippedProposals\": true,\n        \"corejs\":2,\n        \"useBuiltIns\": \"usage\"\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n}\n"
  },
  {
    "path": ".circleci/config.yml",
    "content": "# Javascript Node CircleCI 2.0 configuration file\n#\n# Check https://circleci.com/docs/2.0/language-javascript/ for more details\n#\nversion: 2\njobs:\n  build:\n    docker:\n      - image: circleci/openjdk:8-jdk-browsers\n        environment:\n          # lang settings required for Meteor's Mongo\n          LANG: C.UTF-8\n          LANGUAGE: C.UTF-8\n          LC_ALL: C.UTF-8\n          LC_NUMERIC: en_US.UTF-8\n          METEOR_BIN_TMP_DIR: /home/circleci/build-temp/\n          METEOR_BIN_TMP_FILE: meteor-bin-temp\n\n    working_directory: ~/app\n\n    steps:\n      # chackout the code from github\n      - checkout\n      # if certain cached files (packages, etc) are presetn, don't redownload them, restore the cached version\n      - restore_cache:\n          key: build-temp-{{ checksum \".meteor/release\" }}-{{ checksum \".circleci/config.yml\" }}\n      - restore_cache:\n          key: meteor-release-{{ checksum \".meteor/release\" }}-{{ checksum \".circleci/config.yml\" }}\n      - restore_cache:\n          key: meteor-packages-{{ checksum \".meteor/versions\" }}-{{ checksum \".circleci/config.yml\" }}\n      - restore_cache:\n          key: npm-packages-{{ checksum \"package.json\" }}-{{ checksum \".circleci/config.yml\" }}\n      - run:\n          name: install build essentials\n          command: sudo apt-get install -y build-essential\n      - run:\n          name: restore cached meteor bin\n          command: |\n            if [ -e ~/build-temp/meteor-bin ]\n            then\n                echo \"Cached Meteor bin found, restoring it\"\n                sudo cp ~/build-temp/meteor-bin /usr/local/bin/meteor\n            else\n                echo \"No cached Meteor bin found.\"\n            fi\n      # if there is no cached meteor version, install it\n      - run:\n          name: install Meteor\n          command: |\n            # only install meteor if bin isn't found\n            command -v meteor >/dev/null 2>&1 || curl https://install.meteor.com | /bin/sh\n      - run:\n          name: check versions\n          command: |\n            echo \"Meteor version:\"\n            meteor --version\n            which meteor\n            echo \"Meteor node version:\"\n            meteor node -v\n            echo \"Meteor npm version:\"\n            meteor npm -v\n            echo \"Java version:\"\n            java -version\n      - run: \n          name: install yarn\n          command: meteor npm i -g yarn\n      - run: \n          name: install npm packages\n          command: meteor yarn\n      - run:\n          name: code linting\n          command: meteor yarn lint\n      # move meteor bin so it can be properly cached\n      - run:\n          name: copy meteor bin to build cache\n          command: |\n            mkdir -p ~/build-temp\n            cp /usr/local/bin/meteor ~/build-temp/meteor-bin\n      # cache meteor& npm packages\n      - save_cache:\n          key: build-temp-{{ checksum \".meteor/release\" }}-{{ checksum \".circleci/config.yml\" }}\n          paths:\n            - ~/build-temp\n      - save_cache:\n          key: meteor-release-{{ checksum \".meteor/release\" }}-{{ checksum \".circleci/config.yml\" }}\n          paths:\n            - ~/.meteor\n      - save_cache:\n          key: meteor-packages-{{ checksum \".meteor/versions\" }}-{{ checksum \".circleci/config.yml\" }}\n          paths:\n            - .meteor/\n      - save_cache:\n          key: npm-packages-{{ checksum \"package.json\" }}-{{ checksum \".circleci/config.yml\" }}\n          paths:\n            - ./node_modules/\n            - ~/.npm/\n\n\n\n"
  },
  {
    "path": ".editorconfig",
    "content": "# editorconfig.org\n\nroot = true\n\n[*.{js,html}]\n\ncharset = utf-8\nend_of_line = lf\nindent_brace_style = 1TBS\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\nmax_line_length = 120\nquote_type = auto\nspaces_around_operators = true\ntrim_trailing_whitespace = true\n\n[*.md]\n\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".eslintignore",
    "content": "packages/_*\n**/*.test.js\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:meteor/recommended\",\n    \"plugin:react/recommended\"\n  ],\n  \"parser\": \"babel-eslint\",\n  \"parserOptions\": {\n    \"allowImportExportEverywhere\": true,\n    \"ecmaVersion\": 6,\n    \"sourceType\": \"module\"\n  },\n  \"rules\": {\n    \"babel/generator-star-spacing\": 0,\n    \"babel/new-cap\": [\n      1,\n      {\n        \"capIsNewExceptions\": [\n          \"Optional\",\n          \"OneOf\",\n          \"Maybe\",\n          \"MailChimpAPI\",\n          \"Juice\",\n          \"Run\",\n          \"AppComposer\",\n          \"Query\"\n        ]\n      }\n    ],\n    \"babel/array-bracket-spacing\": 1,\n    \"babel/object-curly-spacing\": 0,\n    # \"babel/object-curly-spacing\": [1, \"always\", { \"objectsInObjects\": false, \"arraysInObjects\": false }],\n    \"babel/object-shorthand\": 0,\n    \"babel/arrow-parens\": 0,\n    \"no-await-in-loop\": 1,\n    \"comma-dangle\": 0,\n    \"key-spacing\": 0,\n    \"meteor/audit-argument-checks\": 0,\n    \"no-case-declarations\": 0,\n    \"no-console\": 1,\n    \"no-extra-boolean-cast\": 0,\n    \"no-undef\": 1,\n    \"no-unused-vars\": [\n      1,\n      {\n        \"vars\": \"all\",\n        \"args\": \"none\",\n        \"varsIgnorePattern\": \"React|PropTypes|Component\"\n      }\n    ],\n    \"no-useless-escape\": 0,\n    \"quotes\": [\n      1,\n      \"single\",\n      \"avoid-escape\"\n    ],\n    \"react/display-name\": 0,\n    \"react/prop-types\": 0,\n    \"semi\": [1, \"always\"]\n  },\n  \"env\": {\n    \"browser\": true,\n    \"commonjs\": true,\n    \"es6\": true,\n    \"meteor\": true,\n    \"node\": true,\n    \"mocha\": true\n  },\n  \"plugins\": [\n    \"babel\",\n    \"meteor\",\n    \"react\",\n    \"prettier\",\n    \"mocha\"\n  ],\n  \"settings\": {\n    \"import/resolver\": \"meteor\"\n  },\n  \"root\": true,\n  \"globals\": {\n    \"param\": true,\n    \"returns\": true\n  }\n}\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "Before starting on a new feature, please [check out the roadmap](https://trello.com/b/dwPR0LTz/vulcanjs-roadmap) and come check-in in the [Vulcan Slack channel](http://slack.telescopeapp.org/).\n\nAlso, all PRs should be made to the `devel` branch, not `master`. "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots (if applicable)**\nIf applicable, add screenshots to help explain your problem.\n\n**Device (if applicable):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 7\n# Issues with these labels will never be considered stale\nexemptLabels:\n  - pinned\n  - security\n# Label to use when marking an issue as stale\nstaleLabel: stale\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false"
  },
  {
    "path": ".gitignore",
    "content": ".eslintcache\nnpm-debug.log\n*.scssc\n.sass-cache/*\n.DS_Store\n*-ck.js\nproviders_secret.js\n*.sublime-project\n\n*.sublime-workspace\ncodekit-config.json\n\nconfig.rb\n\ndeploy.sh\n\n.demeteorized\n\ndump/\ndump/*\n\nsettings.json\nsettings.json.not-used\nproduction.settings.json\n.idea\n\nscratch\n\n.deploy\n.deploy/*\n### Meteor template\n.meteor/local\n.meteor/meteorite\nmup.json\n\npackages_update.py\n\nversions\n\nget_file_list.sh\npackages_update.py\npublish_packages.sh\n\nnode_modules\n\nbundle.tar.gz\n\njsdoc-conf.json\njsdoc.json\npackages/nova-router/.npm\nnpm-debug.log\n\ntypings\n\nstorybook-static\ndocs/storybook-material\ndocs/storybook-bootstrap\nschema.graphql\n.logs\n"
  },
  {
    "path": ".jshintrc",
    "content": "//.jshintrc\n{\n  // JSHint Meteor Configuration File\n  // Match the Meteor Style Guide\n  //\n  // By @raix with contributions from @aldeed and @awatson1978\n  // Source https://github.com/raix/Meteor-jshintrc\n  //\n  // See http://jshint.com/docs/ for more details\n\n  \"maxerr\"        : 50,       // {int} Maximum error before stopping\n\n  // Enforcing\n  \"bitwise\"       : true,     // true: Prohibit bitwise operators (&, |, ^, etc.)\n  \"camelcase\"     : true,     // true: Identifiers must be in camelCase\n  // \"curly\"         : true,     // true: Require {} for every new block or scope\n  \"eqeqeq\"        : true,     // true: Require triple equals (===) for comparison\n  \"forin\"         : true,     // true: Require filtering for..in loops with obj.hasOwnProperty()\n  \"immed\"         : false,    // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`\n  \"indent\"        : 2,        // {int} Number of spaces to use for indentation\n  \"latedef\"       : false,    // true: Require variables/functions to be defined before being used\n  \"newcap\"        : false,    // true: Require capitalization of all constructor functions e.g. `new F()`\n  \"noarg\"         : true,     // true: Prohibit use of `arguments.caller` and `arguments.callee`\n  \"noempty\"       : true,     // true: Prohibit use of empty blocks\n  \"nonew\"         : false,    // true: Prohibit use of constructors for side-effects (without assignment)\n  \"plusplus\"      : false,    // true: Prohibit use of `++` & `--`\n  \"quotmark\"      : false,    // Quotation mark consistency:\n                              //   false    : do nothing (default)\n                              //   true     : ensure whatever is used is consistent\n                              //   \"single\" : require single quotes\n                              //   \"double\" : require double quotes\n  \"undef\"         : true,     // true: Require all non-global variables to be declared (prevents global leaks)\n  \"unused\"        : true,     // true: Require all defined variables be used\n  \"strict\"        : false,     // true: Requires all functions run in ES5 Strict Mode\n  \"trailing\"      : true,     // true: Prohibit trailing whitespaces\n  \"maxparams\"     : false,    // {int} Max number of formal params allowed per function\n  \"maxdepth\"      : false,    // {int} Max depth of nested blocks (within functions)\n  \"maxstatements\" : false,    // {int} Max number statements per function\n  \"maxcomplexity\" : false,    // {int} Max cyclomatic complexity per function\n  \"maxlen\"        : false,       // {int} Max number of characters per line\n\n  // Relaxing\n  \"asi\"           : false,     // true: Tolerate Automatic Semicolon Insertion (no semicolons)\n  \"boss\"          : false,     // true: Tolerate assignments where comparisons would be expected\n  \"debug\"         : false,     // true: Allow debugger statements e.g. browser breakpoints.\n  \"eqnull\"        : false,     // true: Tolerate use of `== null`\n  \"es5\"           : false,     // true: Allow ES5 syntax (ex: getters and setters)\n  \"esnext\"        : false,     // true: Allow ES.next (ES6) syntax (ex: `const`)\n  \"moz\"           : false,     // true: Allow Mozilla specific syntax (extends and overrides esnext features)\n                               // (ex: `for each`, multiple try/catch, function expression…)\n  \"evil\"          : false,     // true: Tolerate use of `eval` and `new Function()`\n  \"expr\"          : false,     // true: Tolerate `ExpressionStatement` as Programs\n  \"funcscope\"     : false,     // true: Tolerate defining variables inside control statements\"\n  \"globalstrict\"  : true,      // true: Allow global \"use strict\" (also enables 'strict')\n  \"iterator\"      : false,     // true: Tolerate using the `__iterator__` property\n  \"lastsemic\"     : false,     // true: Tolerate omitting a semicolon for the last statement of a 1-line block\n  \"laxbreak\"      : false,     // true: Tolerate possibly unsafe line breakings\n  \"laxcomma\"      : false,     // true: Tolerate comma-first style coding\n  \"loopfunc\"      : false,     // true: Tolerate functions being defined in loops\n  \"multistr\"      : false,     // true: Tolerate multi-line strings\n  \"proto\"         : false,     // true: Tolerate using the `__proto__` property\n  \"scripturl\"     : false,     // true: Tolerate script-targeted URLs\n  \"smarttabs\"     : false,     // true: Tolerate mixed tabs/spaces when used for alignment\n  \"shadow\"        : false,     // true: Allows re-define variables later in code e.g. `var x=1; x=2;`\n  \"sub\"           : false,     // true: Tolerate using `[]` notation when it can still be expressed in dot notation\n  \"supernew\"      : false,     // true: Tolerate `new function () { ... };` and `new Object;`\n  \"validthis\"     : false,     // true: Tolerate using this in a non-constructor function\n\n  // Environments\n  \"browser\"       : true,     // Web Browser (window, document, etc)\n  \"couch\"         : false,    // CouchDB\n  \"devel\"         : true,     // Development/debugging (alert, confirm, etc)\n  \"dojo\"          : false,    // Dojo Toolkit\n  \"jasmine\"       : true,     // Jasmine testing framework\n  \"jquery\"        : false,    // jQuery\n  \"mootools\"      : false,    // MooTools\n  \"node\"          : false,    // Node.js\n  \"nonstandard\"   : false,    // Widely adopted globals (escape, unescape, etc)\n  \"prototypejs\"   : false,    // Prototype and Scriptaculous\n  \"rhino\"         : false,    // Rhino\n  \"worker\"        : false,    // Web Workers\n  \"wsh\"           : false,    // Windows Scripting Host\n  \"yui\"           : false,    // Yahoo User Interface\n  //\"meteor\"        : false,    // Meteor.js\n\n  // Legacy\n  \"nomen\"         : false,    // true: Prohibit dangling `_` in variables\n  \"onevar\"        : false,    // true: Allow only one `var` statement per function\n  \"passfail\"      : false,    // true: Stop on first error\n  \"white\"         : false,    // true: Check against strict whitespace and indentation rules\n\n  // Custom globals, from http://docs.meteor.com, in the order they appear there\n  \"globals\"       : {\n    \"Meteor\": false,\n    \"DDP\": false,\n    \"Mongo\": false, //Meteor.Collection renamed to Mongo.Collection\n    \"Session\": false,\n    \"Accounts\": false,\n    \"Template\": false,\n    \"Blaze\": false,  //UI is being renamed Blaze\n    \"UI\": false,\n    \"Match\": false,\n    \"check\": false,\n    \"Tracker\": false, //Deps renamed to Tracker\n    \"Deps\": false,\n    \"ReactiveVar\": false,\n    \"EJSON\": false,\n    \"HTTP\": false,\n    \"Email\": false,\n    \"Assets\": false,\n    \"Handlebars\": false,      // https://github.com/meteor/meteor/wiki/Handlebars\n    \"Package\": false,\n\n    // Meteor internals\n    \"DDPServer\": false,\n    \"global\": false,\n    \"Log\": false,\n    \"MongoInternals\": false,\n    \"process\": false,\n    \"WebApp\": false,\n    \"WebAppInternals\": false,\n\n    // globals useful when creating Meteor packages\n    \"Npm\": false,\n    \"Tinytest\": false,\n\n    // Meteor packages\n    \"$\": false,\n    \"_\": false,\n    \"__\": false,\n    \"AccountsTemplates\": false,\n    \"AutoForm\": false,\n    \"Avatar\": false,\n    \"Cookie\": false,\n    \"FastRender\": false,\n    \"Gravatar\": false,\n    \"Herald\": false,\n    \"Kadira\": false,\n    \"moment\": false,\n    \"Random\": false,\n    \"RouteController\": false,\n    \"Router\": false,\n    \"SEO\": false,\n    \"SimpleSchema\": false,\n    \"SubsManager\": false,\n    \"SyncedCron\": false,\n    \"TAPi18n\": false,\n    \"FlowRouter\": false,\n\n    // Telescope collections\n    \"Categories\": true,\n    \"Comments\": true,\n    \"Feeds\": true,\n    \"Invites\": true,\n    \"Migrations\": true,\n    \"Posts\": true,\n    \"Releases\": true,\n    \"Searches\": true,\n    \"Users\": true,\n\n    // Telescope objects\n    \"buildAndSendEmail\": true,\n    \"buildEmailNotification\": true,\n    \"buildEmailTemplate\": true,\n    \"compareVersions\": true,\n    \"coreSubscriptions\": true,\n    \"daysPerPage\": true,\n    \"deleteDummyContent\": true,\n    \"Events\": true,\n    \"fetchFeeds\": true,\n    \"getCategoryUrl\": true,\n    \"getEmailTemplate\": true,\n    \"getPostCategories\": true,\n    \"getTemplate\": true,\n    \"getVotePower\": true,\n    \"i18n\": true,\n    \"InviteSchema\": true,\n    \"logEvent\": true,\n    \"marked\": true,\n    \"Messages\": true,\n    \"Pages\": true,\n    \"sendEmail\": true,\n    \"serveAPI\": true,\n    \"Settings\": true,\n    \"Telescope\": true,\n    \"templates\": true,\n    \"themeSettings\": true\n  },\n  \"esnext\": true\n}\n"
  },
  {
    "path": ".meteor/.finished-upgraders",
    "content": "# This file contains information which helps Meteor properly upgrade your\n# app when you run 'meteor update'. You should check it into version control\n# with your project.\n\nnotices-for-0.9.0\nnotices-for-0.9.1\n0.9.4-platform-file\nnotices-for-facebook-graph-api-2\n1.2.0-standard-minifiers-package\n1.2.0-meteor-platform-split\n1.2.0-cordova-changes\n1.2.0-breaking-changes\n1.3.0-split-minifiers-package\n1.3.5-remove-old-dev-bundle-link\n1.4.0-remove-old-dev-bundle-link\n1.4.1-add-shell-server-package\n1.4.3-split-account-service-packages\n1.5-add-dynamic-import-package\n1.7-split-underscore-from-meteor-base\n1.8.3-split-jquery-from-blaze\n"
  },
  {
    "path": ".meteor/.gitignore",
    "content": "dev_bundle\nlocal\nmeteorite\n"
  },
  {
    "path": ".meteor/.id",
    "content": "# This file contains a token that is unique to your project.\n# Check it into your repository along with the rest of this directory.\n# It can be used for purposes such as:\n#   - ensuring you don't accidentally deploy one app on top of another\n#   - providing package authors with aggregated statistics\n\n1txv9r51kxht481ysl8bb\n"
  },
  {
    "path": ".meteor/cordova-plugins",
    "content": ""
  },
  {
    "path": ".meteor/packages",
    "content": "# see http://docs.vulcanjs.org/packages\n\nvulcan:core\n\n############ Language Packages ############\n\nvulcan:i18n-en-us\n# vulcan:i18n-es-es\n\n############ Accounts Packages ############\n\naccounts-base@2.0.0\naccounts-password@2.0.0\n# accounts-twitter\n# accounts-facebook\n\nvulcan:debug\n# To run the backoffice\nvulcan:backoffice\nvulcan:accounts\n# Ui\nvulcan:ui-bootstrap\n# vulcan:ui-material\n\nmeteortesting:mocha\napollo\nservice-configuration@1.1.0\n"
  },
  {
    "path": ".meteor/platforms",
    "content": "server\nbrowser\n"
  },
  {
    "path": ".meteor/release",
    "content": "METEOR@2.3.4\n"
  },
  {
    "path": ".meteorignore",
    "content": "stories\nstorybook-static\n"
  },
  {
    "path": ".nvmrc",
    "content": "v12.21.0\n"
  },
  {
    "path": ".prettierrc.js",
    "content": "'use strict';\n\nconst {esNextPaths} = require('./.vulcan/shared/pathsByLanguageVersion');\n\nmodule.exports = {\n  bracketSpacing: true,\n  singleQuote: true,\n  jsxBracketSameLine: true,\n  trailingComma: 'es5',\n  printWidth: 140,\n  parser: 'babylon',\n\n  overrides: [\n    {\n      files: esNextPaths,\n      options: {\n        trailingComma: 'all',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": ".storybook/addons.js",
    "content": "// Init storybook addons here\nimport '@storybook/addon-knobs/register';\nimport '@storybook/addon-actions/register';\nimport '@storybook/addon-links/register';\nimport 'storybook-addon-intl/register';\n"
  },
  {
    "path": ".storybook/config.js",
    "content": "/**\n * Global configuration of the stories\n */\nimport { addDecorator, configure } from '@storybook/react';\n\nif (process.env.STORYBOOK_UI === 'material') {\n  // init UI using a Decorator\n  console.log('Running storybook with Material UI');\n  const MaterialUIDecorator = require('./decorators/MaterialUIDecorator').default;\n  addDecorator(MaterialUIDecorator);\n} else {\n  console.log('Running storybook with Bootstrap');\n  const BootstrapDecorator = require('./decorators/BootstrapDecorator').default;\n  addDecorator(BootstrapDecorator);\n}\n\nimport onStorybookStart from './startup';\nonStorybookStart(() => console.log('Storybook started'));\n// load the components in the app so that <Component.Whatever /> is defined\nimport { populateComponentsApp, initializeFragments } from 'meteor/vulcan:lib';\nonStorybookStart(() => {\n  // we need registered fragments to be initialized because populateComponentsApp will run\n  // hocs, like withUpdate, that rely on fragments\n  initializeFragments();\n  // actually fills the Components object\n  populateComponentsApp();\n});\n\n/*\n\nStandard Config\n\n*/\n// automatically import all files ending in *.stories.js\nconst req = require.context('../stories', true, /.stories.js$/);\nfunction loadStories() {\n  req.keys().forEach(filename => req(filename));\n}\n\n/*\n\nReact Router Config\nSee https://github.com/gvaldambrini/storybook-router/tree/master/packages/react\n\n*/\nimport StoryRouter from 'storybook-react-router';\naddDecorator(StoryRouter());\n\n/*\n\nVulcan core Components\n\n*/\n\nimport 'meteor/vulcan:core';\n\n/*\n\ni18n\n\nSee https://github.com/truffls/storybook-addon-intl\n\n*/\n\nimport 'meteor/vulcan:i18n-en-us/lib/en_US.js';\nimport { setIntlConfig, withIntl } from 'storybook-addon-intl';\nimport { addLocaleData } from 'react-intl';\nimport { Strings, Locales } from './helpers.js';\n\nconst getMessages = locale => Strings[locale];\n\n/*\n\nEn\n\n*/\nimport enLocaleData from 'react-intl/locale-data/en';\naddLocaleData(enLocaleData);\n//import 'EnUS';\n\n// Set intl configuration\nsetIntlConfig({\n  locales: Locales.map(locale => locale.id),\n  defaultLocale: 'en',\n  getMessages,\n});\n\n// Register decorator\naddDecorator(withIntl);\n\n// Run storybook\nconfigure(loadStories, module);\n"
  },
  {
    "path": ".storybook/decorators/BootstrapDecorator.js",
    "content": "/**\n * Use this decorator to setup Bootstrap\n */\nimport React from 'react'\n\nimport 'meteor/vulcan:ui-bootstrap/lib/stylesheets/bootstrap.min.css';\nimport 'meteor/vulcan:ui-bootstrap/lib/stylesheets/style.scss';\nimport 'meteor/vulcan:ui-bootstrap/lib/stylesheets/datetime.scss';\n\n// load UI components\nimport 'meteor/vulcan:ui-bootstrap/lib/modules/components.js';\n\nexport default  storyFn => (<div>{storyFn()}</div>)"
  },
  {
    "path": ".storybook/decorators/MaterialUIDecorator.js",
    "content": "/*\n\nUse this decorator to load Material UI\n\n*/\nimport { Components } from 'meteor/vulcan:lib';\n// load UI components\nimport React from 'react'\nimport 'meteor/vulcan:ui-material/lib/modules/components.js';\nimport { wrapWithMuiTheme } from 'meteor/vulcan:ui-material';\n\n\nexport default  storyFn => (\n  <Components.ThemeProvider>\n    <div>{storyFn()}</div>\n  </Components.ThemeProvider>\n)\n"
  },
  {
    "path": ".storybook/helpers.js",
    "content": "import merge from 'lodash/merge';\n\n/*\n\nSimplified versions of Vulcan APIs and helpers\n\n*/\n\n/*\n\nComponents\n\n*/\nexport const Components = {}; // will be populated on startup\n\nexport const ComponentsMockProps = {};\n\nexport const getMockProps = (componentName, overrideProps) => {\n  return merge({}, ComponentsMockProps[componentName], overrideProps);\n};\n\nexport function registerComponent(name, rawComponent, ...hocs) {\n  // support single-argument syntax\n  if (typeof arguments[0] === 'object') {\n    // note: cannot use `const` because name, components, hocs are already defined\n    // as arguments so destructuring cannot work\n    // eslint-disable-next-line no-redeclare\n    var { name, component, hocs = [] } = arguments[0];\n    rawComponent = component;\n  }\n  // store the component in the table\n  Components[name] = rawComponent\n}\n\nexport const replaceComponent = registerComponent;\n\nexport const instantiateComponent = (component, props) => {\n  if (!component) {\n    return null;\n  } else if (typeof component === 'string') {\n    const Component = getComponent(component);\n    return <Component {...props} />;\n  } else if (\n    typeof component === 'function' &&\n    component.prototype &&\n    component.prototype.isReactComponent\n  ) {\n    const Component = component;\n    return <Component {...props} />;\n  } else if (typeof component === 'function') {\n    return component(props);\n  } else {\n    return component;\n  }\n};\n\nexport const coreComponents = [\n  'Alert',\n  'Button',\n  'Dropdown',\n  'Modal',\n  'ModalTrigger',\n  'Table',\n  'FormComponentCheckbox',\n  'FormComponentCheckboxGroup',\n  'FormComponentDate',\n  'FormComponentDate2',\n  'FormComponentDateTime',\n  'FormComponentDefault',\n  'FormComponentText',\n  'FormComponentEmail',\n  'FormComponentNumber',\n  'FormComponentRadioGroup',\n  'FormComponentSelect',\n  'FormComponentSelectMultiple',\n  'FormComponentStaticText',\n  'FormComponentTextarea',\n  'FormComponentTime',\n  'FormComponentUrl',\n  'FormControl',\n  'FormElement',\n  'FormItem',\n];\n\n/*\n\ni18n\n\n*/\n\nexport const Strings = {};\n\nexport const addStrings = (language, strings) => {\n  if (typeof Strings[language] === 'undefined') {\n    Strings[language] = {};\n  }\n  Strings[language] = {\n    ...Strings[language],\n    ...strings\n  };\n};\n\nexport const Locales = [];\n\nexport const registerLocale = locale => {\n  Locales.push(locale);\n};\n\n/*\n\nUsers\n\n*/\n\nexport const isAdmin = () => true;\nexport const getProfileUrl = (user, isAbsolute) => {\n  if (typeof user === 'undefined') {\n    return '';\n  }\n  isAbsolute = typeof isAbsolute === 'undefined' ? false : isAbsolute; // default to false\n  var prefix = isAbsolute ? Utils.getSiteUrl().slice(0, -1) : '';\n  if (user.slug) {\n    return `${prefix}/users/${user.slug}`;\n  } else {\n    return '';\n  }\n};\nexport const getDisplayName = (user) => {\n  if (!user) {\n    return '';\n  } else {\n    return user.displayName ? user.displayName : Users.getUserName(user);\n  }\n};\nexport const avatar = {\n  getUrl: user => 'https://api.adorable.io/avatars/285/abotaat@adorable.io.png',\n  getInitials: user => 'SG',\n}\n\n/*\n\nHelpers\n\n*/\n\nexport function capitalize(string) {\n  return string.replace(/\\-/, ' ').split(' ').map(word => {\n    return word.charAt(0).toUpperCase() + word.slice(1);\n  }).join(' ');\n}\n\n/*\n\nOther Exports\n\n*/\n\nexport const getSetting = (name, defaultSetting) => defaultSetting;\n\nexport const track = () => {};\nexport const addCallback = () => {};\n\nexport const withCurrentUser = c => c;\nexport const withUpdate = c => c;\n"
  },
  {
    "path": ".storybook/loaders/starter-example-loader.js",
    "content": "/**\n * \n * Load the local Vulcan packages, inspired by vulcan-loader\n * \n */\nconst { getOptions } = require('loader-utils');\nmodule.exports = function loader(source) {\n    const options = getOptions(this)\n\n    const {  packagesDir, environment = 'client' } = options\n    // prefixing your packages name makes it easier to write a loader\n    const prefix = `${packagesDir}/example-`\n    const defaultPath = `/lib/${environment}/main.js`\n\n    const result = source.replace(\n        // This regex will match:\n        // meteor/example-{packageName}{some-optional-import-path}\n        //\n        // Example:\n        // meteor/example-forum => match, packageName=\"forum\"\n        // meteor/example-forum/foobar.js => match, packageName=\"forum\", importPath=\"/foobar.js\"\n        // meteor/another-package => do not match\n        //\n        // Explanation:\n        // .+?(?=something) matches every char until \"something\" is met, excluding something\n        // we use it to matche the package name, until we meet a ' or \"\n        /meteor\\/example-(.*?(?=\\/|'|\"))(.*?(?=\\'|\\\"))/g, // match Meteor packages that are lfg packages, + the import path (without the quotes)\n        (match, packageName, importPath) => {\n            console.log(\"Found Starter example package\", packageName)\n            if (importPath){\n                return `${prefix}${packageName}${importPath}`\n            }\n            return `${prefix}${packageName}${defaultPath}`\n        }\n    )\n    return result\n}"
  },
  {
    "path": ".storybook/mocks/Meteor.js",
    "content": "// FIXME: we can't use ES6 imports in mocks, not sure why\nmodule.exports = {\n    settings: {},\n    startup: () => { },\n    _localStorage: window ? window.localStorage : { setItem: () => {}, getItem: () => {} },\n    isClient: () => true,\n    isServer: () => false,\n    absoluteUrl: () => 'http://vulcanjs.org/'\n}"
  },
  {
    "path": ".storybook/mocks/Mongo.js",
    "content": "module.exports = {\n    Collection: class Collection {}\n}"
  },
  {
    "path": ".storybook/mocks/Vulcan.js",
    "content": "module.exports = {\n\n}"
  },
  {
    "path": ".storybook/mocks/meteor-apollo.js",
    "content": "module.exports = {\n    MeteorAccountsLink: class MeteorAccountsLink {}\n}"
  },
  {
    "path": ".storybook/mocks/meteor-server-render.js",
    "content": "module.exports = {\n    onPageLoad: () => { }\n}"
  },
  {
    "path": ".storybook/mocks/vulcan-email.js",
    "content": "module.exports = {\n    addEmails: () => {}\n}"
  },
  {
    "path": ".storybook/startup.js",
    "content": "/**\n * Allow to run callbacks on Storybook startup, after stories are imported\n * Based on Meteor.startup client side implementation\n * @see https://github.com/meteor/meteor/blob/24865b28a0689de8b4949fb69ea1f95da647cd7a/packages/meteor/startup_client.js\n */\nvar callbackQueue = [];\nvar isLoadingCompleted = false;\nvar isReady = false;\n\n// Keeps track of how many events to wait for in addition to loading completing,\n// before we're considered ready.\nvar readyHoldsCount = 0;\n\nvar maybeReady = function () {\n  if (isReady || !isLoadingCompleted || readyHoldsCount > 0)\n    return;\n\n  isReady = true;\n\n  // Run startup callbacks\n  while (callbackQueue.length)\n    (callbackQueue.shift())();\n\n};\n\nvar loadingCompleted = function () {\n  if (!isLoadingCompleted) {\n    isLoadingCompleted = true;\n    maybeReady();\n  }\n}\n\nif (document.readyState === 'complete' || document.readyState === 'loaded') {\n  // Loading has completed,\n  // but allow other scripts the opportunity to hold ready\n  window.setTimeout(loadingCompleted);\n} else { // Attach event listeners to wait for loading to complete\n  if (document.addEventListener) {\n    document.addEventListener('DOMContentLoaded', loadingCompleted, false);\n    window.addEventListener('load', loadingCompleted, false);\n  } else { // Use IE event model for < IE9\n    document.attachEvent('onreadystatechange', function () {\n      if (document.readyState === \"complete\") {\n        loadingCompleted();\n      }\n    });\n    window.attachEvent('load', loadingCompleted);\n  }\n}\n\n/**\n * @summary Run code when a client or a server starts.\n * @locus Anywhere\n * @param {Function} func A function to run on startup.\n */\nconst onStartup = function (callback) {\n  // Fix for < IE9, see http://javascript.nwbox.com/IEContentLoaded/\n  var doScroll = !document.addEventListener &&\n    document.documentElement.doScroll;\n\n  if (!doScroll || window !== top) {\n    if (isReady)\n      callback();\n    else\n      callbackQueue.push(callback);\n  } else {\n    try { doScroll('left'); }\n    catch (error) {\n      setTimeout(function () { onStartup(callback); }, 50);\n      return;\n    };\n    callback();\n  }\n};\n\nexport default onStartup"
  },
  {
    "path": ".storybook/webpack.config.js",
    "content": "/*\n\nWebpack setup\n\nAdapt with your own loaders and config if necessary\n\n*/\n\nconst path = require('path');\nconst webpack = require('webpack');\n\n// Find Vulcan install, should not be modified\n\n/**\n * Smart function to find Vulcan packages\n *\n * You can either provide a path to Vulcan as VULCAN_DIR env\n * or set the METEOR_PACKAGE_DIR variable\n */\nconst findPathToVulcanPackages = () => {\n  // look for VULCAN_DIR env variable\n  if (process.env.VULCAN_DIR) return `${process.env.VULCAN_DIR}/packages`;\n  // look for METEOR_PACKAGE_DIRS variable\n  const rawPackageDirs = process.env.METEOR_PACKAGE_DIRS;\n  if (rawPackageDirs) {\n    const dirs = rawPackageDirs.split(':');\n    // Vulcan dir should be '/some-folder/Vulcan/packages'\n    const vulcanPackagesDir = dirs.find(dir => !!dir.match(/\\/Vulcan\\//));\n    if (vulcanPackagesDir) {\n      return vulcanPackagesDir;\n    }\n    console.log(`\n      Please either set the VULCAN_DIR variable to your Vulcan folder or\n      set METEOR_PACKAGE_DIRS to your <Vulcan>/packages folder.\n      Fallback to default value: '../../Vulcan'.`);\n  }\n  // default value\n  return '../../Vulcan/packages';\n};\n// path to your Vulcan repo (see 2-repo install in docs)\nconst pathToVulcanPackages = path.resolve(__dirname, findPathToVulcanPackages());\n\nmodule.exports = ({ config }) => {\n  // Define aliases. Allow to mock some packages.\n  config.resolve = {\n    ...config.resolve,\n    // this way node_modules are always those of current project and not of Vulcan\n    alias: {\n      ...config.resolve.alias,\n      // Vulcan Packages\n      'meteor/vulcan:email': path.resolve(__dirname, './mocks/vulcan-email'),\n      //'meteor/vulcan:i18n': 'react-intl',\n      // Other packages\n      'meteor/apollo': path.resolve(__dirname, './mocks/meteor-apollo'),\n      'meteor/server-render': path.resolve(__dirname, './mocks/meteor-server-render'),\n    },\n  };\n  // Mock global variables\n  config.plugins.push(\n    new webpack.ProvidePlugin({\n      // mock global variables\n      Meteor: path.resolve(__dirname, './mocks/Meteor'),\n      Vulcan: path.resolve(__dirname, './mocks/Vulcan'),\n      Mongo: path.resolve(__dirname, './mocks/Mongo'),\n      _: 'underscore',\n    })\n  );\n\n  // force the config to use local node_modules instead the modules from Vulcan install\n  // Should not be modified\n  config.resolve.modules.push(path.resolve(__dirname, '../node_modules'));\n\n  // handle meteor packages\n  // Add your custom loaders here if necessary\n  config.module.rules.push({\n    test: /\\.(js|jsx)$/,\n    exclude: /node_modules/,\n    loaders: [\n      // Remove meteor package (last step)\n      {\n        loader: 'scrap-meteor-loader',\n        options: {\n          // those package will be preserved, we provide a mock instead\n          preserve: ['meteor/apollo', 'meteor/vulcan:email', 'meteor/server-render'],\n        },\n      },\n      // Load Vulcan core packages\n      {\n        loader: 'vulcan-loader',\n        options: {\n          vulcanPackagesDir: pathToVulcanPackages,\n          environment: 'client',\n          // those package are mocked using an alias instead or just ignored\n          exclude: ['meteor/vulcan:email', 'meteor/vulcan:accounts'],\n        },\n      },\n      // Add your loaders here for your own local vulcan-packages\n      // Example for Vulcan Starter:\n      {\n        loader: path.resolve(__dirname, './loaders/starter-example-loader'),\n        options: {\n          packagesDir: path.resolve(__dirname, '../packages'),\n          environment: 'client',\n        },\n      },\n    ],\n  });\n\n  // Parse JSX files outside of Storybook directory\n  // Should not be modified\n  config.module.rules.push({\n    test: /\\.(js|jsx)$/,\n    loaders: [\n      {\n        loader: 'babel-loader',\n        query: {\n          presets: [\n            '@babel/react',\n            {\n              plugins: [\n                '@babel/plugin-proposal-class-properties',\n                '@babel/plugin-syntax-dynamic-import',\n                '@babel/plugin-proposal-optional-chaining',\n                '@babel/plugin-proposal-nullish-coalescing-operator',\n              ],\n            },\n          ],\n        },\n      },\n    ],\n  });\n\n  // Parse SCSS files\n  // Should not be modfied\n  config.module.rules.push({\n    test: /\\.scss$/,\n    loaders: ['style-loader', 'css-loader', 'sass-loader'],\n    // include: path.resolve(__dirname, \"../\")\n  });\n\n  // Return the altered config\n  return config;\n};\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"type\": \"chrome\",\n            \"request\": \"launch\",\n            \"name\": \"Launch Chrome against localhost\",\n            \"url\": \"http://localhost:8080\",\n            \"webRoot\": \"${workspaceFolder}\"\n        }\n    ]\n}"
  },
  {
    "path": ".vulcan/.gitignore",
    "content": "bkp\npackage.json\n"
  },
  {
    "path": ".vulcan/prestart_vulcan.js",
    "content": "#!/usr/bin/env node\n\n//Functions\nvar fs = require('fs');\nfunction existsSync(filePath){\n  try{\n    fs.statSync(filePath);\n  }catch(err){\n    if(err.code == 'ENOENT') return false;\n  }\n  return true;\n}\n\nfunction copySync(origin,target){\n  try{\n    fs.writeFileSync(target, fs.readFileSync(origin));\n  }catch(err){\n    if(err.code == 'ENOENT') return false;\n  }\n  return true;\n}\n\n//Add Definition Colors\nconst chalk = require('chalk');\n\n//Vulcan letters\nconsole.log(chalk.gray(' ___    ___ '));\nconsole.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('/'));\nconsole.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'));\nconsole.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'));\nconsole.log(chalk.gray('    ────     '));\n\n\nvar os = require('os');\nvar exec = require('child_process').execSync;\nvar options = {\n  encoding: 'utf8'\n};\n//Check Meteor and install if not installed\nvar checker = exec(\"meteor --version\", options);\nif (!checker.includes(\"Meteor \")) {\nconsole.log(\"Vulcan requires Meteor but it's not installed. Trying to Install...\");\n  //Check platform\n  if (os.platform()=='darwin') {\n    //Mac OS platform\n    console.log(\"🌋  \"+chalk.bold.yellow(\"Good news you have a Mac and we will install it now! }\"));\n    console.log(exec(\"curl https://install.meteor.com/ | bash\", options));\n  } else if (os.platform()=='linux') {\n    //GNU/Linux platform\n    console.log(\"🌋  \"+chalk.bold.yellow(\"Good news you are on  GNU/Linux platform and we will install Meteor now!\"));\n    console.log(exec(\"curl https://install.meteor.com/ | bash\", options));\n  } else if (os.platform()=='win32') {\n    //Windows NT platform\n      console.log(\">  \"+chalk.bold.yellow(\"Oh no! you are on a Windows platform and you will need to install Meteor Manually!\"));\n      console.log(\">  \"+chalk.dim.yellow(\"Meteor for Windows is available at: \")+chalk.redBright(\"https://install.meteor.com/windows\"));\n      process.exit(-1)\n  }\n} else {\n//Check exist file settings and create if not exist\nif (!existsSync(\"settings.json\")) {\n  console.log(\">  \"+chalk.bold.yellow(\"Creating your own settings.json file...\\n\"));\n  if (!copySync(\"sample_settings.json\",\"settings.json\")) {\n    console.log(\">  \"+chalk.bold.red(\"Error Creating your own settings.json file...check files and permissions\\n\"));\n    process.exit(-1);\n  }\n}\n\n  console.log(\">  \"+chalk.bold.yellow(\"Happy hacking with Vulcan!\"));\n  console.log(\">  \"+chalk.dim.yellow(\"The docs are available at: \")+chalk.redBright(\"http://docs.vulcanjs.org\"));\n}\n"
  },
  {
    "path": ".vulcan/prettier/index.js",
    "content": "\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';\n\n// Based on similar script in Jest\n// https://github.com/facebook/jest/blob/a7acc5ae519613647ff2c253dd21933d6f94b47f/scripts/prettier.js\n\nconst chalk = require('chalk');\nconst glob = require('glob');\nconst prettier = require('prettier');\nconst fs = require('fs');\nconst listChangedFiles = require('../shared/listChangedFiles');\nconst prettierConfigPath = require.resolve('../../.prettierrc');\n\nconst mode = process.argv[2] || 'check';\nconst shouldWrite = mode === 'write' || mode === 'write-changed';\nconst onlyChanged = mode === 'check-changed' || mode === 'write-changed';\n\nconst changedFiles = onlyChanged ? listChangedFiles() : null;\nlet didWarn = false;\nlet didError = false;\n\nconst files = glob\n  .sync('**/*.js', {ignore: '**/node_modules/**'})\n  .filter(f => !onlyChanged || changedFiles.has(f));\n\nif (!files.length) {\n  return;\n}\n\nfiles.forEach(file => {\n  const options = prettier.resolveConfig.sync(file, {\n    config: prettierConfigPath,\n  });\n  try {\n    const input = fs.readFileSync(file, 'utf8');\n    if (shouldWrite) {\n      const output = prettier.format(input, options);\n      if (output !== input) {\n        fs.writeFileSync(file, output, 'utf8');\n      }\n    } else {\n      if (!prettier.check(input, options)) {\n        if (!didWarn) {\n          console.log(\n            '\\n' +\n              chalk.red(\n                `  This project uses prettier to format all JavaScript code.\\n`\n              ) +\n              chalk.dim(`    Please run `) +\n              chalk.reset('yarn prettier-all') +\n              chalk.dim(\n                ` and add changes to files listed below to your commit:`\n              ) +\n              `\\n\\n`\n          );\n          didWarn = true;\n        }\n        console.log(file);\n      }\n    }\n  } catch (error) {\n    didError = true;\n    console.log('\\n\\n' + error.message);\n    console.log(file);\n  }\n});\n\nif (didWarn || didError) {\n  process.exit(1);\n}"
  },
  {
    "path": ".vulcan/shared/listChangedFiles.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';\n\nconst execFileSync = require('child_process').execFileSync;\n\nconst exec = (command, args) => {\n  console.log('> ' + [command].concat(args).join(' '));\n  const options = {\n    cwd: process.cwd(),\n    env: process.env,\n    stdio: 'pipe',\n    encoding: 'utf-8',\n  };\n  return execFileSync(command, args, options);\n};\n\nconst execGitCmd = args =>\n  exec('git', args)\n    .trim()\n    .toString()\n    .split('\\n');\n\nconst listChangedFiles = () => {\n  const mergeBase = execGitCmd(['merge-base', 'HEAD', 'devel']);\n  return new Set([\n    ...execGitCmd(['diff', '--name-only', '--diff-filter=ACMRTUB', mergeBase]),\n    ...execGitCmd(['ls-files', '--others', '--exclude-standard']),\n  ]);\n};\n\nmodule.exports = listChangedFiles;\n"
  },
  {
    "path": ".vulcan/shared/pathsByLanguageVersion.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';\n\n// Files that are transformed and can use ES6/Flow/JSX.\nconst esNextPaths = [\n  // Internal forwarding modules\n  'packages/*/*.js',\n  'packages/*/*.jsx',\n];\n\nmodule.exports = {\n  esNextPaths,\n};"
  },
  {
    "path": ".vulcan/update_package.js",
    "content": "#!/usr/bin/env node\n\n/*\n\n### Usage\n\nPlace Vulcan's package.json in .vulcan/package.json and run meteor npm run update-package-json form your project's folder.\n\nYou'll have to manually manage breaking updates (example, from ^2.0.1 to ^3.0.2).\n\n### Features\n\n- makes a backup of the project's package.json\n- only merges dependencies, devDependencies and peerDependencies\n- if full merge is successful, shows a list of updated versions\n- will store vulcanVersion in package.json for future updates\n\n*/\n\nvar fs = require('fs');\nvar mergePackages = require('@userfrosting/merge-package-dependencies');\nvar jsdiff = require('diff');\nrequire('colors');\n\nfunction diffPartReducer(accumulator, part) {\n  // green for additions, red for deletions\n  // grey for common parts\n  var color = part.added ? 'green' : (part.removed ? 'red' : 'grey');\n\n  return {\n    text: (accumulator.text || '') + part.value[color],\n    count: (accumulator.count || 0) + (part.added || part.removed ? 1 : 0),\n  };\n}\n\n// copied from sort-object-keys package\nfunction sortObjectByKeyNameList(object, sortWith) {\n  var keys;\n  var sortFn;\n\n  if (typeof sortWith === 'function') {\n    sortFn = sortWith;\n  } else {\n    keys = sortWith;\n  }\n  return (keys || []).concat(Object.keys(object).sort(sortFn)).reduce(function(total, key) {\n    total[key] = object[key];\n    return total;\n  }, Object.create({}));\n}\n\n\nvar appDirPath = './';\nvar vulcanDirPath = './.vulcan/';\n\nif (!fs.existsSync(vulcanDirPath + 'package.json')) {\n  console.log('Could not find \\'' + vulcanDirPath + 'package.json\\'');\n} else if (!fs.existsSync(appDirPath + 'package.json')) {\n  console.log('Could not find \\'' + appDirPath + 'package.json\\'');\n} else {\n  var appPackageFile = fs.readFileSync(appDirPath + '/package.json');\n  var appPackageJson = JSON.parse(appPackageFile);\n  var vulcanPackageFile = fs.readFileSync(vulcanDirPath + 'package.json');\n  var vulcanPackageJson = JSON.parse(vulcanPackageFile);\n\n  if (appPackageJson.vulcanVersion) {\n    console.log(appPackageJson.name + '@' + appPackageJson.version +\n      ' \\'package.json\\' will be updated from Vulcan@' + appPackageJson.vulcanVersion +\n      ' to Vulcan@' + vulcanPackageJson.version +\n      ' dependencies.');\n  } else {\n    console.log(appPackageJson.name + '@' + appPackageJson.version +\n      ' \\'package.json\\' will be updated with Vulcan@' + vulcanPackageJson.version +\n      ' dependencies.');\n  }\n\n  var backupDirPath = vulcanDirPath + 'bkp/';\n  if (!fs.existsSync(backupDirPath)) {\n    fs.mkdirSync(backupDirPath);\n  }\n  var backupFilePath = backupDirPath + 'package-' + Date.now() + '.json';\n  console.log('Saving a backup of \\'' + appDirPath + 'package.json\\' in \\'' + backupFilePath + '\\'');\n  fs.writeFileSync(backupFilePath, appPackageFile);\n\n  var updatedAppPackageJson = mergePackages.npm(\n    // IMPORTANT: parse again because mergePackages.npm mutates json\n    JSON.parse(appPackageFile),\n    [vulcanDirPath]\n  );\n\n  updatedAppPackageJson.vulcanVersion = vulcanPackageJson.version;\n\n  [\n    'dependencies',\n    'devDependencies',\n    'peerDependencies'\n  ].forEach(function(key) {\n    if (updatedAppPackageJson[key]) {\n      updatedAppPackageJson[key] = sortObjectByKeyNameList(updatedAppPackageJson[key]);\n    }\n\n    const diff = jsdiff.diffJson(\n      sortObjectByKeyNameList(appPackageJson[key] || {}),\n      updatedAppPackageJson[key] || {}\n    ).reduce(diffPartReducer, {});\n    if (diff.count) {\n      console.log('Changes in \"' + key + '\":');\n      console.log(diff.text);\n    } else {\n      console.log('No changes in \"' + key + '\".');\n    }\n  });\n\n  fs.writeFileSync(appDirPath + 'package.json', JSON.stringify(updatedAppPackageJson, null, '  '));\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "### Changelog\n\nAll notable changes to this project will be documented in this file. Dates are displayed in UTC.\n\nGenerated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).\n\n#### [Unreleased](https://github.com/VulcanJS/Vulcan/compare/1.15.0...HEAD)\n\n- Fixed issue #2664 + Fixed canUpdate on Card.jsx [`#2694`](https://github.com/VulcanJS/Vulcan/pull/2694)\n- add to storybook plugin-proposal-optional-chaining to accept component.jsx [`#2662`](https://github.com/VulcanJS/Vulcan/pull/2662)\n- Unify Server and Client ThemeProviders and pass apolloClient as prop [`#2691`](https://github.com/VulcanJS/Vulcan/pull/2691)\n- Connector filter calbacks [`#2690`](https://github.com/VulcanJS/Vulcan/pull/2690)\n- SmartForm minor bug fixes Jan 2020 [`#2687`](https://github.com/VulcanJS/Vulcan/pull/2687)\n- Flash messages bug fixes [`#2692`](https://github.com/VulcanJS/Vulcan/pull/2692)\n- add graphql.context callback [`#2689`](https://github.com/VulcanJS/Vulcan/pull/2689)\n- Material UI minor changes 01-2021 [`#2685`](https://github.com/VulcanJS/Vulcan/pull/2685)\n- Flash messages in reactive state [`#2683`](https://github.com/VulcanJS/Vulcan/pull/2683)\n- Get string refactor and document [`#2681`](https://github.com/VulcanJS/Vulcan/pull/2681)\n- Reactive State [`#2682`](https://github.com/VulcanJS/Vulcan/pull/2682)\n- Material UI Number input [`#2684`](https://github.com/VulcanJS/Vulcan/pull/2684)\n- Random [`#2675`](https://github.com/VulcanJS/Vulcan/pull/2675)\n- getString Refactor And Document [`#2671`](https://github.com/VulcanJS/Vulcan/pull/2671)\n- FormDescription to ui-bootstrap [`#2674`](https://github.com/VulcanJS/Vulcan/pull/2674)\n- Query filters to mongo params [`#2672`](https://github.com/VulcanJS/Vulcan/pull/2672)\n- Fix error message for app.operation_not_allowed [`#2673`](https://github.com/VulcanJS/Vulcan/pull/2673)\n- Feature RTL support in locale [`#2670`](https://github.com/VulcanJS/Vulcan/pull/2670)\n- React Node Support in getString() [`#2667`](https://github.com/VulcanJS/Vulcan/pull/2667)\n- Form Component New Names Fix 2 [`#2666`](https://github.com/VulcanJS/Vulcan/pull/2666)\n- Form Component New Names Fix [`#2665`](https://github.com/VulcanJS/Vulcan/pull/2665)\n- Form Component New Names [`#2659`](https://github.com/VulcanJS/Vulcan/pull/2659)\n- add init callback to smartform [`#2663`](https://github.com/VulcanJS/Vulcan/pull/2663)\n- VerifyEmail Bug [`#2660`](https://github.com/VulcanJS/Vulcan/pull/2660)\n- i18n Plural Support [`#2654`](https://github.com/VulcanJS/Vulcan/pull/2654)\n- Material UI Minor Updates Nov 2020 [`#2655`](https://github.com/VulcanJS/Vulcan/pull/2655)\n- only Write and log graphql schema when different from existing file [`#2653`](https://github.com/VulcanJS/Vulcan/pull/2653)\n- update permissions for debug schemas [`#2652`](https://github.com/VulcanJS/Vulcan/pull/2652)\n- Apollo Client 3 Import Fixes [`#2644`](https://github.com/VulcanJS/Vulcan/pull/2644)\n- Material UI Minor Updates Oct 2020 [`#2649`](https://github.com/VulcanJS/Vulcan/pull/2649)\n- Multi HoC/Hook Returned Props [`#2651`](https://github.com/VulcanJS/Vulcan/pull/2651)\n- Move FormLabel to vulcan:ui-bootstrap [`#2645`](https://github.com/VulcanJS/Vulcan/pull/2645)\n- Minor Formatting Oct 2020 [`#2646`](https://github.com/VulcanJS/Vulcan/pull/2646)\n- Minor Form Bugs Oct 2020 [`#2647`](https://github.com/VulcanJS/Vulcan/pull/2647)\n- Minor Data Layer Fixes Oct 2020 [`#2648`](https://github.com/VulcanJS/Vulcan/pull/2648)\n- Add placeholder to the whitelist [`#2643`](https://github.com/VulcanJS/Vulcan/pull/2643)\n- Bump lodash.merge from 4.6.1 to 4.6.2 [`#2629`](https://github.com/VulcanJS/Vulcan/pull/2629)\n- Bump lodash.mergewith from 4.6.0 to 4.6.2 [`#2630`](https://github.com/VulcanJS/Vulcan/pull/2630)\n- Use Symbol.for for vulcan-accounts const STATES [`#2637`](https://github.com/VulcanJS/Vulcan/pull/2637)\n- Feature/http only cookie [`#2631`](https://github.com/VulcanJS/Vulcan/pull/2631)\n- Upgrade to Meteor 1.10.2 [`#2604`](https://github.com/VulcanJS/Vulcan/pull/2604)\n- Validate document permissions bug fix [`#2622`](https://github.com/VulcanJS/Vulcan/pull/2622)\n- CurrentUserRefetch [`#2621`](https://github.com/VulcanJS/Vulcan/pull/2621)\n- MaterialUiMinorChanges8 [`#2620`](https://github.com/VulcanJS/Vulcan/pull/2620)\n- Bump lodash from 4.17.15 to 4.17.19 [`#2600`](https://github.com/VulcanJS/Vulcan/pull/2600)\n- FormInputUrlMailSocialScrubbing [`#2606`](https://github.com/VulcanJS/Vulcan/pull/2606)\n- MuiInputWithNoLabel [`#2612`](https://github.com/VulcanJS/Vulcan/pull/2612)\n- MuiSuggestUpgrades [`#2611`](https://github.com/VulcanJS/Vulcan/pull/2611)\n- TooltipButtonUpgrades [`#2610`](https://github.com/VulcanJS/Vulcan/pull/2610)\n- MuiModalChanges [`#2609`](https://github.com/VulcanJS/Vulcan/pull/2609)\n- SmartFormEnhancements [`#2607`](https://github.com/VulcanJS/Vulcan/pull/2607)\n- MaterialUiMinorChanges7 [`#2605`](https://github.com/VulcanJS/Vulcan/pull/2605)\n- Upgrade to MUI 4.11.0 [`#2603`](https://github.com/VulcanJS/Vulcan/pull/2603)\n- MuiDatatableUpgrades [`#2619`](https://github.com/VulcanJS/Vulcan/pull/2619)\n- MuiScrollTriggerUpgrades [`#2618`](https://github.com/VulcanJS/Vulcan/pull/2618)\n- RegisterComponentJSDoc [`#2616`](https://github.com/VulcanJS/Vulcan/pull/2616)\n- GraphQLComments [`#2615`](https://github.com/VulcanJS/Vulcan/pull/2615)\n- SegmentClientFix [`#2614`](https://github.com/VulcanJS/Vulcan/pull/2614)\n- GenericMutationWrappersResponse [`#2613`](https://github.com/VulcanJS/Vulcan/pull/2613)\n- Bugfix/add correct selector to delete [`#2601`](https://github.com/VulcanJS/Vulcan/pull/2601)\n- Feature/showother ui material [`#2592`](https://github.com/VulcanJS/Vulcan/pull/2592)\n- Forms submit target verification [`#2568`](https://github.com/VulcanJS/Vulcan/pull/2568)\n- add itemProperties openNested option for nested schema [`#2594`](https://github.com/VulcanJS/Vulcan/pull/2594)\n- add new update check api to ui-material Card.jsx [`#2593`](https://github.com/VulcanJS/Vulcan/pull/2593)\n- fix allowed values for radiogroup [`#2588`](https://github.com/VulcanJS/Vulcan/pull/2588)\n- change cookie secure to false on localhost [`#2587`](https://github.com/VulcanJS/Vulcan/pull/2587)\n- Jun2020BugFixes1 [`#2583`](https://github.com/VulcanJS/Vulcan/pull/2583)\n- change Mui to expect handleChange in props [`#2577`](https://github.com/VulcanJS/Vulcan/pull/2577)\n- bugfix - SmartForm cannot update nested input fields [`#2574`](https://github.com/VulcanJS/Vulcan/pull/2574)\n- Handle options in FormComponents (fix #2578) [`#2578`](https://github.com/VulcanJS/Vulcan/issues/2578)\n- use more robust operationName mock link [`6cd9ec5`](https://github.com/VulcanJS/Vulcan/commit/6cd9ec592df8e25ecb98b9227dcd52ff2ea41585)\n- Small clean up; fix form delete [`03c41d6`](https://github.com/VulcanJS/Vulcan/commit/03c41d678b601a10363e7e416b2add5d52ca13aa)\n- stop cors validation as early as possible to avoid conflicting checks [`fa42f6e`](https://github.com/VulcanJS/Vulcan/commit/fa42f6e0068bdaca8f5dfaed3d7e49f795581b80)\n\n#### [1.15.0](https://github.com/VulcanJS/Vulcan/compare/1.14.1...1.15.0)\n\n> 10 May 2020\n\n- Bugfix/2550 readable fields with document [`#2564`](https://github.com/VulcanJS/Vulcan/pull/2564)\n- feature/update react-helmet [`#2563`](https://github.com/VulcanJS/Vulcan/pull/2563)\n- Add useSignout hook to the accounts package [`#2521`](https://github.com/VulcanJS/Vulcan/pull/2521)\n- Enable OpenCRUD backwards compatibility [`#2559`](https://github.com/VulcanJS/Vulcan/pull/2559)\n- Don't run `options` function until `loading` is false, and `data` is populated [`#2561`](https://github.com/VulcanJS/Vulcan/pull/2561)\n- Account/auth features available outside DDP using GraphQL [`#2530`](https://github.com/VulcanJS/Vulcan/pull/2530)\n- Use Meteor's version of npm over system default [`#2560`](https://github.com/VulcanJS/Vulcan/pull/2560)\n- move muicheckboxgroup proptypes name and options to inputProperties a… [`#2554`](https://github.com/VulcanJS/Vulcan/pull/2554)\n- add switch/checkbox alternative in material ui [`#2553`](https://github.com/VulcanJS/Vulcan/pull/2553)\n- Bump acorn from 5.7.1 to 5.7.4 [`#2546`](https://github.com/VulcanJS/Vulcan/pull/2546)\n- add Express + use bodyParserGraphQL [`#2532`](https://github.com/VulcanJS/Vulcan/pull/2532)\n- Update to current valid strikethrough tags [`#2543`](https://github.com/VulcanJS/Vulcan/pull/2543)\n- Use Meteor's version of npm over system default [`#2506`](https://github.com/VulcanJS/Vulcan/issues/2506)\n- Log out GraphQL schema to schema.graphql on every app start (fix #2517) [`#2517`](https://github.com/VulcanJS/Vulcan/issues/2517)\n- add stories for modals [`b6729de`](https://github.com/VulcanJS/Vulcan/commit/b6729dec65afe1de8c5ab489a03958ea6c0bf5f1)\n- fix for same origin [`5bfb696`](https://github.com/VulcanJS/Vulcan/commit/5bfb696e18d44c2aa3f28f2a719e703ba5940d3e)\n- fix install with Meteor 1.10 [`138fc7c`](https://github.com/VulcanJS/Vulcan/commit/138fc7c914f8a34cd327ee1dbc1bdcd8782d8597)\n\n#### [1.14.1](https://github.com/VulcanJS/Vulcan/compare/1.14.0...1.14.1)\n\n> 19 February 2020\n\n- Enhancement/forms accessibility [`#2519`](https://github.com/VulcanJS/Vulcan/pull/2519)\n- Add spanish translations [`#2518`](https://github.com/VulcanJS/Vulcan/pull/2518)\n- Use multiQueryUpdater in update2 to properly sort/filter [`#2497`](https://github.com/VulcanJS/Vulcan/pull/2497)\n- change tooltip aria-label [`#2515`](https://github.com/VulcanJS/Vulcan/pull/2515)\n- fix call to sortBy in Datatable.jsx [`#2514`](https://github.com/VulcanJS/Vulcan/pull/2514)\n- Bump handlebars from 4.0.11 to 4.3.0 [`#2491`](https://github.com/VulcanJS/Vulcan/pull/2491)\n- Enhancement/mui radio [`#2503`](https://github.com/VulcanJS/Vulcan/pull/2503)\n- fix material ui seletmultiple [`#2502`](https://github.com/VulcanJS/Vulcan/pull/2502)\n- datatable fix for material ui [`#2493`](https://github.com/VulcanJS/Vulcan/pull/2493)\n- Allow itemProperties to be passed on to FormComponent [`#2505`](https://github.com/VulcanJS/Vulcan/pull/2505)\n- Prevent calling forEachDocumentField on fields without a schema [`#2504`](https://github.com/VulcanJS/Vulcan/pull/2504)\n- Semantized classname [`#2499`](https://github.com/VulcanJS/Vulcan/pull/2499)\n- fix field permission validation bug [`#2498`](https://github.com/VulcanJS/Vulcan/pull/2498)\n- fix bugs in extendCollection (#16) [`#2496`](https://github.com/VulcanJS/Vulcan/pull/2496)\n- fix bugs in extendCollection [`#16`](https://github.com/VulcanJS/Vulcan/pull/16)\n- Remove ! when parsing typename [`#2468`](https://github.com/VulcanJS/Vulcan/pull/2468)\n- Implement _is_null in mongoParams [`#2469`](https://github.com/VulcanJS/Vulcan/pull/2469)\n- Jt/extend collection fix [`#2490`](https://github.com/VulcanJS/Vulcan/pull/2490)\n- add connection remoteAdress in context [`#2488`](https://github.com/VulcanJS/Vulcan/pull/2488)\n- Remove hardcoded head tag in template-web.browser [`#2485`](https://github.com/VulcanJS/Vulcan/pull/2485)\n- Fix datatable-loading crash in the storybook [`#2482`](https://github.com/VulcanJS/Vulcan/pull/2482)\n- Add GraphQLSchema.context to context in runGraphQL [`#2474`](https://github.com/VulcanJS/Vulcan/pull/2474)\n- Pass full doc and document to validate to validateDocumentPermissions [`#2478`](https://github.com/VulcanJS/Vulcan/pull/2478)\n- Add story to demonstrate ref forwarding in Vulcan Components [`#2473`](https://github.com/VulcanJS/Vulcan/pull/2473)\n- Add a password component [`#2472`](https://github.com/VulcanJS/Vulcan/pull/2472)\n- Add buildResult functionality to useCreate2 to directly access created document [`#2470`](https://github.com/VulcanJS/Vulcan/pull/2470)\n- use apollo client in multiqueryupdater to broadcast to watched queries [`#2471`](https://github.com/VulcanJS/Vulcan/pull/2471)\n- Rewrite client side date converter to handle nesting [`#2465`](https://github.com/VulcanJS/Vulcan/pull/2465)\n- Field level permissions validation take nested objects into account (read, create, update) [`#2451`](https://github.com/VulcanJS/Vulcan/pull/2451)\n- Feature/backoffice 1.14 [`#2467`](https://github.com/VulcanJS/Vulcan/pull/2467)\n- Add Vulcan.generateGraphQLQueries to debug.js [`#2463`](https://github.com/VulcanJS/Vulcan/pull/2463)\n- Omit username from schema validation after deleting it from options [`#2466`](https://github.com/VulcanJS/Vulcan/pull/2466)\n- fix default_mutations check if user owns and belongs to authorised group [`#2460`](https://github.com/VulcanJS/Vulcan/pull/2460)\n- Pass document to Users.isMemberOf in permissions.js in [`#2461`](https://github.com/VulcanJS/Vulcan/pull/2461)\n- add back package-lock to avoid issues [`3dbd9d9`](https://github.com/VulcanJS/Vulcan/commit/3dbd9d9115af43fad79cf5de3e9742116bfd80b0)\n- locked react version [`05fcc19`](https://github.com/VulcanJS/Vulcan/commit/05fcc19619362b80ba4d609d72a5ec373197cc97)\n- remove explicit install of airbnb-js-shims after fix [`862028e`](https://github.com/VulcanJS/Vulcan/commit/862028e2dc0ca3f4d388a4eb7015000f1afe917c)\n\n#### [1.14.0](https://github.com/VulcanJS/Vulcan/compare/1.13.5...1.14.0)\n\n> 3 December 2019\n\n- Add support for `itemProperties` in schemas [`#2456`](https://github.com/VulcanJS/Vulcan/pull/2456)\n- check options.input.limit and loadMoreInc accepts input [`#2459`](https://github.com/VulcanJS/Vulcan/pull/2459)\n- remove async to fix issue in mongoParams.js filterFunction [`#2458`](https://github.com/VulcanJS/Vulcan/pull/2458)\n- Pass document to Users.isMemberOf in permissions.js [`#2454`](https://github.com/VulcanJS/Vulcan/pull/2454)\n- Set non-null to false for data input type on update/upsert [`#2453`](https://github.com/VulcanJS/Vulcan/pull/2453)\n- Feature/add redux startup [`#2442`](https://github.com/VulcanJS/Vulcan/pull/2442)\n- add preventDefault to ui-material ModalTrigger [`#2450`](https://github.com/VulcanJS/Vulcan/pull/2450)\n- add @material-ui packages [`#2447`](https://github.com/VulcanJS/Vulcan/pull/2447)\n- testing getVariablesListFromCache, verifiying right variables are returned [`#2439`](https://github.com/VulcanJS/Vulcan/pull/2439)\n- Check if results is non-emtpy, not just present [`#2437`](https://github.com/VulcanJS/Vulcan/pull/2437)\n- split getVariablesListFromCache test in two [`#2435`](https://github.com/VulcanJS/Vulcan/pull/2435)\n- prevent cacheUpdate from returning variables of a query similar name  [`#2434`](https://github.com/VulcanJS/Vulcan/pull/2434)\n- Bugfix/form component inner prop [`#2433`](https://github.com/VulcanJS/Vulcan/pull/2433)\n- Update of outdated links in README.md [`#2411`](https://github.com/VulcanJS/Vulcan/pull/2411)\n- Fixed links to changelog and migration documentation to [`#2413`](https://github.com/VulcanJS/Vulcan/pull/2413)\n- Incorrect link to telescopeapp.org [`#2412`](https://github.com/VulcanJS/Vulcan/pull/2412)\n- Condition '!operation' is always false at this point because it is redundant.  [`#2426`](https://github.com/VulcanJS/Vulcan/pull/2426)\n- Mailchimp [`#2425`](https://github.com/VulcanJS/Vulcan/pull/2425)\n- Condition 'value === ''' is always false at this point [`#2427`](https://github.com/VulcanJS/Vulcan/pull/2427)\n- Bugfix/close modal [`#2429`](https://github.com/VulcanJS/Vulcan/pull/2429)\n- Add pluralize [`518ec70`](https://github.com/VulcanJS/Vulcan/commit/518ec70d88e24eb81a3a15c1b8a15e650d1b666c)\n- Better handling of getting collection name/type name [`827daa4`](https://github.com/VulcanJS/Vulcan/commit/827daa4f8ea3f50ea817d712da06218a7ccc67fb)\n- Make filtering async [`9b8bf5d`](https://github.com/VulcanJS/Vulcan/commit/9b8bf5d26d06d0c7f4da4e4c28550cb7e58e873c)\n\n#### [1.13.5](https://github.com/VulcanJS/Vulcan/compare/1.13.3...1.13.5)\n\n> 25 October 2019\n\n- TooltipIconButton arialabel titletext bugfix [`#2414`](https://github.com/VulcanJS/Vulcan/pull/2414)\n- Bugfix/get viewable fields [`#2418`](https://github.com/VulcanJS/Vulcan/pull/2418)\n- Enhance/form stories [`#2410`](https://github.com/VulcanJS/Vulcan/pull/2410)\n- Can deactivate SSR [`#2397`](https://github.com/VulcanJS/Vulcan/pull/2397)\n- add story of card [`#2408`](https://github.com/VulcanJS/Vulcan/pull/2408)\n- split Card.jsx in differents files [`#2402`](https://github.com/VulcanJS/Vulcan/pull/2402)\n- populate.before callback [`#2405`](https://github.com/VulcanJS/Vulcan/pull/2405)\n- Bugfix/datatable props [`#2401`](https://github.com/VulcanJS/Vulcan/pull/2401)\n- Feature/stories marieqg & juliensl [`#2400`](https://github.com/VulcanJS/Vulcan/pull/2400)\n- fix multiQuery update error with non null selector [`#2396`](https://github.com/VulcanJS/Vulcan/pull/2396)\n- FormGroupHiddenProp [`#2386`](https://github.com/VulcanJS/Vulcan/pull/2386)\n- MaterialUiMinorChanges6 [`#2385`](https://github.com/VulcanJS/Vulcan/pull/2385)\n- single2, create2, update2, delete2, upsert2 with new input param [`11e6d50`](https://github.com/VulcanJS/Vulcan/commit/11e6d50367025765d70e4aa3a24db5964c9e0939)\n- hocs mutations use input directly [`1988bea`](https://github.com/VulcanJS/Vulcan/commit/1988bea602b2a9f263a751017272222295f2a291)\n- bump 1.13.5 [`d844caa`](https://github.com/VulcanJS/Vulcan/commit/d844caa557f333508c19675a8462cdf80dae51d2)\n\n#### [1.13.3](https://github.com/VulcanJS/Vulcan/compare/1.13.2...1.13.3)\n\n> 7 October 2019\n\n- Bugfix/watched mutations [`#2382`](https://github.com/VulcanJS/Vulcan/pull/2382)\n- Material-UI - README Installation - Set Version of react-jss [`#2384`](https://github.com/VulcanJS/Vulcan/pull/2384)\n- 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)\n- Hooks for mutations and other utilities [`#2377`](https://github.com/VulcanJS/Vulcan/pull/2377)\n- MaterialUiStartAdornment [`#2376`](https://github.com/VulcanJS/Vulcan/pull/2376)\n- MaterialUiMinorChanges5 [`#2372`](https://github.com/VulcanJS/Vulcan/pull/2372)\n- ErrorsGetUserPayload [`#2373`](https://github.com/VulcanJS/Vulcan/pull/2373)\n- Reenable data injection during SSR [`#2369`](https://github.com/VulcanJS/Vulcan/pull/2369)\n- Bugfix/intl polyfill devel [`#2371`](https://github.com/VulcanJS/Vulcan/pull/2371)\n- Feature/backoffice: merged devel [`#2366`](https://github.com/VulcanJS/Vulcan/pull/2366)\n- Bugfix/mui ssr [`#2352`](https://github.com/VulcanJS/Vulcan/pull/2352)\n- Feature/apollo register link [`#2353`](https://github.com/VulcanJS/Vulcan/pull/2353)\n- Re-enable RouterHook component so that pageview events are sent to analytics [`#2364`](https://github.com/VulcanJS/Vulcan/pull/2364)\n- MaterialUiMinorChanges6 [`b499708`](https://github.com/VulcanJS/Vulcan/commit/b499708cb3963344aa12faf70ac1ff337034ef42)\n- add alerts to material ui datatable [`dc225e6`](https://github.com/VulcanJS/Vulcan/commit/dc225e6b59eb2d9b1980d82ae6d9c77e3e3194de)\n- Add hasMany; add relation guessing based on schema field type [`c9518bc`](https://github.com/VulcanJS/Vulcan/commit/c9518bcbde7376f6efc0feba07c2459e81ef0b5c)\n\n#### [1.13.2](https://github.com/VulcanJS/Vulcan/compare/1.13.1...1.13.2)\n\n> 27 August 2019\n\n- add to utils a getSchemaFieldAllowedValues function [`#2336`](https://github.com/VulcanJS/Vulcan/pull/2336)\n- Update ThemeStyles.jsx [`#2337`](https://github.com/VulcanJS/Vulcan/pull/2337)\n- Bugfix/button props [`#2357`](https://github.com/VulcanJS/Vulcan/pull/2357)\n- add from props [`#2358`](https://github.com/VulcanJS/Vulcan/pull/2358)\n- Fix bug in Utils.getUnusedSlug() [`#2359`](https://github.com/VulcanJS/Vulcan/pull/2359)\n- EJSONStackOverflow [`#2348`](https://github.com/VulcanJS/Vulcan/pull/2348)\n- lock storybook version to prevent compatibility issues [`d964c2c`](https://github.com/VulcanJS/Vulcan/commit/d964c2ce1f1b17f8b42fbc4dc3b1c38431925bdc)\n- remove autogenerated doc folder [`3e9335a`](https://github.com/VulcanJS/Vulcan/commit/3e9335aed9b91dc810894442fb46efcbb6adcd72)\n- cleanup [`53b8b8f`](https://github.com/VulcanJS/Vulcan/commit/53b8b8f688bc50448d7c8951ca1060222511f8f3)\n\n#### [1.13.1](https://github.com/VulcanJS/Vulcan/compare/1.13.0...1.13.1)\n\n> 24 July 2019\n\n- MuiSuggestShowAllOptions [`#2346`](https://github.com/VulcanJS/Vulcan/pull/2346)\n- VersionNumberCorrections [`#2345`](https://github.com/VulcanJS/Vulcan/pull/2345)\n- UiMaterialMinorFixes3 [`#2341`](https://github.com/VulcanJS/Vulcan/pull/2341)\n- SimpleSchemaIntegerFieldType [`#2340`](https://github.com/VulcanJS/Vulcan/pull/2340)\n- ApolloEngineSettings [`#2339`](https://github.com/VulcanJS/Vulcan/pull/2339)\n- Migrate accounts components to React Router 4 [`#2327`](https://github.com/VulcanJS/Vulcan/pull/2327)\n- Bugfix/changepwd ssr [`#2344`](https://github.com/VulcanJS/Vulcan/pull/2344)\n- Add default group only if necessary [`#2331`](https://github.com/VulcanJS/Vulcan/pull/2331)\n- Bugfix/material ui floating [`#2330`](https://github.com/VulcanJS/Vulcan/pull/2330)\n- MinorBugInMuiSampleTheme [`#2328`](https://github.com/VulcanJS/Vulcan/pull/2328)\n- FormGroupAdminsOnlyOption [`#2326`](https://github.com/VulcanJS/Vulcan/pull/2326)\n- VulcanAdminMinorTweaks [`#2325`](https://github.com/VulcanJS/Vulcan/pull/2325)\n- MaterialUiMinorBugs [`#2315`](https://github.com/VulcanJS/Vulcan/pull/2315)\n- FormNestedArrayLayout [`#2314`](https://github.com/VulcanJS/Vulcan/pull/2314)\n- Storybook+Avatar+Button [`#2317`](https://github.com/VulcanJS/Vulcan/pull/2317)\n- user.create.async callback [`#2311`](https://github.com/VulcanJS/Vulcan/pull/2311)\n- ResetStoreCallbackNotBeingRun [`#2313`](https://github.com/VulcanJS/Vulcan/pull/2313)\n- MuiCheckboxGroup maxCount [`#2312`](https://github.com/VulcanJS/Vulcan/pull/2312)\n- vulcan-ui-material bug fixes [`#2310`](https://github.com/VulcanJS/Vulcan/pull/2310)\n- instantiateComponent with React element [`#2309`](https://github.com/VulcanJS/Vulcan/pull/2309)\n- Email template vars [`#2294`](https://github.com/VulcanJS/Vulcan/pull/2294)\n- Move getLabel from vulcan:intl to vulcan:lib [`#2307`](https://github.com/VulcanJS/Vulcan/pull/2307)\n- MaterialUiFormGroupLayouts [`#2297`](https://github.com/VulcanJS/Vulcan/pull/2297)\n- vulcan-lib: New function: componentExists(name) [`#2292`](https://github.com/VulcanJS/Vulcan/pull/2292)\n- FormGroupOptionalParameters [`#2296`](https://github.com/VulcanJS/Vulcan/pull/2296)\n- material-ui : i18n files were not imported [`#2298`](https://github.com/VulcanJS/Vulcan/pull/2298)\n- inputProperties function parameters bug [`#2295`](https://github.com/VulcanJS/Vulcan/pull/2295)\n- FormComponentInner calling an inexisting element (FormNested)  [`#2306`](https://github.com/VulcanJS/Vulcan/pull/2306)\n- FormNestedArray Footer modification via props fixing [`#2305`](https://github.com/VulcanJS/Vulcan/pull/2305)\n- improve props cleanup [`#2304`](https://github.com/VulcanJS/Vulcan/pull/2304)\n- Deprecated redux devtools function replaced [`#2302`](https://github.com/VulcanJS/Vulcan/pull/2302)\n- merging [`#1`](https://github.com/VulcanJS/Vulcan/pull/1)\n- Update FormNestedObject.jsx [`#2289`](https://github.com/VulcanJS/Vulcan/pull/2289)\n-  Fixed MuiCheckBoxGroup value computation [`#2300`](https://github.com/VulcanJS/Vulcan/pull/2300)\n- Update FormNestedArrayLayout.jsx [`#2288`](https://github.com/VulcanJS/Vulcan/pull/2288)\n- Feature/better form array [`#2291`](https://github.com/VulcanJS/Vulcan/pull/2291)\n- [WIP] fix MUI inputProperties handling [`#2286`](https://github.com/VulcanJS/Vulcan/pull/2286)\n- Add storybook, few fixes on FormError + Intl messages [`#2284`](https://github.com/VulcanJS/Vulcan/pull/2284)\n- add consideration to default values on accounts extra fields [`#2282`](https://github.com/VulcanJS/Vulcan/pull/2282)\n- Feature/backoffice [`#2276`](https://github.com/VulcanJS/Vulcan/pull/2276)\n- ui-material: updated Datatable.jsx - header fix [`#2275`](https://github.com/VulcanJS/Vulcan/pull/2275)\n- Fix a falsy-vs-undefined bug in getSetting [`#2274`](https://github.com/VulcanJS/Vulcan/pull/2274)\n- make Datatable edit modal title customizable [`#2269`](https://github.com/VulcanJS/Vulcan/pull/2269)\n- FormInnerComponent unify Bootstrap and Material UI API [`#2272`](https://github.com/VulcanJS/Vulcan/pull/2272)\n- Additional tests [`#2262`](https://github.com/VulcanJS/Vulcan/pull/2262)\n- Fix a crash when using addFields together with a recursive schema [`#2268`](https://github.com/VulcanJS/Vulcan/pull/2268)\n- Fix typo that prevented FormNestedObjectLayout from having error class [`#2265`](https://github.com/VulcanJS/Vulcan/pull/2265)\n- Fix punctuation of password-change message [`#2264`](https://github.com/VulcanJS/Vulcan/pull/2264)\n- Update synced-cron across a maintainer change [`#2263`](https://github.com/VulcanJS/Vulcan/pull/2263)\n- Persian locale added [`#2261`](https://github.com/VulcanJS/Vulcan/pull/2261)\n- Update mutators.js [`#2259`](https://github.com/VulcanJS/Vulcan/pull/2259)\n- Unit Testing Update [`2357d82`](https://github.com/VulcanJS/Vulcan/commit/2357d826181271182cad4ee2bfa836946bc07e83)\n- add material ui lib in dev mode [`de8e39c`](https://github.com/VulcanJS/Vulcan/commit/de8e39cd9834c450c5ecae6713b330fefaeda95f)\n- StorybookAndMaterialUi [`f4c43a8`](https://github.com/VulcanJS/Vulcan/commit/f4c43a81133b07409b0cb334685fdba463ae8f4c)\n\n#### [1.13.0](https://github.com/VulcanJS/Vulcan/compare/1.12.17...1.13.0)\n\n> 28 March 2019\n\n- Fix denormalization when bio set to empty [`#2253`](https://github.com/VulcanJS/Vulcan/pull/2253)\n- Re-enable CheckboxGroup component for Material UI  [`#2249`](https://github.com/VulcanJS/Vulcan/pull/2249)\n- Re-enable Switch component for Material UI [`#2248`](https://github.com/VulcanJS/Vulcan/pull/2248)\n- Re-enable Select component for Material UI [`#2250`](https://github.com/VulcanJS/Vulcan/pull/2250)\n- Fix labeling in select component. [`#2243`](https://github.com/VulcanJS/Vulcan/pull/2243)\n- Add debug messages for missing strings and default locale on per-message level. [`#2238`](https://github.com/VulcanJS/Vulcan/pull/2238)\n- Add locale on signup. [`#2240`](https://github.com/VulcanJS/Vulcan/pull/2240)\n- fix values property for handlebars helper, use context as default [`#2236`](https://github.com/VulcanJS/Vulcan/pull/2236)\n- Add wrapperless Emails. [`#2235`](https://github.com/VulcanJS/Vulcan/pull/2235)\n- Add default locale to getString. [`#2234`](https://github.com/VulcanJS/Vulcan/pull/2234)\n- Upgrade react-router [`#2232`](https://github.com/VulcanJS/Vulcan/pull/2232)\n- Fix const assignment. [`#2222`](https://github.com/VulcanJS/Vulcan/pull/2222)\n- Add classname for required fields. [`#2215`](https://github.com/VulcanJS/Vulcan/pull/2215)\n- fix a spelling error [`#2209`](https://github.com/VulcanJS/Vulcan/pull/2209)\n- material-ui [`a743022`](https://github.com/VulcanJS/Vulcan/commit/a7430225458a18a3061496de760b93ebcd2a2c4b)\n- Component fixes [`4261470`](https://github.com/VulcanJS/Vulcan/commit/4261470fada2b0da620f689c65b169d4aa5beff1)\n- create vulcan:ui-material, update to Apollo2 RR4 [`b7b8725`](https://github.com/VulcanJS/Vulcan/commit/b7b872591a9ca35fe8b7d0dd36ef773f9e8d0af3)\n\n#### [1.12.17](https://github.com/VulcanJS/Vulcan/compare/1.12.16...1.12.17)\n\n> 16 February 2019\n\n- Form test [`#2186`](https://github.com/VulcanJS/Vulcan/pull/2186)\n- Fix type casting in FormComponent's handleChange (fix #2197) [`#2197`](https://github.com/VulcanJS/Vulcan/issues/2197)\n- Show warning when core components are missing (fix #2196) [`#2196`](https://github.com/VulcanJS/Vulcan/issues/2196)\n- prettier commit [`083a7d6`](https://github.com/VulcanJS/Vulcan/commit/083a7d676bc6f271223f4b293e1eb85731c4bea4)\n- prettier & lint code [`815a65d`](https://github.com/VulcanJS/Vulcan/commit/815a65d853ae9ef0e4c9f8848723db4163ca44d1)\n- prettier commit [`0f29cc7`](https://github.com/VulcanJS/Vulcan/commit/0f29cc7cdd7921dac3fb55fd93d56f81039558e1)\n\n#### [1.12.16](https://github.com/VulcanJS/Vulcan/compare/1.12.15...1.12.16)\n\n> 4 February 2019\n\n#### [1.12.15](https://github.com/VulcanJS/Vulcan/compare/1.12.14...1.12.15)\n\n> 4 February 2019\n\n- Default view sort doesn't take precedence over selected view [`#2192`](https://github.com/VulcanJS/Vulcan/pull/2192)\n- Fix users onCreate [`921e7cd`](https://github.com/VulcanJS/Vulcan/commit/921e7cd851149096c1dadc5a88c358912ca51515)\n- Remove Formsy dependency [`f56293b`](https://github.com/VulcanJS/Vulcan/commit/f56293b0f6360f41412ffb497c4a9a84f6709db4)\n\n#### [1.12.14](https://github.com/VulcanJS/Vulcan/compare/1.12.13...1.12.14)\n\n> 31 January 2019\n\n- Add script to update package.json dependencies [`#2174`](https://github.com/VulcanJS/Vulcan/pull/2174)\n- Go back to using FormNestedFoot in Nested Array Fields [`#2170`](https://github.com/VulcanJS/Vulcan/pull/2170)\n- Debug & Admin layouts [`#2177`](https://github.com/VulcanJS/Vulcan/pull/2177)\n- Apollo2 finalization (backward compatibility) [`#2157`](https://github.com/VulcanJS/Vulcan/pull/2157)\n- Use qs package [`40be66b`](https://github.com/VulcanJS/Vulcan/commit/40be66b8cf82d43e92d76493af3440027cd4617f)\n- linting, removing apollo package [`eeada2d`](https://github.com/VulcanJS/Vulcan/commit/eeada2d231160d14f057cad32f919eeb2215eb35)\n- Fixed versions issues (react-bootstrap + apollo) [`02cfafa`](https://github.com/VulcanJS/Vulcan/commit/02cfafabc2ce4c93dd2ab9beddd5def952482dd1)\n\n#### [1.12.13](https://github.com/VulcanJS/Vulcan/compare/1.12.12...1.12.13)\n\n> 2 January 2019\n\n- Fix semicolons and other linting issues [`111e00e`](https://github.com/VulcanJS/Vulcan/commit/111e00ecae68a08a4f5c3a0321982f49c4be8c99)\n- Fix ESLint unused variables [`3e1571e`](https://github.com/VulcanJS/Vulcan/commit/3e1571e1e8fdd1cf4b843713341b14c01e9c2545)\n- Comment out sendy integration for now [`7ddcc28`](https://github.com/VulcanJS/Vulcan/commit/7ddcc28b97beb2f445b79fb6e12daef083eb9745)\n\n#### [1.12.12](https://github.com/VulcanJS/Vulcan/compare/1.12.11...1.12.12)\n\n> 31 December 2018\n\n- Add ES translations for error messages [`#2165`](https://github.com/VulcanJS/Vulcan/pull/2165)\n- forgot to add variable [`#2163`](https://github.com/VulcanJS/Vulcan/pull/2163)\n- fixed nested array field error [`#2162`](https://github.com/VulcanJS/Vulcan/pull/2162)\n- fixed helmet for testing [`#2156`](https://github.com/VulcanJS/Vulcan/pull/2156)\n- fixed whitespace to pass linting test [`#2154`](https://github.com/VulcanJS/Vulcan/pull/2154)\n- Feature/smart form reset [`#2131`](https://github.com/VulcanJS/Vulcan/pull/2131)\n- Add async hook to RouterHook and provide props as argument [`#2065`](https://github.com/VulcanJS/Vulcan/pull/2065)\n- Cleanup Datatable / withComponents pattern [`#2126`](https://github.com/VulcanJS/Vulcan/pull/2126)\n- Add Prettier and Husky [`#2130`](https://github.com/VulcanJS/Vulcan/pull/2130)\n- Support form id attribute in SmartForm [`#2152`](https://github.com/VulcanJS/Vulcan/pull/2152)\n- clean up NPM packages [`4f49350`](https://github.com/VulcanJS/Vulcan/commit/4f49350d709bdc2be63c3f6a6b6765fd2ce02756)\n- Fix NPM vulnerabilities [`8d00e60`](https://github.com/VulcanJS/Vulcan/commit/8d00e6091d5bf89d234a6be7f845bde70f0f8ccf)\n- add redux [`19df560`](https://github.com/VulcanJS/Vulcan/commit/19df560545c4bc607e6c99907a6bf0f732c2dc68)\n\n#### [1.12.11](https://github.com/VulcanJS/Vulcan/compare/1.12.10...1.12.11)\n\n> 15 December 2018\n\n- add minCount and maxCount to SmartForm nested arrays [`#2141`](https://github.com/VulcanJS/Vulcan/pull/2141)\n- Allow user to customize apollo json parser options [`#2147`](https://github.com/VulcanJS/Vulcan/pull/2147)\n- added collection creation hook [`#2148`](https://github.com/VulcanJS/Vulcan/pull/2148)\n- add refetch to props on withSingle [`#2137`](https://github.com/VulcanJS/Vulcan/pull/2137)\n- add english field errors [`#2136`](https://github.com/VulcanJS/Vulcan/pull/2136)\n- Apollo 2 SSR [`#2128`](https://github.com/VulcanJS/Vulcan/pull/2128)\n- Add Prettier and Husky [`b0f4ecd`](https://github.com/VulcanJS/Vulcan/commit/b0f4ecdae71a0f49c1270414a355f6c7dffd77a6)\n- add bootstrap-ui to allow form mounting [`fde4d90`](https://github.com/VulcanJS/Vulcan/commit/fde4d90924ab3fae3e1673f6ab8cf04fbe4958c4)\n- split Datatable into purely visual components [`7162870`](https://github.com/VulcanJS/Vulcan/commit/71628705646f3539a9489bad52d69f91fd8d5965)\n\n#### [1.12.10](https://github.com/VulcanJS/Vulcan/compare/1.12.8...1.12.10)\n\n> 24 November 2018\n\n- Respect user.isAdmin on creation [`#2122`](https://github.com/VulcanJS/Vulcan/pull/2122)\n- rollback callbacks test [`#2123`](https://github.com/VulcanJS/Vulcan/pull/2123)\n- Mutation button small fixes [`#2116`](https://github.com/VulcanJS/Vulcan/pull/2116)\n- Fix datatable bug when sorting a column [`#2113`](https://github.com/VulcanJS/Vulcan/pull/2113)\n- single resolve documentId undefined -&gt; now returns null [`#2112`](https://github.com/VulcanJS/Vulcan/pull/2112)\n- Cleanup forms, add `changeCallback` [`#2108`](https://github.com/VulcanJS/Vulcan/pull/2108)\n- set default email and siteName for Accounts related emails [`#2110`](https://github.com/VulcanJS/Vulcan/pull/2110)\n- Warn when no searchable field is set and terms.query is set [`#2106`](https://github.com/VulcanJS/Vulcan/pull/2106)\n- Pass context to the defaultView too [`#2109`](https://github.com/VulcanJS/Vulcan/pull/2109)\n- Support arrays with primitives [`#2057`](https://github.com/VulcanJS/Vulcan/pull/2057)\n- vulcan-form-tags: refactor and fix [`#2099`](https://github.com/VulcanJS/Vulcan/pull/2099)\n- Update fr_FR.js: add datatable.search entry [`#2104`](https://github.com/VulcanJS/Vulcan/pull/2104)\n- datatable: add i18n for the search field [`#2103`](https://github.com/VulcanJS/Vulcan/pull/2103)\n- Revert changes made to getUnusedSlug in #2075 [`#2102`](https://github.com/VulcanJS/Vulcan/pull/2102)\n- Custom form components [`#2080`](https://github.com/VulcanJS/Vulcan/pull/2080)\n- [WIP] Apollo2 server [`#2094`](https://github.com/VulcanJS/Vulcan/pull/2094)\n- Fix OpenCollective part of readme [`#2097`](https://github.com/VulcanJS/Vulcan/pull/2097)\n- only clear current values for new document's form [`#2060`](https://github.com/VulcanJS/Vulcan/pull/2060)\n- Restore Edge support [`#2093`](https://github.com/VulcanJS/Vulcan/pull/2093)\n- SmartForm: use prop `schema`, if given [`#2092`](https://github.com/VulcanJS/Vulcan/pull/2092)\n- Add /i18n debug page [`#2091`](https://github.com/VulcanJS/Vulcan/pull/2091)\n- Users' slug is updated on displayname change [`#2075`](https://github.com/VulcanJS/Vulcan/pull/2075)\n- SubmitButtonLabels [`#2082`](https://github.com/VulcanJS/Vulcan/pull/2082)\n- Don't merge schema in Vulcan, only do it with SimpleSchema [`#2086`](https://github.com/VulcanJS/Vulcan/pull/2086)\n- Email subjects shouldn't be coupled with sitename [`#2088`](https://github.com/VulcanJS/Vulcan/pull/2088)\n- Apollo2 withMessages [`#2089`](https://github.com/VulcanJS/Vulcan/pull/2089)\n- [WIP] Apollo 2: apollo-state-link and RR4 [`#2083`](https://github.com/VulcanJS/Vulcan/pull/2083)\n- Support email headers property and simplify logic  [`#2087`](https://github.com/VulcanJS/Vulcan/pull/2087)\n- SmartForm.getLabel() intl string fallback [`#2077`](https://github.com/VulcanJS/Vulcan/pull/2077)\n- Remove bootstrap from vulcan-forms [`#2076`](https://github.com/VulcanJS/Vulcan/pull/2076)\n- Field resolvers should check for access (fix #2124) [`#2124`](https://github.com/VulcanJS/Vulcan/issues/2124)\n- Revert some of the changes in #2112 (fix #2118) [`#2118`](https://github.com/VulcanJS/Vulcan/issues/2118)\n- Pass query results to email's data() function as second argument (fix #2048) [`#2048`](https://github.com/VulcanJS/Vulcan/issues/2048)\n- copy-pasted meteor/apollo, updated to RR4 [`2bacae7`](https://github.com/VulcanJS/Vulcan/commit/2bacae7a0e012caa99ecfab80d40206a09fe568b)\n- Comment out/disable legacy code for now [`6275108`](https://github.com/VulcanJS/Vulcan/commit/6275108d41dab331991a339d65a0d57f6394d1df)\n- basic example with apollo-server, not yet working [`8d3120a`](https://github.com/VulcanJS/Vulcan/commit/8d3120abc77b7cb65c4e9b1cd23280b87e302663)\n\n#### [1.12.8](https://github.com/VulcanJS/Vulcan/compare/1.12.0...1.12.8)\n\n> 17 September 2018\n\n- fix bug preflight from apollo [`#2070`](https://github.com/VulcanJS/Vulcan/pull/2070)\n- Pass locale as key to IntlProvider to force a rerender on locale change [`#2072`](https://github.com/VulcanJS/Vulcan/pull/2072)\n- cleanup: Fix naming after withList -&gt; withMulti change [`#2071`](https://github.com/VulcanJS/Vulcan/pull/2071)\n- Added support for cc, bcc, and replyTo fields for email API [`#2062`](https://github.com/VulcanJS/Vulcan/pull/2062)\n- [BugFix] remove `document` from `canCreateField` [`#2069`](https://github.com/VulcanJS/Vulcan/pull/2069)\n- Minor bug fixes [`#2068`](https://github.com/VulcanJS/Vulcan/pull/2068)\n- Allow \"guests\" in withAccess [`#2063`](https://github.com/VulcanJS/Vulcan/pull/2063)\n- Minor changes and bug fixes in withMulti, single resolver, schema generation [`#2059`](https://github.com/VulcanJS/Vulcan/pull/2059)\n- vulcan-users - permissions - filter out array based fields [`#2056`](https://github.com/VulcanJS/Vulcan/pull/2056)\n- Cleaned up the options management in hocs [`#2053`](https://github.com/VulcanJS/Vulcan/pull/2053)\n- Allow to edit username, and override 3rd party name if it is set [`#2052`](https://github.com/VulcanJS/Vulcan/pull/2052)\n- Allow user to return nothing in submitCallback [`#2051`](https://github.com/VulcanJS/Vulcan/pull/2051)\n- Pass collection as property when running new API mutators' callbacks [`#2050`](https://github.com/VulcanJS/Vulcan/pull/2050)\n- fixed issue that hardcoded email test queries to work only with single queries [`#2047`](https://github.com/VulcanJS/Vulcan/pull/2047)\n- Add only document once on withMulti query reducer [`#2049`](https://github.com/VulcanJS/Vulcan/pull/2049)\n- Pass request headers through context  [`#2046`](https://github.com/VulcanJS/Vulcan/pull/2046)\n- When changing email address on an account, mark the new address as unverified [`#2043`](https://github.com/VulcanJS/Vulcan/pull/2043)\n- Refactor registerComponent to fix #2061 (see also #2031) [`#2061`](https://github.com/VulcanJS/Vulcan/issues/2061)\n- Fields with \"$\" should never be included in generated fragments (fix #2044) [`#2044`](https://github.com/VulcanJS/Vulcan/issues/2044)\n- Fix ESLint [`5fc0e30`](https://github.com/VulcanJS/Vulcan/commit/5fc0e30f40183ae084e3344ec18c49ba1a1e866b)\n- cleaned up the options management in hocs [`17f9671`](https://github.com/VulcanJS/Vulcan/commit/17f96712ff44ce9c86ebda0e14407a1f2f0d90f4)\n- ESLint fixes [`dfa4c77`](https://github.com/VulcanJS/Vulcan/commit/dfa4c77314b9eaf7cb048d7a47512e30aad5ed2f)\n\n#### [1.12.0](https://github.com/VulcanJS/Vulcan/compare/v1.11.1...1.12.0)\n\n> 29 August 2018\n\n- Add support for email address verification. [`#2040`](https://github.com/VulcanJS/Vulcan/pull/2040)\n- defer creation of apolloClient until it is first used [`#2041`](https://github.com/VulcanJS/Vulcan/pull/2041)\n- Users totalCount and removed one console.log [`#2035`](https://github.com/VulcanJS/Vulcan/pull/2035)\n- OpenCRUD fixes [`#2034`](https://github.com/VulcanJS/Vulcan/pull/2034)\n- [opencrud] update Users mutations to match {selector, data} args [`#2033`](https://github.com/VulcanJS/Vulcan/pull/2033)\n- [Forms] Add currentDocument to clearForm [`#2030`](https://github.com/VulcanJS/Vulcan/pull/2030)\n- Warn when replacing a non-registered component and register it anyway [`#2029`](https://github.com/VulcanJS/Vulcan/pull/2029)\n- Minor default resolvers and mutations improvements [`#2028`](https://github.com/VulcanJS/Vulcan/pull/2028)\n- Dynamic fragment initalization [`#2025`](https://github.com/VulcanJS/Vulcan/pull/2025)\n- Pass opencrud field properties to the form [`#2024`](https://github.com/VulcanJS/Vulcan/pull/2024)\n- Bugfix/nested objects b34f0a25ce0c775a29a14241b14e9bc0e47976c8 [`#2022`](https://github.com/VulcanJS/Vulcan/pull/2022)\n- collection.getParameters handles schema extension for searchable fields [`#2021`](https://github.com/VulcanJS/Vulcan/pull/2021)\n- Open Collective Updates [`#2020`](https://github.com/VulcanJS/Vulcan/pull/2020)\n- Dynamic loader improvements [`#2013`](https://github.com/VulcanJS/Vulcan/pull/2013)\n- FormComponent value handling improvements [`#2011`](https://github.com/VulcanJS/Vulcan/pull/2011)\n- New default for Apollo tracing [`#2009`](https://github.com/VulcanJS/Vulcan/pull/2009)\n- Remove hardcoded limit to users List resolver [`#2008`](https://github.com/VulcanJS/Vulcan/pull/2008)\n- Fix async callbacks called with no arguments [`#2007`](https://github.com/VulcanJS/Vulcan/pull/2007)\n- Allow passing multiple args to HOCs [`#2005`](https://github.com/VulcanJS/Vulcan/pull/2005)\n- [vulcan:ui-bootstrap] fix duplicate \"noneOption\" on rerendering the Select component [`#2003`](https://github.com/VulcanJS/Vulcan/pull/2003)\n- Fix #2027 [`#2027`](https://github.com/VulcanJS/Vulcan/issues/2027)\n- Fix #1998 part 2 [`#1998`](https://github.com/VulcanJS/Vulcan/issues/1998)\n- Fix #1998 [`#1998`](https://github.com/VulcanJS/Vulcan/issues/1998)\n- Wrap checkboxgroup (fix #2004) [`#2004`](https://github.com/VulcanJS/Vulcan/issues/2004)\n- Add debug statements and fix #2001 [`#2001`](https://github.com/VulcanJS/Vulcan/issues/2001)\n- setup tests [`4744561`](https://github.com/VulcanJS/Vulcan/commit/474456148eddf72a33afe6faae92f4f38984e13f)\n- setup an example test with Tinytest and added Jest's expect to dependencies [`6deab6b`](https://github.com/VulcanJS/Vulcan/commit/6deab6bb8f660582b2ddf541e004c0c012997425)\n- Splitted FormNested between objects an arrays [`b5e54ea`](https://github.com/VulcanJS/Vulcan/commit/b5e54ead1733e361a2640e6a921579833d44603f)\n\n#### [v1.11.1](https://github.com/VulcanJS/Vulcan/compare/1.9.1...v1.11.1)\n\n> 13 June 2018\n\n- runQuery-&gt;runGraphQL [`#1995`](https://github.com/VulcanJS/Vulcan/pull/1995)\n- Popup warning on page closing for SmartForm unsaved changes [`#1989`](https://github.com/VulcanJS/Vulcan/pull/1989)\n- Await between hooks in runCallbacks [`#1984`](https://github.com/VulcanJS/Vulcan/pull/1984)\n- update FR translation for SmartForm changes [`#1986`](https://github.com/VulcanJS/Vulcan/pull/1986)\n- Restrict value altering to allow multiple datatypes in FormComponent [`#1982`](https://github.com/VulcanJS/Vulcan/pull/1982)\n- check for redirect before trying to redirect [`#1978`](https://github.com/VulcanJS/Vulcan/pull/1978)\n- Add MongoDB aggregation to Collections [`#1961`](https://github.com/VulcanJS/Vulcan/pull/1961)\n- withList Loading prop adjustment [`#1975`](https://github.com/VulcanJS/Vulcan/pull/1975)\n- Include document prop in mutationErrorCallback method [`#1969`](https://github.com/VulcanJS/Vulcan/pull/1969)\n- Update Flash component and withMessages container [`#1973`](https://github.com/VulcanJS/Vulcan/pull/1973)\n- Update FormSubmit.jsx [`#1972`](https://github.com/VulcanJS/Vulcan/pull/1972)\n- ui-bootstrap: Add a separate Modal component and refactor ModalTrigger to use it. [`#1971`](https://github.com/VulcanJS/Vulcan/pull/1971)\n- Fix field errors display [`#1964`](https://github.com/VulcanJS/Vulcan/pull/1964)\n- More graphql error logging [`#1965`](https://github.com/VulcanJS/Vulcan/pull/1965)\n- change from telescope-newsletter to vulcan-newsletter [`#1966`](https://github.com/VulcanJS/Vulcan/pull/1966)\n- Fix Form props [`#1960`](https://github.com/VulcanJS/Vulcan/pull/1960)\n- Give CheckboxGroup its own custom event handler (fix #1998) [`#1998`](https://github.com/VulcanJS/Vulcan/issues/1998)\n- Update packages [`c70f32a`](https://github.com/VulcanJS/Vulcan/commit/c70f32a52a94fe5f6d1dcc7700066a1a1c3ccf21)\n- Changes to SmartForm behaviour [`c3f33cb`](https://github.com/VulcanJS/Vulcan/commit/c3f33cb7e0d623c89526b980904d35beaea0b7ad)\n- Refactored GraphQL schema generation code to use new GraphQL templates [`1d39212`](https://github.com/VulcanJS/Vulcan/commit/1d3921287c2a1cad0ca52546c94ac1bc2491ad52)\n\n#### [1.9.1](https://github.com/VulcanJS/Vulcan/compare/1.8.1...1.9.1)\n\n> 21 April 2018\n\n- Fix nested forms after props renaming [`#1959`](https://github.com/VulcanJS/Vulcan/pull/1959)\n- add error_incorrect_password to FR package [`#1954`](https://github.com/VulcanJS/Vulcan/pull/1954)\n- fix: connection -&gt; connexion [`#1950`](https://github.com/VulcanJS/Vulcan/pull/1950)\n- Vulcan FR language package [`#1943`](https://github.com/VulcanJS/Vulcan/pull/1943)\n- use this.getCollection() instead of props.collection [`#1941`](https://github.com/VulcanJS/Vulcan/pull/1941)\n- Update mingo to 2.2.0 [`#1928`](https://github.com/VulcanJS/Vulcan/pull/1928)\n- Remove sanitizeHtml import on client [`#1925`](https://github.com/VulcanJS/Vulcan/pull/1925)\n- vulcan:debug : Components Dashboard now shows all hocs [`#1923`](https://github.com/VulcanJS/Vulcan/pull/1923)\n- Update es_ES.js [`#1918`](https://github.com/VulcanJS/Vulcan/pull/1918)\n- Fix TrackerComponent and add missing i18n string [`#1913`](https://github.com/VulcanJS/Vulcan/pull/1913)\n- Fix TrackerComponent and include in repo to fix Accounts.LoginFormInner [`#1907`](https://github.com/VulcanJS/Vulcan/pull/1907)\n- Various minor bug fixes [`#1904`](https://github.com/VulcanJS/Vulcan/pull/1904)\n- Fix Charges Insert [`#1902`](https://github.com/VulcanJS/Vulcan/pull/1902)\n- Missing es-ES translation [`#1892`](https://github.com/VulcanJS/Vulcan/pull/1892)\n- Add stripe callbacks [`#1887`](https://github.com/VulcanJS/Vulcan/pull/1887)\n- Fix async register callback for vulcan-payments [`#1886`](https://github.com/VulcanJS/Vulcan/pull/1886)\n- Fix stripe plan startup [`#1885`](https://github.com/VulcanJS/Vulcan/pull/1885)\n- Create Stripe Subscriptions on startup if requested [`#1879`](https://github.com/VulcanJS/Vulcan/pull/1879)\n- Export stripe singleton [`#1880`](https://github.com/VulcanJS/Vulcan/pull/1880)\n- Datatable - replace SPACE with - in the datatable-item-* className [`#1868`](https://github.com/VulcanJS/Vulcan/pull/1868)\n- Remove console log from routes dashboard [`#1877`](https://github.com/VulcanJS/Vulcan/pull/1877)\n- Fix the Edit and Reply cancel methods on Comments component in example-forum package [`#1876`](https://github.com/VulcanJS/Vulcan/pull/1876)\n- #1865 Fix upsert [`#1869`](https://github.com/VulcanJS/Vulcan/pull/1869)\n- Fix example-forum events callback [`#1874`](https://github.com/VulcanJS/Vulcan/pull/1874)\n- Fix embed for posts callback [`#1872`](https://github.com/VulcanJS/Vulcan/pull/1872)\n- Export Cloudinary singleton [`#1873`](https://github.com/VulcanJS/Vulcan/pull/1873)\n- Fix sample_settings.json for Google Analytics [`#1864`](https://github.com/VulcanJS/Vulcan/pull/1864)\n- packages update to meteor 1.6.1 [`#1861`](https://github.com/VulcanJS/Vulcan/pull/1861)\n- Add upsert mutation [`#1858`](https://github.com/VulcanJS/Vulcan/pull/1858)\n- Fix linter errors [`#1857`](https://github.com/VulcanJS/Vulcan/pull/1857)\n- CircleCI Config [`#1848`](https://github.com/VulcanJS/Vulcan/pull/1848)\n- Respect checkAccess for total resolver [`#1853`](https://github.com/VulcanJS/Vulcan/pull/1853)\n- optionsAsStrings was not defined [`#1855`](https://github.com/VulcanJS/Vulcan/pull/1855)\n- Added getCollection to withEdit & withRemove [`#1851`](https://github.com/VulcanJS/Vulcan/pull/1851)\n- Fix seeding not being sequential & fix broken example packages [`#1849`](https://github.com/VulcanJS/Vulcan/pull/1849)\n- Added getCollection to withNew [`#1850`](https://github.com/VulcanJS/Vulcan/pull/1850)\n- Fix React prop warnings for SmartForm [`#1847`](https://github.com/VulcanJS/Vulcan/pull/1847)\n- Consolidate getCollection code [`#1844`](https://github.com/VulcanJS/Vulcan/pull/1844)\n- revert allVotes [`#1843`](https://github.com/VulcanJS/Vulcan/pull/1843)\n- Fix circular dependency between Utils and settings [`#1841`](https://github.com/VulcanJS/Vulcan/pull/1841)\n- Allow SmartForm to use collection or collectionName [`#1840`](https://github.com/VulcanJS/Vulcan/pull/1840)\n- Revert \"showEdit on Card\" [`#1839`](https://github.com/VulcanJS/Vulcan/pull/1839)\n- Revert 1828 fix allvotes [`#1838`](https://github.com/VulcanJS/Vulcan/pull/1838)\n- fix allVotes resolveAs [`#1828`](https://github.com/VulcanJS/Vulcan/pull/1828)\n-  Add package Spanish Translation i18n-es-es [`#1824`](https://github.com/VulcanJS/Vulcan/pull/1824)\n- showEdit on Card [`#1836`](https://github.com/VulcanJS/Vulcan/pull/1836)\n- Fix circular dependencies between schemas and collections [`#1837`](https://github.com/VulcanJS/Vulcan/pull/1837)\n- Fixed graphQL schema for example-forum [`#1830`](https://github.com/VulcanJS/Vulcan/pull/1830)\n- Fixed voting bugs [`#1827`](https://github.com/VulcanJS/Vulcan/pull/1827)\n- Update to 1.8.5 [`#1`](https://github.com/VulcanJS/Vulcan/pull/1)\n- Added comments from the example-simple video tutorial for reference f… [`#1820`](https://github.com/VulcanJS/Vulcan/pull/1820)\n- Fix Newsletter Banner SSR [`#1817`](https://github.com/VulcanJS/Vulcan/pull/1817)\n- Set default waiting state to false to avoid SSR override [`#1816`](https://github.com/VulcanJS/Vulcan/pull/1816)\n- Use getComponent for childRoutes [`#1813`](https://github.com/VulcanJS/Vulcan/pull/1813)\n- Allow the ApolloEngine LogLevel to be set via the settings [`#1810`](https://github.com/VulcanJS/Vulcan/pull/1810)\n- Form loading state fix [`#1811`](https://github.com/VulcanJS/Vulcan/pull/1811)\n- fix admin delete user hide bug [`#1803`](https://github.com/VulcanJS/Vulcan/pull/1803)\n- Add VSCode jsconfig [`#1801`](https://github.com/VulcanJS/Vulcan/pull/1801)\n- Debug Groups & Colors [`#1802`](https://github.com/VulcanJS/Vulcan/pull/1802)\n- Fix warnings from React 16 update [`#1800`](https://github.com/VulcanJS/Vulcan/pull/1800)\n- Upgrade to React 16.2 [`#1799`](https://github.com/VulcanJS/Vulcan/pull/1799)\n- fix duplicate email returns \"unknown error'  [`#1795`](https://github.com/VulcanJS/Vulcan/pull/1795)\n- Remove optics-agent from package.json [`#1791`](https://github.com/VulcanJS/Vulcan/pull/1791)\n- Cleanup 1.8.1 [`#1790`](https://github.com/VulcanJS/Vulcan/pull/1790)\n- Fix #1933 [`#1933`](https://github.com/VulcanJS/Vulcan/issues/1933)\n- Move isAdmin init code to callback, fix #1917 [`#1917`](https://github.com/VulcanJS/Vulcan/issues/1917)\n- 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)\n- 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)\n- Pass down props (fix #1856) [`#1856`](https://github.com/VulcanJS/Vulcan/issues/1856)\n- Remove all example code from core repo (it lives in Starter repo now instead) [`7d17b57`](https://github.com/VulcanJS/Vulcan/commit/7d17b57f0591a820fbf41bf4d36a67562181c104)\n- Package upgrade [`7ec4f4d`](https://github.com/VulcanJS/Vulcan/commit/7ec4f4ddd00c6e2f475d8ec5d21d669f5c33038e)\n- Super-hacky fix to the accounts setState issue [`6facf15`](https://github.com/VulcanJS/Vulcan/commit/6facf15e17fec0eff0804d35360579513e7f586f)\n\n#### [1.8.1](https://github.com/VulcanJS/Vulcan/compare/v1.7.0...1.8.1)\n\n> 27 December 2017\n\n- Add pre-validate callback on new user creation [`#1778`](https://github.com/VulcanJS/Vulcan/pull/1778)\n- Fix issue #1770 [`#1771`](https://github.com/VulcanJS/Vulcan/pull/1771)\n- Changes to support vulcan-material-ui [`#1772`](https://github.com/VulcanJS/Vulcan/pull/1772)\n- Improve speed of vote score updates (Mongo aggregator + bulkwrite) [`#1759`](https://github.com/VulcanJS/Vulcan/pull/1759)\n- 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)\n- Order of operation fix in scoring.js [`#1751`](https://github.com/VulcanJS/Vulcan/pull/1751)\n- Added index to Votes on startup [`#1752`](https://github.com/VulcanJS/Vulcan/pull/1752)\n- Update package.json [`#1747`](https://github.com/VulcanJS/Vulcan/pull/1747)\n- Update README.md [`#1746`](https://github.com/VulcanJS/Vulcan/pull/1746)\n- Bump the react-bootstrap version to get rid of \"isMounted is deprecated...\" error when opening modals [`#1744`](https://github.com/VulcanJS/Vulcan/pull/1744)\n- Abstract out bootstrap-specific components in vulcan:forms [`#1750`](https://github.com/VulcanJS/Vulcan/pull/1750)\n- Dropped isomorphic-fetch in favor of cross-fetch [`#1738`](https://github.com/VulcanJS/Vulcan/pull/1738)\n- Update allow/deny package [`#1734`](https://github.com/VulcanJS/Vulcan/pull/1734)\n- fixed some minor bugs in the documentation for subscribeMutationsGenerator [`#1728`](https://github.com/VulcanJS/Vulcan/pull/1728)\n- Remove \"unique\" identifier file. [`#1723`](https://github.com/VulcanJS/Vulcan/pull/1723)\n- Fix example simple [`#1724`](https://github.com/VulcanJS/Vulcan/pull/1724)\n- Added submit option to `updateCurrentValues` in Form.jsx context [`#1722`](https://github.com/VulcanJS/Vulcan/pull/1722)\n- Display errors on password reset form [`#1716`](https://github.com/VulcanJS/Vulcan/pull/1716)\n- RegisterFragment should respect comments in fragment literals [`#1713`](https://github.com/VulcanJS/Vulcan/pull/1713)\n- Update CONTRIBUTING.md [`#1712`](https://github.com/VulcanJS/Vulcan/pull/1712)\n- [fix] give default value to unset [`#1703`](https://github.com/VulcanJS/Vulcan/pull/1703)\n- Update sample_settings.json with new mailchimp schema properties [`#1698`](https://github.com/VulcanJS/Vulcan/pull/1698)\n- Update CONTRIBUTING.md [`#1696`](https://github.com/VulcanJS/Vulcan/pull/1696)\n- Clean up forum packages [`65eda4b`](https://github.com/VulcanJS/Vulcan/commit/65eda4b0334c96ab760ac4d084d722f971a62a24)\n- refactoring example-forum package [`0a48d0c`](https://github.com/VulcanJS/Vulcan/commit/0a48d0ccbf282c03577b0a56b1cfeda74dda2a99)\n- Dropped isomorphic-fetch in favor of cross-fetch (React Native compatible). [`30aad4c`](https://github.com/VulcanJS/Vulcan/commit/30aad4c2f543ca7fc575e97acbc86451e7ef379c)\n\n#### [v1.7.0](https://github.com/VulcanJS/Vulcan/compare/v1.6.0...v1.7.0)\n\n> 2 August 2017\n\n- add function to remove tags from head [`#1678`](https://github.com/VulcanJS/Vulcan/pull/1678)\n- Fix for Users.getTwitterName() [`#1683`](https://github.com/VulcanJS/Vulcan/pull/1683)\n- #1658 Fix broken validation error Messages in LoginForm [`#1680`](https://github.com/VulcanJS/Vulcan/pull/1680)\n- Fix remove permission strings [`#1673`](https://github.com/VulcanJS/Vulcan/pull/1673)\n- create new example-permissions package to showcase groups & permissions API [`66e527f`](https://github.com/VulcanJS/Vulcan/commit/66e527fab461fa484d9a5bbf6251c9e588207f76)\n- Add Membership example [`919ffaf`](https://github.com/VulcanJS/Vulcan/commit/919ffafab3cb1b4eb04ace99c42a1f5cbc5eb500)\n- Added example-interfaces package [`2c6526f`](https://github.com/VulcanJS/Vulcan/commit/2c6526f81f78db913f2182323e42cfa45ef3f061)\n\n#### [v1.6.0](https://github.com/VulcanJS/Vulcan/compare/v1.5.0...v1.6.0)\n\n> 14 July 2017\n\n- Added failure form callbacks and success form callbacks to forms [`#1666`](https://github.com/VulcanJS/Vulcan/pull/1666)\n- (update_version): package:dymanic-import from 0.1.0 -&gt; 0.1.1 to fix  [`#1654`](https://github.com/VulcanJS/Vulcan/pull/1654)\n- use correct code for Mailchimp \"already subscribed\" state [`#1651`](https://github.com/VulcanJS/Vulcan/pull/1651)\n- (update_version): package:dymanic-import from 0.1.0 -&gt; 0.1.1 to fix https://github.com/meteor/meteor/issues/8751 [`#8751`](https://github.com/meteor/meteor/issues/8751)\n- Use default resolvers and mutations for instagram example [`31d0490`](https://github.com/VulcanJS/Vulcan/commit/31d0490a3697b44dc0a3239faa76cc74ab868200)\n- add new default resolvers and default mutations; improve the way field resolvers are defined [`7ff1ada`](https://github.com/VulcanJS/Vulcan/commit/7ff1ada7d9d59ff4258ab727b526d7ca33191592)\n- use new resolveAs syntax [`3345914`](https://github.com/VulcanJS/Vulcan/commit/334591450dc4242be2ad8ec8545a5eafaf976c77)\n\n#### [v1.5.0](https://github.com/VulcanJS/Vulcan/compare/v1.2.0...v1.5.0)\n\n> 12 June 2017\n\n- Fixed typo in proptypes [`#1646`](https://github.com/VulcanJS/Vulcan/pull/1646)\n- Fixed typo in formatMessage call [`#1645`](https://github.com/VulcanJS/Vulcan/pull/1645)\n- Added PropType package and changed from component to PureComponent [`#1640`](https://github.com/VulcanJS/Vulcan/pull/1640)\n- let addMediaAfterSubmit return post at the end [`#1633`](https://github.com/VulcanJS/Vulcan/pull/1633)\n- Utils.getNestedProperty signature typo fix for email schema property [`#1630`](https://github.com/VulcanJS/Vulcan/pull/1630)\n- fix bug where All Categories didn't clear the category filter [`#1616`](https://github.com/VulcanJS/Vulcan/pull/1616)\n- Should call the property of parentRouteName properly [`#1612`](https://github.com/VulcanJS/Vulcan/pull/1612)\n- Semi-colon Updates [`#1601`](https://github.com/VulcanJS/Vulcan/pull/1601)\n- Workaround for Issue #1580 [`#1600`](https://github.com/VulcanJS/Vulcan/pull/1600)\n- Enable facebook sharing of posts by supporting facebook scraper reqs [`#1596`](https://github.com/VulcanJS/Vulcan/pull/1596)\n- small fix for bash install meteor [`#1597`](https://github.com/VulcanJS/Vulcan/pull/1597)\n- Unable to assign category to a post (fix #1592) [`#1593`](https://github.com/VulcanJS/Vulcan/pull/1593)\n- a make sense modified std:accounts-ui for telescope [`#1589`](https://github.com/VulcanJS/Vulcan/pull/1589)\n- use npm simpl-schema, meteor aldeed:collection2-core package [`#1587`](https://github.com/VulcanJS/Vulcan/pull/1587)\n- Fixed out-of-the-box newsletter config settings that causes constant … [`#1582`](https://github.com/VulcanJS/Vulcan/pull/1582)\n- fix safari issues, remove defineName, improve apolloClientReducer to get the initialState [`#1583`](https://github.com/VulcanJS/Vulcan/pull/1583)\n- add missing dependency (fix #1598) [`#1598`](https://github.com/VulcanJS/Vulcan/issues/1598)\n- Merge pull request #1593 from comus/ss-patch [`#1592`](https://github.com/VulcanJS/Vulcan/issues/1592)\n- Unable to assign category to a post (fix #1592) [`#1592`](https://github.com/VulcanJS/Vulcan/issues/1592)\n- Include bootstrap CSS in movies example packages for now [`308280d`](https://github.com/VulcanJS/Vulcan/commit/308280d74904a2172824c260d6ef953737116818)\n- Added PropTypes from 'prop-types'; [`14f5ba8`](https://github.com/VulcanJS/Vulcan/commit/14f5ba897175174f3d4d9e3f8734b967f213f8d8)\n- vulcan:payments [`ca226ac`](https://github.com/VulcanJS/Vulcan/commit/ca226acca370818ef75c34613242685524dd16d6)\n\n#### [v1.2.0](https://github.com/VulcanJS/Vulcan/compare/1.1.0...v1.2.0)\n\n> 8 March 2017\n\n- Adding Check and/or preinstall Meteor [`#1578`](https://github.com/VulcanJS/Vulcan/pull/1578)\n- Nova 1.2.0 🚀 [`81c74db`](https://github.com/VulcanJS/Vulcan/commit/81c74db42fa9a4f1cc1ff7e2fb077153df7fdb36)\n- experiment batching, let's see if it has a performance impact [`113b68f`](https://github.com/VulcanJS/Vulcan/commit/113b68f5edf73728cd3c9be6fe0bfd5b047c8d52)\n- don't do \"half-batching/caching\", prepare for nova 1.2 [`0c19136`](https://github.com/VulcanJS/Vulcan/commit/0c19136298ab6519ddedb268184cd75416305e7a)\n\n#### [1.1.0](https://github.com/VulcanJS/Vulcan/compare/v1.0.0...1.1.0)\n\n> 16 February 2017\n\n- better core/lib improvement and update [`#1569`](https://github.com/VulcanJS/Vulcan/pull/1569)\n- renderContext fix [`#1565`](https://github.com/VulcanJS/Vulcan/pull/1565)\n- 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)\n- Add username tooltip to user's avatar [`#1555`](https://github.com/VulcanJS/Vulcan/pull/1555)\n- new routing [`9992f00`](https://github.com/VulcanJS/Vulcan/commit/9992f0063ee6e73809400cbbe7c0bac153345c1f)\n- work on notifications [`b67989f`](https://github.com/VulcanJS/Vulcan/commit/b67989fbc23f2b3b1f6e3ddca262de84af24476d)\n- Adapt withEdit/withNew to support new fragments API [`afebadb`](https://github.com/VulcanJS/Vulcan/commit/afebadba55bf8d736a8028e11dae0086cbccfa7e)\n\n### [v1.0.0](https://github.com/VulcanJS/Vulcan/compare/v0.27.5...v1.0.0)\n\n> 2 February 2017\n\n- separate client side and server side routing [`#1543`](https://github.com/VulcanJS/Vulcan/pull/1543)\n- devel - revert commits related to simpl-schema [`#1537`](https://github.com/VulcanJS/Vulcan/pull/1537)\n- #1517 - Implement configurable excerpt lengths [`#1536`](https://github.com/VulcanJS/Vulcan/pull/1536)\n- fixed deployment instructions typo [`#1534`](https://github.com/VulcanJS/Vulcan/pull/1534)\n- addRoute function improve, not use array.concat because it will retur… [`#1532`](https://github.com/VulcanJS/Vulcan/pull/1532)\n- using nova:forms shows issues when being imported [`#1530`](https://github.com/VulcanJS/Vulcan/pull/1530)\n- Allow redux middleware extensions [`#1528`](https://github.com/VulcanJS/Vulcan/pull/1528)\n- default avatar image's URL for ssr [`#1526`](https://github.com/VulcanJS/Vulcan/pull/1526)\n- adds pt-PT localization to the list [`#1521`](https://github.com/VulcanJS/Vulcan/pull/1521)\n- 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)\n- Pass Apollo client object to parameters callback to fix #1546 [`#1546`](https://github.com/VulcanJS/Vulcan/issues/1546)\n- fix #1529 [`#1529`](https://github.com/VulcanJS/Vulcan/issues/1529)\n- Nova 1.0.0 stable on master [`4baa939`](https://github.com/VulcanJS/Vulcan/commit/4baa9399256532bd4616037f490bad34e47913e3)\n- Remove “__” prefix to avoid conflicts with GraphQL introspection types and simplify code [`db17e91`](https://github.com/VulcanJS/Vulcan/commit/db17e917f823ee8c5faae76adc2871f152bb379c)\n- clean-up [`1c058b6`](https://github.com/VulcanJS/Vulcan/commit/1c058b60c68bfbdfadf864448a2764072a1b043c)\n\n#### [v0.27.5](https://github.com/VulcanJS/Vulcan/compare/v0.27.4...v0.27.5)\n\n> 30 November 2016\n\n- v0.27.5 - really the latest full Meteor version [`#1518`](https://github.com/VulcanJS/Vulcan/pull/1518)\n- eslint & clean up code, also fixed some bugs [`#1515`](https://github.com/VulcanJS/Vulcan/pull/1515)\n- Newsletter subcription fixes [`#1513`](https://github.com/VulcanJS/Vulcan/pull/1513)\n- npm run lint support for jsx files [`#1511`](https://github.com/VulcanJS/Vulcan/pull/1511)\n- fix wrong comment in deep function [`#1512`](https://github.com/VulcanJS/Vulcan/pull/1512)\n- clean up i18n files [`cbcfc1b`](https://github.com/VulcanJS/Vulcan/commit/cbcfc1bcafa5d1ffd0db9e81f24efaa499300282)\n- clean up packages names [`d0c72c9`](https://github.com/VulcanJS/Vulcan/commit/d0c72c98f1d09f7bdbc25e8a38456e6791975229)\n- adapt the `Telescope.createCollection` api to all the collections, some clean up in old containers files [`1137fb9`](https://github.com/VulcanJS/Vulcan/commit/1137fb96aa99456b454e0de72edc0a8f826ce507)\n\n#### [v0.27.4](https://github.com/VulcanJS/Vulcan/compare/v0.27.3...v0.27.4)\n\n> 15 November 2016\n\n- v0.27.4 - latest version before Apollo official release [`#1508`](https://github.com/VulcanJS/Vulcan/pull/1508)\n- add eslint with basic plugins and configuration. fixes #1470 [`#1474`](https://github.com/VulcanJS/Vulcan/pull/1474)\n- Fix react setState race condition [`#1507`](https://github.com/VulcanJS/Vulcan/pull/1507)\n- Only show comment reply button for logged in users [`#1504`](https://github.com/VulcanJS/Vulcan/pull/1504)\n- Add zh-CN i18n package [`#1503`](https://github.com/VulcanJS/Vulcan/pull/1503)\n- Add i18n messages for no more posts, no results, and load more days [`#1499`](https://github.com/VulcanJS/Vulcan/pull/1499)\n- No comments.deleteById simulation for now [`#1497`](https://github.com/VulcanJS/Vulcan/pull/1497)\n- Add Reset Password Feature [`#1491`](https://github.com/VulcanJS/Vulcan/pull/1491)\n- fix typo in style class name [`#1487`](https://github.com/VulcanJS/Vulcan/pull/1487)\n- meteor update npm-mongo ; meteor update mongo [`#1482`](https://github.com/VulcanJS/Vulcan/pull/1482)\n- Merge pull request #1474 from moimikey/patch-1 [`#1470`](https://github.com/VulcanJS/Vulcan/issues/1470)\n- modify getUnusedSlug to handle edge case on Users collection, fix #1501 and related to #1213 [`#1501`](https://github.com/VulcanJS/Vulcan/issues/1501)\n- clean up callbacks by moving logic to mutations and schema (autoValue) [`8689a4d`](https://github.com/VulcanJS/Vulcan/commit/8689a4de73647bc949c51e9d1c90837cb52d7e22)\n- refactoring PostsListContainer and CommentsListContainer into HoCs [`0ed0f24`](https://github.com/VulcanJS/Vulcan/commit/0ed0f24303219505e4ddbbff65aeedb1235a9520)\n- move namespace to prefix on user schema: user.telescope.xxx by user.nova_xxx [`460efe5`](https://github.com/VulcanJS/Vulcan/commit/460efe52f606c1a77b745eb2ff61738cd0b0ac58)\n\n#### [v0.27.3](https://github.com/VulcanJS/Vulcan/compare/v0.25.7...v0.27.3)\n\n> 19 October 2016\n\n- v0.27.3 [`#1475`](https://github.com/VulcanJS/Vulcan/pull/1475)\n- Updated _posts.scss [`#1469`](https://github.com/VulcanJS/Vulcan/pull/1469)\n- Clean some old code & fix some errors [`#1461`](https://github.com/VulcanJS/Vulcan/pull/1461)\n- Add shortcut to submit form, close #1471 [`#1472`](https://github.com/VulcanJS/Vulcan/pull/1472)\n- Update subscriberIdsToNotify to send unique emails [`#1466`](https://github.com/VulcanJS/Vulcan/pull/1466)\n- Tell folks how to deploy Nova with latest Meteor Up (kadirahq/meteor-up), closes #1455 [`#1456`](https://github.com/VulcanJS/Vulcan/pull/1456)\n- v0.27.2 [`#1454`](https://github.com/VulcanJS/Vulcan/pull/1454)\n- Patch v0.27.1 proposal [`#1446`](https://github.com/VulcanJS/Vulcan/pull/1446)\n- Added Brazilian Portuguese package to README.md [`#1444`](https://github.com/VulcanJS/Vulcan/pull/1444)\n- Update groups.js (clarity) [`#1445`](https://github.com/VulcanJS/Vulcan/pull/1445)\n- Update callback.js to include Linkedin [`#1432`](https://github.com/VulcanJS/Vulcan/pull/1432)\n- new nova i18n package (de_DE) [`#1430`](https://github.com/VulcanJS/Vulcan/pull/1430)\n- little detail makes big difference for nob like me [`#1428`](https://github.com/VulcanJS/Vulcan/pull/1428)\n- nova:subscribe all the things [`#1425`](https://github.com/VulcanJS/Vulcan/pull/1425)\n- Syntax highlighting added (where missing) ✨ [`#1424`](https://github.com/VulcanJS/Vulcan/pull/1424)\n- Changing subscription method names & better error handling [`#1422`](https://github.com/VulcanJS/Vulcan/pull/1422)\n- Extendable Subscribe component & locale [`#1412`](https://github.com/VulcanJS/Vulcan/pull/1412)\n- Corrected imports for debug convenience globals [`#1420`](https://github.com/VulcanJS/Vulcan/pull/1420)\n- Proposal for a CanDo Higher-Order Component [`#1417`](https://github.com/VulcanJS/Vulcan/pull/1417)\n- Refactored subscribe-to-posts [`#1410`](https://github.com/VulcanJS/Vulcan/pull/1410)\n- NovaForm: custom control has access to document as a props [`#1403`](https://github.com/VulcanJS/Vulcan/pull/1403)\n- Nova i18n ru_RU package. [`#1392`](https://github.com/VulcanJS/Vulcan/pull/1392)\n- pl_PL locale [`#1394`](https://github.com/VulcanJS/Vulcan/pull/1394)\n- fixed types comparison in Posts.isApproved helper [`#1393`](https://github.com/VulcanJS/Vulcan/pull/1393)\n- set locale in settings [`#1391`](https://github.com/VulcanJS/Vulcan/pull/1391)\n- Fix Facebook settings error in sample_settings.json and README.md [`#1381`](https://github.com/VulcanJS/Vulcan/pull/1381)\n- README: Remove duplicate in #optional-packages [`#1389`](https://github.com/VulcanJS/Vulcan/pull/1389)\n- require react 15.0.x specifically [`#1385`](https://github.com/VulcanJS/Vulcan/pull/1385)\n- Collection typo error in README.md [`#1380`](https://github.com/VulcanJS/Vulcan/pull/1380)\n- don't do modification on a var if undefined =&gt; fixes #1375 error [`#1379`](https://github.com/VulcanJS/Vulcan/pull/1379)\n- Meta SSR with react-helmet [`#1376`](https://github.com/VulcanJS/Vulcan/pull/1376)\n- Different fixes [`#1373`](https://github.com/VulcanJS/Vulcan/pull/1373)\n- Decouple components actions from Meteor [`#1370`](https://github.com/VulcanJS/Vulcan/pull/1370)\n- added order support for custom fields [`#1364`](https://github.com/VulcanJS/Vulcan/pull/1364)\n- use original PostsItem component [`#1362`](https://github.com/VulcanJS/Vulcan/pull/1362)\n- 🐙 Fix custom package [`#1356`](https://github.com/VulcanJS/Vulcan/pull/1356)\n- Hook up siteImage in settings to the open graph meta tags. [`#1342`](https://github.com/VulcanJS/Vulcan/pull/1342)\n- Fixes - Episode II [`#1349`](https://github.com/VulcanJS/Vulcan/pull/1349)\n- Fixes [`#1348`](https://github.com/VulcanJS/Vulcan/pull/1348)\n- fix import statements in demo [`#1337`](https://github.com/VulcanJS/Vulcan/pull/1337)\n- Newsletter + Mailchimp subscription enhancement [`#1332`](https://github.com/VulcanJS/Vulcan/pull/1332)\n- Update README.md [`#1334`](https://github.com/VulcanJS/Vulcan/pull/1334)\n- Complete soft delete feature for comments (Revision 2) [`#1323`](https://github.com/VulcanJS/Vulcan/pull/1323)\n- Load categories at startup in load_categories.js [`#1324`](https://github.com/VulcanJS/Vulcan/pull/1324)\n- Update README.md [`#1312`](https://github.com/VulcanJS/Vulcan/pull/1312)\n- Only admin see post stats [`#1318`](https://github.com/VulcanJS/Vulcan/pull/1318)\n- Fix social login anchor link [`#1311`](https://github.com/VulcanJS/Vulcan/pull/1311)\n- fix 404Error page [`#1304`](https://github.com/VulcanJS/Vulcan/pull/1304)\n- Completed profile hook [`#1301`](https://github.com/VulcanJS/Vulcan/pull/1301)\n- Add siteUrl in front of action link. The link is wrong [`#1242`](https://github.com/VulcanJS/Vulcan/pull/1242)\n- Move head tags to layout [`#1298`](https://github.com/VulcanJS/Vulcan/pull/1298)\n- Helper: handle thumbnails from embedly, an external website or hosted on the app [`#1295`](https://github.com/VulcanJS/Vulcan/pull/1295)\n- Fix HeadTags &lt;-&gt; Flexbox + add 2 helpers for images [`#1292`](https://github.com/VulcanJS/Vulcan/pull/1292)\n- HeadTags: dochead instead of react-helmet [`#1291`](https://github.com/VulcanJS/Vulcan/pull/1291)\n- Nova package updates [`#1287`](https://github.com/VulcanJS/Vulcan/pull/1287)\n- Bugfixes for std:accounts-ui [`#1286`](https://github.com/VulcanJS/Vulcan/pull/1286)\n- fixing typo in newCommentSubscribed notification [`#1279`](https://github.com/VulcanJS/Vulcan/pull/1279)\n- update documentation link to skip readme.io welcome page [`#1276`](https://github.com/VulcanJS/Vulcan/pull/1276)\n- Added current version 'fourseven:scss@3.4.1' to nova:share. [`#1269`](https://github.com/VulcanJS/Vulcan/pull/1269)\n- update to version of alt:react-accounts* that doesn't depend on react-runtime [`#1264`](https://github.com/VulcanJS/Vulcan/pull/1264)\n- [Nova] Update share package [`#1260`](https://github.com/VulcanJS/Vulcan/pull/1260)\n- update alt:react-accounts for full SSR [`#1258`](https://github.com/VulcanJS/Vulcan/pull/1258)\n- fix #1255 - add comment incrementing to Posts when adding a comment [`#1256`](https://github.com/VulcanJS/Vulcan/pull/1256)\n- Fix Username already exists issue [403] [`#1252`](https://github.com/VulcanJS/Vulcan/pull/1252)\n- re-add Dockerfile, fix #1477 [`#1477`](https://github.com/VulcanJS/Vulcan/issues/1477)\n- add eslint with basic plugins and configuration. fixes #1470 [`#1470`](https://github.com/VulcanJS/Vulcan/issues/1470)\n- Merge pull request #1472 from aszx87410/devel [`#1471`](https://github.com/VulcanJS/Vulcan/issues/1471)\n- Add shortcut to submit form, close #1471 [`#1471`](https://github.com/VulcanJS/Vulcan/issues/1471)\n- ensure user slug unicity, fixes #1213 [`#1213`](https://github.com/VulcanJS/Vulcan/issues/1213)\n- add slug to newPendingPost notification, closes #1254 [`#1254`](https://github.com/VulcanJS/Vulcan/issues/1254)\n- Merge pull request #1456 from asmita005/master [`#1455`](https://github.com/VulcanJS/Vulcan/issues/1455)\n- complete license, fix #1117 [`#1117`](https://github.com/VulcanJS/Vulcan/issues/1117)\n- fix #247 [`#247`](https://github.com/VulcanJS/Vulcan/issues/247)\n- fix #1449 [`#1449`](https://github.com/VulcanJS/Vulcan/issues/1449)\n- fix #1447, remove unnecessary load-script dependency [`#1447`](https://github.com/VulcanJS/Vulcan/issues/1447)\n- fix #1423 [`#1423`](https://github.com/VulcanJS/Vulcan/issues/1423)\n- require react 15.0.x specifically (fixes #1384) [`#1384`](https://github.com/VulcanJS/Vulcan/issues/1384)\n- Merge pull request #1379 from xavcz/bang-bang-image-settings [`#1375`](https://github.com/VulcanJS/Vulcan/issues/1375)\n- don't do modification on a var if undefined =&gt; fixes #1375 error at startup on HeadTags [`#1375`](https://github.com/VulcanJS/Vulcan/issues/1375)\n- fix #1327 [`#1327`](https://github.com/VulcanJS/Vulcan/issues/1327)\n- Merge pull request #1256 from paulmolluzzo/fix-comment-incrementing [`#1255`](https://github.com/VulcanJS/Vulcan/issues/1255)\n- fix #1255 - add comment incrementing to Posts when adding a comment [`#1255`](https://github.com/VulcanJS/Vulcan/issues/1255)\n- cleaning up nova:subscribe [`99a70a3`](https://github.com/VulcanJS/Vulcan/commit/99a70a326233b3cbdf251aaebbeab24e7a8d2b9f)\n- clean up [`5a08bb6`](https://github.com/VulcanJS/Vulcan/commit/5a08bb634fa107b77ef9932c9ea886c3c2015a75)\n- change old reference to AutoForm (legacy): field schema \"autoform\" -&gt; \"form\" [`7775838`](https://github.com/VulcanJS/Vulcan/commit/7775838980d4c182d204312c03508a3c9c587b7e)\n\n#### [v0.25.7](https://github.com/VulcanJS/Vulcan/compare/v0.25.5...v0.25.7)\n\n> 6 February 2016\n\n- supply default email based on 3rd party login, if possible [`#1223`](https://github.com/VulcanJS/Vulcan/pull/1223)\n- Set counter name to category id instead of category slug [`#1229`](https://github.com/VulcanJS/Vulcan/pull/1229)\n- Fixed url not defined in postPages[post.url] line 53 [`#1174`](https://github.com/VulcanJS/Vulcan/pull/1174)\n- Fixing issue #1170 [`#1187`](https://github.com/VulcanJS/Vulcan/pull/1187)\n- refactor permission code; make spam/pending/etc. posts unaccessible (fix #1219) [`#1219`](https://github.com/VulcanJS/Vulcan/issues/1219)\n- make comment's postId uneditable in schema (fix #1231) [`#1231`](https://github.com/VulcanJS/Vulcan/issues/1231)\n- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`a247c5c`](https://github.com/VulcanJS/Vulcan/commit/a247c5cfc28c2a6efe7c331169bbb64666f7c467)\n- fix i18n formatting [`6232923`](https://github.com/VulcanJS/Vulcan/commit/6232923904439eea2801baf47fd5f390b4bc1100)\n- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`bdc5c00`](https://github.com/VulcanJS/Vulcan/commit/bdc5c0056e7b71ac2e4dc9bca32bf9e73914ad88)\n\n#### [v0.25.5](https://github.com/VulcanJS/Vulcan/compare/v0.25.4...v0.25.5)\n\n> 28 October 2015\n\n- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`18b3f44`](https://github.com/VulcanJS/Vulcan/commit/18b3f4405115d7f87454a32de257d0f5e930473c)\n- fix i18n syntax; add i18n files to pretender package [`5620d36`](https://github.com/VulcanJS/Vulcan/commit/5620d3670a826f8317872c081d6d2f5c0b756bba)\n- version 0.25.5 [`fef1818`](https://github.com/VulcanJS/Vulcan/commit/fef1818f4ad778eef07be8ee597523f014760c5c)\n\n#### [v0.25.4](https://github.com/VulcanJS/Vulcan/compare/v0.25.2...v0.25.4)\n\n> 22 October 2015\n\n- reformatting i18n files for tap:i18n compatibility [`f34b797`](https://github.com/VulcanJS/Vulcan/commit/f34b797fed8c83a5c5f20e63bb8f564d03ff7c56)\n- version bump (0.25.3) [`c389514`](https://github.com/VulcanJS/Vulcan/commit/c38951414961650c314656ce1062379dcf887fb2)\n- added telescope:prerender package [`eb8f7dc`](https://github.com/VulcanJS/Vulcan/commit/eb8f7dc141d8670f392fe7eac91c48d59c4330a4)\n\n#### [v0.25.2](https://github.com/VulcanJS/Vulcan/compare/v0.25.0...v0.25.2)\n\n> 16 October 2015\n\n- Fixing `(error.error === 603)` always results false [`#1167`](https://github.com/VulcanJS/Vulcan/pull/1167)\n- i18n.t bg translation + adding missing ones [`#1164`](https://github.com/VulcanJS/Vulcan/pull/1164)\n- Fixes #1161 - Template.layout `pageName` should be reactive as route changes [`#1163`](https://github.com/VulcanJS/Vulcan/pull/1163)\n- Fix e-mail template overrides by adding the \"custom\" prefix server-side [`#1159`](https://github.com/VulcanJS/Vulcan/pull/1159)\n- replaced getUserName with getDisplayName for comments [`#1155`](https://github.com/VulcanJS/Vulcan/pull/1155)\n- Fix bug that $set and $unset categories same time. [`#1152`](https://github.com/VulcanJS/Vulcan/pull/1152)\n- Merge pull request #1163 from shilman/fix-1161 [`#1161`](https://github.com/VulcanJS/Vulcan/issues/1161)\n- Fixes #1161 - Template.layout `pageName` should be reactive as route changes [`#1161`](https://github.com/VulcanJS/Vulcan/issues/1161)\n- Fix bug that $set and $unset categories same time. [`#1150`](https://github.com/VulcanJS/Vulcan/issues/1150)\n- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`45ff625`](https://github.com/VulcanJS/Vulcan/commit/45ff62551068497996c0a42da975d23b9bf99fea)\n- extracting menu component into its own package [`aed1f5a`](https://github.com/VulcanJS/Vulcan/commit/aed1f5a590757a1ce274630546deb2310c858237)\n- move menu component to its own separate repo [`50633ff`](https://github.com/VulcanJS/Vulcan/commit/50633ff089fa5babf6339fe155fbb3f098b7f08e)\n\n#### [v0.25.0](https://github.com/VulcanJS/Vulcan/compare/v0.24.0...v0.25.0)\n\n> 24 September 2015\n\n- Fix schema i18n by moving internationalize to collections [`#1115`](https://github.com/VulcanJS/Vulcan/pull/1115)\n- Use abstraction of adminUsers consistently [`#1121`](https://github.com/VulcanJS/Vulcan/pull/1121)\n- migrating to Flow Router (WIP) [`50c4874`](https://github.com/VulcanJS/Vulcan/commit/50c48745a30af6151902705c8c659e6488280342)\n- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`9b8d25d`](https://github.com/VulcanJS/Vulcan/commit/9b8d25d64f055bcf24c20f4c83a2afc6af2caaaf)\n- sign-in/sign-up routes; clean up [`4894d5f`](https://github.com/VulcanJS/Vulcan/commit/4894d5f4f28e04934bc006c8d4d53b26ed1208e2)\n\n#### [v0.24.0](https://github.com/VulcanJS/Vulcan/compare/v0.22.1...v0.24.0)\n\n> 15 August 2015\n\n- Fix Removing URL on Edit [`#1015`](https://github.com/VulcanJS/Vulcan/pull/1015)\n- Show Share button on desktop version [`#1091`](https://github.com/VulcanJS/Vulcan/pull/1091)\n- Correctly get url for sitemap using slug [`#1098`](https://github.com/VulcanJS/Vulcan/pull/1098)\n- Added a RSS route that returns posts filtered by category [`#1100`](https://github.com/VulcanJS/Vulcan/pull/1100)\n- make sure the postView is a function [`#1102`](https://github.com/VulcanJS/Vulcan/pull/1102)\n- match anything (fix #1103) [`#1103`](https://github.com/VulcanJS/Vulcan/issues/1103)\n- getDate -&gt; date (fix #1092) [`#1092`](https://github.com/VulcanJS/Vulcan/issues/1092)\n- fix #1009 [`#1009`](https://github.com/VulcanJS/Vulcan/issues/1009)\n- 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)\n- give priority to field label over i18n string, if it exists (fix #1070) [`#1070`](https://github.com/VulcanJS/Vulcan/issues/1070)\n- fix i18n son parsing issue [`83e5af6`](https://github.com/VulcanJS/Vulcan/commit/83e5af6b233493429a2ad8e32d8dfe7fab8b2c3a)\n- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`5fb9e8c`](https://github.com/VulcanJS/Vulcan/commit/5fb9e8c76f04f05b39e24cdf4925bdf36d3986c4)\n- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`d78b6b6`](https://github.com/VulcanJS/Vulcan/commit/d78b6b6fd0871a2c5fcb984dacb5d6483a3df2c5)\n\n#### [v0.22.1](https://github.com/VulcanJS/Vulcan/compare/v0.21.1...v0.22.1)\n\n> 27 July 2015\n\n- use absolute URL for Users.getProfileUrl [`#1077`](https://github.com/VulcanJS/Vulcan/pull/1077)\n- Title Links on Avatars [`#1067`](https://github.com/VulcanJS/Vulcan/pull/1067)\n- allow hero modules to be full width of viewport [`#1065`](https://github.com/VulcanJS/Vulcan/pull/1065)\n- Fix decrease inviteCount [`#1054`](https://github.com/VulcanJS/Vulcan/pull/1054)\n- Added .startOf('day'); to `today` variable [`#1027`](https://github.com/VulcanJS/Vulcan/pull/1027)\n- fixed syntax for passing in error type [`#1043`](https://github.com/VulcanJS/Vulcan/pull/1043)\n- Display trimmed down version of htmlBody and fix #1069 [`#1069`](https://github.com/VulcanJS/Vulcan/issues/1069)\n- add setting for pointing RSS links to discussion page; add pageUrl to API (fix #1038) [`#1038`](https://github.com/VulcanJS/Vulcan/issues/1038)\n- version bump (0.22.1) [`1353f0a`](https://github.com/VulcanJS/Vulcan/commit/1353f0a74dc268e69d50967b31350e5c34402108)\n- removing module template to simplify template structure [`b02b568`](https://github.com/VulcanJS/Vulcan/commit/b02b5688b31de281178d2ae901900c9ee7df53b9)\n- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`a8a8a61`](https://github.com/VulcanJS/Vulcan/commit/a8a8a61e09df050b22b04f8721d3c1f157b6690c)\n\n#### [v0.21.1](https://github.com/VulcanJS/Vulcan/compare/v0.20.5...v0.21.1)\n\n> 1 July 2015\n\n- fix settings publication to hide private fields (take 2) [`#1024`](https://github.com/VulcanJS/Vulcan/pull/1024)\n- Add Extra CSS [`#1019`](https://github.com/VulcanJS/Vulcan/pull/1019)\n- Add option for each day of week for newsletter. Resolves #1034 [`#1034`](https://github.com/VulcanJS/Vulcan/issues/1034)\n- Add Extra CSS settings field. Fixes #949 [`#949`](https://github.com/VulcanJS/Vulcan/issues/949)\n- Return null if bootstrap-url is blank. Fixes #1012 [`#1012`](https://github.com/VulcanJS/Vulcan/issues/1012)\n- Fix arrow key navigation for Single Day view. Fixes #986 [`#986`](https://github.com/VulcanJS/Vulcan/issues/986)\n- Created and pushed by LingoHub. Project: 'Telescope-Test4' by User: 'hello@telescopeapp.org'. [`f80d9d8`](https://github.com/VulcanJS/Vulcan/commit/f80d9d85849e89b92b68df9dde81d04e941b811b)\n- working on i18n [`1575aeb`](https://github.com/VulcanJS/Vulcan/commit/1575aeb43be981f40365e68e8b65c22e7e9da9bc)\n- separating more languages [`a55f40c`](https://github.com/VulcanJS/Vulcan/commit/a55f40c36c41ae8b7cacd72ebee517ff73822948)\n\n#### [v0.20.5](https://github.com/VulcanJS/Vulcan/compare/v0.15.1...v0.20.5)\n\n> 9 June 2015\n\n- Add ability to filter post views by category id [`#966`](https://github.com/VulcanJS/Vulcan/pull/966)\n- Add Docker deployment support [`#962`](https://github.com/VulcanJS/Vulcan/pull/962)\n- #959 - Fixed plural hardcoding issue by adding pointsUnitDisplayText … [`#960`](https://github.com/VulcanJS/Vulcan/pull/960)\n- cosmetic remove some trailing commas from telescope-posts [`#961`](https://github.com/VulcanJS/Vulcan/pull/961)\n- fix for nearly all of the getting started package issues [`#945`](https://github.com/VulcanJS/Vulcan/pull/945)\n- Add topLevelCommentId field to comments. [`#943`](https://github.com/VulcanJS/Vulcan/pull/943)\n- Improve jsHint consistency [`#934`](https://github.com/VulcanJS/Vulcan/pull/934)\n- check post existence before access on postUsers publication [`#925`](https://github.com/VulcanJS/Vulcan/pull/925)\n- Change color names in email package [`#908`](https://github.com/VulcanJS/Vulcan/pull/908)\n- Fix #903 [`#905`](https://github.com/VulcanJS/Vulcan/pull/905)\n- fix #1001 [`#1001`](https://github.com/VulcanJS/Vulcan/issues/1001)\n- refactor views menu code to fix #1000 [`#1000`](https://github.com/VulcanJS/Vulcan/issues/1000)\n- make subscribeUserOnCreation callback run asynchronously to fix #933 [`#933`](https://github.com/VulcanJS/Vulcan/issues/933)\n- fix #974 [`#974`](https://github.com/VulcanJS/Vulcan/issues/974)\n- fix #972 and fix uninvited users being allowed to post bug [`#972`](https://github.com/VulcanJS/Vulcan/issues/972)\n- fix #952 [`#952`](https://github.com/VulcanJS/Vulcan/issues/952)\n- fix #955 [`#955`](https://github.com/VulcanJS/Vulcan/issues/955)\n- check post existence before access on postUsers publication [`#915`](https://github.com/VulcanJS/Vulcan/issues/915)\n- Merge pull request #905 from saimeunt/devel [`#903`](https://github.com/VulcanJS/Vulcan/issues/903)\n- Fix #903 [`#903`](https://github.com/VulcanJS/Vulcan/issues/903)\n- Revert \"Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'.\" [`a6a904e`](https://github.com/VulcanJS/Vulcan/commit/a6a904e8c58f2e66eb0b1dd0d7e30230d28981a5)\n- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`caa7ae4`](https://github.com/VulcanJS/Vulcan/commit/caa7ae421dcbd42d707459d20df721b35a2dd5dc)\n- owner -&gt; member; set allow/deny for posts, comments, users [`fc8af1c`](https://github.com/VulcanJS/Vulcan/commit/fc8af1c9dac8c9affd4b7940c2e0f8eaf190631d)\n\n#### [v0.15.1](https://github.com/VulcanJS/Vulcan/compare/v0.15.1-rc...v0.15.1)\n\n> 9 April 2015\n\n- adding pages package [`2b05abf`](https://github.com/VulcanJS/Vulcan/commit/2b05abf527936e2201d85b1d262b65f2f9a8ba47)\n- collapse user menu by default [`cb7b416`](https://github.com/VulcanJS/Vulcan/commit/cb7b4164ab4d83edf3227525a19b7525de3bc9e1)\n- don't display pages menu if there are no pages [`b7d3898`](https://github.com/VulcanJS/Vulcan/commit/b7d38982ff0e4b0b42e843741f51de52ef6a7b48)\n\n#### [v0.15.1-rc](https://github.com/VulcanJS/Vulcan/compare/0.14.3...v0.15.1-rc)\n\n> 8 April 2015\n\n- Swedish translation [`#880`](https://github.com/VulcanJS/Vulcan/pull/880)\n- Additional accessibility fixes [`#896`](https://github.com/VulcanJS/Vulcan/pull/896)\n- Fix a bug where the post submit autoform hook wasn't called [`#883`](https://github.com/VulcanJS/Vulcan/pull/883)\n- deleted unnecessary dot [`#884`](https://github.com/VulcanJS/Vulcan/pull/884)\n- post-by-feed: Normalize encoding to utf-8 [`#882`](https://github.com/VulcanJS/Vulcan/pull/882)\n- Unify posts hooks [`#875`](https://github.com/VulcanJS/Vulcan/pull/875)\n- Update fr.i18n.json [`#867`](https://github.com/VulcanJS/Vulcan/pull/867)\n- Improve comments loading performance on long threads [`#860`](https://github.com/VulcanJS/Vulcan/pull/860)\n- Use this.userId, not Meteor.user in publications [`#855`](https://github.com/VulcanJS/Vulcan/pull/855)\n- Add userId param to changeEmail method [`#854`](https://github.com/VulcanJS/Vulcan/pull/854)\n- Validate post's categories on server. [`#835`](https://github.com/VulcanJS/Vulcan/pull/835)\n- Fix: numberOfItemsInPast24Hours always returns 0 [`#830`](https://github.com/VulcanJS/Vulcan/pull/830)\n- fix bug where last character in search keyword couldn't be cleared [`#833`](https://github.com/VulcanJS/Vulcan/pull/833)\n- Added missing translations in Brazilian Portuguese [`#837`](https://github.com/VulcanJS/Vulcan/pull/837)\n- missing `var` keyword for `defaultProperties` [`#827`](https://github.com/VulcanJS/Vulcan/pull/827)\n- Fix #887 (thanks @kai101) [`#887`](https://github.com/VulcanJS/Vulcan/issues/887)\n- Fix #892 (feeds not getting imported) [`#892`](https://github.com/VulcanJS/Vulcan/issues/892)\n- Set canonical URL without overriding other params [`#878`](https://github.com/VulcanJS/Vulcan/issues/878)\n- post-by-feed: Normalize encoding to utf-8 [`#729`](https://github.com/VulcanJS/Vulcan/issues/729)\n- fix #868 [`#868`](https://github.com/VulcanJS/Vulcan/issues/868)\n- Fix #822 [`#822`](https://github.com/VulcanJS/Vulcan/issues/822)\n- update fourseven:sass (fix #859) [`#859`](https://github.com/VulcanJS/Vulcan/issues/859)\n- updating AutoForm (fix #834) [`#834`](https://github.com/VulcanJS/Vulcan/issues/834)\n- Use this.userId, not Meteor.user in publications [`#853`](https://github.com/VulcanJS/Vulcan/issues/853)\n- Add userId param to changeEmail method [`#852`](https://github.com/VulcanJS/Vulcan/issues/852)\n- Fix #754 [`#754`](https://github.com/VulcanJS/Vulcan/issues/754)\n- Settings package [`057580b`](https://github.com/VulcanJS/Vulcan/commit/057580b7937c2cef3ce632fe3550fafca9ae2cc4)\n- Translated to Swedish. [`208c154`](https://github.com/VulcanJS/Vulcan/commit/208c1546285f2c6119c95b745348b7669bcd124c)\n- Update SEO package for master, remove page titles [`c3c8aab`](https://github.com/VulcanJS/Vulcan/commit/c3c8aab94a61e60b712d9df5be5cb8168e978d72)\n\n#### [0.14.3](https://github.com/VulcanJS/Vulcan/compare/v0.14.2...0.14.3)\n\n> 16 March 2015\n\n- Refactor postAfterEditMethodCallbacks execution on server [`#814`](https://github.com/VulcanJS/Vulcan/pull/814)\n- Correct base score calculation in vote.js [`#811`](https://github.com/VulcanJS/Vulcan/pull/811)\n- Mailchimp limits email subject to 150 characters [`#783`](https://github.com/VulcanJS/Vulcan/pull/783)\n- Fix broken twitter avatars (for real this time) [`#810`](https://github.com/VulcanJS/Vulcan/pull/810)\n- Fix broken twitter avatars [`#809`](https://github.com/VulcanJS/Vulcan/pull/809)\n- Improved vote accessability for packages [`#797`](https://github.com/VulcanJS/Vulcan/pull/797)\n- Better spanish [`#795`](https://github.com/VulcanJS/Vulcan/pull/795)\n- Add greek translation, fix english translation [`#794`](https://github.com/VulcanJS/Vulcan/pull/794)\n- extraCode helper in layout template was not defined [`#785`](https://github.com/VulcanJS/Vulcan/pull/785)\n- Various accessibility fixes, mainly hiding unnecessary elements from scr... [`#766`](https://github.com/VulcanJS/Vulcan/pull/766)\n- update to bengott:avatar version 0.7.5 [`#799`](https://github.com/VulcanJS/Vulcan/pull/799)\n- fix #790 [`#790`](https://github.com/VulcanJS/Vulcan/issues/790)\n- add new Greek i18n translation [`1c920e4`](https://github.com/VulcanJS/Vulcan/commit/1c920e4d3648f2eba76a2de17a073f7a523df1c6)\n- refactoring sidebar menu to use main nav [`d3665b9`](https://github.com/VulcanJS/Vulcan/commit/d3665b98959032471bb6aa0251cbc3ec0deca732)\n- side nav prototype [`63e2aca`](https://github.com/VulcanJS/Vulcan/commit/63e2acabf5e2d9392528c876760794c06d0e448e)\n\n#### [v0.14.2](https://github.com/VulcanJS/Vulcan/compare/v0.14.1...v0.14.2)\n\n> 23 February 2015\n\n- More bulgarian translations [`#781`](https://github.com/VulcanJS/Vulcan/pull/781)\n- added swedish [`1952221`](https://github.com/VulcanJS/Vulcan/commit/19522215f7784cd825ac08d514c786130c727469)\n- rebuild user management page with reactive-table [`8fd9de3`](https://github.com/VulcanJS/Vulcan/commit/8fd9de3266264c3cbeffdca959afb1ec60350745)\n- auth methods are now a setting [`c8e1d60`](https://github.com/VulcanJS/Vulcan/commit/c8e1d608113f5fb90e04a77cae8ae72410d83fd0)\n\n#### [v0.14.1](https://github.com/VulcanJS/Vulcan/compare/v0.14.0...v0.14.1)\n\n> 11 February 2015\n\n- Fixed CSS [`#753`](https://github.com/VulcanJS/Vulcan/pull/753)\n- Typo fix [`#752`](https://github.com/VulcanJS/Vulcan/pull/752)\n- Small improvements [`#748`](https://github.com/VulcanJS/Vulcan/pull/748)\n- polish translation - only lang files [`#747`](https://github.com/VulcanJS/Vulcan/pull/747)\n- Brazilian portuguese translation [`#744`](https://github.com/VulcanJS/Vulcan/pull/744)\n- Vietnamese translation [`#736`](https://github.com/VulcanJS/Vulcan/pull/736)\n- Fix Google Analytics [`#741`](https://github.com/VulcanJS/Vulcan/pull/741)\n- Update tr.i18n.json [`#738`](https://github.com/VulcanJS/Vulcan/pull/738)\n- Es translation missing for es.i18.json [`#735`](https://github.com/VulcanJS/Vulcan/pull/735)\n- Fixes #719. Allowing mobile nav to close if user clicks anywhere outside of it [`#724`](https://github.com/VulcanJS/Vulcan/pull/724)\n- Changed Sign-up to Register [`#726`](https://github.com/VulcanJS/Vulcan/pull/726)\n- tweak button color specificity so that social sign-in button color is not affected (fix #481) [`#481`](https://github.com/VulcanJS/Vulcan/issues/481)\n- fix #480 [`#480`](https://github.com/VulcanJS/Vulcan/issues/480)\n- fix #699 [`#699`](https://github.com/VulcanJS/Vulcan/issues/699)\n- *really* fix #743 [`#743`](https://github.com/VulcanJS/Vulcan/issues/743)\n- Revert \"fix #743\" [`#743`](https://github.com/VulcanJS/Vulcan/issues/743)\n- fix #743 [`#743`](https://github.com/VulcanJS/Vulcan/issues/743)\n- fix #742 [`#742`](https://github.com/VulcanJS/Vulcan/issues/742)\n- fix #737 [`#737`](https://github.com/VulcanJS/Vulcan/issues/737)\n- Merge pull request #724 from anthonymayer/mobile-nav-click-outside [`#719`](https://github.com/VulcanJS/Vulcan/issues/719)\n- Update _posts.scss [`a774b5b`](https://github.com/VulcanJS/Vulcan/commit/a774b5b2e4fcfbf118ec37d405e7ba771b4df60c)\n- Translated to Brazilian Portuguese Completed [`34e7d0e`](https://github.com/VulcanJS/Vulcan/commit/34e7d0e113e859800f3fee8ec5d351a52dbfff55)\n- Update vn.i18n.json [`eef8520`](https://github.com/VulcanJS/Vulcan/commit/eef8520c58d92e7b9cd481d73b4b3d090a90bdf6)\n\n#### [v0.14.0](https://github.com/VulcanJS/Vulcan/compare/v0.14.0-rc...v0.14.0)\n\n> 27 January 2015\n\n- change sign in for register [`#722`](https://github.com/VulcanJS/Vulcan/pull/722)\n- Adding newsletter time setting [`#712`](https://github.com/VulcanJS/Vulcan/pull/712)\n- Update _posts.scss [`#716`](https://github.com/VulcanJS/Vulcan/pull/716)\n- Fixes #719. Allowing mobile nav to close if user clicks anywhere outside of it. [`#719`](https://github.com/VulcanJS/Vulcan/issues/719)\n- fixing mobile version for grid layout [`1aefbea`](https://github.com/VulcanJS/Vulcan/commit/1aefbea3cdd7cefc71bf6bd83710cb75ba4c640c)\n- fix bug preventing posting comments [`a0ebc73`](https://github.com/VulcanJS/Vulcan/commit/a0ebc73cf7f5c0176ca935343d1892aa726ed933)\n- fixing email notification templates [`c38c1c6`](https://github.com/VulcanJS/Vulcan/commit/c38c1c64348ac7aeb181a0c6a4481e389dcfd625)\n\n#### [v0.14.0-rc](https://github.com/VulcanJS/Vulcan/compare/v0.13.0...v0.14.0-rc)\n\n> 21 January 2015\n\n- Cleaning up vote click handling functions and adding tests. [`#708`](https://github.com/VulcanJS/Vulcan/pull/708)\n- Making both Travis and CodeClimate integrations works [`#706`](https://github.com/VulcanJS/Vulcan/pull/706)\n- adding subscribe-to-posts package [`cf01d01`](https://github.com/VulcanJS/Vulcan/commit/cf01d01dbd242ebf21350dfb6d9a5afd8bbd2b6c)\n- organising posts css [`b3804e4`](https://github.com/VulcanJS/Vulcan/commit/b3804e43ca26f00d29ff0a11468ebc6a3361b3c6)\n- working on grid layout; added callback for injecting CSS classes for post items [`35ae630`](https://github.com/VulcanJS/Vulcan/commit/35ae630ebdd89e8667b449318e48a42104ad78ae)\n\n#### [v0.13.0](https://github.com/VulcanJS/Vulcan/compare/v0.12.1...v0.13.0)\n\n> 18 January 2015\n\n- enabled trim and lowercase option for username field [`#696`](https://github.com/VulcanJS/Vulcan/pull/696)\n- https is better [`#694`](https://github.com/VulcanJS/Vulcan/pull/694)\n- Update posts.js [`#686`](https://github.com/VulcanJS/Vulcan/pull/686)\n- Getting rid of redundant permissions functions. [`#672`](https://github.com/VulcanJS/Vulcan/pull/672)\n- add html to .editorconfig [`#651`](https://github.com/VulcanJS/Vulcan/pull/651)\n- Update helpers.js [`#677`](https://github.com/VulcanJS/Vulcan/pull/677)\n- Add Bulgarian translation [`#669`](https://github.com/VulcanJS/Vulcan/pull/669)\n- remove unnecessary decodeUrl (fix #675) [`#675`](https://github.com/VulcanJS/Vulcan/issues/675)\n- Getting rid of redundant permissions functions [`f9d9891`](https://github.com/VulcanJS/Vulcan/commit/f9d9891fba27cfbb404f440c47ac06dc55b2c741)\n- fixing newsletter sync/async issue [`1fd47b2`](https://github.com/VulcanJS/Vulcan/commit/1fd47b23f023aad730a1e89bd2ebf7911472a15f)\n- rename files in singleDay package [`47ace39`](https://github.com/VulcanJS/Vulcan/commit/47ace39e26b492e1da19bbe064382732d181e756)\n\n#### [v0.12.1](https://github.com/VulcanJS/Vulcan/compare/v0.12.0-pre...v0.12.1)\n\n> 5 January 2015\n\n- clean up packages [`bc048d2`](https://github.com/VulcanJS/Vulcan/commit/bc048d24d6612737b1cd5ed1e9ea91dedb060764)\n- history & updated getting started [`67671d4`](https://github.com/VulcanJS/Vulcan/commit/67671d4ebd7122479a314d06eefb82c72b739611)\n- disabling tests for now [`b75355d`](https://github.com/VulcanJS/Vulcan/commit/b75355d89db85712188d92bd3f7ce28b10ec9b31)\n\n#### [v0.12.0](https://github.com/VulcanJS/Vulcan/compare/v0.11.1...v0.12.0)\n\n> 3 January 2015\n\n- Adding nav client unit test [`#662`](https://github.com/VulcanJS/Vulcan/pull/662)\n- make primary and secondary nav sortable (fix #642) [`#642`](https://github.com/VulcanJS/Vulcan/issues/642)\n- export PostsDigestController (fix #643) [`#643`](https://github.com/VulcanJS/Vulcan/issues/643)\n- working on getting started package [`6a8a6ee`](https://github.com/VulcanJS/Vulcan/commit/6a8a6ee8bb007eeef31f46ad8e131ad18588164c)\n- make release notes into a package [`ecad51b`](https://github.com/VulcanJS/Vulcan/commit/ecad51bbbd764405649829e5346b3f8a12dfcd11)\n- clean-up [`778c08d`](https://github.com/VulcanJS/Vulcan/commit/778c08d544dd22d7b31bd1be79a43a9df13baeac)\n\n#### [v0.12.0-pre](https://github.com/VulcanJS/Vulcan/compare/v0.12.0...v0.12.0-pre)\n\n> 5 January 2015\n\n- Add Bulgarian translation [`5ef1693`](https://github.com/VulcanJS/Vulcan/commit/5ef1693e44651541e536605f0219e243b9d6f54a)\n- renaming viewNav to viewsMenu and adminNav to adminMenu [`f5354bf`](https://github.com/VulcanJS/Vulcan/commit/f5354bf69da2f592c999544fa65a3225a166fb57)\n- css tweaks [`e789511`](https://github.com/VulcanJS/Vulcan/commit/e789511d8baeb752c4a559547d64b482a1f88ae5)\n\n#### [v0.11.1](https://github.com/VulcanJS/Vulcan/compare/v0.11.0...v0.11.1)\n\n> 29 December 2014\n\n- fixed migrations.js when telescope-tags are removed [`#656`](https://github.com/VulcanJS/Vulcan/pull/656)\n- Couple of small Newsletter fixes [`#655`](https://github.com/VulcanJS/Vulcan/pull/655)\n- Update zh-CN.i18n.json [`#652`](https://github.com/VulcanJS/Vulcan/pull/652)\n- Update to bengott:avatar@0.7.3 [`#647`](https://github.com/VulcanJS/Vulcan/pull/647)\n- Update to bengott:avatar@0.7.2 [`#645`](https://github.com/VulcanJS/Vulcan/pull/645)\n- Update to bengott:avatar@0.7.1 [`#644`](https://github.com/VulcanJS/Vulcan/pull/644)\n- update to bengott:avatar@0.7.0 [`#640`](https://github.com/VulcanJS/Vulcan/pull/640)\n- refactor voting code to accept function calls from server [`24a0f9b`](https://github.com/VulcanJS/Vulcan/commit/24a0f9b8306c8cfaf5d76840872e7cc672b419ba)\n- subscribe post [`f6583aa`](https://github.com/VulcanJS/Vulcan/commit/f6583aad5e21d8541b70da7cadf59d9aabf1c536)\n- working on post-by-feed package [`0b751d0`](https://github.com/VulcanJS/Vulcan/commit/0b751d086c52a9d59ab21eee6028349f7c5cd1d4)\n\n#### [v0.11.0](https://github.com/VulcanJS/Vulcan/compare/v0.10.0...v0.11.0)\n\n> 17 December 2014\n\n- Add editorconfig for consistency [`#636`](https://github.com/VulcanJS/Vulcan/pull/636)\n- Minor tweaks [`#634`](https://github.com/VulcanJS/Vulcan/pull/634)\n- Russian translation [`#629`](https://github.com/VulcanJS/Vulcan/pull/629)\n- Fix various url problems by taking siteUrl into account when getting route urls. [`#611`](https://github.com/VulcanJS/Vulcan/pull/611)\n- fixed #631 [`#631`](https://github.com/VulcanJS/Vulcan/issues/631)\n- fixed #632 - Update to useraccounts:unstyled@1.4.0 [`#632`](https://github.com/VulcanJS/Vulcan/issues/632)\n- Auto post via RSS urls. Fixes #453 [`#453`](https://github.com/VulcanJS/Vulcan/issues/453)\n- fix #617 [`#617`](https://github.com/VulcanJS/Vulcan/issues/617)\n- Add link for clearing thumbnail (fix #607) [`#607`](https://github.com/VulcanJS/Vulcan/issues/607)\n- use console.log() instead of throwing error to prevent post submit interruption (fix #607) [`#607`](https://github.com/VulcanJS/Vulcan/issues/607)\n- fix digest parameters bug (fix #609) [`#609`](https://github.com/VulcanJS/Vulcan/issues/609)\n- Update SEO package for master, remove page titles [`e25034c`](https://github.com/VulcanJS/Vulcan/commit/e25034c4db0c2776be7dd931d47ca929d21d133c)\n- Refactor for getDescription and package style [`d17c447`](https://github.com/VulcanJS/Vulcan/commit/d17c447561b8722e562bb119d68f197b5b514638)\n- Added Russian translation [`4331407`](https://github.com/VulcanJS/Vulcan/commit/4331407e7563d23608cda37b74ff247fbb4b7167)\n\n#### [v0.10.0](https://github.com/VulcanJS/Vulcan/compare/v0.9.11...v0.10.0)\n\n> 9 December 2014\n\n- Switching from manually generating urls to using IronRouter functions. [`#588`](https://github.com/VulcanJS/Vulcan/pull/588)\n- Fixes #444 - Adding UserEditController to show invites correctly [`#581`](https://github.com/VulcanJS/Vulcan/pull/581)\n- Fixes #562 - Adds site link to email header. [`#587`](https://github.com/VulcanJS/Vulcan/pull/587)\n- Look for settings in Meteor.settings too (fix #561) [`#561`](https://github.com/VulcanJS/Vulcan/issues/561)\n- finish epic editor clean up and fix #591 [`#591`](https://github.com/VulcanJS/Vulcan/issues/591)\n- don't need updateCategoryInPosts method anymore (fix #590) [`#590`](https://github.com/VulcanJS/Vulcan/issues/590)\n- do not make call to CDN when language is english (fix #589) [`#589`](https://github.com/VulcanJS/Vulcan/issues/589)\n- Merge pull request #581 from anthonymayer/invites-cleanup [`#444`](https://github.com/VulcanJS/Vulcan/issues/444)\n- Merge pull request #587 from anthonymayer/email-header-site-link [`#562`](https://github.com/VulcanJS/Vulcan/issues/562)\n- removing Epic Editor files [`17431df`](https://github.com/VulcanJS/Vulcan/commit/17431dfb8717df3fcc42a309321f9ca08db3affc)\n- renaming errors to messages [`b6c54c1`](https://github.com/VulcanJS/Vulcan/commit/b6c54c106da4f72ee25e06c500c7d8a555d9c7c4)\n- extracting digest into its own package [`75bd8d9`](https://github.com/VulcanJS/Vulcan/commit/75bd8d99201961f8d4039c6356701247d4f5d9da)\n\n#### [v0.9.11](https://github.com/VulcanJS/Vulcan/compare/v0.9.10...v0.9.11)\n\n> 3 December 2014\n\n- Fixes #572 - Expands search box when focused or not empty. [`#580`](https://github.com/VulcanJS/Vulcan/pull/580)\n- Fixes #543 - duplicate search logs. [`#571`](https://github.com/VulcanJS/Vulcan/pull/571)\n- Hide mobile nav dropdowns [`#573`](https://github.com/VulcanJS/Vulcan/pull/573)\n- Add Bulgarian-bg translation [`#558`](https://github.com/VulcanJS/Vulcan/pull/558)\n- ru.i18n.json [`#557`](https://github.com/VulcanJS/Vulcan/pull/557)\n- Compiling scss as part of build rather than with compass. [`#547`](https://github.com/VulcanJS/Vulcan/pull/547)\n- Fixes #555. [`#556`](https://github.com/VulcanJS/Vulcan/pull/556)\n- Fix telescope-search route for iron:router 1.0 [`#549`](https://github.com/VulcanJS/Vulcan/pull/549)\n- tr.i18n.json [`#553`](https://github.com/VulcanJS/Vulcan/pull/553)\n- Correcting emailNewPost template [`#554`](https://github.com/VulcanJS/Vulcan/pull/554)\n- Fix #584 [`#584`](https://github.com/VulcanJS/Vulcan/issues/584)\n- Fixes #444 - Adding UserEditController to show invites correctly [`#444`](https://github.com/VulcanJS/Vulcan/issues/444)\n- Merge pull request #580 from anthonymayer/expanding-search-box [`#572`](https://github.com/VulcanJS/Vulcan/issues/572)\n- Fixes #572 - Expands search box when focused or not empty. [`#572`](https://github.com/VulcanJS/Vulcan/issues/572)\n- Merge pull request #571 from anthonymayer/fix-duplicate-search-logs [`#543`](https://github.com/VulcanJS/Vulcan/issues/543)\n- Fixes #562 - Adds site link to email header. [`#562`](https://github.com/VulcanJS/Vulcan/issues/562)\n- Merge pull request #556 from anthonymayer/missing_i18n_keys [`#555`](https://github.com/VulcanJS/Vulcan/issues/555)\n- Fixes #555. [`#555`](https://github.com/VulcanJS/Vulcan/issues/555)\n- create datetimepicker custom field type package [`9617639`](https://github.com/VulcanJS/Vulcan/commit/96176398e30fbfad700045faa2fee01602a61b25)\n- working on post edit form [`6183716`](https://github.com/VulcanJS/Vulcan/commit/618371636de294af45d4486b8ed3902e075134f7)\n- working on post submit form [`672be96`](https://github.com/VulcanJS/Vulcan/commit/672be96c9be7bbbd336b106163e7c2c57a984b5f)\n\n#### [v0.9.10](https://github.com/VulcanJS/Vulcan/compare/v0.9.9-for-real...v0.9.10)\n\n> 25 November 2014\n\n- Upgrade to bengott:avatar 0.6.0 [`#548`](https://github.com/VulcanJS/Vulcan/pull/548)\n- Fixes #541 [`#542`](https://github.com/VulcanJS/Vulcan/pull/542)\n- Search webkit appearance [`#540`](https://github.com/VulcanJS/Vulcan/pull/540)\n- Adding back title setting [`#537`](https://github.com/VulcanJS/Vulcan/pull/537)\n- Merge pull request #542 from anthonymayer/filter-by-links-not-working [`#541`](https://github.com/VulcanJS/Vulcan/issues/541)\n- Fixes #541 [`#541`](https://github.com/VulcanJS/Vulcan/issues/541)\n- Fixes #538 in source scss, rather than in generated css [`#538`](https://github.com/VulcanJS/Vulcan/issues/538)\n- Fixes #538 [`#538`](https://github.com/VulcanJS/Vulcan/issues/538)\n- Compiling scss as part of build rather than with compass. [`30ca412`](https://github.com/VulcanJS/Vulcan/commit/30ca412921c28e5817cc1eb554d0591bac38039b)\n- updating package versions [`f3ddf53`](https://github.com/VulcanJS/Vulcan/commit/f3ddf53cf7f25c15f12931c3e6e067019a192140)\n- internationalizing packages [`0a696ce`](https://github.com/VulcanJS/Vulcan/commit/0a696ce1e3d8ac7ba20803625b6cccaa9a67a2b6)\n\n#### [v0.9.9](https://github.com/VulcanJS/Vulcan/compare/v0.9.8...v0.9.9)\n\n> 18 November 2014\n\n- Splitting out router.js in multiple files. [`23079ff`](https://github.com/VulcanJS/Vulcan/commit/23079ff9f238ecd1b6f79558c7aea57e8254e73b)\n- updating to Meteor 1.0 [`0ceda58`](https://github.com/VulcanJS/Vulcan/commit/0ceda58124bb5e0d30ed7f368196a1780825aa89)\n- Working on IR 1.0 update [`73cb59a`](https://github.com/VulcanJS/Vulcan/commit/73cb59a088cd18180483aa823bc6227d203513b8)\n\n#### [v0.9.9-for-real](https://github.com/VulcanJS/Vulcan/compare/v0.9.9...v0.9.9-for-real)\n\n> 19 November 2014\n\n- Convert translation keys format to tap:i18n standard [`2605dcb`](https://github.com/VulcanJS/Vulcan/commit/2605dcb27c514365933fe69271eeb0e94b8729b5)\n- Convert lang js files to i18n.json [`c9c8f3e`](https://github.com/VulcanJS/Vulcan/commit/c9c8f3ea8df532244f1c9886e1ab85ee438dc1f9)\n- refactor server-side email template routes [`eb08247`](https://github.com/VulcanJS/Vulcan/commit/eb082473ed0f0138591e1968f56a1c1dc2eadf57)\n\n#### [v0.9.8](https://github.com/VulcanJS/Vulcan/compare/v0.9.7...v0.9.8)\n\n> 18 October 2014\n\n- Update to bengott:avatar 0.2.1 [`#493`](https://github.com/VulcanJS/Vulcan/pull/493)\n- Fix email_hash bug (Issue #393) [`#491`](https://github.com/VulcanJS/Vulcan/pull/491)\n- Update to bengott:avatar 0.1.4 [`#488`](https://github.com/VulcanJS/Vulcan/pull/488)\n- Update to use bengott:avatar 0.1.2 [`#487`](https://github.com/VulcanJS/Vulcan/pull/487)\n- Add missing adminMongoQuery and notAdminMongoQuery [`#472`](https://github.com/VulcanJS/Vulcan/pull/472)\n- Add a Gitter chat badge to README.md [`#466`](https://github.com/VulcanJS/Vulcan/pull/466)\n- Kadira package update to latest release 2.11.2 [`#469`](https://github.com/VulcanJS/Vulcan/pull/469)\n- Update it.js [`#468`](https://github.com/VulcanJS/Vulcan/pull/468)\n- Update to use bengott:avatar package for user avatars [`#454`](https://github.com/VulcanJS/Vulcan/pull/454)\n- Add querystring updates to search [`#462`](https://github.com/VulcanJS/Vulcan/pull/462)\n- Fully abstract isAdmin [`#463`](https://github.com/VulcanJS/Vulcan/pull/463)\n- German translation (de.js) [`#458`](https://github.com/VulcanJS/Vulcan/pull/458)\n- Posts rss refactor [`#450`](https://github.com/VulcanJS/Vulcan/pull/450)\n- Hide future posts [`#449`](https://github.com/VulcanJS/Vulcan/pull/449)\n- update to accounts-templates-unstyled 0.9.7 [`#448`](https://github.com/VulcanJS/Vulcan/pull/448)\n- herald integration [`9be1bd7`](https://github.com/VulcanJS/Vulcan/commit/9be1bd7169ce407920019c2f8b65f791b2866a84)\n- working on quick form for post submit [`ccf0ea7`](https://github.com/VulcanJS/Vulcan/commit/ccf0ea7820cadec85747b5c41e45f68c7fc2d34c)\n- Make it possible to hide fields from quickform; cleanup [`73d1098`](https://github.com/VulcanJS/Vulcan/commit/73d1098646b45b943fb0f4f97c241ef77e896399)\n\n#### [v0.9.7](https://github.com/VulcanJS/Vulcan/compare/v0.9.6...v0.9.7)\n\n> 29 September 2014\n\n- Avatar Tweaks [`#438`](https://github.com/VulcanJS/Vulcan/pull/438)\n- Fixed issue that user would always be redirected to \"/\" after sign up and enables validation. [`#433`](https://github.com/VulcanJS/Vulcan/pull/433)\n- Turn Gravatars from random helpers into a component [`#436`](https://github.com/VulcanJS/Vulcan/pull/436)\n- Exclude posts scheduled in the future from the RSS feed [`#431`](https://github.com/VulcanJS/Vulcan/pull/431)\n- fix #441 [`#441`](https://github.com/VulcanJS/Vulcan/issues/441)\n- splitting settings form into field sets [`51de4d7`](https://github.com/VulcanJS/Vulcan/commit/51de4d79db807455ac1cda70fed9110928830e93)\n- updating meteor [`95a2157`](https://github.com/VulcanJS/Vulcan/commit/95a21577686296176c6e114d18a498bd572d5f37)\n- Adding instructions to settings form [`f00ffd8`](https://github.com/VulcanJS/Vulcan/commit/f00ffd8498f5fe6cc4da48d0d4e877c14c7237b5)\n\n#### [v0.9.6](https://github.com/VulcanJS/Vulcan/compare/v0.9.5...v0.9.6)\n\n> 26 September 2014\n\n- Retinize gravatar image size [`#429`](https://github.com/VulcanJS/Vulcan/pull/429)\n- comment rss [`#423`](https://github.com/VulcanJS/Vulcan/pull/423)\n- fix #401 profile url collisions [`#420`](https://github.com/VulcanJS/Vulcan/pull/420)\n- Publication validation [`#377`](https://github.com/VulcanJS/Vulcan/pull/377)\n- Fix #430 [`#430`](https://github.com/VulcanJS/Vulcan/issues/430)\n- Merge pull request #420 from GoodEveningMiss/slugify-collisions [`#401`](https://github.com/VulcanJS/Vulcan/issues/401)\n- fix #401 [`#401`](https://github.com/VulcanJS/Vulcan/issues/401)\n- adding telescope-kadira package [`b54b7b6`](https://github.com/VulcanJS/Vulcan/commit/b54b7b60d88917adcc6df777bd153b07d6e29323)\n- working on CSS [`e04a4e9`](https://github.com/VulcanJS/Vulcan/commit/e04a4e98e3a25e7da117a6a27c0609fe29c102cf)\n- finishing css tweaks [`25f5fcd`](https://github.com/VulcanJS/Vulcan/commit/25f5fcd778af1d5026bc73970ee5e64460cd7060)\n\n#### [v0.9.5](https://github.com/VulcanJS/Vulcan/compare/v0.9.4...v0.9.5)\n\n> 20 September 2014\n\n- Fixes #415: prevent invalid up/downvotes when concurrent requests [`#416`](https://github.com/VulcanJS/Vulcan/pull/416)\n- Corrected path to /forgot-password [`#414`](https://github.com/VulcanJS/Vulcan/pull/414)\n- Update README.nitrous.md [`#412`](https://github.com/VulcanJS/Vulcan/pull/412)\n- Update README.nitrous.md [`#411`](https://github.com/VulcanJS/Vulcan/pull/411)\n- swap order of subtract() args due to deprecation [`#410`](https://github.com/VulcanJS/Vulcan/pull/410)\n- Fix issue #403 - Replaced deprecated \"schema\" property with \"attachSchema\" method. [`#407`](https://github.com/VulcanJS/Vulcan/pull/407)\n- cache jQuery; cleanup [`#404`](https://github.com/VulcanJS/Vulcan/pull/404)\n- Merge pull request #416 from spifd/fix-concurrent-updownvotes [`#415`](https://github.com/VulcanJS/Vulcan/issues/415)\n- Making notifications into their own package [`2a91121`](https://github.com/VulcanJS/Vulcan/commit/2a911217e9fb4637d66276e7d5e881a83b96fd86)\n- added italian locales [`64018cb`](https://github.com/VulcanJS/Vulcan/commit/64018cbf427b55897e94a1905f3ea6c89ad36dfb)\n- cleanup while getting familiar with the codebase [`6fc6b9e`](https://github.com/VulcanJS/Vulcan/commit/6fc6b9eb785d408dbde42db54e8176a743899e50)\n\n#### v0.9.4\n\n> 16 September 2014\n\n- use UI.dynamic for incoming posts template [`#402`](https://github.com/VulcanJS/Vulcan/pull/402)\n- Allow images in body. [`#397`](https://github.com/VulcanJS/Vulcan/pull/397)\n- Correcting the if statement for profile.site url [`#396`](https://github.com/VulcanJS/Vulcan/pull/396)\n- Use epic editor autogrow feature [`#395`](https://github.com/VulcanJS/Vulcan/pull/395)\n- update epiceditor to latest(0.2.2) and unminified version [`#394`](https://github.com/VulcanJS/Vulcan/pull/394)\n- use // instead of http:// for images [`#392`](https://github.com/VulcanJS/Vulcan/pull/392)\n- fix email hash of gravatar [`#391`](https://github.com/VulcanJS/Vulcan/pull/391)\n- uncommented and line 18 [`#384`](https://github.com/VulcanJS/Vulcan/pull/384)\n- Fix header logo position [`#380`](https://github.com/VulcanJS/Vulcan/pull/380)\n- Adds comments to API [`#378`](https://github.com/VulcanJS/Vulcan/pull/378)\n- Remove unused signin [`#376`](https://github.com/VulcanJS/Vulcan/pull/376)\n- update bootstrap datepicker [`#369`](https://github.com/VulcanJS/Vulcan/pull/369)\n- Changed the default sign-in route from /signin to /sign-in [`#367`](https://github.com/VulcanJS/Vulcan/pull/367)\n- Small customization enhancements and fix [`#351`](https://github.com/VulcanJS/Vulcan/pull/351)\n- Don't iterate all the users for finding who to send notifications to. [`#338`](https://github.com/VulcanJS/Vulcan/pull/338)\n- Fix digest date issues [`#321`](https://github.com/VulcanJS/Vulcan/pull/321)\n- fixed - not showing user profiles [`#308`](https://github.com/VulcanJS/Vulcan/pull/308)\n- [fix] undefined title in posts [`#305`](https://github.com/VulcanJS/Vulcan/pull/305)\n- Fixed locale en [`#285`](https://github.com/VulcanJS/Vulcan/pull/285)\n- Translations variable change to object [`#286`](https://github.com/VulcanJS/Vulcan/pull/286)\n- fix for downvoting comments [`#278`](https://github.com/VulcanJS/Vulcan/pull/278)\n- fixed typo related to profile picture Fetching [`#275`](https://github.com/VulcanJS/Vulcan/pull/275)\n- Facebook integration [`#273`](https://github.com/VulcanJS/Vulcan/pull/273)\n- Added Hack on Nitrous.IO button [`#271`](https://github.com/VulcanJS/Vulcan/pull/271)\n- specify required versions of iron router and meteor in smart.json [`#268`](https://github.com/VulcanJS/Vulcan/pull/268)\n- \"New Posts\" string for en.js was in French [`#264`](https://github.com/VulcanJS/Vulcan/pull/264)\n- better redirection,error msg after post is deleted [`#263`](https://github.com/VulcanJS/Vulcan/pull/263)\n- fixed issue for broken redirection to template after commment deletion [`#262`](https://github.com/VulcanJS/Vulcan/pull/262)\n- Fixes #255: now canView do not wait for 'settingsLoaded' which was removed in e622c112 [`#256`](https://github.com/VulcanJS/Vulcan/pull/256)\n- Use the outgoing click tracking for rss [`#250`](https://github.com/VulcanJS/Vulcan/pull/250)\n- updated meteor to latest version [`#245`](https://github.com/VulcanJS/Vulcan/pull/245)\n- categories are sorted by name [`#244`](https://github.com/VulcanJS/Vulcan/pull/244)\n- Fix new post checkbox [`#242`](https://github.com/VulcanJS/Vulcan/pull/242)\n- Only show category list on post submit/edit if there are categories [`#240`](https://github.com/VulcanJS/Vulcan/pull/240)\n- Best practice to pass object than to check for optional parameter value [`#235`](https://github.com/VulcanJS/Vulcan/pull/235)\n- added fast-render support [`#228`](https://github.com/VulcanJS/Vulcan/pull/228)\n- #220: now all pages are waitOn('categories') [`#227`](https://github.com/VulcanJS/Vulcan/pull/227)\n- #218: post_submit: &lt;input name=category&gt;s are now checkboxes, not radio [`#223`](https://github.com/VulcanJS/Vulcan/pull/223)\n- 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)\n- #217: fixed bug with 'You have to be an admin' message displayed to admins [`#222`](https://github.com/VulcanJS/Vulcan/pull/222)\n- #213: symlinks was removed from /packages/ - they should be locally created by Meteorite [`#221`](https://github.com/VulcanJS/Vulcan/pull/221)\n- #194: fixed bug with preserving category name in posts after renaming [`#219`](https://github.com/VulcanJS/Vulcan/pull/219)\n- #184: fixed subscription to Notifications collection [`#216`](https://github.com/VulcanJS/Vulcan/pull/216)\n- 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)\n- For for #209: createNotifications() is a server-side function now [`#214`](https://github.com/VulcanJS/Vulcan/pull/214)\n- Update from depreciated style events [`#212`](https://github.com/VulcanJS/Vulcan/pull/212)\n- Spanish revised [`#205`](https://github.com/VulcanJS/Vulcan/pull/205)\n- missing comma on line 178 [`#199`](https://github.com/VulcanJS/Vulcan/pull/199)\n- add  i18n chinese support [`#195`](https://github.com/VulcanJS/Vulcan/pull/195)\n- Add Spanish i18n [`#198`](https://github.com/VulcanJS/Vulcan/pull/198)\n- Update Events declaration style [`#188`](https://github.com/VulcanJS/Vulcan/pull/188)\n- Nice search transition [`#187`](https://github.com/VulcanJS/Vulcan/pull/187)\n- Use higher quality gravatar image [`#168`](https://github.com/VulcanJS/Vulcan/pull/168)\n- Fix subscription for spiderable [`#167`](https://github.com/VulcanJS/Vulcan/pull/167)\n- Use cursor to iterate lists of users [`#166`](https://github.com/VulcanJS/Vulcan/pull/166)\n- Fix downvoting, cancelling upvoting & cancelling downvoting [`#164`](https://github.com/VulcanJS/Vulcan/pull/164)\n- Show nothing instead of null [`#163`](https://github.com/VulcanJS/Vulcan/pull/163)\n- Have no title if there isn't a title set instead of undefined [`#162`](https://github.com/VulcanJS/Vulcan/pull/162)\n- Don't trust client ids [`#161`](https://github.com/VulcanJS/Vulcan/pull/161)\n- Fix ability to delete posts [`#160`](https://github.com/VulcanJS/Vulcan/pull/160)\n- Prevent weird deploy problem on some versions of node [`#155`](https://github.com/VulcanJS/Vulcan/pull/155)\n- Check that people setting post.userId are actually admins before we set it [`#153`](https://github.com/VulcanJS/Vulcan/pull/153)\n- Move analyticsRequest() [`#148`](https://github.com/VulcanJS/Vulcan/pull/148)\n- delete post comments too when a post is deleted [`#136`](https://github.com/VulcanJS/Vulcan/pull/136)\n- Added Error for Trying to Post Empty Comments [`#135`](https://github.com/VulcanJS/Vulcan/pull/135)\n- Set document title to post headline [`#125`](https://github.com/VulcanJS/Vulcan/pull/125)\n- Update epic-light.css to set a minimum height for the 'Message' text ent... [`#133`](https://github.com/VulcanJS/Vulcan/pull/133)\n- Make upvote cleanup prior downvote and vice-versa [`#127`](https://github.com/VulcanJS/Vulcan/pull/127)\n- Duplicate Template.post_submit.rendered assignment in post_submit.js [`#119`](https://github.com/VulcanJS/Vulcan/pull/119)\n- update google+ share button style [`#123`](https://github.com/VulcanJS/Vulcan/pull/123)\n- Added a template helper to address '1 points' [`#115`](https://github.com/VulcanJS/Vulcan/pull/115)\n- Fix for nothing happening when editing another user [`#109`](https://github.com/VulcanJS/Vulcan/pull/109)\n- 1 comments -&gt; 1 comment [`#104`](https://github.com/VulcanJS/Vulcan/pull/104)\n- fix avatar in user_profile page for oauth-login [`#98`](https://github.com/VulcanJS/Vulcan/pull/98)\n- Adjust Logout button size for consistency [`#96`](https://github.com/VulcanJS/Vulcan/pull/96)\n- Updating deny.update() and deny.remove() to v0.5.8 [`#95`](https://github.com/VulcanJS/Vulcan/pull/95)\n- Add ability to pass 'limit' query string parameter [`#90`](https://github.com/VulcanJS/Vulcan/pull/90)\n- Finally extracted database-forms into its own package [`#77`](https://github.com/VulcanJS/Vulcan/pull/77)\n- Loading class is not removed if no url is provided on the Post page [`#66`](https://github.com/VulcanJS/Vulcan/pull/66)\n- Commenting was broken  [`#62`](https://github.com/VulcanJS/Vulcan/pull/62)\n- Use generic getAvatarUrl instead of Gravatar. [`#54`](https://github.com/VulcanJS/Vulcan/pull/54)\n- Add post link to mobile nav [`#47`](https://github.com/VulcanJS/Vulcan/pull/47)\n- Update README.md [`#46`](https://github.com/VulcanJS/Vulcan/pull/46)\n- Towards generic forms [`#31`](https://github.com/VulcanJS/Vulcan/pull/31)\n- Various [`#9`](https://github.com/VulcanJS/Vulcan/pull/9)\n- User karma [`#7`](https://github.com/VulcanJS/Vulcan/pull/7)\n- Use this.userId() in publish rather than an arg. [`#6`](https://github.com/VulcanJS/Vulcan/pull/6)\n- Meteor includes json2.js [`#5`](https://github.com/VulcanJS/Vulcan/pull/5)\n- User profile pages [`#4`](https://github.com/VulcanJS/Vulcan/pull/4)\n- Added rendered hooks (fix #330) [`#330`](https://github.com/VulcanJS/Vulcan/issues/330)\n- fix #347 [`#347`](https://github.com/VulcanJS/Vulcan/issues/347)\n- fix #320 [`#320`](https://github.com/VulcanJS/Vulcan/issues/320)\n- fix #333 [`#333`](https://github.com/VulcanJS/Vulcan/issues/333)\n- fix #331 [`#331`](https://github.com/VulcanJS/Vulcan/issues/331)\n- fix #329 [`#329`](https://github.com/VulcanJS/Vulcan/issues/329)\n- fix #327 [`#327`](https://github.com/VulcanJS/Vulcan/issues/327)\n- 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)\n- Merge pull request #256 from yeputons/issue-255 [`#255`](https://github.com/VulcanJS/Vulcan/issues/255)\n- Fixes #255: now canView do not wait for 'settingsLoaded' which was removed in e622c112 [`#255`](https://github.com/VulcanJS/Vulcan/issues/255)\n- fix #179 [`#179`](https://github.com/VulcanJS/Vulcan/issues/179)\n- publish mailchimp data for admins (fix #176) [`#176`](https://github.com/VulcanJS/Vulcan/issues/176)\n- Fix #159 [`#159`](https://github.com/VulcanJS/Vulcan/issues/159)\n- Fixed #93 [`#93`](https://github.com/VulcanJS/Vulcan/issues/93)\n- Separating themes; adding accounts-entry [`4ad0201`](https://github.com/VulcanJS/Vulcan/commit/4ad020174c76c7b0699ee1fb0dfb06dc3204d669)\n- update epiceditor to latest and unminified version [`6de9c35`](https://github.com/VulcanJS/Vulcan/commit/6de9c35cb41168b4a438a50669b9ec02386548e7)\n- add embedly and newsletter packages [`733f367`](https://github.com/VulcanJS/Vulcan/commit/733f367f37e81cca54ebe7d3d9a54e6e8755cd9c)\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at hello@vulcanjs.org. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Etiquette\n\n- **All PRs should be made to the `devel` branch, not `master`.**\n\n- Come check-in in the [Vulcan Slack channel](http://slack.telescopeapp.org/). 👋\n\n- 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.\n\n- ~~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!\n\n- Be nice 😉\n\n## Branches\n\n- `master` branch matches the latest version published on Atmosphere\n- `devel` branch is the bleeding edge\n- 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.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM abernix/meteord:onbuild"
  },
  {
    "path": "MIGRATING.md",
    "content": "# Migrations\n\nDoc to help updating downstream applications. Breaking changes and packages updates are listed here.\n\nPlease open an issue or a pull request if you feel this doc is incomplete.\n\n## Updating Meteor\n\n- 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\n- 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.\n- 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.\n\n## From 1.16 to 1.16.1\n\n- `meteor update`\n- `meteor npm i --save string-similarity @apollo/client`\n- Migrate your code to Apollo client v3: https://www.apollographql.com/docs/react/migrating/apollo-client-3-migration/\n- 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`.\n \n## From 1.15 to 1.16\n\n- `meteor npm i --save node-cache`\n- Read Vulcan blog article related to 1.16\n- Schemas without \"_id\" or \"userId\" won't have those fields in the default form fragment anymore (extremely edge case)\n\n## From 1.14.1 to 1.15\n\n- Update Meteor with `meteor update`\n- /!\\ Carefully update NPM packages versions based on the current package.json, otherwise install will fail\n- `single2` hoc and hooks will return the whole `error` object, not just `error.graphQLErrors[0]`. This will help catching network errors too.\n- Install `npm i --save body-parser-graphql`\n- CORS are now disabled as a default in production. Use `apolloServer.corsWhitelist` to whitelist some domains, or `apolloServer.corsEnableAll` to allow all \nconnections.\n\n\n## From 1.13.5 to 1.14\n\n- See migration article from [Vulcan Blog](https://blog.vulcanjs.org/)\n- `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/)\n- `validateModifier` takes `data` as the second param (`validateModifier(modifier, data, document)` instead of `validateModifier(modifier, document)`)\n\n### Material UI\n- Update to v4 `meteor npm i --save-exact @material-ui/core@4.5.1`\n- `import MuiThemeProvider from @material-ui/core/styles/MuiThemeProvider\"` becomes `import { MuiThemeProvider } from \"@material-ui/core/styles\"`\n- More broadly follow https://material-ui.com/guides/migration-v3/ to update Material UI to v4\n- Follow the composition doc to handle `forwardRef` warnings: https://material-ui.com/guides/composition/#caveat-with-refs\n\n## From 1.13.3 to 1.13.5\n\n- `npm install apollo-utilities` (to run tests)\n- Replace `Users.getViewableFields` by `Users.getReadableProjection` \n\n\n## From 1.13.2 to 1.13.3\n\n- Update React to a version over 16.8 (and under 17 which will bring breaking changes) to access hooks\n- 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`\n- `compose` is not exported by `react-apollo`, use `recompose` instead.\n- More broadly see [`react-apollo` changelog](https://github.com/apollographql/react-apollo/blob/master/Changelog.md) for breaking changes\n- `editMutation`, `newMutation` etc. are deprecated, use the new `updateFoo`, `createFoo` syntax. An error message is thrown where deprecated mutations are used to help debugging\n- 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.\nExample call: `useMulti({collection: Foos, queryOptions: { errorPolicy: \"all\" } })`.\n- No need to call `registerComponent` anymore to use Vulcan HOC. You can call them directly even if the underlying fragment is not yet registered.\n- Watched Mutations has been removed because it didn't work anymore, in favour to better Apollo's `update` option for mutations.\n\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://d3vv6lp55qjaqc.cloudfront.net/items/2B3C1z2V2y421p2I0P42/vulcan-logo-noborder.png\" width=\"200\">\n\n# The repository is now archived.\n\nTime 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)\n\n[![Backers on Open Collective](https://opencollective.com/vulcan/backers/badge.svg)](#backers)\n\n [![Sponsors on Open Collective](https://opencollective.com/vulcan/sponsors/badge.svg)](#sponsors) \n\n# Vulcan\n\n\nVulcan is a React+GraphQL framework for Meteor. \n\n[You might want to discover Vulcan Next](https://github.com/VulcanJS/vulcan-next), a port of Vulcan toward Next.js.\n\n### Install\n\n- [Full video tutorial](https://www.youtube.com/watch?v=aCjR9UrNqVk)\n\nInstall the latest version of Node and NPM. We recommend the usage of [NVM](http://nvm.sh).\n\nYou can then install [Meteor](https://www.meteor.com/install), which is used as the Vulcan build tool.\n\nClone the [Vulcan Starter repo](https://github.com/VulcanJS/Vulcan-Starter) locally.\n\nRename your `sample_settings.json` file to `settings.json`, then:\n\n```sh\nmeteor npm install\nmeteor npm start\n```\n\nAnd open `http://localhost:3000/` in your browser.\n\n\nFind more info in the [documentation](http://docs.vulcanjs.org/#Install).\n\n### Links\n\n- [Vulcan Homepage](http://vulcanjs.org)\n- [Documentation](http://docs.vulcanjs.org)\n- [Old Telescope Homepage](http://www.telescopeapp.org)\n\n### Other Versions\n\n[See all releases](https://github.com/VulcanJS/Vulcan/releases).\n\nTo update an existing Vulcan app, [see migration doc](MIGRATING.md)) and [changelog](CHANGELOG.md).\n\nYou can find the older, non-Apollo version of Telescope Nova on the [nova-classic](https://github.com/VulcanJS/Vulcan/tree/nova-classic) branch. \n\nYou can find the even older, non-React version of Telescope on the [legacy](https://github.com/VulcanJS/Vulcan/tree/legacy) branch.\n\n## Credits\n\n### Contributors\n\nThis project exists thanks to all the people who contribute.\n\n<a href=\"https://github.com/VulcanJS/Vulcan/graphs/contributors\"><img src=\"https://opencollective.com/vulcan/contributors.svg?width=890&button=false\" /></a>\n\n### Backers\n\nThank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/vulcan#contribute)]\n\n<a href=\"https://opencollective.com/vulcan#contributors\" target=\"_blank\"><img src=\"https://opencollective.com/vulcan/backers.svg?width=890\"></a>\n\n### Sponsors\n\nSupport 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)]\n\n<a href=\"https://opencollective.com/vulcan#contributors\" target=\"_blank\"><img src=\"https://opencollective.com/vulcan/sponsors.svg?width=890\"></a>\n"
  },
  {
    "path": "RELEASE.md",
    "content": "# Release process\n\n## Updating\n\nWe 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. \n\nChanges will be tracked in the changelog file.\n\n\n### In Vulcan core repository\n\n- 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).\n- Go to a `release/your-version` branch.\n- Cleanup and reinstall everything \n- Run unit tests, and apply relevant fixes \n- Test that Storybook runs correctly\n- Run tests, apply fixes if necessary\n\n```sh\nmeteor reset && rm -Rf node_modules &&  meteor npm install\nmeteor npm run test\nmeteor npm run storybook\n```\n\n- Update packages versions in each package.\n- Update package.json version.\n- Update the CHANGELOG.md.\n\n```sh\nmeteor npm run generate-changelog\n```\n\n- Merge release branch into `devel` (so that fixes from the release branch are shared) and then `master`.\n- Go to `master` branch\n- Create a tag for this version `git tag 1.x.x`.\n- Push with `--tags` option: `git push && git push --tags`\n- Deploy on Atmosphere\n\n### In Vulcan-Starter\n\nNo need to maintain specific branches for versions, as the Starter is only meant to be used once for new projects initialization. \nWe only use `devel` and `master` branches.\n\n- Go to `devel` branch.\n- Update Vulcan packages versions in `.meteor/packages`.\n- Check that the packages are working as expected, solve breaking changes.\n- Check that `package.json` versions matches Vulcan's `package.json`.\n- Cleanup and reinstall everything \n- Run unit tests, and apply relevant fixes \n- Test that Storybook runs correctly\n\n```sh\nmeteor reset && rm -Rf node_modules &&  meteor npm install\nMETEOR_PACKAGE_DIRS=\"X/Vulcan/packages\" meteor npm run test\nMETEOR_PACKAGE_DIRS=\"X/Vulcan/packages\" meteor npm run storybook\n```\n\n- Test different example packages.\n- Merge devel in to  `master`.\n- Update version `npm version patch` (for 1.16.1 > 1.16.2) or `npm version minor` for 1.16 > 1.17\n- Push with `--tags`: `git push && git push --tags`.\n\n### In the docs\n\n- 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).\n- Do relevant updates on `devel` branch\n- Merge into `master`\n- Update the docs after packages are releved"
  },
  {
    "path": "jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES6\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"meteor/vulcan:styled-components\": [\n        \"packages/vulcan-styled-components/lib/server/main.js\",\n        \"packages/vulcan-styled-components/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:payments\": [\n        \"packages/vulcan-payments/lib/server/main.js\",\n        \"packages/vulcan-payments/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:events-intercom\": [\n        \"packages/vulcan-events-intercom/lib/client/main.js\",\n        \"packages/vulcan-events-intercom/lib/server/main.js\"\n      ],\n      \"meteor/vulcan:embed\": [\n        \"packages/vulcan-embed/lib/client/main.js\",\n        \"packages/vulcan-embed/lib/server/main.js\"\n      ],\n      \"meteor/boilerplate-generator\": [\n        \"packages/_boilerplate-generator/generator.js\"\n      ],\n      \"meteor/vulcan:lib\": [\n        \"packages/vulcan-lib/lib/server/main.js\",\n        \"packages/vulcan-lib/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:forms\": [\n        \"packages/vulcan-forms/lib/client/main.js\",\n        \"packages/vulcan-forms/lib/server/main.js\"\n      ],\n      \"meteor/vulcan:events\": [\n        \"packages/vulcan-events/lib/server/main.js\",\n        \"packages/vulcan-events/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:email\": [\n        \"packages/vulcan-email/lib/server.js\",\n        \"packages/vulcan-email/lib/client.js\"\n      ],\n      \"meteor/meteortesting:mocha\": [\n        \"packages/meteor-mocha/client.js\",\n        \"packages/meteor-mocha/server.js\"\n      ],\n      \"meteor/vulcan:admin\": [\n        \"packages/vulcan-admin/lib/server/main.js\",\n        \"packages/vulcan-admin/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:forms-tags\": [\n        \"packages/vulcan-forms-tags/lib/export.js\"\n      ],\n      \"meteor/vulcan:ui-bootstrap\": [\n        \"packages/vulcan-ui-bootstrap/lib/server/main.js\",\n        \"packages/vulcan-ui-bootstrap/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:forms-upload\": [\n        \"packages/vulcan-forms-upload/lib/modules.js\"\n      ],\n      \"meteor/vulcan:i18n\": [\n        \"packages/vulcan-i18n/lib/server/main.js\",\n        \"packages/vulcan-i18n/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:errors-sentry\": [\n        \"packages/vulcan-errors-sentry/lib/server/main.js\",\n        \"packages/vulcan-errors-sentry/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:errors\": [\n        \"packages/vulcan-errors/lib/server/main.js\",\n        \"packages/vulcan-errors/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:events-segment\": [\n        \"packages/vulcan-events-segment/lib/server/main.js\",\n        \"packages/vulcan-events-segment/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:ui-material\": [\n        \"packages/vulcan-ui-material/lib/client/main.js\",\n        \"packages/vulcan-ui-material/lib/server/main.js\"\n      ],\n      \"meteor/vulcan:events-internal\": [\n        \"packages/vulcan-events-internal/lib/server/main.js\",\n        \"packages/vulcan-events-internal/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:events-ga\": [\n        \"packages/vulcan-events-ga/lib/server/main.js\",\n        \"packages/vulcan-events-ga/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:voting\": [\n        \"packages/vulcan-voting/lib/server/main.js\",\n        \"packages/vulcan-voting/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:newsletter\": [\n        \"packages/vulcan-newsletter/lib/server/main.js\",\n        \"packages/vulcan-newsletter/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:users\": [\n        \"packages/vulcan-users/lib/server/main.js\",\n        \"packages/vulcan-users/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:subscribe\": [\n        \"packages/vulcan-subscribe/lib/modules.js\",\n        \"packages/vulcan-subscribe/lib/modules.js\"\n      ],\n      \"meteor/vulcan:core\": [\n        \"packages/vulcan-core/lib/server/main.js\",\n        \"packages/vulcan-core/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:test\": [\n        \"packages/vulcan-test/lib/server/main.js\",\n        \"packages/vulcan-test/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:redux\": [\n        \"packages/vulcan-redux/lib/server/main.js\",\n        \"packages/vulcan-redux/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:debug\": [\n        \"packages/vulcan-debug/lib/server/main.js\",\n        \"packages/vulcan-debug/lib/client/main.js\"\n      ],\n      \"meteor/vulcan:cloudinary\": [\n        \"packages/vulcan-cloudinary/lib/client/main.js\",\n        \"packages/vulcan-cloudinary/lib/server/main.js\"\n      ],\n      \"meteor/vulcan:accounts\": [\n        \"packages/vulcan-accounts/main_client.js\",\n        \"packages/vulcan-accounts/main_server.js\"\n      ]\n    }\n  },\n  \"include\": [\n    \"packages/**/*\"\n  ],\n  \"exclude\": [\n    \"packages/_buffer\",\n    \"packages/_boilerplate-generator\"\n  ],\n  \"typeAcquisition\": {\n    \"enable\": true\n  }\n}"
  },
  {
    "path": "license.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017 Telescope Nova\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"vulcan-meteor\",\n  \"version\": \"1.16.9\",\n  \"engines\": {\n    \"npm\": \"^3.0\"\n  },\n  \"scripts\": {\n    \"start\": \"meteor --settings settings.json\",\n    \"visualizer\": \"meteor --extra-packages bundle-visualizer --production --settings settings.json\",\n    \"lint\": \"eslint --cache --ext .jsx,js packages\",\n    \"lintfix\": \"eslint --cache --fix --ext .jsx,js packages\",\n    \"test-ci\": \"npm run test-server -- --once\",\n    \"test-unit\": \"meteor test-packages ./packages/* --port 60859 --driver-package meteortesting:mocha\",\n    \"open-test-client\": \"xdg-open http://localhost:60859\",\n    \"test-client\": \"TEST_SERVER=0 meteor test-packages ./packages/* --port 60859 --driver-package meteortesting:mocha\",\n    \"test-server\": \"TEST_CLIENT=0 meteor test-packages ./packages/* --port 60859 --driver-package meteortesting:mocha\",\n    \"test\": \"npm run test-unit\",\n    \"prettier\": \"node ./.vulcan/prettier/index.js write-changed\",\n    \"prettier-all\": \"node ./.vulcan/prettier/index.js write\",\n    \"update-package-json\": \"node ./.vulcan/update_package.js\",\n    \"storybook\": \"VULCAN_DIR=\\\"..\\\" start-storybook -p 6006\",\n    \"storybook-material\": \"STORYBOOK_UI=material VULCAN_DIR=\\\"..\\\" start-storybook -p 6006\",\n    \"build-storybook\": \"STORYBOOK_UI=material build-storybook -o docs/storybook-material && STORYBOOK_UI=bootstrap build-storybook -o docs/storybook-bootstrap\",\n    \"generate-changelog\": \"auto-changelog -u\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"npm run lint\"\n    }\n  },\n  \"dependencies\": {\n    \"@apollo/client\": \"^3.2.5\",\n    \"@babel/runtime\": \"^7.13.9\",\n    \"analytics-node\": \"^2.1.1\",\n    \"apollo-cache\": \"^1.3.2\",\n    \"apollo-cache-inmemory\": \"1.3.12\",\n    \"apollo-client\": \"^2.6.4\",\n    \"apollo-errors\": \"^1.9.0\",\n    \"apollo-link\": \"^1.2.12\",\n    \"apollo-link-error\": \"^1.1.11\",\n    \"apollo-link-http\": \"^1.5.15\",\n    \"apollo-link-schema\": \"^1.2.3\",\n    \"apollo-link-state\": \"^0.4.2\",\n    \"apollo-server\": \"2.8.2\",\n    \"apollo-server-express\": \"2.8.2\",\n    \"apollo-utilities\": \"^1.3.2\",\n    \"bcrypt\": \"^5.0.0\",\n    \"body-parser\": \"^1.18.3\",\n    \"body-parser-graphql\": \"^1.1.0\",\n    \"chalk\": \"2.2.0\",\n    \"classnames\": \"^2.2.3\",\n    \"combined-stream2\": \"^1.1.2\",\n    \"compression\": \"^1.7.2\",\n    \"cookie-parser\": \"^1.4.3\",\n    \"cors\": \"^2.8.5\",\n    \"cross-fetch\": \"^0.0.8\",\n    \"crypto-js\": \"^3.1.9-1\",\n    \"crypto-random-string\": \"^3.3.0\",\n    \"dataloader\": \"^1.4.0\",\n    \"deepmerge\": \"^1.2.0\",\n    \"dot-object\": \"^1.7.0\",\n    \"enzyme-adapter-react-16\": \"^1.14.0\",\n    \"escape-string-regexp\": \"^1.0.5\",\n    \"express\": \"^4.17.1\",\n    \"flat\": \"^4.0.0\",\n    \"graphql\": \"14.4.2\",\n    \"graphql-anywhere\": \"4.1.13\",\n    \"graphql-date\": \"^1.0.3\",\n    \"graphql-tag\": \"^2.9.2\",\n    \"graphql-tools\": \"^4.0.4\",\n    \"graphql-type-json\": \"^0.1.4\",\n    \"graphql-voyager\": \"^1.0.0-rc.26\",\n    \"handlebars\": \"^4.4.3\",\n    \"he\": \"^1.1.1\",\n    \"history\": \"^3.0.0\",\n    \"html-to-text\": \"^2.1.0\",\n    \"immutability-helper\": \"^2.7.0\",\n    \"import\": \"0.0.6\",\n    \"intl\": \"^1.2.4\",\n    \"intl-locales-supported\": \"1.4.6\",\n    \"juice\": \"^5.1.0\",\n    \"lodash\": \"^4.17.19\",\n    \"mailchimp\": \"^1.1.6\",\n    \"marked\": \"^0.7.0\",\n    \"meteor-node-stubs\": \"^0.4.1\",\n    \"mingo\": \"^2.2.0\",\n    \"moment\": \"^2.13.0\",\n    \"node-cache\": \"^5.1.2\",\n    \"pluralize\": \"7.0.0\",\n    \"prop-types\": \"^15.7.2\",\n    \"qs\": \"^6.6.0\",\n    \"react\": \"16.12.0\",\n    \"react-addons-pure-render-mixin\": \"^15.4.1\",\n    \"react-bootstrap\": \"^1.0.0-beta.5\",\n    \"react-bootstrap-datetimepicker\": \"0.0.22\",\n    \"react-bootstrap-typeahead\": \"^4.2.0\",\n    \"react-cookie\": \"^4.0.3\",\n    \"react-datetime\": \"^2.11.1\",\n    \"react-dom\": \"16.12.0\",\n    \"react-dropzone\": \"11.0.1\",\n    \"react-helmet\": \"^6.0.0\",\n    \"react-intl\": \"^2.1.3\",\n    \"react-loadable\": \"^4.0.3\",\n    \"react-markdown\": \"^3.1.5\",\n    \"react-no-ssr\": \"^1.1.0\",\n    \"react-overlays\": \"^1.0.0-beta.17\",\n    \"react-places-autocomplete\": \"^5.0.0\",\n    \"react-redux\": \"^5.0.6\",\n    \"react-router\": \"^5.0.1\",\n    \"react-router-bootstrap\": \"0.25.0\",\n    \"react-router-dom\": \"^5.0.1\",\n    \"react-router-scroll\": \"^0.4.4\",\n    \"react-select\": \"^1.2.1\",\n    \"react-stripe-checkout\": \"^2.4.0\",\n    \"recompose\": \"^0.26.0\",\n    \"redux\": \"^3.6.0\",\n    \"rss\": \"^1.2.1\",\n    \"sanitize-html\": \"^1.16.3\",\n    \"simpl-schema\": \"^1.4.2\",\n    \"speakingurl\": \"^9.0.0\",\n    \"string-similarity\": \"^4.0.2\",\n    \"stripe\": \"^4.23.1\",\n    \"tracker-component\": \"^1.3.14\",\n    \"underscore\": \"^1.8.3\",\n    \"universal-cookie-express\": \"^2.1.5\",\n    \"url\": \"^0.11.0\"\n  },\n  \"private\": true,\n  \"devDependencies\": {\n    \"@apollo/react-testing\": \"3.1.4\",\n    \"@babel/plugin-proposal-nullish-coalescing-operator\": \"^7.13.8\",\n    \"@babel/plugin-proposal-optional-chaining\": \"^7.13.8\",\n    \"@babel/plugin-syntax-dynamic-import\": \"^7.2.0\",\n    \"@material-ui/core\": \"^4.11.0\",\n    \"@material-ui/icons\": \"^4.9.1\",\n    \"@material-ui/lab\": \"^4.0.0-alpha.57\",\n    \"@material-ui/styles\": \"4.10.0\",\n    \"@storybook/addon-actions\": \"5.0.8\",\n    \"@storybook/addon-knobs\": \"5.0.8\",\n    \"@storybook/addon-links\": \"5.0.1\",\n    \"@storybook/addons\": \"5.0.1\",\n    \"@storybook/react\": \"5.0.8\",\n    \"@storybook/theming\": \"5.0.8\",\n    \"@userfrosting/merge-package-dependencies\": \"^1.2.0\",\n    \"apollo-server-testing\": \"^2.10.1\",\n    \"auto-changelog\": \"^1.16.1\",\n    \"autoprefixer\": \"^6.3.6\",\n    \"autosize-input\": \"^1.0.2\",\n    \"autosuggest-highlight\": \"^3.1.1\",\n    \"babel-eslint\": \"^10.0.1\",\n    \"babel-runtime\": \"^6.26.0\",\n    \"babylon\": \"^6.18.0\",\n    \"chromedriver\": \"^2.46.0\",\n    \"colors\": \"^1.3.2\",\n    \"css-loader\": \"^2.1.1\",\n    \"diff\": \"^3.5.0\",\n    \"dompurify\": \"^2.2.6\",\n    \"enzyme\": \"^3.3.0\",\n    \"eslint\": \"^5.16.0\",\n    \"eslint-config-airbnb\": \"^17.1.0\",\n    \"eslint-config-meteor\": \"0.1.1\",\n    \"eslint-import-resolver-meteor\": \"^0.4.0\",\n    \"eslint-plugin-babel\": \"^5.3.0\",\n    \"eslint-plugin-import\": \"^2.16.00\",\n    \"eslint-plugin-jsx-a11y\": \"^6.2.1\",\n    \"eslint-plugin-meteor\": \"^5.1.0\",\n    \"eslint-plugin-mocha\": \"^5.3.0\",\n    \"eslint-plugin-prettier\": \"^3.0.1\",\n    \"eslint-plugin-react\": \"^7.12.4\",\n    \"expect\": \"^24.7.1\",\n    \"glob\": \"^7.1.3\",\n    \"husky\": \"^1.2.0\",\n    \"jsdom\": \"^11.11.0\",\n    \"jsdom-global\": \"^3.0.2\",\n    \"mdi-material-ui\": \"^6.16.0\",\n    \"moment-timezone\": \"^0.5.25\",\n    \"node-sass\": \"^4.14.0\",\n    \"operation-name-mock-link\": \"0.0.4\",\n    \"prettier\": \"^1.15.2\",\n    \"react-autosuggest\": \"^9.4.3\",\n    \"react-isolated-scroll\": \"^0.1.1\",\n    \"react-jss\": \"^8.6.1\",\n    \"react-keyboard-event-handler\": \"1.5.4\",\n    \"sass-loader\": \"^7.1.0\",\n    \"scrap-meteor-loader\": \"0.0.1\",\n    \"selenium-webdriver\": \"^3.6.0\",\n    \"sinon\": \"^6.3.5\",\n    \"storybook-addon-intl\": \"^2.4.1\",\n    \"storybook-react-router\": \"^1.0.5\",\n    \"supertest\": \"^4.0.2\",\n    \"vulcan-loader\": \"0.0.1\",\n    \"waait\": \"^1.0.5\",\n    \"webpack\": \"^4.31.0\"\n  },\n  \"postcss\": {\n    \"plugins\": {\n      \"autoprefixer\": {\n        \"browsers\": [\n          \"last 2 versions\"\n        ]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/.gitignore",
    "content": "/bootstrap3-datepicker\n/npm-container"
  },
  {
    "path": "packages/_boilerplate-generator/.gitignore",
    "content": ".build*\n"
  },
  {
    "path": "packages/_boilerplate-generator/.npm/package/.gitignore",
    "content": "node_modules\n"
  },
  {
    "path": "packages/_boilerplate-generator/.npm/package/README",
    "content": "This directory and the files immediately inside it are automatically generated\nwhen you change this package's NPM dependencies. Commit the files in this\ndirectory (npm-shrinkwrap.json, .gitignore, and this README) to source control\nso that others run the same versions of sub-dependencies.\n\nYou should NOT check in the node_modules directory that Meteor automatically\ncreates; if you are using git, the .gitignore file tells git to ignore it.\n"
  },
  {
    "path": "packages/_boilerplate-generator/.npm/package/npm-shrinkwrap.json",
    "content": "{\n  \"lockfileVersion\": 1,\n  \"dependencies\": {\n    \"bluebird\": {\n      \"version\": \"2.11.0\",\n      \"resolved\": \"https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz\",\n      \"integrity\": \"sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=\"\n    },\n    \"combined-stream2\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/combined-stream2/-/combined-stream2-1.1.2.tgz\",\n      \"integrity\": \"sha1-9uFLegFWZvjHsKH6xQYkAWSsNXA=\"\n    },\n    \"debug\": {\n      \"version\": \"2.6.9\",\n      \"resolved\": \"https://registry.npmjs.org/debug/-/debug-2.6.9.tgz\",\n      \"integrity\": \"sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==\"\n    },\n    \"ms\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/ms/-/ms-2.0.0.tgz\",\n      \"integrity\": \"sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=\"\n    },\n    \"stream-length\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz\",\n      \"integrity\": \"sha1-gnfzy+5JpNqrz9tOL0qbXp8snwA=\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/_boilerplate-generator/README.md",
    "content": "# boilerplate-generator\n[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)\n***\n\nThis is an internal Meteor package."
  },
  {
    "path": "packages/_boilerplate-generator/generator.js",
    "content": "import { readFile } from 'fs';\nimport { create as createStream } from \"combined-stream2\";\n\nimport WebBrowserTemplate from './template-web.browser';\nimport WebCordovaTemplate from './template-web.cordova';\n\n// Copied from webapp_server\nconst readUtf8FileSync = filename => Meteor.wrapAsync(readFile)(filename, 'utf8');\n\nconst identity = value => value;\n\nfunction appendToStream(chunk, stream) {\n  if (typeof chunk === \"string\") {\n    stream.append(Buffer.from(chunk, \"utf8\"));\n  } else if (Buffer.isBuffer(chunk) ||\n             typeof chunk.read === \"function\") {\n    stream.append(chunk);\n  }\n}\n\nlet shouldWarnAboutToHTMLDeprecation = ! Meteor.isProduction;\n\nexport class Boilerplate {\n  constructor(arch, manifest, options = {}) {\n    const { headTemplate, closeTemplate } = getTemplate(arch);\n    this.headTemplate = headTemplate;\n    this.closeTemplate = closeTemplate;\n    this.baseData = null;\n\n    this._generateBoilerplateFromManifest(\n      manifest,\n      options\n    );\n  }\n\n  toHTML(extraData) {\n    if (shouldWarnAboutToHTMLDeprecation) {\n      shouldWarnAboutToHTMLDeprecation = false;\n      console.error(\n        \"The Boilerplate#toHTML method has been deprecated. \" +\n          \"Please use Boilerplate#toHTMLStream instead.\"\n      );\n      console.trace();\n    }\n\n    // Calling .await() requires a Fiber.\n    return this.toHTMLAsync(extraData).await();\n  }\n\n  // Returns a Promise that resolves to a string of HTML.\n  toHTMLAsync(extraData) {\n    return new Promise((resolve, reject) => {\n      const stream = this.toHTMLStream(extraData);\n      const chunks = [];\n      stream.on(\"data\", chunk => chunks.push(chunk));\n      stream.on(\"end\", () => {\n        resolve(Buffer.concat(chunks).toString(\"utf8\"));\n      });\n      stream.on(\"error\", reject);\n    });\n  }\n\n  // The 'extraData' argument can be used to extend 'self.baseData'. Its\n  // purpose is to allow you to specify data that you might not know at\n  // the time that you construct the Boilerplate object. (e.g. it is used\n  // by 'webapp' to specify data that is only known at request-time).\n  // this returns a stream\n  toHTMLStream(extraData) {\n    if (!this.baseData || !this.headTemplate || !this.closeTemplate) {\n      throw new Error('Boilerplate did not instantiate correctly.');\n    }\n\n    const data = {...this.baseData, ...extraData};\n    const start = \"<!DOCTYPE html>\\n\" + this.headTemplate(data);\n\n    const { body, dynamicBody } = data;\n\n    const end = this.closeTemplate(data);\n    const response = createStream();\n\n    appendToStream(start, response);\n\n    if (body) {\n      appendToStream(body, response);\n    }\n\n    if (dynamicBody) {\n      appendToStream(dynamicBody, response);\n    }\n\n    appendToStream(end, response);\n\n    return response;\n  }\n\n  // XXX Exported to allow client-side only changes to rebuild the boilerplate\n  // without requiring a full server restart.\n  // Produces an HTML string with given manifest and boilerplateSource.\n  // Optionally takes urlMapper in case urls from manifest need to be prefixed\n  // or rewritten.\n  // Optionally takes pathMapper for resolving relative file system paths.\n  // Optionally allows to override fields of the data context.\n  _generateBoilerplateFromManifest(manifest, {\n    urlMapper = identity,\n    pathMapper = identity,\n    baseDataExtension,\n    inline,\n  } = {}) {\n\n    const boilerplateBaseData = {\n      css: [],\n      js: [],\n      head: '',\n      body: '',\n      meteorManifest: JSON.stringify(manifest),\n      ...baseDataExtension,\n    };\n\n    manifest.forEach(item => {\n      const urlPath = urlMapper(item.url);\n      const itemObj = { url: urlPath };\n\n      if (inline) {\n        itemObj.scriptContent = readUtf8FileSync(\n          pathMapper(item.path));\n        itemObj.inline = true;\n      }\n\n      if (item.type === 'css' && item.where === 'client') {\n        boilerplateBaseData.css.push(itemObj);\n      }\n\n      if (item.type === 'js' && item.where === 'client' &&\n        // Dynamic JS modules should not be loaded eagerly in the\n        // initial HTML of the app.\n        !item.path.startsWith('dynamic/')) {\n        boilerplateBaseData.js.push(itemObj);\n      }\n\n      if (item.type === 'head') {\n        boilerplateBaseData.head =\n          readUtf8FileSync(pathMapper(item.path));\n      }\n\n      if (item.type === 'body') {\n        boilerplateBaseData.body =\n          readUtf8FileSync(pathMapper(item.path));\n      }\n    });\n\n    this.baseData = boilerplateBaseData;\n  }\n};\n\n// Returns a template function that, when called, produces the boilerplate\n// html as a string.\nfunction getTemplate(arch) {\n  const prefix = arch.split(\".\", 2).join(\".\");\n\n  if (prefix === \"web.browser\") {\n    return WebBrowserTemplate;\n  }\n\n  if (prefix === \"web.cordova\") {\n    return WebCordovaTemplate;\n  }\n\n  throw new Error(\"Unsupported arch: \" + arch);\n}"
  },
  {
    "path": "packages/_boilerplate-generator/package.js",
    "content": "Package.describe({\n  summary: \"Generates the boilerplate html from program's manifest\",\n  version: '1.7.1',\n  name: 'boilerplate-generator-not-used',\n});\n\nNpm.depends({\n  'combined-stream2': '1.1.2',\n});\n\nPackage.onUse(api => {\n  api.use('ecmascript');\n  api.use('underscore', 'server');\n  api.mainModule('generator.js', 'server');\n  api.export('Boilerplate', 'server');\n});\n"
  },
  {
    "path": "packages/_boilerplate-generator/template-web.browser.js",
    "content": "import template from './template';\n\nexport const headTemplate = ({\n  css,\n  htmlAttributes,\n  bundledJsCssUrlRewriteHook,\n  head,\n  dynamicHead,\n}) => {\n  var headSections = head.split(/<meteor-bundled-css[^<>]*>/, 2);\n  var cssBundle = [...(css || []).map(file =>\n    template('  <link rel=\"stylesheet\" type=\"text/css\" class=\"__meteor-css__\" href=\"<%- href %>\">')({\n      href: bundledJsCssUrlRewriteHook(file.url),\n    })\n  )].join('\\n');\n\n  return [\n    '<html' + Object.keys(htmlAttributes || {}).map(\n      key => template(' <%= attrName %>=\"<%- attrValue %>\"')({\n        attrName: key,\n        attrValue: htmlAttributes[key],\n      })\n    ).join('') + '>',\n    \n    '<head>',\n\n    dynamicHead,\n\n    (headSections.length === 1)\n      ? [cssBundle, headSections[0]].join('\\n')\n      : [headSections[0], cssBundle, headSections[1]].join('\\n'),\n\n    '</head>',\n    '<body>',\n  ].join('\\n');\n};\n\n// Template function for rendering the boilerplate html for browsers\nexport const closeTemplate = ({\n  meteorRuntimeConfig,\n  rootUrlPathPrefix,\n  inlineScriptsAllowed,\n  js,\n  additionalStaticJs,\n  bundledJsCssUrlRewriteHook,\n}) => [\n  '',\n  inlineScriptsAllowed\n    ? template('  <script type=\"text/javascript\">__meteor_runtime_config__ = JSON.parse(decodeURIComponent(<%= conf %>))</script>')({\n      conf: meteorRuntimeConfig,\n    })\n    : template('  <script type=\"text/javascript\" src=\"<%- src %>/meteor_runtime_config.js\"></script>')({\n      src: rootUrlPathPrefix,\n    }),\n  '',\n\n  ...(js || []).map(file =>\n    template('  <script type=\"text/javascript\" src=\"<%- src %>\"></script>')({\n      src: bundledJsCssUrlRewriteHook(file.url),\n    })\n  ),\n\n  ...(additionalStaticJs || []).map(({ contents, pathname }) => (\n    inlineScriptsAllowed\n      ? template('  <script><%= contents %></script>')({\n        contents,\n      })\n      : template('  <script type=\"text/javascript\" src=\"<%- src %>\"></script>')({\n        src: rootUrlPathPrefix + pathname,\n      })\n  )),\n\n  '',\n  '',\n  '</body>',\n  '</html>'\n].join('\\n');"
  },
  {
    "path": "packages/_boilerplate-generator/template-web.cordova.js",
    "content": "import template from './template';\n\n// Template function for rendering the boilerplate html for cordova\nexport const headTemplate = ({\n  meteorRuntimeConfig,\n  rootUrlPathPrefix,\n  inlineScriptsAllowed,\n  css,\n  js,\n  additionalStaticJs,\n  htmlAttributes,\n  bundledJsCssUrlRewriteHook,\n  head,\n  dynamicHead,\n}) => {\n  var headSections = head.split(/<meteor-bundled-css[^<>]*>/, 2);\n  var cssBundle = [\n    // We are explicitly not using bundledJsCssUrlRewriteHook: in cordova we serve assets up directly from disk, so rewriting the URL does not make sense\n    ...(css || []).map(file =>\n      template('  <link rel=\"stylesheet\" type=\"text/css\" class=\"__meteor-css__\" href=\"<%- href %>\">')({\n        href: file.url,\n      })\n  )].join('\\n');\n\n  return [\n    '<html>',\n    '<head>',\n    '  <meta charset=\"utf-8\">',\n    '  <meta name=\"format-detection\" content=\"telephone=no\">',\n    '  <meta name=\"viewport\" content=\"user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, viewport-fit=cover\">',\n    '  <meta name=\"msapplication-tap-highlight\" content=\"no\">',\n    '  <meta http-equiv=\"Content-Security-Policy\" content=\"default-src * gap: data: blob: \\'unsafe-inline\\' \\'unsafe-eval\\' ws: wss:;\">',\n\n  (headSections.length === 1)\n    ? [cssBundle, headSections[0]].join('\\n')\n    : [headSections[0], cssBundle, headSections[1]].join('\\n'),\n\n    '  <script type=\"text/javascript\">',\n    template('    __meteor_runtime_config__ = JSON.parse(decodeURIComponent(<%= conf %>));')({\n      conf: meteorRuntimeConfig,\n    }),\n    '    if (/Android/i.test(navigator.userAgent)) {',\n    // When Android app is emulated, it cannot connect to localhost,\n    // instead it should connect to 10.0.2.2\n    // (unless we\\'re using an http proxy; then it works!)\n    '      if (!__meteor_runtime_config__.httpProxyPort) {',\n    '        __meteor_runtime_config__.ROOT_URL = (__meteor_runtime_config__.ROOT_URL || \\'\\').replace(/localhost/i, \\'10.0.2.2\\');',\n    '        __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL = (__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL || \\'\\').replace(/localhost/i, \\'10.0.2.2\\');',\n    '      }',\n    '    }',\n    '  </script>',\n    '',\n    '  <script type=\"text/javascript\" src=\"/cordova.js\"></script>',\n\n    ...(js || []).map(file =>\n      template('  <script type=\"text/javascript\" src=\"<%- src %>\"></script>')({\n        src: file.url,\n      })\n    ),\n\n    ...(additionalStaticJs || []).map(({ contents, pathname }) => (\n      inlineScriptsAllowed\n        ? template('  <script><%= contents %></script>')({\n          contents,\n        })\n        : template('  <script type=\"text/javascript\" src=\"<%- src %>\"></script>')({\n          src: rootUrlPathPrefix + pathname\n        })\n    )),\n    '',\n    head,\n    '</head>',\n    '',\n    '<body>',\n  ].join('\\n');\n};\n\nexport function closeTemplate() {\n  return \"</body>\\n</html>\";\n}"
  },
  {
    "path": "packages/_boilerplate-generator/template.js",
    "content": "import { _ } from 'meteor/underscore';\n\n// As identified in issue #9149, when an application overrides the default\n// _.template settings using _.templateSettings, those new settings are\n// used anywhere _.template is used, including within the\n// boilerplate-generator. To handle this, _.template settings that have\n// been verified to work are overridden here on each _.template call.\nexport default function template(text) {\n  return _.template(text, null, {\n    evaluate    : /<%([\\s\\S]+?)%>/g,\n    interpolate : /<%=([\\s\\S]+?)%>/g,\n    escape      : /<%-([\\s\\S]+?)%>/g,\n  });\n};"
  },
  {
    "path": "packages/_buffer/buffer.js",
    "content": "global.Buffer = global.Buffer || require(\"buffer\").Buffer;"
  },
  {
    "path": "packages/_buffer/package.js",
    "content": "Package.describe({\n  name: \"buffer\"\n});\n\nPackage.onUse( function(api) {\n  \n  api.use([\n    'ecmascript'\n  ]);\n\n  api.addFiles([\n    'buffer.js'\n  ], ['client']);\n\n});\n"
  },
  {
    "path": "packages/meteor-mocha/browser-shim.js",
    "content": "/* eslint-disable */\n/**\n * Sourced from: https://github.com/nathanboktae/mocha-phantomjs-core\n */\n(function () {\n\n    // A shim for non ES5 supporting browsers, like PhantomJS. Lovingly inspired by:\n    // http://www.angrycoding.com/2011/09/to-bind-or-not-to-bind-that-is-in.html\n    if (!('bind' in Function.prototype)) {\n        Function.prototype.bind = function () {\n            var funcObj = this;\n            var extraArgs = Array.prototype.slice.call(arguments);\n            var thisObj = extraArgs.shift();\n            return function () {\n                return funcObj.apply(thisObj, extraArgs.concat(Array.prototype.slice.call(arguments)));\n            };\n        };\n    }\n\n    function isFileReady(readyState) {\n        // Check to see if any of the ways a file can be ready are available as properties on the file's element\n        return (!readyState || readyState == 'loaded' || readyState == 'complete' || readyState == 'uninitialized');\n    }\n\n    function shimMochaProcess(M) {\n        // Mocha needs a process.stdout.write in order to change the cursor position.\n        M.process = M.process || {};\n        M.process.stdout = M.process.stdout || process.stdout;\n        M.process.stdout.write = function (s) { window.callPhantom({ stdout: s }); };\n        window.callPhantom({ getColWith: true });\n    }\n\n    function shimMochaInstance(m) {\n        var origRun = m.run, origUi = m.ui;\n        m.ui = function () {\n            var retval = origUi.apply(mocha, arguments);\n            window.callPhantom({ configureMocha: true });\n            m.reporter = function () { };\n            return retval;\n        };\n        m.run = function () {\n            window.callPhantom({ testRunStarted: m.suite.suites.length });\n            m.runner = origRun.apply(mocha, arguments);\n            if (m.runner.stats && m.runner.stats.end) {\n                window.callPhantom({ testRunEnded: m.runner });\n            } else {\n                m.runner.on('end', function () {\n                    window.callPhantom({ testRunEnded: m.runner });\n                });\n            }\n            return m.runner;\n        };\n    }\n\n    Object.defineProperty(window, 'checkForMocha', {\n        value: function () {\n            var scriptTags = document.querySelectorAll('script'),\n                mochaScript = Array.prototype.filter.call(scriptTags, function (s) {\n                    var src = s.getAttribute('src');\n                    return src && src.match(/mocha\\.js$/);\n                })[0];\n\n            if (mochaScript) {\n                mochaScript.onreadystatechange = mochaScript.onload = function () {\n                    if (isFileReady(mochaScript.readyState)) {\n                        initMochaPhantomJS();\n                    }\n                };\n            }\n        }\n    });\n\n    Object.defineProperty(window, 'initMochaPhantomJS', {\n        value: function () {\n            shimMochaProcess(Mocha);\n            shimMochaInstance(mocha);\n            delete window.initMochaPhantomJS;\n        },\n        configurable: true\n    });\n\n    // Mocha needs the formating feature of console.log so copy node's format function and\n    // monkey-patch it into place. This code is copied from node's, links copyright applies.\n    // https://github.com/joyent/node/blob/master/lib/util.js\n    if (!console.format) {\n        console.format = function (f) {\n            if (typeof f !== 'string') {\n                return Array.prototype.map.call(arguments, function (arg) {\n                    try {\n                        return JSON.stringify(arg);\n                    }\n                    catch (_) {\n                        return '[Circular]';\n                    }\n                }).join(' ');\n            }\n            var i = 1;\n            var args = arguments;\n            var len = args.length;\n            var str = String(f).replace(/%[sdj%]/g, function (x) {\n                if (x === '%%') return '%';\n                if (i >= len) return x;\n                switch (x) {\n                    case '%s': return String(args[i++]);\n                    case '%d': return Number(args[i++]);\n                    case '%j':\n                        try {\n                            return JSON.stringify(args[i++]);\n                        } catch (_) {\n                            return '[Circular]';\n                        }\n                    default:\n                        return x;\n                }\n            });\n            for (var x = args[i]; i < len; x = args[++i]) {\n                if (x === null || typeof x !== 'object') {\n                    str += ' ' + x;\n                } else {\n                    str += ' ' + JSON.stringify(x);\n                }\n            }\n            return str;\n        };\n        var origError = console.error;\n        console.error = function () { origError.call(console, console.format.apply(console, arguments)); };\n        var origLog = console.log;\n        console.log = function () { origLog.call(console, console.format.apply(console, arguments)); };\n    }\n\n})();\n"
  },
  {
    "path": "packages/meteor-mocha/client.js",
    "content": "/* eslint-disable no-console */\n/* global Package: false */\nimport { mocha } from 'meteor/meteortesting:mocha-core';\nimport prepForHTMLReporter from './prepForHTMLReporter';\nimport './browser-shim';\n\nlet uncaughtExceptions = 0;\nwindow.addEventListener('error', () => {\n  uncaughtExceptions++;\n});\n\nfunction saveCoverage(config, done) {\n  if (!config) {\n    done();\n    return;\n  }\n\n  if (typeof Package === 'undefined' || !Package.meteor || !Package.meteor.Meteor || !Package.meteor.Meteor.sendCoverage) {\n    console.error('Coverage package missing or not correctly launched');\n    done();\n    return;\n  }\n\n  Package.meteor.Meteor.sendCoverage((stats, err) => {\n    console.log('Meteor-coverage is saving client side coverage to the server. Client js files saved ', JSON.stringify(stats));\n    if (err) {\n      console.error('Failed to send client coverage');\n    }\n\n    done();\n  });\n}\n\n// Run the client tests. Meteor calls the `runTests` function exported by\n// the driver package on the client.\nfunction runTests() {\n  // We need to set the reporter when the tests actually run. This ensures that the\n  // correct reporter is used in the case where another Mocha test driver package is also\n  // added to the app. Since both are testOnly packages, top-level client code in both\n  // will run, potentially changing the reporter.\n  const { mochaOptions, runnerOptions, coverageOptions } = Meteor.settings.public.mochaRuntimeArgs || {};\n\n  if (!runnerOptions.runClient) return;\n\n  const { clientReporter, grep, invert, reporter } = mochaOptions || {};\n  if (grep) mocha.grep(grep);\n  if (invert) mocha.options.invert = invert;\n\n  // The chrome/webdriver logging adapter seems to escape color\n  // codes, so we can't support colors for that adapter.\n  // Feel free to fix this if you know how.\n  if (runnerOptions.browserDriver !== 'chrome') {\n    mocha.options.useColors = true;\n  }\n\n  let currentReporter = clientReporter || reporter;\n  if (!currentReporter) {\n    currentReporter = runnerOptions.browserDriver ? 'spec' : 'html';\n  }\n\n  if (currentReporter === 'html') {\n    // If we're not running client tests automatically in a headless browser, then we\n    // probably are going to want to see an HTML reporter when we load the page.\n    prepForHTMLReporter(mocha);\n  }\n\n  mocha.reporter(currentReporter);\n\n  // These `window` properties are all used by the client testing script in the\n  // browser-tests package to know what is happening.\n  window.testsAreRunning = true;\n  mocha.run((failures) => {\n    saveCoverage(coverageOptions, () => {\n      window.testsAreRunning = false;\n      window.testFailures = failures + uncaughtExceptions;\n      window.testsDone = true;\n    });\n  });\n}\n\nexport { runTests };\n"
  },
  {
    "path": "packages/meteor-mocha/package.js",
    "content": "Package.describe({\n  name: 'meteortesting:mocha',\n  summary: 'Run Meteor package or app tests with Mocha',\n  git: 'https://github.com/meteortesting/meteor-mocha.git',\n  documentation: '../README.md',\n  version: '2.0.2',\n  testOnly: true,\n});\n\nPackage.onUse(function onUse(api) {\n  api.use([\n    'meteortesting:mocha-core@8.1.2',\n    'ecmascript@0.3.0',\n    'lmieulet:meteor-coverage@4.0.0',\n  ]);\n\n  api.use(['meteortesting:browser-tests@1.3.4', 'http@2.0.0'], 'server');\n  api.use('browser-policy@1.1.0', 'server', { weak: true });\n\n  api.mainModule('client.js', 'client');\n  api.mainModule('server.js', 'server');\n});\n"
  },
  {
    "path": "packages/meteor-mocha/package.json",
    "content": "{\n  \"name\": \"mocha\",\n  \"version\": \"0.0.0-semantic-release\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/meteortesting/meteor-mocha\"\n  },\n  \"author\": \"Dispatch Technologies, Inc. (http://www.dispatch.me/)\",\n  \"license\": \"MIT\",\n  \"release\": {\n    \"verifyConditions\": [\"semantic-release-meteor\", \"@semantic-release/github\"],\n    \"getLastRelease\": \"semantic-release-meteor\",\n    \"publish\": [\"semantic-release-meteor\", \"@semantic-release/github\"]\n  }\n}\n"
  },
  {
    "path": "packages/meteor-mocha/prepForHTMLReporter.js",
    "content": "export default function prepForHTMLReporter() {\n  // Add the CSS from CDN\n  const link = document.createElement('link');\n  link.setAttribute('rel', 'stylesheet');\n  link.setAttribute('href', 'https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css');\n  document.head.appendChild(link);\n\n  // Add the div#mocha in which test results HTML will be placed\n  const div = document.createElement('div');\n  div.setAttribute('id', 'mocha');\n  document.body.appendChild(div);\n}\n"
  },
  {
    "path": "packages/meteor-mocha/runtimeArgs.js",
    "content": "export default function setArgs() {\n  const {\n    MOCHA_GREP,\n    MOCHA_INVERT,\n    MOCHA_REPORTER,\n    CLIENT_TEST_REPORTER,\n    SERVER_TEST_REPORTER,\n    TEST_BROWSER_DRIVER,\n    TEST_CLIENT,\n    TEST_PARALLEL,\n    TEST_SERVER,\n    TEST_WATCH,\n    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\n    XUNIT_FILE,\n    SERVER_MOCHA_OUTPUT,\n    CLIENT_MOCHA_OUTPUT,\n    COVERAGE,\n    COVERAGE_VERBOSE,\n    COVERAGE_IN_COVERAGE,\n    COVERAGE_OUT_COVERAGE,\n    COVERAGE_OUT_LCOVONLY,\n    COVERAGE_OUT_HTML,\n    COVERAGE_OUT_JSON,\n    COVERAGE_OUT_JSON_SUMMARY,\n    COVERAGE_OUT_TEXT_SUMMARY,\n    COVERAGE_OUT_REMAP,\n  } = process.env;\n\n  const runtimeArgs = {\n    mochaOptions: {\n      grep: MOCHA_GREP || false,\n      invert: !!MOCHA_INVERT,\n      reporter: MOCHA_REPORTER,\n      serverReporter: SERVER_TEST_REPORTER || XUNIT_FILE, // XUNIT_FILE is left in here for compatibility to older versions\n      clientReporter: CLIENT_TEST_REPORTER,\n      serverOutput: SERVER_MOCHA_OUTPUT,\n      clientOutput: CLIENT_MOCHA_OUTPUT,\n    },\n    runnerOptions: {\n      runClient: (TEST_CLIENT !== 'false' && TEST_CLIENT !== '0'),\n      runServer: (TEST_SERVER !== 'false' && TEST_SERVER !== '0'),\n      browserDriver: TEST_BROWSER_DRIVER,\n      testWatch: TEST_WATCH || METEOR_AUTO_RESTART === 'true',\n      runParallel: !!TEST_PARALLEL,\n    },\n  };\n\n  if (COVERAGE === '1') {\n    runtimeArgs.coverageOptions = {\n      verbose: COVERAGE_VERBOSE === '1',\n      in: {\n        coverage: COVERAGE_IN_COVERAGE === 'true' || COVERAGE_IN_COVERAGE === '1',\n      },\n      out: {\n        coverage: COVERAGE_OUT_COVERAGE === 'true' || COVERAGE_OUT_COVERAGE === '1',\n        lcovonly: COVERAGE_OUT_LCOVONLY === 'true' || COVERAGE_OUT_LCOVONLY === '1',\n        html: COVERAGE_OUT_HTML === 'true' || COVERAGE_OUT_HTML === '1',\n        json: COVERAGE_OUT_JSON === 'true' || COVERAGE_OUT_JSON === '1',\n        json_summary: COVERAGE_OUT_JSON_SUMMARY === 'true' || COVERAGE_OUT_JSON_SUMMARY === '1',\n        text_summary: COVERAGE_OUT_TEXT_SUMMARY === 'true' || COVERAGE_OUT_TEXT_SUMMARY === '1',\n        remap: COVERAGE_OUT_REMAP === 'true' || COVERAGE_OUT_REMAP === '1',\n      },\n    };\n  }\n\n  // Set the variables for the client to access as well.\n  Meteor.settings.public = Meteor.settings.public || {};\n  Meteor.settings.public.mochaRuntimeArgs = runtimeArgs;\n\n  return runtimeArgs;\n}\n"
  },
  {
    "path": "packages/meteor-mocha/server.handleCoverage.js",
    "content": "/* eslint-disable no-console */\nimport { HTTP } from 'meteor/http';\n\nexport default (coverageOptions) => {\n  let promise = Promise.resolve(true);\n\n  if (coverageOptions) {\n    const cLog = (...args) => {\n      if (coverageOptions.verbose) {\n        console.log(...args);\n      }\n    };\n\n    cLog('Export code coverage');\n\n    const importCoverageDump = () => new Promise((resolve, reject) => {\n      cLog('- In coverage');\n      HTTP.get(Meteor.absoluteUrl('coverage/import'), (error, response) => {\n        if (error) {\n          reject(new Error('Failed to import coverage file'));\n          return;\n        }\n\n        const { statusCode } = response;\n\n        if (statusCode !== 200) {\n          reject(new Error('Failed to import coverage file'));\n        }\n        resolve();\n      });\n    });\n\n    const exportReport = (fileType, reportType) => new Promise((resolve, reject) => {\n      cLog(`- Out ${fileType}`);\n      const url = Meteor.absoluteUrl(`/coverage/export/${fileType}`);\n      HTTP.get(url, (error, response) => {\n        if (error) {\n          reject(new Error(`Failed to save ${fileType} ${reportType}`));\n          return;\n        }\n\n        const { statusCode } = response;\n\n        if (statusCode !== 200) {\n          reject(new Error(`Failed to save ${fileType} ${reportType}`));\n        }\n        resolve();\n      });\n    });\n\n    const exportRemap = () => new Promise((resolve, reject) => {\n      cLog('- Out remap');\n      HTTP.get(Meteor.absoluteUrl('/coverage/export/remap'), (error, response) => {\n        if (error) {\n          reject(new Error('Failed to remap your coverage'));\n          return;\n        }\n\n        const { statusCode } = response;\n\n        if (statusCode !== 200) {\n          reject(new Error('Failed to remap your coverage'));\n        }\n        resolve();\n      });\n    });\n\n    if (coverageOptions.in.coverage) {\n      promise = promise.then(() => importCoverageDump());\n    }\n\n    if (coverageOptions.out.coverage) {\n      promise = promise.then(() => exportReport('coverage', 'dump'));\n    }\n\n    if (coverageOptions.out.lcovonly) {\n      promise = promise.then(() => exportReport('lcovonly', 'coverage'));\n    }\n\n    if (coverageOptions.out.html) {\n      promise = promise.then(() => exportReport('html', 'report'));\n    }\n\n    if (coverageOptions.out.json) {\n      promise = promise.then(() => exportReport('json', 'report'));\n    }\n\n    if (coverageOptions.out.text_summary) {\n      promise = promise.then(() => exportReport('text-summary', 'report'));\n    }\n\n    if (coverageOptions.out.remap) {\n      promise = promise.then(() => exportRemap());\n    }\n\n    if (coverageOptions.out.json_summary) {\n      promise = promise.then(() => exportReport('json-summary', 'dump'));\n    }\n\n    promise = promise.catch(console.error);\n  }\n\n  return promise;\n};\n"
  },
  {
    "path": "packages/meteor-mocha/server.js",
    "content": "/* global Package */\n/* eslint-disable no-console */\nimport { mochaInstance } from 'meteor/meteortesting:mocha-core';\nimport { startBrowser } from 'meteor/meteortesting:browser-tests';\n\nimport fs from 'fs';\n\nimport setArgs from './runtimeArgs';\nimport handleCoverage from './server.handleCoverage';\n\nif (Package['browser-policy-common'] && Package['browser-policy-content']) {\n  const { BrowserPolicy } = Package['browser-policy-common'];\n\n  // Allow the remote mocha.css file to be inserted, in case any CSP stuff\n  // exists for the domain.\n  BrowserPolicy.content.allowInlineStyles();\n  BrowserPolicy.content.allowStyleOrigin('https://cdn.rawgit.com');\n}\n\nconst { mochaOptions, runnerOptions, coverageOptions } = setArgs();\nconst { grep, invert, reporter, serverReporter, serverOutput, clientOutput } = mochaOptions || {};\n\n// Since intermingling client and server log lines would be confusing,\n// the idea here is to buffer all client logs until server tests have\n// finished running and then dump the buffer to the screen and continue\n// logging in real time after that if client tests are still running.\n\nlet serverTestsDone = false;\nconst clientLines = [];\nfunction clientLogBuffer(line) {\n  if (serverTestsDone) {\n    // printing and removing the extra new-line character. The first was added by the client log, the second here.\n    console.log(line.replace(/\\n$/, ''));\n  } else {\n    clientLines.push(line);\n  }\n}\n\nfunction printHeader(type) {\n  const lines = [\n    '\\n--------------------------------',\n    Meteor.isAppTest ? `--- RUNNING APP ${type} TESTS ---` : `----- RUNNING ${type} TESTS -----`,\n    '--------------------------------\\n',\n  ];\n  lines.forEach((line) => {\n    if (type === 'CLIENT') {\n      clientLogBuffer(line);\n    } else {\n      console.log(line);\n    }\n  });\n}\n\nlet callCount = 0;\nlet clientFailures = 0;\nlet serverFailures = 0;\nfunction exitIfDone(type, failures) {\n  callCount++;\n  if (type === 'client') {\n    clientFailures = failures;\n  } else {\n    serverFailures = failures;\n    serverTestsDone = true;\n    clientLines.forEach((line) => {\n      // printing and removing the extra new-line character. The first was added by the client log, the second here.\n      console.log(line.replace(/\\n$/, ''));\n    });\n  }\n\n  if (callCount === 2) {\n    // We only need to show this final summary if we ran both kinds of tests in the same console\n    if (runnerOptions.runServer && runnerOptions.runClient && runnerOptions.browserDriver) {\n      console.log('All tests finished!\\n');\n      console.log('--------------------------------');\n      console.log(`${Meteor.isAppTest ? 'APP ' : ''}SERVER FAILURES: ${serverFailures}`);\n      console.log(`${Meteor.isAppTest ? 'APP ' : ''}CLIENT FAILURES: ${clientFailures}`);\n      console.log('--------------------------------');\n    }\n\n    handleCoverage(coverageOptions).then(() => {\n      // if no env for TEST_WATCH, tests should exit when done\n      if (!runnerOptions.testWatch) {\n        if (clientFailures + serverFailures > 0) {\n          process.exit(1); // exit with non-zero status if there were failures\n        } else {\n          process.exit(0);\n        }\n      }\n    });\n  }\n}\n\nfunction serverTests(cb) {\n  if (!runnerOptions.runServer) {\n    console.log('SKIPPING SERVER TESTS BECAUSE TEST_SERVER=0');\n    exitIfDone('server', 0);\n    if (cb) cb();\n    return;\n  }\n\n  printHeader('SERVER');\n\n  if (grep) mochaInstance.grep(grep);\n  if (invert) mochaInstance.options.invert = invert;\n  mochaInstance.options.useColors = true;\n\n  // We need to set the reporter when the tests actually run to ensure no conflicts with\n  // other test driver packages that may be added to the app but are not actually being\n  // used on this run.\n  mochaInstance.reporter(serverReporter || reporter || 'spec', {\n    output: serverOutput,\n  });\n\n  mochaInstance.run((failureCount) => {\n    if (typeof failureCount !== 'number') {\n      console.log('Mocha did not return a failure count for server tests as expected');\n      exitIfDone('server', 1);\n    } else {\n      exitIfDone('server', failureCount);\n    }\n    if (cb) cb();\n  });\n}\n\nfunction clientTests() {\n  if (!runnerOptions.runClient) {\n    console.log('SKIPPING CLIENT TESTS BECAUSE TEST_CLIENT=0');\n    exitIfDone('client', 0);\n    return;\n  }\n\n  if (!runnerOptions.browserDriver) {\n    console.log('Load the app in a browser to run client tests, or set the TEST_BROWSER_DRIVER environment variable. '\n      + 'See https://github.com/meteortesting/meteor-mocha/blob/master/README.md#run-app-tests');\n    exitIfDone('client', 0);\n    return;\n  }\n\n  printHeader('CLIENT');\n\n  startBrowser({\n    stdout(data) {\n      if (clientOutput) {\n        fs.appendFileSync(clientOutput, data.toString());\n      } else {\n        clientLogBuffer(data.toString());\n      }\n    },\n    writebuffer(data) {\n      if (clientOutput) {\n        fs.appendFileSync(clientOutput, data.toString());\n      } else {\n        clientLogBuffer(data.toString());\n      }\n    },\n    stderr(data) {\n      if (clientOutput) {\n        fs.appendFileSync(clientOutput, data.toString());\n      } else {\n        clientLogBuffer(data.toString());\n      }\n    },\n    done(failureCount) {\n      if (typeof failureCount !== 'number') {\n        console.log('The browser driver package did not return a failure count for server tests as expected');\n        exitIfDone('client', 1);\n      } else {\n        exitIfDone('client', failureCount);\n      }\n    },\n  });\n}\n\n// Before Meteor calls the `start` function, app tests will be parsed and loaded by Mocha\nfunction start() {\n  // Run in PARALLEL or SERIES\n  // Running in series is a better default since it avoids db and state conflicts for newbs.\n  // If you want parallel you will know these risks.\n  if (runnerOptions.runParallel) {\n    console.log('Warning: Running in parallel can cause side-effects from state/db sharing');\n\n    serverTests();\n    clientTests();\n  } else {\n    serverTests(() => {\n      clientTests();\n    });\n  }\n}\n\nexport { start };\n"
  },
  {
    "path": "packages/vulcan-accounts/README.md",
    "content": "Vulcan accounts package (forked from https://github.com/studiointeract/accounts-ui)"
  },
  {
    "path": "packages/vulcan-accounts/imports/accounts_ui.js",
    "content": "import { Accounts } from 'meteor/accounts-base';\nimport { redirect } from './helpers.js';\n\n/**\n * @summary Accounts UI\n * @namespace\n * @memberOf Accounts\n */\nAccounts.ui = {};\n\nAccounts.ui._options = {\n  requestPermissions: [],\n  requestOfflineToken: {},\n  forceApprovalPrompt: {},\n  requireEmailVerification: false,\n  passwordSignupFields: 'USERNAME_AND_EMAIL',\n  minimumPasswordLength: 7,\n  loginPath: '/',\n  signUpPath: '/',\n  resetPasswordPath: null,\n  profilePath: '/',\n  changePasswordPath: null,\n  homeRoutePath: '/',\n  onSubmitHook: () => {},\n  onPreSignUpHook: () => new Promise(resolve => resolve()),\n  onPostSignUpHook: () => redirect(`${Accounts.ui._options.signUpPath}`),\n  onEnrollAccountHook: () => redirect(`${Accounts.ui._options.loginPath}`),\n  onResetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),\n  onVerifyEmailHook: () => redirect(`${Accounts.ui._options.profilePath}`),\n  onSignedInHook: () => redirect(`${Accounts.ui._options.homeRoutePath}`),\n  onSignedOutHook: () => redirect(`${Accounts.ui._options.homeRoutePath}`),\n  emailPattern: new RegExp('[^@]+@[^@\\.]{2,}\\.[^\\.@]+'),\n};\n\n/**\n * @summary Configure the behavior of [`<Accounts.ui.LoginForm />`](#react-accounts-ui).\n * @anywhere\n * @param {Object} options\n * @param {Object} options.requestPermissions Which [permissions](#requestpermissions) to request from the user for each external service.\n * @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.\n * @param {Object} options.forceApprovalPrompt If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google.\n * @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`'.\n */\nAccounts.ui.config = function(options) {\n  // validate options keys\n  const VALID_KEYS = [\n    'passwordSignupFields',\n    'requestPermissions',\n    'requestOfflineToken',\n    'forbidClientAccountCreation',\n    'requireEmailVerification',\n    'minimumPasswordLength',\n    'loginPath',\n    'signUpPath',\n    'resetPasswordPath',\n    'profilePath',\n    'changePasswordPath',\n    'homeRoutePath',\n    'onSubmitHook',\n    'onPreSignUpHook',\n    'onPostSignUpHook',\n    'onEnrollAccountHook',\n    'onResetPasswordHook',\n    'onVerifyEmailHook',\n    'onSignedInHook',\n    'onSignedOutHook',\n    'validateField',\n    'emailPattern',\n  ];\n\n  _.each(_.keys(options), function (key) {\n    if (!_.contains(VALID_KEYS, key))\n      throw new Error('Accounts.ui.config: Invalid key: ' + key);\n  });\n\n  // Deal with `passwordSignupFields`\n  if (options.passwordSignupFields) {\n    if (_.contains([\n      'USERNAME_AND_EMAIL',\n      'USERNAME_AND_OPTIONAL_EMAIL',\n      'USERNAME_ONLY',\n      'EMAIL_ONLY',\n    ], options.passwordSignupFields)) {\n      Accounts.ui._options.passwordSignupFields = options.passwordSignupFields;\n    }\n    else {\n      throw new Error('Accounts.ui.config: Invalid option for `passwordSignupFields`: ' + options.passwordSignupFields);\n    }\n  }\n\n  // Deal with `requestPermissions`\n  if (options.requestPermissions) {\n    _.each(options.requestPermissions, function (scope, service) {\n      if (Accounts.ui._options.requestPermissions[service]) {\n        throw new Error('Accounts.ui.config: Can\\'t set `requestPermissions` more than once for ' + service);\n      }\n      else if (!(scope instanceof Array)) {\n        throw new Error('Accounts.ui.config: Value for `requestPermissions` must be an array');\n      }\n      else {\n        Accounts.ui._options.requestPermissions[service] = scope;\n      }\n    });\n  }\n\n  // Deal with `requestOfflineToken`\n  if (options.requestOfflineToken) {\n    _.each(options.requestOfflineToken, function (value, service) {\n      if (service !== 'google')\n        throw new Error('Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment.');\n\n      if (Accounts.ui._options.requestOfflineToken[service]) {\n        throw new Error('Accounts.ui.config: Can\\'t set `requestOfflineToken` more than once for ' + service);\n      }\n      else {\n        Accounts.ui._options.requestOfflineToken[service] = value;\n      }\n    });\n  }\n\n  // Deal with `forceApprovalPrompt`\n  if (options.forceApprovalPrompt) {\n    _.each(options.forceApprovalPrompt, function (value, service) {\n      if (service !== 'google')\n        throw new Error('Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment.');\n\n      if (Accounts.ui._options.forceApprovalPrompt[service]) {\n        throw new Error('Accounts.ui.config: Can\\'t set `forceApprovalPrompt` more than once for ' + service);\n      }\n      else {\n        Accounts.ui._options.forceApprovalPrompt[service] = value;\n      }\n    });\n  }\n\n  // Deal with `requireEmailVerification`\n  if (options.requireEmailVerification) {\n    if (typeof options.requireEmailVerification != 'boolean') {\n      throw new Error('Accounts.ui.config: \"requireEmailVerification\" not a boolean');\n    }\n    else {\n      Accounts.ui._options.requireEmailVerification = options.requireEmailVerification;\n    }\n  }\n\n  // Deal with `minimumPasswordLength`\n  if (options.minimumPasswordLength) {\n    if (typeof options.minimumPasswordLength != 'number') {\n      throw new Error('Accounts.ui.config: \"minimumPasswordLength\" not a number');\n    }\n    else {\n      Accounts.ui._options.minimumPasswordLength = options.minimumPasswordLength;\n    }\n  }\n\n  // Deal with the hooks.\n  for (let hook of [\n    'onSubmitHook',\n    'onPreSignUpHook',\n    'onPostSignUpHook',\n  ]) {\n    if (options[hook]) {\n      if (typeof options[hook] != 'function') {\n        throw new Error(`Accounts.ui.config: \"${hook}\" not a function`);\n      }\n      else {\n        Accounts.ui._options[hook] = options[hook];\n      }\n    }\n  }\n\n  // Deal with pattern.\n  for (let hook of [\n    'emailPattern',\n  ]) {\n    if (options[hook]) {\n      if (!(options[hook] instanceof RegExp)) {\n        throw new Error(`Accounts.ui.config: \"${hook}\" not a Regular Expression`);\n      }\n      else {\n        Accounts.ui._options[hook] = options[hook];\n      }\n    }\n  }\n\n  // deal with the paths.\n  for (let path of [\n    'loginPath',\n    'signUpPath',\n    'resetPasswordPath',\n    'profilePath',\n    'changePasswordPath',\n    'homeRoutePath'\n  ]) {\n    if (typeof options[path] !== 'undefined') {\n      if (options[path] !== null && typeof options[path] !== 'string') {\n        throw new Error(`Accounts.ui.config: ${path} is not a string or null`);\n      }\n      else {\n        Accounts.ui._options[path] = options[path];\n      }\n    }\n  }\n\n  // deal with redirect hooks.\n  for (let hook of [\n      'onEnrollAccountHook',\n      'onResetPasswordHook',\n      'onVerifyEmailHook',\n      'onSignedInHook',\n      'onSignedOutHook']) {\n    if (options[hook]) {\n      if (typeof options[hook] == 'function') {\n        Accounts.ui._options[hook] = options[hook];\n      }\n      else if (typeof options[hook] == 'string') {\n        Accounts.ui._options[hook] = () => redirect(options[hook]);\n      }\n      else {\n        throw new Error(`Accounts.ui.config: \"${hook}\" not a function or an absolute or relative path`);\n      }\n    }\n  }\n};\n\nexport default Accounts;\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/api/server/servicesListPublication.js",
    "content": "import { Meteor } from 'meteor/meteor';\nimport { getLoginServices } from '../../helpers.js';\n\nMeteor.publish('servicesList', function() {\n  let services = getLoginServices();\n  if (Package['accounts-password']) {\n    services.push({name: 'password'});\n  }\n  let fields = {};\n  // Publish the existing services for a user, only name or nothing else.\n  services.forEach(service => fields[`services.${service.name}.name`] = 1);\n  return Meteor.users.find({ _id: this.userId }, { fields: fields});\n});\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/components.js",
    "content": "import './ui/components/Button.jsx';\nimport './ui/components/Buttons.jsx';\nimport './ui/components/Field.jsx';\nimport './ui/components/Fields.jsx';\nimport './ui/components/Form.jsx';\nimport './ui/components/FormMessage.jsx';\nimport './ui/components/FormMessages.jsx';\nimport './ui/components/StateSwitcher.jsx';\nimport './ui/components/LoginForm.jsx';\nimport './ui/components/LoginFormInner.jsx';\nimport './ui/components/PasswordOrService.jsx';\nimport './ui/components/SocialButtons.jsx';\nimport './ui/components/ResetPassword.jsx';\nimport './ui/components/EnrollAccount.jsx';\nimport './ui/components/VerifyEmail.jsx';\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/emailTemplates.js",
    "content": "import {Accounts} from 'meteor/accounts-base';\nimport {getSetting} from 'meteor/vulcan:core';\n\n// the emailTemplates are made available by accounts-password, which we don't want to depend on\nif (Package['accounts-password']) {\n  Accounts.emailTemplates.siteName = getSetting('public.title', '');\n  Accounts.emailTemplates.from =\n    getSetting('public.title', '') +\n    ' <' +\n    getSetting('defaultEmail', 'no-reply@example.com') +\n    '>';\n}\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/helpers.js",
    "content": "import { Accounts } from 'meteor/accounts-base';\n\nlet browserHistory;\ntry {\n  browserHistory = require('react-router').browserHistory;\n} catch(e) {\n  // swallow errors\n}\nexport const loginButtonsSession = Accounts._loginButtonsSession;\nexport const STATES = {\n  SIGN_IN: Symbol.for('SIGN_IN'),\n  SIGN_UP: Symbol.for('SIGN_UP'),\n  PROFILE: Symbol.for('PROFILE'),\n  PASSWORD_CHANGE: Symbol.for('PASSWORD_CHANGE'),\n  PASSWORD_RESET: Symbol.for('PASSWORD_RESET'),\n  ENROLL_ACCOUNT: Symbol.for('ENROLL_ACCOUNT')\n};\n\nexport function getLoginServices() {\n  // First look for OAuth services.\n  const services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : [];\n\n  // Be equally kind to all login services. This also preserves\n  // backwards-compatibility.\n  services.sort();\n\n  return _.map(services, function(name){\n    return {name: name};\n  });\n}\n// Export getLoginServices using old style globals for accounts-base which\n// requires it.\nthis.getLoginServices = getLoginServices;\n\nexport function hasPasswordService() {\n  // First look for OAuth services.\n  return !!Package['accounts-password'];\n}\n\nexport function loginResultCallback(service, err) {\n  if (!err) {\n    // Do nothing\n  } else if (err instanceof Accounts.LoginCancelledError) {\n    // Do nothing\n  } else if (err instanceof ServiceConfiguration.ConfigError) {\n    // Do nothing\n  } else {\n    // loginButtonsSession.errorMessage(err.reason || \"Unknown error\");\n  }\n\n  if (Meteor.isClient) {\n    if (typeof redirect === 'string'){\n      window.location.href = '/';\n    }\n\n    if (typeof service === 'function'){\n      service();\n    }\n  }\n}\n\nexport function passwordSignupFields() {\n  return Accounts.ui._options.passwordSignupFields || 'USERNAME_AND_EMAIL';\n}\n\nexport function validateEmail(email, showMessage, clearMessage) {\n  if (passwordSignupFields() === 'USERNAME_AND_OPTIONAL_EMAIL' && email === '') {\n    return true;\n  }\n  if (Accounts.ui._options.emailPattern.test(email)) {\n    return true;\n  } else if (!email || email.length === 0) {\n    showMessage('accounts.error_email_required', 'warning', false, 'email');\n    return false;\n  } else {\n    showMessage('accounts.error_invalid_email', 'warning', false, 'email');\n    return false;\n  }\n}\n\nexport function validatePassword(password = '', showMessage, clearMessage){\n  if (password.length >= Accounts.ui._options.minimumPasswordLength) {\n    return true;\n  } else {\n    const errMsg = 'accounts.error_minchar';\n    showMessage(errMsg, 'warning', false, 'password');\n    return false;\n  }\n}\n\nexport function validateUsername(username, showMessage, clearMessage, formState) {\n  if ( username ) {\n    return true;\n  } else {\n    const fieldName = (passwordSignupFields() === 'USERNAME_ONLY' || formState === STATES.SIGN_UP) ? 'username' : 'usernameOrEmail';\n    showMessage('accounts.error_username_required', 'warning', false, fieldName);\n    return false;\n  }\n}\n\nexport function redirect(redirect) {\n  if (Meteor.isClient) {\n    if (window.history) {\n      // Run after all app specific redirects, i.e. to the login screen.\n      Meteor.setTimeout(() => {\n        if (Package['kadira:flow-router']) {\n          Package['kadira:flow-router'].FlowRouter.go(redirect);\n        } else if (Package['kadira:flow-router-ssr']) {\n          Package['kadira:flow-router-ssr'].FlowRouter.go(redirect);\n        } else if (browserHistory) {\n          browserHistory.push(redirect);\n        } else {\n          window.history.pushState( {} , 'redirect', redirect );\n        }\n      }, 100);\n    }\n  }\n}\n\nexport function capitalize(string) {\n  return string.replace(/\\-/, ' ').split(' ').map(word => {\n    return word.charAt(0).toUpperCase() + word.slice(1);\n  }).join(' ');\n}\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/login_session.js",
    "content": "/* eslint-disable meteor/no-session */\nimport { Accounts } from 'meteor/accounts-base';\nimport { loginResultCallback, getLoginServices } from './helpers.js';\n\nconst VALID_KEYS = [\n  'dropdownVisible',\n\n  // XXX consider replacing these with one key that has an enum for values.\n  'inSignupFlow',\n  'inForgotPasswordFlow',\n  'inChangePasswordFlow',\n  'inMessageOnlyFlow',\n\n  'errorMessage',\n  'infoMessage',\n\n  // dialogs with messages (info and error)\n  'resetPasswordToken',\n  'enrollAccountToken',\n  'justVerifiedEmail',\n  'justResetPassword',\n\n  'configureLoginServiceDialogVisible',\n  'configureLoginServiceDialogServiceName',\n  'configureLoginServiceDialogSaveDisabled',\n  'configureOnDesktopVisible'\n];\n\nexport const validateKey = function (key) {\n  if (!_.contains(VALID_KEYS, key))\n    throw new Error('Invalid key in loginButtonsSession: ' + key);\n};\n\nexport const KEY_PREFIX = 'Meteor.loginButtons.';\n\n// XXX This should probably be package scope rather than exported\n// (there was even a comment to that effect here from before we had\n// namespacing) but accounts-ui-viewer uses it, so leave it as is for\n// now\nAccounts._loginButtonsSession = {\n  set: function(key, value) {\n    validateKey(key);\n    if (_.contains(['errorMessage', 'infoMessage'], key))\n      throw new Error('Don\\'t set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage().');\n\n    this._set(key, value);\n  },\n\n  _set: function(key, value) {\n    Session.set(KEY_PREFIX + key, value);\n  },\n\n  get: function(key) {\n    validateKey(key);\n    return Session.get(KEY_PREFIX + key);\n  }\n};\n\nif (Meteor.isClient){\n  // In the login redirect flow, we'll have the result of the login\n  // attempt at page load time when we're redirected back to the\n  // application.  Register a callback to update the UI (i.e. to close\n  // the dialog on a successful login or display the error on a failed\n  // login).\n  //\n  Accounts.onPageLoadLogin(function (attemptInfo) {\n    // Ignore if we have a left over login attempt for a service that is no longer registered.\n    if (_.contains(_.pluck(getLoginServices(), 'name'), attemptInfo.type))\n      loginResultCallback(attemptInfo.type, attemptInfo.error);\n  });\n\n  // let doneCallback;\n\n  Accounts.onResetPasswordLink(function (token, done) {\n    Accounts._loginButtonsSession.set('resetPasswordToken', token);\n    Session.set(KEY_PREFIX + 'state', 'resetPasswordToken');\n    // doneCallback = done;\n\n    Accounts.ui._options.onResetPasswordHook();\n  });\n\n  Accounts.onEnrollmentLink(function (token, done) {\n    Accounts._loginButtonsSession.set('enrollAccountToken', token);\n    Session.set(KEY_PREFIX + 'state', 'enrollAccountToken');\n    // doneCallback = done;\n\n    Accounts.ui._options.onEnrollAccountHook();\n  });\n\n  Accounts.onEmailVerificationLink(function (token, done) {\n    Accounts.verifyEmail(token, function (error) {\n      if (! error) {\n        Accounts._loginButtonsSession.set('justVerifiedEmail', true);\n        Session.set(KEY_PREFIX + 'state', 'justVerifiedEmail');\n        Accounts.ui._options.onSignedInHook();\n      }\n      else {\n        Accounts.ui._options.onVerifyEmailHook();\n      }\n\n      done();\n    });\n  });\n}\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/oauth_config.js",
    "content": "import { ServiceConfiguration } from 'meteor/service-configuration';\nimport { getSetting } from 'meteor/vulcan:lib';\n\nconst services = getSetting('oAuth');\n\nif (services) {\n  Object.keys(services).forEach(serviceName => {\n    ServiceConfiguration.configurations.upsert({service: serviceName}, {\n      $set: services[serviceName]\n    });\n  });\n}\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/routes.js",
    "content": "import { addRoute } from 'meteor/vulcan:core';\n\naddRoute({name: 'resetPassword', path: '/reset-password/:token', componentName: 'AccountsResetPassword'});\naddRoute({name: 'enrollAccount', path: '/enroll-account/:token', componentName: 'AccountsEnrollAccount'});\naddRoute({name: 'verifyEmail', path: '/verify-email/:token', componentName: 'AccountsVerifyEmail'});\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/Button.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\n\nexport class AccountsButton extends PureComponent {\n  render () {\n\n    const {\n      label,\n      // href = null,\n      type,\n      disabled = false,\n      id,\n      className,\n      onClick\n    } = this.props;\n\n    return type === 'link' ? \n      <a\n        href=\"#\"\n        id={id}\n        className={className}\n        onClick={onClick}\n        style={{marginRight: '10px'}}>\n        {label}\n      </a> :\n      <Components.Button\n        style={{marginRight: '10px'}}\n        variant=\"primary\"\n        id={id}\n        className={className}\n        type={type}\n        disabled={disabled}\n        onClick={onClick}>\n        {label}\n      </Components.Button>;\n  }\n}\nAccountsButton.propTypes = {\n  onClick: PropTypes.func\n};\n\nregisterComponent('AccountsButton', AccountsButton);\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/Buttons.jsx",
    "content": "import React from 'react';\nimport './Button.jsx';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\n\nexport class Buttons extends React.Component {\n  render () {\n    let { buttons = {}, className = 'buttons' } = this.props;\n    return (\n      <div className={ className }>\n        {Object.keys(buttons).map((id, i) =>\n          <Components.AccountsButton {...buttons[id]} key={i} />\n        )}\n      </div>\n    );\n  }\n}\n\nregisterComponent('AccountsButtons', Buttons);"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/EnrollAccount.jsx",
    "content": "import { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';\nimport React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { withRouter } from 'react-router';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { STATES } from '../../helpers.js';\n\nclass AccountsEnrollAccount extends PureComponent {\n  componentDidMount() {\n    const token = this.props.match.params.token;\n    Accounts._loginButtonsSession.set('enrollAccountToken', token);\n  }\n\n  render() {\n    if (!this.props.currentUser) {\n      return (\n        <Components.AccountsLoginForm\n          formState={ STATES.ENROLL_ACCOUNT }\n        />\n      );\n    } else {\n      return (\n        <div className='password-reset-form'>\n          <div>{this.context.intl.formatMessage({id: 'accounts.info_password_changed'})}</div>\n        </div>\n      );\n    }\n  }\n}\n\nAccountsEnrollAccount.contextTypes = {\n  intl: intlShape\n};\n\nAccountsEnrollAccount.propsTypes = {\n  currentUser: PropTypes.object,\n  match: PropTypes.object.isRequired,\n};\n\nAccountsEnrollAccount.displayName = 'AccountsEnrollAccount';\n\nregisterComponent('AccountsEnrollAccount', AccountsEnrollAccount, withCurrentUser, withRouter);\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/Field.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\n\nconst autocompleteValues = {\n  'username': 'username',\n  'usernameOrEmail': 'email',\n  'email': 'email',\n  'password': 'current-password'\n};\n\nexport class AccountsField extends PureComponent {\n  constructor(props) {\n    super(props);\n    this.state = {\n      mount: true\n    };\n  }\n\n  triggerUpdate() {\n    // Trigger an onChange on inital load, to support browser prefilled values.\n    const { onChange } = this.props;\n    if (this.input && onChange) {\n      onChange({ target: { value: this.input.value } });\n    }\n  }\n\n  componentDidMount() {\n    this.triggerUpdate();\n  }\n\n  componentDidUpdate(prevProps) {\n    // Re-mount component so that we don't expose browser prefilled passwords if the component was\n    // a password before and now something else.\n    if (prevProps.id !== this.props.id) {\n      this.setState({mount: false});\n    }\n    else if (!this.state.mount) {\n      this.setState({mount: true});\n      this.triggerUpdate();\n    }\n  }\n\n  render() {\n    const {\n      id,\n      hint,\n      label,\n      type = 'text',\n      onChange,\n      required = false,\n      defaultValue = '',\n      message,\n    } = this.props;\n    let { className = 'field' } = this.state;\n    \n    const { mount = true } = this.state;\n    if (type == 'notice') {\n      return <div className={ className }>{ label }</div>;\n    }\n    \n    const autoComplete = autocompleteValues[id];\n    if(required)\n      className += ' required';\n    \n    return mount ? (\n      <div className={ className } style={{marginBottom: '10px'}}>\n        <Components.FormControl id={ id } type={ type } onChange={ onChange } placeholder={ hint } defaultValue={ defaultValue } autoComplete={autoComplete }/>\n        {message && (\n          <span className={['message', message.type].join(' ').trim()}>\n            {message.message}</span>\n        )}\n      </div>\n    ) : null;\n  }\n}\nAccountsField.propTypes = {\n  onChange: PropTypes.func\n};\n\nregisterComponent('AccountsField', AccountsField);\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/Fields.jsx",
    "content": "import React from 'react';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\n\nexport class AccountsFields extends React.Component {\n  render () {\n    let { fields = {}, className = 'fields' } = this.props;\n    return (\n      <div className={ className }>\n        {Object.keys(fields).map((id, i) =>\n          <Components.AccountsField {...fields[id]} key={i} />\n        )}\n      </div>\n    );\n  }\n}\n\nregisterComponent('AccountsFields', AccountsFields);"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/Form.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport classnames from 'classnames';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\n\nexport class AccountsForm extends PureComponent {\n  componentDidMount() {\n    let form = this.form;\n    if (form) {\n      form.addEventListener('submit', (e) => {\n        e.preventDefault();\n      });\n    }\n  }\n\n  render() {\n    const {\n      // hasPasswordService,\n      oauthServices,\n      fields,\n      buttons,\n      // error,\n      messages,\n      ready = true,\n      className,\n    } = this.props;\n    const _className = classnames('accounts-ui', { ready }, className);\n    return (\n      <form\n        ref={(ref) => this.form = ref}\n        className={_className}\n        noValidate\n      >\n        <Components.AccountsFields fields={ fields } />\n        <Components.AccountsButtons buttons={ buttons } />\n        <Components.AccountsPasswordOrService oauthServices={ oauthServices } />\n        <Components.AccountsSocialButtons oauthServices={ oauthServices } />\n        <Components.AccountsFormMessages messages={messages} />\n      </form>\n    );\n  }\n}\nAccountsForm.propTypes = {\n  oauthServices: PropTypes.object,\n  fields: PropTypes.object.isRequired,\n  buttons: PropTypes.object.isRequired,\n  error: PropTypes.string,\n  ready: PropTypes.bool\n};\n\nregisterComponent('AccountsForm', AccountsForm);"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/FormMessage.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:core';\n\nexport class AccountsFormMessage extends React.Component {\n  render () {\n    let { message, type, className = 'message', style = {} } = this.props;\n    message = _.isObject(message) ? message.message : message; // If message is object, then try to get message from it\n    return message ? (\n      <div style={style} className={[className, type].join(' ')}>{ message }</div>\n    ) : null;\n  }\n}\n\nregisterComponent('AccountsFormMessage', AccountsFormMessage);\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/FormMessages.jsx",
    "content": "import React, { Component } from 'react';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\n\nexport class AccountsFormMessages extends Component {\n  render () {\n    const { messages = [], className = 'messages', style = {} } = this.props;\n    return messages.length > 0 && (\n      <div className={className} style={style}>\n        {messages\n          .filter(message => !('field' in message))\n          .map(({ message, type }, i) =>\n          <Components.AccountsFormMessage\n            message={message}\n            type={type}\n            key={i}\n          />\n        )}\n      </div>\n    );\n  }\n}\n\nregisterComponent('AccountsFormMessages', AccountsFormMessages);"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/LoginForm.jsx",
    "content": "import React from 'react';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\n\nexport class AccountsLoginForm extends React.Component {\n\n  render() {\n    return(\n      <Components.AccountsStateSwitcher {...this.props}/>\n    );\n  }\n}\n\nregisterComponent('AccountsLoginForm', AccountsLoginForm);"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/LoginFormInner.jsx",
    "content": "/* eslint-disable meteor/no-session */\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport { Accounts } from 'meteor/accounts-base';\nimport { KEY_PREFIX } from '../../login_session.js';\nimport { Components, registerComponent, withCurrentUser, Callbacks, runCallbacks } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { withApollo } from '@apollo/client/react/hoc';\n\nimport TrackerComponent from './TrackerComponent.jsx';\n\nimport {\n  STATES,\n  passwordSignupFields,\n  validateEmail,\n  validatePassword,\n  validateUsername,\n  loginResultCallback,\n  getLoginServices,\n  hasPasswordService,\n  capitalize,\n} from '../../helpers.js';\n\nexport class AccountsLoginFormInner extends TrackerComponent {\n  constructor(props) {\n    super(props);\n\n    if (props.formState === STATES.SIGN_IN && Package['accounts-password']) {\n      // eslint-disable-next-line no-console\n      console.warn(\n        'Do not force the state to SIGN_IN on Accounts.ui.LoginFormInner, it will make it impossible to reset password in your app, this state is also the default state if logged out, so no need to force it.'\n      );\n    }\n\n    const currentUser = props.currentUser;\n\n    const resetStoreAndThen = hook => {\n      return () => {\n        const resetStoreCallback = () => {\n          hook();\n          removeResetStoreCallback(resetStoreCallback);\n        };\n        const removeResetStoreCallback = props.client.onResetStore(resetStoreCallback);\n        props.client.resetStore();\n      };\n    };\n\n    const postLogInAndThen = hook => {\n      return () => {\n        const resetStoreCallback = () => {\n          if (Callbacks['users.postlogin']) {\n            // execute any post-sign-in callbacks\n            runCallbacks('users.postlogin');\n          } else {\n            // or else execute the hook\n            hook();\n          }\n          removeResetStoreCallback(resetStoreCallback);\n        };\n        const removeResetStoreCallback = props.client.onResetStore(resetStoreCallback);\n        props.client.resetStore();\n      };\n    };\n\n    const doNothing = () => {};\n\n    const defaultHooks = {\n      onPreSignUpHook: props.redirect ? Accounts.ui._options.onPreSignUpHook : doNothing,\n      onPostSignUpHook: props.redirect ? Accounts.ui._options.onPostSignUpHook : doNothing,\n      onEnrollAccountHook: props.redirect ? Accounts.ui._options.onEnrollAccountHook : doNothing,\n      onResetPasswordHook: props.redirect ? Accounts.ui._options.onResetPasswordHook : doNothing,\n      onVerifyEmailHook: props.redirect ? Accounts.ui._options.onVerifyEmailHook : doNothing,\n      onSignedInHook: props.redirect ? Accounts.ui._options.onSignedInHook : doNothing,\n      onSignedOutHook: props.redirect ? Accounts.ui._options.onSignedOutHook : doNothing,\n    };\n\n    // Set inital state.\n    this.state = {\n      email: props.email || '',\n      messages: [],\n      waiting: false,\n      formState: props.formState ? props.formState : currentUser ? STATES.PROFILE : STATES.SIGN_IN,\n      onSubmitHook: props.onSubmitHook || Accounts.ui._options.onSubmitHook,\n      onSignedInHook: postLogInAndThen(props.onSignedInHook || defaultHooks.onSignedInHook),\n      onSignedOutHook: resetStoreAndThen(props.onSignedOutHook || defaultHooks.onSignedOutHook),\n      onPreSignUpHook: props.onPreSignUpHook || defaultHooks.onPreSignUpHook,\n      onPostSignUpHook: postLogInAndThen(props.onPostSignUpHook || defaultHooks.onPostSignUpHook),\n    };\n  }\n\n  componentDidMount() {\n    let changeState = Session.get(KEY_PREFIX + 'state');\n    switch (changeState) {\n      case 'enrollAccountToken':\n        this.setState(prevState => ({\n          formState: STATES.ENROLL_ACCOUNT,\n        }));\n        Session.set(KEY_PREFIX + 'state', null);\n        break;\n      case 'resetPasswordToken':\n        this.setState(prevState => ({\n          formState: STATES.PASSWORD_CHANGE,\n        }));\n        Session.set(KEY_PREFIX + 'state', null);\n        break;\n\n      case 'justVerifiedEmail':\n        this.setState(prevState => ({\n          formState: STATES.PROFILE,\n        }));\n        Session.set(KEY_PREFIX + 'state', null);\n        break;\n    }\n\n    // Add default field values once the form did mount on the client\n    this.setState(prevState => ({\n      ...this.getDefaultFieldValues(),\n    }));\n\n    // if extra fields have been specified, add their default values\n    if (this.props.extraFields) {\n      this.props.extraFields.forEach(field => {\n        this.setState({ [field.id]: field.defaultValue });\n      });\n    }\n\n    // Listen for the user to login/logout.\n    this.autorun(() => {\n      // Add the services list to the user.\n      this.subscribe('servicesList');\n      this.setState({\n        currentUser: Accounts.user(),\n        waiting: !Accounts.loginServicesConfigured(),\n      });\n    });\n  }\n\n  UNSAFE_componentWillReceiveProps(nextProps, nextContext) {\n    if (nextProps.formState && nextProps.formState !== this.state.formState) {\n      this.setState({\n        formState: nextProps.formState,\n        ...this.getDefaultFieldValues(),\n      });\n    }\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    if (typeof this.props.currentUser !== 'undefined') {\n      if (!prevProps.currentUser !== !this.props.currentUser) {\n        this.setState({\n          formState: this.props.currentUser ? STATES.PROFILE : STATES.SIGN_IN,\n        });\n      }\n\n      const loggingInMessage = 'accounts.logging_in';\n\n      if (this.state.formState == STATES.PROFILE) {\n        if (!this.props.currentUser && this.state.messages.length === 0) {\n          // this.showMessage(loggingInMessage); // don't show logging in message for now\n        } else if (this.props.currentUser && this.state.messages.find(({ message }) => message === loggingInMessage)) {\n          this.clearMessage(loggingInMessage);\n        }\n      } else if (prevState.formState == STATES.PROFILE && this.state.messages.find(({ message }) => message === loggingInMessage)) {\n        this.clearMessage(loggingInMessage);\n      }\n    } else {\n      if (!prevState.currentUser !== !this.state.currentUser) {\n        this.setState({\n          formState: this.state.currentUser ? STATES.PROFILE : STATES.SIGN_IN,\n        });\n      }\n    }\n  }\n\n  validateField(field, value) {\n    const { formState } = this.state;\n    switch (field) {\n      case 'email':\n        return validateEmail(value, this.showMessage.bind(this), this.clearMessage.bind(this));\n      case 'password':\n        return validatePassword(value, this.showMessage.bind(this), this.clearMessage.bind(this));\n      case 'username':\n        return validateUsername(value, this.showMessage.bind(this), this.clearMessage.bind(this), formState);\n    }\n  }\n\n  getUsernameOrEmailField() {\n    return {\n      id: 'usernameOrEmail',\n      hint: this.context.intl.formatMessage({ id: 'accounts.enter_username_or_email' }),\n      label: this.context.intl.formatMessage({ id: 'accounts.username_or_email' }),\n      required: true,\n      defaultValue: this.state.currentUsername || '',\n      onChange: this.handleChange.bind(this, 'usernameOrEmail'),\n      message: this.getMessageForField('usernameOrEmail'),\n    };\n  }\n\n  getUsernameField() {\n    return {\n      id: 'username',\n      hint: this.context.intl.formatMessage({ id: 'accounts.enter_username' }),\n      label: this.context.intl.formatMessage({ id: 'accounts.username' }),\n      required: true,\n      defaultValue: this.state.currentUsername || '',\n      onChange: this.handleChange.bind(this, 'username'),\n      message: this.getMessageForField('username'),\n    };\n  }\n\n  getEmailField() {\n    return {\n      id: 'email',\n      hint: this.context.intl.formatMessage({ id: 'accounts.enter_email' }),\n      label: this.context.intl.formatMessage({ id: 'accounts.email' }),\n      type: 'email',\n      required: true,\n      defaultValue: this.state.email || '',\n      onChange: this.handleChange.bind(this, 'email'),\n      message: this.getMessageForField('email'),\n    };\n  }\n\n  getPasswordField() {\n    return {\n      id: 'password',\n      hint: this.context.intl.formatMessage({ id: 'accounts.enter_password' }),\n      label: this.context.intl.formatMessage({ id: 'accounts.password' }),\n      type: 'password',\n      required: true,\n      defaultValue: this.state.password || '',\n      onChange: this.handleChange.bind(this, 'password'),\n      message: this.getMessageForField('password'),\n    };\n  }\n\n  getSetPasswordField() {\n    return {\n      id: 'newPassword',\n      hint: this.context.intl.formatMessage({ id: 'accounts.enter_password' }),\n      label: this.context.intl.formatMessage({ id: 'accounts.choose_password' }),\n      type: 'password',\n      required: true,\n      onChange: this.handleChange.bind(this, 'newPassword'),\n    };\n  }\n\n  getNewPasswordField() {\n    return {\n      id: 'newPassword',\n      hint: this.context.intl.formatMessage({ id: 'accounts.enter_new_password' }),\n      label: this.context.intl.formatMessage({ id: 'accounts.new_password' }),\n      type: 'password',\n      required: true,\n      onChange: this.handleChange.bind(this, 'newPassword'),\n      message: this.getMessageForField('password'),\n    };\n  }\n\n  handleChange(field, evt) {\n    let value = evt.target.value;\n    switch (field) {\n      case 'password':\n        break;\n      default:\n        value = value.trim();\n        break;\n    }\n    this.setState({ [field]: value });\n    this.setDefaultFieldValues({ [field]: value });\n  }\n\n  fields() {\n    let loginFields = [];\n    const { formState } = this.state;\n\n    // if extra fields have been specified, add onChange handler to them\n    if (this.props.extraFields) {\n      loginFields = this.props.extraFields.map(field => {\n        const { id } = field;\n        return {\n          ...field,\n          onChange: this.handleChange.bind(this, id),\n        };\n      });\n    }\n\n    if (!hasPasswordService() && getLoginServices().length == 0) {\n      loginFields.push({\n        label: 'No login service added, i.e. accounts-password',\n        type: 'notice',\n      });\n    }\n\n    if (hasPasswordService() && formState == STATES.SIGN_IN) {\n      if (_.contains(['USERNAME_AND_EMAIL', 'USERNAME_AND_OPTIONAL_EMAIL'], passwordSignupFields())) {\n        loginFields.push(this.getUsernameOrEmailField());\n      }\n\n      if (passwordSignupFields() === 'USERNAME_ONLY') {\n        loginFields.push(this.getUsernameField());\n      }\n\n      if (_.contains(['EMAIL_ONLY'], passwordSignupFields())) {\n        loginFields.push(this.getEmailField());\n      }\n\n      loginFields.push(this.getPasswordField());\n    }\n\n    if (hasPasswordService() && formState == STATES.SIGN_UP) {\n      if (_.contains(['USERNAME_AND_EMAIL', 'USERNAME_AND_OPTIONAL_EMAIL', 'USERNAME_ONLY'], passwordSignupFields())) {\n        loginFields.push(this.getUsernameField());\n      }\n\n      if (_.contains(['USERNAME_AND_EMAIL', 'EMAIL_ONLY'], passwordSignupFields())) {\n        loginFields.push(this.getEmailField());\n      }\n\n      if (_.contains(['USERNAME_AND_OPTIONAL_EMAIL'], passwordSignupFields())) {\n        loginFields.push(Object.assign(this.getEmailField(), { required: false }));\n      }\n\n      loginFields.push(this.getPasswordField());\n    }\n\n    if (formState == STATES.PASSWORD_RESET) {\n      loginFields.push(this.getEmailField());\n    }\n\n    if (this.showPasswordChangeForm()) {\n      if (Meteor.isClient && !Accounts._loginButtonsSession.get('resetPasswordToken')) {\n        loginFields.push(this.getPasswordField());\n      }\n      loginFields.push(this.getNewPasswordField());\n    }\n\n    if (this.showEnrollAccountForm()) {\n      loginFields.push(this.getSetPasswordField());\n    }\n\n    return _.indexBy(loginFields, 'id');\n  }\n\n  buttons() {\n    const {\n      loginPath = Accounts.ui._options.loginPath,\n      signUpPath = Accounts.ui._options.signUpPath,\n      resetPasswordPath = Accounts.ui._options.resetPasswordPath,\n      changePasswordPath = Accounts.ui._options.changePasswordPath,\n      profilePath = Accounts.ui._options.profilePath,\n    } = this.props;\n    const { formState, waiting } = this.state;\n    let loginButtons = [];\n    const currentUser = typeof this.props.currentUser !== 'undefined' ? this.props.currentUser : this.state.currentUser;\n\n    if (currentUser && formState == STATES.PROFILE) {\n      loginButtons.push({\n        id: 'signOut',\n        label: this.context.intl.formatMessage({ id: 'accounts.sign_out' }),\n        disabled: waiting,\n        onClick: this.signOut.bind(this),\n      });\n    }\n\n    if (this.showCreateAccountLink() && this.props.showSignUpLink) {\n      loginButtons.push({\n        id: 'switchToSignUp',\n        label: this.context.intl.formatMessage({ id: 'accounts.switch_to_sign_up' }) || this.context.intl.formatMessage({ id: 'accounts.sign_up' }),\n        type: 'link',\n        href: signUpPath,\n        onClick: this.switchToSignUp.bind(this),\n      });\n    }\n\n    if ((formState == STATES.SIGN_UP || formState == STATES.PASSWORD_RESET) && this.props.showSignInLink) {\n      loginButtons.push({\n        id: 'switchToSignIn',\n        label:  this.context.intl.formatMessage({ id: 'accounts.switch_to_sign_in' }) || this.context.intl.formatMessage({ id: 'accounts.sign_in' }),\n        type: 'link',\n        href: loginPath,\n        onClick: this.switchToSignIn.bind(this),\n      });\n    }\n\n    if (this.showForgotPasswordLink()) {\n      loginButtons.push({\n        id: 'switchToPasswordReset',\n        label: this.context.intl.formatMessage({ id: 'accounts.forgot_password' }),\n        type: 'link',\n        href: resetPasswordPath,\n        onClick: this.switchToPasswordReset.bind(this),\n      });\n    }\n\n    if (\n      currentUser &&\n      formState == STATES.PROFILE\n      // note: user.services is not published so change password link would never be shown\n      // && (currentUser.services && 'password' in currentUser.services)\n    ) {\n      loginButtons.push({\n        id: 'switchToChangePassword',\n        label: this.context.intl.formatMessage({ id: 'accounts.change_password' }),\n        type: 'link',\n        href: changePasswordPath,\n        onClick: this.switchToChangePassword.bind(this),\n      });\n    }\n\n    if (formState == STATES.SIGN_UP) {\n      loginButtons.push({\n        id: 'signUp',\n        label: this.context.intl.formatMessage({ id: 'accounts.sign_up' }),\n        type: hasPasswordService() ? 'submit' : 'link',\n        className: 'active',\n        disabled: waiting,\n        onClick: hasPasswordService() ? this.signUp.bind(this, {}) : null,\n      });\n    }\n\n    if (this.showSignInLink()) {\n      loginButtons.push({\n        id: 'signIn',\n        label: this.context.intl.formatMessage({ id: 'accounts.sign_in' }),\n        type: hasPasswordService() ? 'submit' : 'link',\n        className: 'active',\n        disabled: waiting,\n        onClick: hasPasswordService() ? this.signIn.bind(this) : null,\n      });\n    }\n\n    if (formState == STATES.PASSWORD_RESET) {\n      loginButtons.push({\n        id: 'emailResetLink',\n        label: this.context.intl.formatMessage({ id: 'accounts.reset_your_password' }),\n        type: 'submit',\n        disabled: waiting,\n        onClick: this.passwordReset.bind(this),\n      });\n    }\n\n    if (this.showPasswordChangeForm() || this.showEnrollAccountForm()) {\n      loginButtons.push({\n        id: 'changePassword',\n        label: this.showPasswordChangeForm()\n          ? this.context.intl.formatMessage({ id: 'accounts.change_password' })\n          : this.context.intl.formatMessage({ id: 'accounts.set_password' }),\n        type: 'submit',\n        disabled: waiting,\n        onClick: this.passwordChange.bind(this),\n      });\n\n      if (currentUser) {\n        loginButtons.push({\n          id: 'switchToSignOut',\n          label: this.context.intl.formatMessage({ id: 'accounts.cancel' }),\n          type: 'link',\n          href: profilePath,\n          onClick: this.switchToSignOut.bind(this),\n        });\n      } else {\n        loginButtons.push({\n          id: 'cancelResetPassword',\n          label: this.context.intl.formatMessage({ id: 'accounts.cancel' }),\n          type: 'link',\n          onClick: this.cancelResetPassword.bind(this),\n        });\n      }\n    }\n\n    // Sort the button array so that the submit button always comes first, and\n    // buttons should also come before links.\n    loginButtons.sort((a, b) => {\n      return (b.type == 'submit' && a.type != undefined) - (a.type == 'submit' && b.type != undefined);\n    });\n\n    return _.indexBy(loginButtons, 'id');\n  }\n\n  showSignInLink() {\n    return this.state.formState == STATES.SIGN_IN && Package['accounts-password'];\n  }\n\n  showPasswordChangeForm() {\n    return Package['accounts-password'] && this.state.formState == STATES.PASSWORD_CHANGE;\n  }\n\n  showEnrollAccountForm() {\n    return Package['accounts-password'] && this.state.formState == STATES.ENROLL_ACCOUNT;\n  }\n\n  showCreateAccountLink() {\n    return this.state.formState == STATES.SIGN_IN && !Accounts._options.forbidClientAccountCreation && Package['accounts-password'];\n  }\n\n  showForgotPasswordLink() {\n    return (\n      this.state.formState == STATES.SIGN_IN &&\n      hasPasswordService() &&\n      _.contains(['USERNAME_AND_EMAIL', 'USERNAME_AND_OPTIONAL_EMAIL', 'EMAIL_ONLY'], passwordSignupFields())\n    );\n  }\n\n  /**\n   * Helper to store field values while using the form.\n   */\n  setDefaultFieldValues(defaults) {\n    if (typeof defaults !== 'object') {\n      throw new Error('Argument to setDefaultFieldValues is not of type object');\n    } else if (typeof localStorage !== 'undefined' && localStorage) {\n      localStorage.setItem(\n        'accounts_ui',\n        JSON.stringify({\n          passwordSignupFields: passwordSignupFields(),\n          ...this.getDefaultFieldValues(),\n          ...defaults,\n        })\n      );\n    }\n  }\n\n  /**\n   * Helper to get field values when switching states in the form.\n   */\n  getDefaultFieldValues() {\n    if (typeof localStorage !== 'undefined' && localStorage) {\n      const defaultFieldValues = JSON.parse(localStorage.getItem('accounts_ui') || null);\n      if (defaultFieldValues && defaultFieldValues.passwordSignupFields === passwordSignupFields()) {\n        return defaultFieldValues;\n      }\n    }\n  }\n\n  /**\n   * Helper to clear field values when signing in, up or out.\n   */\n  clearDefaultFieldValues() {\n    if (typeof localStorage !== 'undefined' && localStorage) {\n      localStorage.removeItem('accounts_ui');\n    }\n  }\n\n  switchToSignUp(event) {\n    event.preventDefault();\n    this.props.handlers.switchToSignUp();\n    // this.setState({\n    //   formState: STATES.SIGN_UP,\n    //   ...this.getDefaultFieldValues(),\n    // });\n    this.clearMessages();\n  }\n\n  switchToSignIn(event) {\n    event.preventDefault();\n    this.props.handlers.switchToSignIn();\n    // this.setState({\n    //   formState: STATES.SIGN_IN,\n    //   ...this.getDefaultFieldValues(),\n    // });\n    this.clearMessages();\n  }\n\n  switchToPasswordReset(event) {\n    event.preventDefault();\n    this.props.handlers.switchToPasswordReset();\n    // this.setState({\n    //   formState: STATES.PASSWORD_RESET,\n    //   ...this.getDefaultFieldValues(),\n    // });\n    this.clearMessages();\n  }\n\n  switchToChangePassword(event) {\n    event.preventDefault();\n    this.props.handlers.switchToChangePassword();\n    // this.setState({\n    //   formState: STATES.PASSWORD_CHANGE,\n    //   ...this.getDefaultFieldValues(),\n    // });\n    this.clearMessages();\n  }\n\n  switchToSignOut(event) {\n    event.preventDefault();\n    this.props.handlers.switchToSignOut();\n    // this.setState({\n    //   formState: STATES.PROFILE,\n    // });\n    this.clearMessages();\n  }\n\n  cancelResetPassword(event) {\n    event.preventDefault();\n    this.props.handlers.cancelResetPassword();\n    // Accounts._loginButtonsSession.set('resetPasswordToken', null);\n    // this.setState({\n    //   formState: STATES.SIGN_IN,\n    //   messages: [],\n    // });\n    this.clearMessages();\n  }\n\n  signOut() {\n    Meteor.logout(() => {\n      this.props.handlers.switchToSignIn();\n      // this.setState({\n      //   formState: STATES.SIGN_IN,\n      //   password: null,\n      // });\n      this.state.onSignedOutHook();\n      this.clearMessages();\n      this.clearDefaultFieldValues();\n    });\n  }\n\n  signIn() {\n    const { username = null, email = null, usernameOrEmail = null, password, formState, onSubmitHook } = this.state;\n    let error = false;\n    let loginSelector;\n    this.clearMessages();\n\n    const self = this;\n\n    if (usernameOrEmail !== null) {\n      if (!this.validateField('username', usernameOrEmail)) {\n        if (this.state.formState == STATES.SIGN_UP) {\n          this.state.onSubmitHook('error.accounts.usernameRequired', this.state.formState);\n        }\n        error = true;\n      } else {\n        loginSelector = usernameOrEmail;\n      }\n    } else if (username !== null) {\n      if (!this.validateField('username', username)) {\n        if (this.state.formState == STATES.SIGN_UP) {\n          this.state.onSubmitHook('error.accounts.usernameRequired', this.state.formState);\n        }\n        error = true;\n      } else {\n        loginSelector = { username: username };\n      }\n    } else if (usernameOrEmail == null) {\n      if (!this.validateField('email', email)) {\n        error = true;\n      } else {\n        loginSelector = { email };\n      }\n    }\n    if (!this.validateField('password', password)) {\n      error = true;\n    }\n\n    if (!error) {\n      Meteor.loginWithPassword(loginSelector, password, (error, result) => {\n        onSubmitHook(error, formState);\n        if (error) {\n          // eslint-disable-next-line no-console\n          console.log(error);\n          const errorId = `accounts.error_${error.reason.toLowerCase().replace(/ /g, '_')}`;\n          if (this.context.intl.formatMessage({ id: errorId })) {\n            self.showMessage(errorId);\n          } else {\n            self.showMessage('accounts.error_unknown');\n          }\n        } else {\n          loginResultCallback(() => this.state.onSignedInHook(this.props));\n          self.props.handlers.switchToProfile();\n          // this.setState({\n          //   formState: STATES.PROFILE,\n          //   password: null,\n          // });\n          self.clearDefaultFieldValues();\n        }\n      });\n    }\n  }\n\n  oauthButtons() {\n    const { formState, waiting } = this.state;\n    let oauthButtons = [];\n    if (formState == STATES.SIGN_IN || formState == STATES.SIGN_UP) {\n      if (Accounts.oauth) {\n        Accounts.oauth.serviceNames().map(service => {\n          oauthButtons.push({\n            id: service,\n            label: capitalize(service),\n            disabled: waiting,\n            type: 'button',\n            className: `btn-${service} ${service}`,\n            onClick: this.oauthSignIn.bind(this, service),\n          });\n        });\n      }\n    }\n    return _.indexBy(oauthButtons, 'id');\n  }\n\n  oauthSignIn(serviceName) {\n    const { formState, /* waiting, currentUser, */ onSubmitHook } = this.state;\n\n    const self = this;\n\n    //Thanks Josh Owens for this one.\n    function capitalService() {\n      return serviceName.charAt(0).toUpperCase() + serviceName.slice(1);\n    }\n\n    if (serviceName === 'meteor-developer') {\n      serviceName = 'meteorDeveloperAccount';\n    }\n\n    const loginWithService = Meteor['loginWith' + capitalService()];\n\n    let options = {}; // use default scope unless specified\n    if (Accounts.ui._options.requestPermissions[serviceName])\n      options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName];\n    if (Accounts.ui._options.requestOfflineToken[serviceName])\n      options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName];\n    if (Accounts.ui._options.forceApprovalPrompt[serviceName])\n      options.forceApprovalPrompt = Accounts.ui._options.forceApprovalPrompt[serviceName];\n\n    this.clearMessages();\n    loginWithService(options, error => {\n      onSubmitHook(error, formState);\n      if (error) {\n        // eslint-disable-next-line no-console\n        console.log(error);\n        if (error instanceof Accounts.LoginCancelledError) {\n          // do nothing\n        } else {\n          const errorId = `accounts.error_${error.message.toLowerCase().replace(/ /g, '_')}`;\n          if (self.context.intl.formatMessage({ id: errorId })) {\n            self.showMessage(errorId);\n          } else {\n            self.showMessage('accounts.error_unknown');\n          }\n        }\n      } else {\n        self.props.handlers.switchToProfile();\n        // this.setState({ formState: STATES.PROFILE });\n        self.clearDefaultFieldValues();\n        loginResultCallback(() => {\n          Meteor.setTimeout(() => this.state.onSignedInHook(this.props), 10);\n        });\n      }\n    });\n  }\n\n  /**\n   * Do NOT try to rewrite using GraphQL calls instead of Meteor methods\n   * This would break all Meteor related code\n   * If you want a form that uses GraphQL calls instead, duplicate this component or refactor\n   * so custom methods can be provided. However the version with Meteor methods must be kept.\n   */\n  signUp(options = {}) {\n    const {\n      username = null,\n      email = null,\n      // usernameOrEmail = null,\n      password,\n      formState,\n      onSubmitHook,\n    } = this.state;\n\n    // add extra fields to options\n    if (this.props.extraFields) {\n      this.props.extraFields.forEach(({ id }) => {\n        options[id] = this.state[id];\n      });\n    }\n\n    const self = this;\n\n    let error = false;\n    this.clearMessages();\n\n    if (username !== null) {\n      if (!this.validateField('username', username)) {\n        if (this.state.formState == STATES.SIGN_UP) {\n          this.state.onSubmitHook('error.accounts.usernameRequired', this.state.formState);\n        }\n        error = true;\n      } else {\n        options.username = username;\n      }\n    } else {\n      if (_.contains(['USERNAME_AND_EMAIL'], passwordSignupFields()) && !this.validateField('username', username)) {\n        if (this.state.formState == STATES.SIGN_UP) {\n          this.state.onSubmitHook('error.accounts.usernameRequired', this.state.formState);\n        }\n        error = true;\n      }\n    }\n\n    if (!this.validateField('email', email)) {\n      error = true;\n    } else {\n      options.email = email;\n    }\n\n    if (!this.validateField('password', password)) {\n      onSubmitHook('Invalid password', formState);\n      error = true;\n    } else {\n      options.password = password;\n    }\n\n    // set the signup locale\n    options.locale = this.context.intl.locale;\n\n    const SignUp = function(_options) {\n      Accounts.createUser(_options, error => {\n        self.setState({ waiting: false });\n\n        if (error) {\n          // eslint-disable-next-line no-console\n          console.log(error);\n\n          const errorId = `accounts.error_${error.reason\n            .toLowerCase()\n            .replace(/ /g, '_')\n            .replace('.', '')}`;\n\n          if (self.context.intl.formatMessage({ id: errorId })) {\n            self.showMessage(errorId, 'error');\n          } else {\n            self.showMessage('accounts.error_unknown', 'error');\n          }\n\n          if (self.context.intl.formatMessage({ id: `error.accounts_${error.reason}` })) {\n            onSubmitHook(`error.accounts.${error.reason}`, formState);\n          } else {\n            onSubmitHook('Unknown error', formState);\n          }\n        } else {\n          onSubmitHook(null, formState);\n          self.props.handlers.switchToProfile();\n          self.clearDefaultFieldValues();\n          // self.setState({ formState: STATES.PROFILE, password: null });\n          let currentUser = Accounts.user();\n          loginResultCallback(self.state.onPostSignUpHook.bind(self, _options, currentUser));\n        }\n      });\n    };\n    if (!error) {\n      this.setState({ waiting: true });\n      // Allow for Promises to return.\n      let promise = this.state.onPreSignUpHook(options);\n      if (promise instanceof Promise) {\n        promise.then(SignUp.bind(this, options));\n      } else {\n        // eslint-disable-next-line babel/new-cap\n        SignUp(options);\n      }\n    }\n  }\n\n  passwordReset() {\n    const { email = '', waiting, formState, onSubmitHook } = this.state;\n\n    if (waiting) {\n      return;\n    }\n\n    this.clearMessages();\n\n    if (this.validateField('email', email)) {\n      this.setState({ waiting: true });\n\n      Accounts.forgotPassword({ email: email }, error => {\n        // eslint-disable-next-line no-console\n        console.log(error);\n        if (error) {\n          const errorId = `accounts.error_${error.reason.toLowerCase().replace(/ /g, '_')}`;\n          this.showMessage(errorId, 'error');\n        } else {\n          this.showMessage('accounts.info_email_sent', 'success', 5000);\n          this.clearDefaultFieldValues();\n        }\n        onSubmitHook(error, formState);\n        this.setState({ waiting: false });\n      });\n    }\n  }\n\n  passwordChange() {\n    const { password, newPassword, formState, onSubmitHook, onSignedInHook } = this.state;\n\n    this.clearMessages();\n\n    if (!this.validateField('password', newPassword)) {\n      onSubmitHook('err.minChar', formState);\n      return;\n    }\n\n    let token = Accounts._loginButtonsSession.get('resetPasswordToken');\n    if (!token) {\n      token = Accounts._loginButtonsSession.get('enrollAccountToken');\n    }\n    if (token) {\n      Accounts.resetPassword(token, newPassword, error => {\n        if (error) {\n          const errorId = `accounts.error_${error.reason.toLowerCase().replace(/ /g, '_')}`;\n          this.showMessage(errorId, 'error');\n          onSubmitHook(error, formState);\n        } else {\n          this.showMessage('accounts.info_password_changed', 'success', 5000);\n          onSubmitHook(null, formState);\n          this.props.handlers.switchToProfile();\n          // this.setState({ formState: STATES.PROFILE });\n          Accounts._loginButtonsSession.set('resetPasswordToken', null);\n          Accounts._loginButtonsSession.set('enrollAccountToken', null);\n          onSignedInHook();\n        }\n      });\n    } else {\n      Accounts.changePassword(password, newPassword, error => {\n        if (error) {\n          const errorId = `accounts.error_${error.reason.toLowerCase().replace(/ /g, '_')}`;\n          this.showMessage(errorId, 'error');\n          onSubmitHook(error, formState);\n        } else {\n          this.showMessage('accounts.info_password_changed', 'success', 5000);\n          onSubmitHook(null, formState);\n          this.props.handlers.switchToProfile();\n          // this.setState({ formState: STATES.PROFILE });\n          this.clearDefaultFieldValues();\n        }\n      });\n    }\n  }\n\n  showMessage(messageId, type, clearTimeout, field) {\n    if (messageId) {\n      this.setState(({ messages = [] }) => {\n        messages.push({\n          message: this.context.intl.formatMessage({ id: messageId }),\n          type,\n          ...(field && { field }),\n        });\n        return { messages };\n      });\n      if (clearTimeout) {\n        this.hideMessageTimout = setTimeout(() => {\n          // Filter out the message that timed out.\n          this.clearMessage(messageId);\n        }, clearTimeout);\n      }\n    }\n  }\n\n  getMessageForField(field) {\n    const { messages = [] } = this.state;\n    return messages.find(({ field: key }) => key === field);\n  }\n\n  clearMessage(message) {\n    if (message) {\n      this.setState(({ messages = [] }) => ({\n        messages: messages.filter(({ message: a }) => a !== message),\n      }));\n    }\n  }\n\n  clearMessages() {\n    if (this.hideMessageTimout) {\n      clearTimeout(this.hideMessageTimout);\n    }\n    this.setState({ messages: [] });\n  }\n\n  componentWillUnmount() {\n    if (this.hideMessageTimout) {\n      clearTimeout(this.hideMessageTimout);\n    }\n  }\n\n  render() {\n    this.oauthButtons();\n    // Backwords compatibility with v1.2.x.\n    const { messages = [] } = this.state;\n    const message = {\n      deprecated: true,\n      message: messages.map(({ message }) => message).join(', '),\n    };\n\n    return (\n      <Components.AccountsForm\n        oauthServices={this.oauthButtons()}\n        fields={this.fields()}\n        buttons={this.buttons()}\n        {...this.state}\n        message={message}\n      />\n    );\n  }\n}\n\nAccountsLoginFormInner.propTypes = {\n  showSignInLink: PropTypes.bool,\n  showSignUpLink: PropTypes.bool,\n};\n\nAccountsLoginFormInner.defaultProps = {\n  showSignInLink: true,\n  showSignUpLink: true,\n  redirect: true,\n};\n\nAccountsLoginFormInner.contextTypes = {\n  intl: intlShape,\n};\n\nregisterComponent('AccountsLoginFormInner', AccountsLoginFormInner, withCurrentUser, withApollo);\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/PasswordOrService.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { hasPasswordService } from '../../helpers.js';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\n\nexport class AccountsPasswordOrService extends PureComponent {\n  render () {\n    let { className = 'password-or-service', style = {} } = this.props;\n    const services = Object.keys(this.props.oauthServices).map(service => {\n      return this.props.oauthServices[service].label;\n    });\n    let labels = services;\n    if (services.length > 2) {\n      labels = [];\n    }\n\n    if (hasPasswordService() && services.length > 0) {\n      return (\n        <div style={style} className={className}>\n          { `${this.context.intl.formatMessage({id: 'accounts.or_use'})} ${ labels.join(' / ') }` }\n        </div>\n      );\n    }\n    return null;\n  }\n}\n\nAccountsPasswordOrService.propTypes = {\n  oauthServices: PropTypes.object\n};\n\nAccountsPasswordOrService.contextTypes = {\n  intl: intlShape\n};\n\nregisterComponent('AccountsPasswordOrService', AccountsPasswordOrService);"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/ResetPassword.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';\nimport { withRouter } from 'react-router';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { STATES } from '../../helpers.js';\n\nclass AccountsResetPassword extends PureComponent {\n  componentDidMount() {\n    const token = this.props.match.params.token;\n    Accounts._loginButtonsSession.set('resetPasswordToken', token);\n  }\n\n  render() {\n    if (!this.props.currentUser) {\n      return (\n        <Components.AccountsLoginForm\n          formState={ STATES.PASSWORD_CHANGE }\n        />\n      );\n    } else {\n      return (\n        <div className='password-reset-form'>\n          <div>{this.context.intl.formatMessage({id: 'accounts.info_password_changed'})}</div>\n        </div>\n      );\n    }\n  }\n}\n\nAccountsResetPassword.contextTypes = {\n  intl: intlShape\n};\n\nAccountsResetPassword.propsTypes = {\n  currentUser: PropTypes.object,\n  match: PropTypes.object.isRequired,\n};\n\nAccountsResetPassword.displayName = 'AccountsResetPassword';\n\nregisterComponent('AccountsResetPassword', AccountsResetPassword, withCurrentUser, withRouter);\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/SocialButtons.jsx",
    "content": "import React from 'react';\nimport './Button.jsx';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\n\n\nexport class AccountsSocialButtons extends React.Component {\n  render() {\n    let { oauthServices = {}, className = 'social-buttons' } = this.props;\n    return(\n      <div className={ className }>\n        {Object.keys(oauthServices).map((id, i) => {\n          return <Components.AccountsButton {...oauthServices[id]} key={i} />;\n        })}\n      </div>\n    );\n  }\n}\n\nregisterComponent('AccountsSocialButtons', AccountsSocialButtons);"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/StateSwitcher.jsx",
    "content": "import React from 'react';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\nimport { Accounts } from 'meteor/accounts-base';\n\nimport {\n  STATES\n} from '../../helpers.js';\n\nexport class AccountsStateSwitcher extends React.Component {\n\n  constructor(props) {\n    super(props);\n\n    this.state = {\n      formState: props.formState\n    };\n  }\n\n  switchToSignUp = (event) => {\n    event && event.preventDefault();\n    this.setState({\n      formState: STATES.SIGN_UP,\n    });\n    // this.clearMessages();\n  }\n\n  switchToSignIn = (event) => {\n    event && event.preventDefault();\n    this.setState({\n      formState: STATES.SIGN_IN,\n    });\n    // this.clearMessages();\n  }\n\n  switchToPasswordReset = (event) => {\n    event && event.preventDefault();\n    this.setState({\n      formState: STATES.PASSWORD_RESET,\n    });\n    // this.clearMessages();\n  }\n\n  switchToChangePassword = (event) => {\n    event && event.preventDefault();\n    this.setState({\n      formState: STATES.PASSWORD_CHANGE,\n    });\n    // this.clearMessages();\n  }\n\n  switchToSignOut = (event) => {\n    event && event.preventDefault();\n    this.setState({\n      formState: STATES.PROFILE,\n    });\n    // this.clearMessages();\n  }\n\n  cancelResetPassword = (event) => {\n    event && event.preventDefault();\n    Accounts._loginButtonsSession.set('resetPasswordToken', null);\n    this.setState({\n      formState: STATES.SIGN_IN,\n    });\n    // this.clearMessages();\n  }\n\n  switchToProfile = (event) => {\n    event && event.preventDefault();\n    this.setState({\n      formState: STATES.PROFILE,\n    });\n    // this.clearMessages();\n  }\n\n  render() {\n    const {\n      switchToSignUp,\n      switchToSignIn,\n      switchToPasswordReset,\n      switchToChangePassword,\n      switchToSignOut,\n      cancelResetPassword,\n      switchToProfile,\n    } = this;\n\n    const handlers = {\n      switchToSignUp,\n      switchToSignIn,\n      switchToPasswordReset,\n      switchToChangePassword,\n      switchToSignOut,\n      cancelResetPassword,\n      switchToProfile,\n    };\n    return (\n      <Components.AccountsLoginFormInner {...this.props} formState={this.state.formState} handlers={handlers} />\n    );\n  }\n}\n\nregisterComponent('AccountsStateSwitcher', AccountsStateSwitcher);"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/TrackerComponent.jsx",
    "content": "/*****************************************************************/\n/* See https://github.com/studiointeract/tracker-component\n/* This is essentially the same component made by studiointeract\n/* but modified to work correctly with modern React.\n/* Only change as of this writing is to remove setState() and let\n/* super handle that.\n/****************************************************************/\nimport React from 'react';\n\nclass TrackerComponent extends React.Component {\n  constructor(props) {\n    super(props);\n    this.__subs = {}, this.__comps = []; this.__live = false;\n    this.__subscribe = props && props.subscribe || Meteor.subscribe;\n  }\n\n  subscribe(name, ...options) {\n    return this.__subs[JSON.stringify(arguments)] =\n      this.__subscribe.apply(this, [name, ...options]);\n  }\n\n  autorun(fn) { return this.__comps.push(Tracker.autorun(c => {\n    this.__live = true; fn(c); this.__live = false;\n  }));}\n\n  componentDidUpdate() { !this.__live && this.__comps.forEach(c => {\n    c.invalidated = c.stopped = false; !c.invalidate();\n  });}\n\n  subscriptionsReady() {\n    return !Object.keys(this.__subs).some(id => !this.__subs[id].ready());\n  }\n\n  componentWillUnmount() {\n    Object.keys(this.__subs).forEach(sub => this.__subs[sub].stop());\n    this.__comps.forEach(comp => comp.stop());\n  }\n\n  render() {\n    const { children } = this.props;\n    const comp = (children instanceof Array ? children : [children]).map(c => React.cloneElement(c, this.state));\n    return comp.length == 1 ? comp[0] : <div>{comp}</div>;\n  }\n}\n\nexport default TrackerComponent;\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/ui/components/VerifyEmail.jsx",
    "content": "import { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';\nimport React, { PureComponent } from 'react';\nimport { withRouter } from 'react-router';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\n\n\nclass AccountsVerifyEmail extends PureComponent {\n  constructor(props) {\n    super(props);\n    this.state = {\n      pending: true,\n      error: null\n    };\n  }\n\n  componentDidMount() {\n    const token = this.props.match.params.token;\n    const currentUserRefetch = this.props.currentUserRefetch;\n\n    Accounts.verifyEmail(token, (verifyEmailResult) => {\n      currentUserRefetch();\n\n      if(verifyEmailResult && verifyEmailResult.error) {\n        this.setState({\n          pending: false,\n          error: verifyEmailResult.reason\n        });\n      } else {\n        this.setState({\n          pending: false,\n          error: null\n        });\n      }\n    });\n  }\n\n  render() {\n    if(this.state.pending) {\n      return <Components.Loading />;\n    } else if(this.state.error) {\n      return (\n        <div className='password-reset-form'>\n          {this.state.error}\n        </div>\n      );\n    } else {\n      return (\n        <div className='password-reset-form'>\n          {this.context.intl.formatMessage({id: 'accounts.email_verified'})}\n        </div>\n      );\n    }\n  }\n}\n\nAccountsVerifyEmail.contextTypes = {\n  intl: intlShape\n};\n\nAccountsVerifyEmail.propsTypes = {\n  currentUser: PropTypes.object,\n  match: PropTypes.object.isRequired,\n};\n\nAccountsVerifyEmail.displayName = 'AccountsEnrollAccount';\n\nregisterComponent('AccountsVerifyEmail', AccountsVerifyEmail, withCurrentUser, withRouter);\n"
  },
  {
    "path": "packages/vulcan-accounts/imports/useMeteorLogout.js",
    "content": "import { useApolloClient } from '@apollo/client';\n\n/**\n *  Hook used to sign the user out.\n *\n * @param {function} callback called after the logout and the Apollo store reset\n * @returns {function} a function to execute when you log the user out\n */\n\nconst useMeteorLogout = (callback = () => {}) => {\n  const client = useApolloClient();\n  return () =>\n    Meteor.logout(() => {\n      const resetStoreCallback = () => {\n        callback();\n        removeResetStoreCallback(resetStoreCallback);\n      };\n      const removeResetStoreCallback = client.onResetStore(resetStoreCallback);\n      client.resetStore();\n    });\n};\n\nexport default useMeteorLogout;\n"
  },
  {
    "path": "packages/vulcan-accounts/main_client.js",
    "content": "import { Accounts } from 'meteor/accounts-base';\nimport './imports/accounts_ui.js';\nimport './imports/components.js';\nimport './imports/login_session.js';\nimport './imports/routes.js';\nimport { STATES } from './imports/helpers.js';\nimport useMeteorLogout from './imports/useMeteorLogout.js';\n\nimport './imports/ui/components/LoginForm.jsx';\n\nexport { Accounts, STATES, useMeteorLogout };\nexport default Accounts;\n"
  },
  {
    "path": "packages/vulcan-accounts/main_server.js",
    "content": "import { Accounts } from 'meteor/accounts-base';\nimport './imports/accounts_ui.js';\nimport './imports/components.js';\nimport './imports/login_session.js';\nimport './imports/routes.js';\nimport './imports/oauth_config.js';\nimport './imports/emailTemplates.js';\nimport { redirect, STATES } from './imports/helpers.js';\nimport './imports/api/server/servicesListPublication.js';\nimport useMeteorLogout from './imports/useMeteorLogout.js';\n\nimport './imports/ui/components/LoginForm.jsx';\n\nexport { Accounts, redirect, STATES, useMeteorLogout };\nexport default Accounts;\n"
  },
  {
    "path": "packages/vulcan-accounts/package.js",
    "content": "Package.describe({\n  name: 'vulcan:accounts',\n  version: '1.16.9',\n  summary: 'Accounts UI for React in Meteor 1.3+',\n  git: 'https://github.com/studiointeract/accounts-ui',\n  documentation: 'README.md',\n});\n\nPackage.onUse(function(api) {\n  api.use('vulcan:core@=1.16.9');\n\n  api.use('tracker@1.2.0');\n  api.use('session@1.2.0');\n  api.use('accounts-oauth@1.3.0', { weak: true });\n  api.use('accounts-password@2.0.0', { weak: true });\n  api.use('service-configuration@1.1.0');\n  api.use('accounts-base@2.0.0');\n\n  api.mainModule('main_client.js', 'client');\n  api.mainModule('main_server.js', 'server');\n});\n"
  },
  {
    "path": "packages/vulcan-admin/README.md",
    "content": "VulcanJS admin package."
  },
  {
    "path": "packages/vulcan-admin/lib/client/main.js",
    "content": "export * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-admin/lib/components/AdminHome.jsx",
    "content": "import React from 'react';\nimport { Components, withCurrentUser, AdminColumns } from 'meteor/vulcan:core';\nimport Users from 'meteor/vulcan:users';\n\nimport '../modules/columns.js';\n\nconst AdminHome = ({ currentUser }) => (\n  <div className=\"admin-home page\">\n    <Components.ShowIf\n      check={Users.isAdmin}\n      document={currentUser}\n      failureComponent={\n        <p className=\"admin-home-message\">\n          <Components.FormattedMessage id=\"app.noPermission\" />\n        </p>\n      }>\n      <Components.Datatable\n        collection={Users}\n        columns={AdminColumns}\n        options={{\n          fragmentName: 'UsersAdmin',\n          terms: { view: 'usersAdmin' },\n          limit: 20,\n        }}\n        title=\"Users\"\n        showEdit={true}\n        showPaper={true}\n      />\n    </Components.ShowIf>\n  </div>\n);\n\nexport default withCurrentUser(AdminHome);\n"
  },
  {
    "path": "packages/vulcan-admin/lib/components/AdminLayout.jsx",
    "content": "/**\n * @Author: Apollinaire Lecocq <apollinaire>\n * @Date:   08-01-19\n * @Last modified by:   apollinaire\n * @Last modified time: 10-01-19\n */\nimport React from 'react';\nimport {registerComponent, Components, withAccess, Dummy} from 'meteor/vulcan:core';\n\nconst RestrictToAdmins = withAccess({groups: ['admins']})(Dummy);\n\n/**\n * A simple component that renders the existing layout and checks whether the currentUser is an admin or not.\n */\n\nfunction AdminLayout({children}) {\n  return (\n    <Components.Layout>\n      <RestrictToAdmins>{children}</RestrictToAdmins>\n    </Components.Layout>\n  );\n}\n\nregisterComponent({\n  name: 'AdminLayout',\n  component: AdminLayout,\n});\n"
  },
  {
    "path": "packages/vulcan-admin/lib/components/users/columns/AdminUsersActions.jsx",
    "content": "import React from 'react';\nimport Users from 'meteor/vulcan:users';\nimport { Components, withRemove } from 'meteor/vulcan:core';\n\nconst AdminUsersActions = ({ document: user, deleteUser }) => {\n  const deleteHandler = e => {\n    e.preventDefault();\n    if (confirm(`Delete user ${Users.getDisplayName(user)}?`)) {\n      deleteUser({ documentId: user._id });\n    }\n  };\n\n  return (\n    <Components.Button variant=\"primary\" onClick={deleteHandler}>\n      Delete\n    </Components.Button>\n  );\n};\n\nconst removeOptions = {\n  collection: Users,\n};\n\nexport default withRemove(removeOptions)(AdminUsersActions);\n"
  },
  {
    "path": "packages/vulcan-admin/lib/components/users/columns/AdminUsersCreated.jsx",
    "content": "import React from 'react';\nimport { Components } from 'meteor/vulcan:core';\nimport moment from 'moment';\n\nconst AdminUsersCreated = ({ document: user }) =>\n  <div>\n    {moment(new Date(user.createdAt)).format('MM/DD/YY')}\n  </div>;\n\nexport default AdminUsersCreated;"
  },
  {
    "path": "packages/vulcan-admin/lib/components/users/columns/AdminUsersEmail.jsx",
    "content": "import React from 'react';\nimport Users from 'meteor/vulcan:users';\nimport { Components } from 'meteor/vulcan:core';\n\nconst AdminUsersEmail = ({ document: user }) =>\n  <a href={`mailto:${Users.getEmail(user)}`}>{Users.getEmail(user)}</a>;\n\nexport default AdminUsersEmail;"
  },
  {
    "path": "packages/vulcan-admin/lib/components/users/columns/AdminUsersName.jsx",
    "content": "import React from 'react';\nimport Users from 'meteor/vulcan:users';\nimport { Components } from 'meteor/vulcan:core';\n\nconst AdminUsersName = ({ document: user, flash }) => \n  <div>\n\n    <Components.Avatar user={user} link={false} gutter=\"right\"/>\n\n    <span>{Users.getDisplayName(user)}</span>\n\n    &nbsp;\n\n    {_.rest(Users.getGroups(user)).map(group => <code style={{ marginLeft: 8 }} key={group}>{group}</code>)}\n  \n  </div>;\n\nexport default AdminUsersName;\n"
  },
  {
    "path": "packages/vulcan-admin/lib/modules/columns.js",
    "content": "import { addAdminColumn } from 'meteor/vulcan:core';\n\nimport AdminUsersName from '../components/users/columns/AdminUsersName.jsx';\nimport AdminUsersEmail from '../components/users/columns/AdminUsersEmail.jsx';\nimport AdminUsersCreated from '../components/users/columns/AdminUsersCreated.jsx';\n\naddAdminColumn([\n  {\n    name: 'name',\n    order: 1,\n    component: AdminUsersName\n  },\n  {\n    name: 'email',\n    order: 10,\n    component: AdminUsersEmail\n  },\n  {\n    name: 'created',\n    order: 20,\n    component: AdminUsersCreated\n  },\n]);"
  },
  {
    "path": "packages/vulcan-admin/lib/modules/fragments.js",
    "content": "import { registerFragment } from 'meteor/vulcan:lib';\n\n// ------------------------------ Vote ------------------------------ //\n\n// note: fragment used by default on the UsersProfile fragment\nregisterFragment(`\n  fragment UsersAdmin on User {\n    _id\n    username\n    createdAt\n    isAdmin\n    displayName\n    email\n    emailHash\n    slug\n    groups\n    services\n    avatarUrl\n    pageUrl\n    pagePath\n  }\n`);\n"
  },
  {
    "path": "packages/vulcan-admin/lib/modules/i18n.js",
    "content": "import { addStrings } from 'meteor/vulcan:core';\n\naddStrings('en', {\n  'users.name': 'Name',\n  'users.created': 'Created',\n  'users.groups': 'Groups',\n  'users.actions': 'Actions',\n  'users.email': 'Email',\n});"
  },
  {
    "path": "packages/vulcan-admin/lib/modules/index.js",
    "content": "import './fragments.js';\nimport './routes.js';\nimport './i18n.js';\nimport '../components/AdminLayout';\n"
  },
  {
    "path": "packages/vulcan-admin/lib/modules/routes.js",
    "content": "import {addRoute, getDynamicComponent} from 'meteor/vulcan:core';\nimport React from 'react';\n\naddRoute({\n  name: 'admin',\n  path: '/admin',\n  component: () => getDynamicComponent(import('../components/AdminHome.jsx')),\n  layoutName: 'AdminLayout',\n});\naddRoute({\n  name: 'admin2',\n  path: '/admin/users',\n  component: () => getDynamicComponent(import('../components/AdminHome.jsx')),\n});\n"
  },
  {
    "path": "packages/vulcan-admin/lib/server/main.js",
    "content": "export * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-admin/lib/stylesheets/style.scss",
    "content": ".datatable-users{\n  .datatable-search{\n    margin-bottom: 10px;\n    padding: 2px 7px;\n  }\n\n  .datatable-item-name{\n    .modal-trigger{\n      display: inline-block;\n      cursor: pointer;\n    }\n    .avatar{\n      display: inline-block;\n      margin-right: 5px;\n    }\n    a{\n      display: flex;\n      align-items: center;\n      div{\n        margin-right: 5px;\n      }\n    }\n  }\n\n  .datatable-item-groups{\n    code{\n      display: inline-block;\n      margin-right: 5px;\n    }\n  }\n\n  .datatable-load-more{\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .avatar img{\n    width: 20px;\n  }\n}"
  },
  {
    "path": "packages/vulcan-admin/package.js",
    "content": "Package.describe({\n  name: 'vulcan:admin',\n  summary: 'Vulcan components package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use([\n    'vulcan:scss@4.12.0',\n    'dynamic-import@0.1.1',\n    // Vulcan packages\n    'vulcan:core@=1.16.9',\n  ]);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n\n  api.addFiles(['lib/stylesheets/style.scss'], ['client']);\n});\n"
  },
  {
    "path": "packages/vulcan-backoffice/.gitignore",
    "content": "./node_modules\nnode_modules/\n"
  },
  {
    "path": "packages/vulcan-backoffice/README.md",
    "content": "Vulcan back-office package, used internally. "
  },
  {
    "path": "packages/vulcan-backoffice/lib/client/main.js",
    "content": "export * from '../modules';\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/components/BackofficeIndex.jsx",
    "content": "import React from 'react';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\nimport NoSSR from 'react-no-ssr';\n\nconst BackofficeIndex = () => { \n  return (\n  <div>\n    <p>Welcome to Vulcan autogenerated backoffice</p>\n    {/** AccountsLoginForm is SSR only */}\n    <div>\n      <NoSSR>\n        <Components.AccountsLoginForm />\n      </NoSSR>\n    </div>\n  </div>\n);};\nregisterComponent({ name: 'VulcanBackofficeIndex', component: BackofficeIndex, hocs: [] });\n\nexport default BackofficeIndex;\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/components/BackofficeLayout.jsx",
    "content": "import React, { useState } from 'react';\nimport { getAuthorizedMenuItems, menuItemProps, registerComponent, withCurrentUser, Components } from 'meteor/vulcan:core';\nimport { Link } from 'react-router-dom';\nimport PropTypes from 'prop-types';\nimport { withRouter } from 'react-router';\n\nconst MenuItem = ({ name, label, path, onClick, labelToken, LeftComponent, RightComponent }, { intl }) => {\n  let Wrapper = React.Fragment;\n  if (path) {\n    const LinkToPath = ({ children }) => <Link to={path}>{children}</Link>;\n    Wrapper = LinkToPath;\n  }\n  return (\n    <Wrapper key={name}>\n      <li\n        //selected={path && router.isActive(path)}\n        onClick={onClick}>\n        {LeftComponent && <LeftComponent />}\n        <span>{label || intl.formatMessage({ id: labelToken })}</span>\n        {RightComponent && <RightComponent />}\n      </li>\n    </Wrapper>\n  );\n};\n\nMenuItem.propTypes = {\n  ...menuItemProps,\n  // parent can pass another onClick callback\n  // eg to close the menu\n  afterClick: PropTypes.func,\n};\n\nconst Layout = ({ children, currentUser }) => {\n  const [open, setOpen] = useState(true);\n\n  const backofficeMenuItems = getAuthorizedMenuItems(currentUser, 'vulcan-backoffice');\n\n  const side = <Components.VerticalNavigation links={backofficeMenuItems} />;\n\n  return (\n    <Components.VulcanBackofficePageLayout>\n      <Components.VulcanBackofficeNavbar\n        onClick={() => {\n          setOpen(!open);\n        }}\n        basePath={'/backoffice'}\n      />\n\n      <Components.VulcanBackofficeVerticalMenuLayout side={side} main={children} open={open} />\n    </Components.VulcanBackofficePageLayout>\n  );\n};\n\nregisterComponent({\n  name: 'VulcanBackofficeLayout',\n  component: Layout,\n  hocs: [withRouter, withCurrentUser],\n});\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/components/CollectionItem.jsx",
    "content": "/**\n * Generic page for a collection element\n *\n * Must be handled by the parent :\n * - the document, using withDocument and options\n */\n\nimport React from 'react';\nimport { registerComponent, Components, withCurrentUser, withAccess } from 'meteor/vulcan:core';\n\n// TODO: get options from backoffice config\nconst accessOptions = {\n  groups: ['admins'],\n  redirect: '/backoffice',\n  message: 'Sorry, you do not have the rights to access this page.',\n};\nconst CollectionItemDetails = props => {\n  if (props.loading) return <Components.Loading />;\n  if (!props.document) return 'Document not found';\n  return <Components.Card {...props} />;\n};\n\nregisterComponent({\n  name: 'VulcanBackofficeCollectionItem',\n  component: CollectionItemDetails,\n  hocs: [withCurrentUser, [withAccess, accessOptions]],\n});\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/components/CollectionList.jsx",
    "content": "/**\n * Generic page for a collection\n * Must be handled by the parent :\n * - providing the documents and callbacks\n */\n\nimport React from 'react';\nimport { Components, registerComponent, withCurrentUser, withAccess } from 'meteor/vulcan:core';\n// TODO: get options from backoffice config\nconst accessOptions = {\n  groups: ['admins'],\n  redirect: '/backoffice',\n  message: 'Sorry, you do not have the rights to access this page.',\n};\n\nexport const CollectionList = props => {\n  return <Components.Datatable collection={props.collection} />;\n};\n\nexport default CollectionList;\nregisterComponent({\n  name: 'VulcanBackofficeCollectionList',\n  component: CollectionList,\n  hocs: [withCurrentUser, [withAccess, accessOptions]],\n});\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/hocs/withDocumentId.js",
    "content": "/**\n * Get the documentId from parent props or from the route\n */\nimport React from 'react';\nexport const withDocumentId = (fieldName = 'documentId') => Component => {\n  const withDocumentId = props => (\n    <Component\n      documentId={props[fieldName] || (props.params && props.params[fieldName]) || undefined}\n      {...props}\n    />\n  );\n  withDocumentId.displayName = `withDocumentId(${Component.displayName})`;\n  return withDocumentId;\n};\nexport default withDocumentId;\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/hocs/withRouteParam.js",
    "content": "/**\n * Pass a route param to its child\n *\n */\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nexport const withRouteParam = fieldName => Component => {\n  const Wrapper = props => (\n    <Component\n      {...props}\n      {...{\n        [fieldName]: props[fieldName] || (props.params && props.params[fieldName]) || undefined,\n      }}\n    />\n  );\n\n  Wrapper.propTypes = {\n    // @see React router 4 withRouter API\n    match: PropTypes.shape({\n      params: PropTypes.object,\n    }),\n  };\n  Wrapper.displayName = `withRouteParam(${fieldName})(${Component.displayName})`;\n  return Wrapper;\n};\nexport default withRouteParam;\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/components.js",
    "content": "// load generic components\nimport '../components/CollectionItem';\nimport '../components/CollectionList';\nimport '../components/BackofficeLayout';\nimport '../components/BackofficeIndex';\n//import '../components/BackofficeVerticalMenuLayout';\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/createCollectionComponents/createCollectionComponents.js",
    "content": "/**\n * Create List and Item components for the provided collection,\n * based on the generic Vulcan backoffice components\n */\nimport createListComponent from './createListComponent';\nimport createItemComponent from './createItemComponent';\nimport { mergeDefaultCollectionOptions } from '../options';\n\nconst createCollectionComponents = (collection, options) => {\n  const mergedOptions = mergeDefaultCollectionOptions(options);\n  const ListComponent = createListComponent(collection, mergedOptions);\n  const ItemComponent = createItemComponent(collection, mergedOptions);\n  return { ListComponent, ItemComponent };\n};\nexport default createCollectionComponents;\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/createCollectionComponents/createItemComponent.js",
    "content": "import React, { PureComponent } from 'react';\nimport { registerComponent, Components, withSingle, withAccess } from 'meteor/vulcan:core';\nimport { getItemComponentName } from '../namingHelpers';\nimport { withRouteParam } from '../../hocs/withRouteParam';\n/**\n * Create the item details page\n */\nconst createItemComponent = (collection, options) => {\n  const componentName = getItemComponentName(collection);\n  const component = class DetailsComponent extends PureComponent {\n    render() {\n      const { loading, document } = this.props;\n      return (\n        <Components.VulcanBackofficeCollectionItem\n          collection={collection}\n          loading={loading}\n          document={document}\n          headerText={options.item.headerText}\n          fields={options.item.fields}\n        />\n      );\n    }\n  };\n  component.displayName = componentName;\n  const withDocumentOptions = {\n    collection,\n  };\n  const withAccessOptions = {\n    groups: options.item.accessGroups,\n    redirect: options.item.accessRedirect,\n  };\n  registerComponent({\n    name: componentName,\n    component: component,\n    hocs: [\n      [withAccess, withAccessOptions],\n      withRouteParam('documentId'),\n      [withSingle, withDocumentOptions],\n    ],\n  });\n  return component; // return if the component is needed\n};\nexport default createItemComponent;\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/createCollectionComponents/createListComponent.js",
    "content": "import React, { PureComponent } from 'react';\nimport { registerComponent, Components, withAccess } from 'meteor/vulcan:core';\nimport { getListComponentName } from '../namingHelpers';\n\nconst createListComponent = (collection, options) => {\n  const component = class ListComponent extends PureComponent {\n    render() {\n      const { ...otherProps } = this.props;\n      const { list } = options;\n      const { ...otherListOptions } = list;\n      return (\n        <Components.VulcanBackofficeCollectionList\n          collection={collection}\n          {...otherListOptions}\n          {...otherProps}\n        />\n      );\n    }\n  };\n\n  const withAccessOptions = {\n    groups: options.list.accessGroups,\n    redirect: options.list.accessRedirect,\n  };\n\n  const componentName = getListComponentName(collection);\n  component.displayName = componentName;\n\n  registerComponent({\n    name: componentName,\n    component: component,\n    hocs: [[withAccess, withAccessOptions]],\n  });\n  return component;\n};\nexport default createListComponent;\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/createCollectionComponents/index.js",
    "content": "export { default as createListComponent } from './createListComponent';\nexport { default as createItemComponent } from './createItemComponent';\n\nexport { default } from './createCollectionComponents';\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/index.js",
    "content": "import './settings';\nimport './startup';\nimport './components';\n\nexport { default as withDocumentId } from '../hocs/withDocumentId';\nexport { default as createCollectionComponents } from './createCollectionComponents';\nexport * from './setupCollectionMenuItems';\n\nexport { default as setupCollectionRoutes } from './setupCollectionRoutes';\n\nexport { default, default as setupBackoffice } from './setupBackoffice';\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/namingHelpers.js",
    "content": "const capitalizeFirstLetter = string => string.charAt(0).toUpperCase() + string.slice(1);\n\nexport const getCollectionName = collection => {\n  return collection.options.collectionName;\n};\nexport const getCollectionDisplayName = collection =>\n  capitalizeFirstLetter(getCollectionName(collection));\n\nconst makeComponentName = suffix => collection =>\n  `VulcanBackoffice${capitalizeFirstLetter(getCollectionName(collection))}${suffix}`;\n\nexport const getItemComponentName = makeComponentName('Item');\nexport const getListComponentName = makeComponentName('List');\nexport const getFormComponentName = makeComponentName('Form');\nexport const getFragmentName = makeComponentName('DefaultFragment');\nexport const getBaseRouteName = collection => getCollectionName(collection).toLowerCase();\n\n// return {basePath}/collection-name\nexport const getBasePath = (collection, basePath) => {\n  const collectionBasePath = '/' + getBaseRouteName(collection);\n  return typeof basePath !== 'undefined' ? basePath + collectionBasePath : collectionBasePath;\n};\nexport const getNewPath = (collection, basePath) => getBasePath(collection, basePath) + '/create';\nexport const getEditPath = (collection, basePath) =>\n  getBasePath(collection, basePath) + '/:documentId/edit';\nexport const getDetailsPath = (collection, basePath) =>\n  getBasePath(collection, basePath) + '/:documentId';\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/options.js",
    "content": "/**\n * Setup default options and provides helper to generate valid options based\n * on these defaults\n */\nimport _merge from 'lodash/merge';\n\nexport const devOptions = {\n  list: { accessGroups: ['guests', 'members', 'admins'] },\n  item: { accessGroups: ['guests', 'members', 'admins'] },\n  menuItem: { groups: ['guests', 'members', 'admins'] },\n  layoutName: 'VulcanBackofficeLayout'\n};\n\nconst defaultCollectionOptions = {\n  list: { accessGroups: ['admins'], accessRedirect: '/' },\n  item: { accessGroups: ['admins'], accessRedirect: '/' },\n  menuItem: {\n    groups: ['admins'],\n  },\n  layoutName: 'VulcanBackofficeLayout',\n};\n\nconst defaultBackofficeOptions = {\n  //generateUI: true,\n  basePath: '/backoffice',\n  ...defaultCollectionOptions,\n};\n\nexport const mergeDefaultCollectionOptions = (collectionOptions, options = {}) =>\n  _merge({}, defaultBackofficeOptions, options, collectionOptions);\nexport const mergeDefaultBackofficeOptions = options =>\n  _merge({}, defaultBackofficeOptions, options);\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/settings.js",
    "content": "import { registerSetting } from 'meteor/vulcan:core';\nregisterSetting('backoffice.enable', Meteor.isDevelopment, 'Automatically generate a backoffice', true);\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/setupBackoffice.js",
    "content": "/** Setup a full fledged backoffice\n * - create components\n * - create routes\n * - register menu items\n */\nimport { addRoute } from 'meteor/vulcan:core';\nimport { mergeDefaultBackofficeOptions, mergeDefaultCollectionOptions } from './options';\nimport { getCollectionName } from './namingHelpers';\nimport createCollectionComponents from './createCollectionComponents';\nimport setupCollectionRoutes from './setupCollectionRoutes';\nimport setupCollectionMenuItems from './setupCollectionMenuItems';\n\nconst setupBackoffice = (collections, providedOptions = {}, collectionsOptions = {}) => {\n  const options = mergeDefaultBackofficeOptions(providedOptions);\n  // pages for each collection\n  collections.forEach(collection => {\n    const collectionName = getCollectionName(collection);\n    const collectionOptions = mergeDefaultCollectionOptions(\n      collectionsOptions[collectionName],\n      options\n    );\n    const { ListComponent, ItemComponent } = createCollectionComponents(\n      collection,\n      collectionOptions\n    );\n    setupCollectionRoutes(collection, collectionOptions);\n    setupCollectionMenuItems(collection, collectionOptions);\n  });\n  // index\n  addRoute({\n    name: 'vulcan-backoffice', path: options.basePath,\n    componentName: 'VulcanBackofficeIndex',\n    layoutName: 'VulcanBackofficeLayout',\n    options\n  }); // setup the route\n};\n\nexport default setupBackoffice;\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/setupCollectionMenuItems.js",
    "content": "/** Add an item to the menu to access the collection */\nimport { addMenuItem, getMenuItems, getAuthorizedMenuItems } from 'meteor/vulcan:core';\nimport { getBasePath, getCollectionName, getCollectionDisplayName } from './namingHelpers';\nimport { mergeDefaultCollectionOptions } from './options';\n\nconst adminMenuName = 'vulcan-backoffice';\n\nexport const setupCollectionMenuItems = (collection, collectionOptions) => {\n  const options = mergeDefaultCollectionOptions(collectionOptions);\n  const labelToken = options.menuItem.labelToken;\n  const label = !labelToken\n    ? options.menuItem.label || getCollectionDisplayName(collection)\n    : undefined;\n  const collectionName = getCollectionName(collection);\n  addMenuItem({\n    name: collectionName,\n    label,\n    labelToken: labelToken,\n    path: options.menuItem.basePath || getBasePath(collection, options.basePath),\n    groups: options.menuItem.groups,\n    menuGroup: adminMenuName,\n  });\n};\n// to retrieve the items\nexport const getBackofficeMenuItems = () => getMenuItems(adminMenuName);\nexport const getAuthorizedBackofficeMenuItems = currentUser =>\n  getAuthorizedMenuItems(currentUser, adminMenuName);\n\nexport default setupCollectionMenuItems;\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/setupCollectionRoutes.js",
    "content": "import { addRoute } from 'meteor/vulcan:core';\nimport {\n  getBasePath,\n  getBaseRouteName,\n  getDetailsPath,\n  getListComponentName,\n  getItemComponentName,\n} from './namingHelpers';\nimport { mergeDefaultCollectionOptions } from './options';\nimport _values from 'lodash/values';\n\nexport const generateRoutes = (collection, options) => {\n  const basePath = getBasePath(collection, options.basePath);\n  const detailsPath = getDetailsPath(collection, options.basePath);\n  const baseRouteName = getBaseRouteName(collection);\n  const routes = {\n    listRoute: {\n      name: 'vulcan-backoffice-' + baseRouteName,\n      path: basePath,\n      componentName: getListComponentName(collection),\n      returnRoute: basePath,\n      layoutName: options.layoutName,\n    },\n    itemRoute: {\n      name: 'vulcan-backoffice-' + baseRouteName + '-details',\n      path: detailsPath,\n      componentName: getItemComponentName(collection),\n      returnRoute: basePath,\n      layoutName: options.layoutName,\n    },\n  };\n  return routes;\n};\nexport default (collection, providedOptions = {}) => {\n  const options = mergeDefaultCollectionOptions(providedOptions);\n  const routes = generateRoutes(collection, options);\n  _values(routes).forEach(route => {\n    addRoute(route);\n  });\n  return routes;\n};\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/modules/startup.js",
    "content": "/**\n * Generate the backoffice on startup\n */\nimport {getSetting, Collections} from 'meteor/vulcan:core';\nimport setupBackoffice from './setupBackoffice';\nimport {devOptions} from './options';\nimport {addCallback} from 'meteor/vulcan:lib';\n\nconst enabled = getSetting('backoffice.enabled', Meteor.isDevelopment);\n\nif (enabled) {\n  const options = Meteor.isDevelopment ? devOptions : undefined; // loose permissions during development\n  // setupBackoffice must be run before routes and components are populated\n  // but after startup so that Collections are available\n  addCallback('populate.before', function _setupBackoffice() {\n    setupBackoffice(Collections, options);\n  });\n}\n"
  },
  {
    "path": "packages/vulcan-backoffice/lib/server/main.js",
    "content": "export * from '../modules';\n"
  },
  {
    "path": "packages/vulcan-backoffice/package.js",
    "content": "Package.describe({\n  name: 'vulcan:backoffice',\n  summary: 'Vulcan automated backoffice generator',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(api => {\n  api.use(['vulcan:core@=1.16.9', 'vulcan:i18n@=1.16.9', 'vulcan:accounts@1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n\nPackage.onTest(function(api) {\n  api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:core']);\n  api.mainModule('./test/index.js');\n});\n"
  },
  {
    "path": "packages/vulcan-backoffice/package.json",
    "content": "{\n  \"name\": \"vulcan-backoffice\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"main\": \"package.js\",\n  \"directories\": {\n    \"lib\": \"lib\"\n  },\n  \"scripts\": {\n    \"test\": \"npm run test-unit\",\n    \"test-unit\": \"TEST_WATCH=1 meteor test-packages ./ --port 3002 --driver-package meteortesting:mocha --raw-logs\"\n  },\n  \"devDependencies\": {\n    \"expect\": \"^23.6.0\"\n  }\n}\n"
  },
  {
    "path": "packages/vulcan-backoffice/test/index.js",
    "content": "import './namingHelpers.test';\n\nimport './routes.test';\n"
  },
  {
    "path": "packages/vulcan-backoffice/test/namingHelpers.test.js",
    "content": "import expect from 'expect';\nimport {\n  getCollectionName,\n  getBasePath,\n  getNewPath,\n  getEditPath,\n  getDetailsPath,\n} from '../lib/modules/namingHelpers';\n\nconst dummyCollectionName = 'Dummies';\nconst DummyCollection = {\n  options: {\n    collectionName: dummyCollectionName,\n  },\n};\ndescribe('vulcan:backoffice/namingHelpers', function () {\n  it('get collection name', function () {\n    const collectionName = getCollectionName(DummyCollection);\n    expect(collectionName).toEqual(dummyCollectionName);\n  });\n  it('get base path', function () {\n    const basePath = getBasePath(DummyCollection);\n    expect(basePath).toEqual('/dummies');\n  });\n  it('add a prefix to base path', function () {\n    const basePath = getBasePath(DummyCollection, '/admin');\n    expect(basePath).toEqual('/admin/dummies');\n  });\n  it('get new path', function () {\n    const Path = getNewPath(DummyCollection);\n    expect(Path).toEqual('/dummies/create');\n  });\n  it('get edit path', function () {\n    const Path = getEditPath(DummyCollection);\n    expect(Path).toEqual('/dummies/:documentId/edit');\n  });\n  it('get details path', function () {\n    const Path = getDetailsPath(DummyCollection);\n    expect(Path).toEqual('/dummies/:documentId');\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-backoffice/test/options.js",
    "content": "import expect from 'expect';\nimport {\n  mergeDefaultBackofficeOptions,\n  mergeDefaultCollectionOptions,\n} from '../lib/modules/options';\ndescribe('options', function () {\n  it('merge defaultOptions', function () {\n    const givenOptions = {\n      menuItem: { groups: ['members', 'admins', 'foobars'] },\n    };\n    const mergedOptions = mergeDefaultBackofficeOptions(givenOptions);\n    expect(mergedOptions.menuItem.groups).toEqual(['members', 'admins', 'foobars']);\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-backoffice/test/routes.test.js",
    "content": "import expect from 'expect';\nimport { generateRoutes } from '../lib/modules/setupCollectionRoutes';\n\nconst DummyCollection = {\n  options: {\n    collectionName: 'Dummies',\n  },\n};\ndescribe('vulcan:backoffice/setupCollectionRoutes', function () {\n  it('generate routes', function () {\n    const options = {}\n    const { listRoute, itemRoute } = generateRoutes(DummyCollection, options);\n    //expect(baseRoute.path).toEqual('/dummies');\n    expect(listRoute.path).toEqual('/dummies');\n    expect(itemRoute.path).toEqual('/dummies/:documentId');\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-cloudinary/README.md",
    "content": "Vulcan file upload package, used internally. \n\n### Custom Posts Fields\n\n- `cloudinaryId`\n- `cloudinaryUrls`\n\n### Public Settings\n\n- `cloudinaryCloudName`\n- `cloudinaryFormats`\n\n### Private Settings\n\n- `cloudinaryAPIKey`\n- `cloudinaryAPISecret`\n\n### Sample Settings\n\n```js\n{\n  \"public\": {\n    \"cloudinaryCloudName\": \"myCloudName\",\n    \"cloudinaryFormats\": [\n      {\n        \"name\": \"small\",\n        \"width\": 120,\n        \"height\": 90\n      },\n      {\n        \"name\": \"medium\",\n        \"width\": 480,\n        \"height\": 360\n      }\n    ]\n  },\n  \"cloudinaryAPIKey\": \"abcfoo\",\n  \"cloudinaryAPISecret\": \"123bar\",\n}\n```"
  },
  {
    "path": "packages/vulcan-cloudinary/lib/client/main.js",
    "content": "export * from '../modules/index.js';\nexport * from './make_cloudinary.js';\n"
  },
  {
    "path": "packages/vulcan-cloudinary/lib/client/make_cloudinary.js",
    "content": "import { addCustomFields } from '../modules/index.js';\n\nexport const makeCloudinary = ({collection, fieldName}) => {\n  addCustomFields(collection);\n};"
  },
  {
    "path": "packages/vulcan-cloudinary/lib/modules/custom_fields.js",
    "content": "export const CloudinaryCollections = [];\n\nexport const addCustomFields = collection => {\n\n  CloudinaryCollections.push(collection);\n\n  collection.addField([\n    {\n      fieldName: 'cloudinaryId',\n      fieldSchema: {\n        type: String,\n        optional: true,\n        canRead: ['guests'],\n      }\n    },\n    {\n      fieldName: 'cloudinaryUrls',\n      fieldSchema: {\n        type: Array,\n        optional: true,\n        canRead: ['guests'],\n      }\n    },\n    {\n      fieldName: 'cloudinaryUrls.$',\n      fieldSchema: {\n        type: Object,\n        blackbox: true,\n        optional: true\n      }\n    },\n\n    // GraphQL only\n    {\n      fieldName: 'cloudinaryUrl',\n      fieldSchema: {\n        type: String,\n        optional: true,\n        canRead: ['guests'],\n        resolveAs: {\n          type: 'String',\n          arguments: 'format: String',\n          resolver: (document, {format}, context) => {\n            const image = format ? _.findWhere(document.cloudinaryUrls, {name: format}) : document.cloudinaryUrls[0];\n            return image && image.url;\n          }\n        },\n      }\n    },\n\n  ]);\n  \n};"
  },
  {
    "path": "packages/vulcan-cloudinary/lib/modules/index.js",
    "content": "export * from './custom_fields.js';"
  },
  {
    "path": "packages/vulcan-cloudinary/lib/server/cloudinary.js",
    "content": "import cloudinary from 'cloudinary';\nimport { Utils, getSetting, registerSetting } from 'meteor/vulcan:core';\n\nregisterSetting('cloudinary', null, 'Cloudinary settings');\n\nexport const Cloudinary = cloudinary.v2;\nconst uploadSync = Meteor.wrapAsync(Cloudinary.uploader.upload);\nconst cloudinarySettings = getSetting('cloudinary');\n\nCloudinary.config({\n  cloud_name: cloudinarySettings.cloudName,\n  api_key: cloudinarySettings.apiKey,\n  api_secret: cloudinarySettings.apiSecret,\n  secure: true,\n});\n\nexport const CloudinaryUtils = {\n\n  // send an image URL to Cloudinary and get a cloudinary result object in return\n  uploadImage(imageUrl) {\n    try {\n      var result = uploadSync(Utils.addHttp(imageUrl));\n      const data = {\n        cloudinaryId: result.public_id,\n        result: result,\n        urls: CloudinaryUtils.getUrls(result.public_id)\n      };\n      return data;\n    } catch (error) {\n      console.log(\"// Cloudinary upload failed for URL: \"+imageUrl); // eslint-disable-line\n      console.log(error); // eslint-disable-line\n    }\n  },\n\n  // generate signed URL for each format based off public_id\n  getUrls(cloudinaryId) {\n    return cloudinarySettings.formats.map(format => {\n      const url = Cloudinary.url(cloudinaryId, {\n        width: format.width,\n        height: format.height,\n        crop: 'fill',\n        sign_url: true,\n        fetch_format: 'auto',\n        quality: 'auto'\n      });\n      return {\n        name: format.name,\n        url: url\n      };\n    });\n  }\n};\n\n// methods\n// Meteor.methods({\n//   testCloudinaryUpload: function (thumbnailUrl) {\n//     if (Users.isAdmin(Meteor.user())) {\n//       thumbnailUrl = typeof thumbnailUrl === \"undefined\" ? \"http://www.telescopeapp.org/images/logo.png\" : thumbnailUrl;\n//       const data = CloudinaryUtils.uploadImage(thumbnailUrl);\n//       console.log(data); // eslint-disable-line\n//     }\n//   },\n//   cachePostThumbnails: function (limit = 20) {\n\n//     if (Users.isAdmin(Meteor.user())) {\n\n//       console.log(`// caching ${limit} thumbnails…`)\n\n//       var postsWithUncachedThumbnails = Posts.find({\n//         thumbnailUrl: { $exists: true },\n//         originalThumbnailUrl: { $exists: false }\n//       }, {sort: {createdAt: -1}, limit: limit});\n\n//       postsWithUncachedThumbnails.forEach(Meteor.bindEnvironment((post, index) => {\n\n//           Meteor.setTimeout(function () {\n//           console.log(`// ${index}. Caching thumbnail for post “${post.title}” (_id: ${post._id})`); // eslint-disable-line\n\n//           const data = CloudinaryUtils.uploadImage(post.thumbnailUrl);\n//           Posts.update(post._id, {$set:{\n//             cloudinaryId: data.cloudinaryId,\n//             cloudinaryUrls: data.urls\n//           }});\n\n//         }, index * 1000);\n\n//       }));\n//     }\n//   }\n// });"
  },
  {
    "path": "packages/vulcan-cloudinary/lib/server/main.js",
    "content": "export * from './cloudinary.js';\nexport * from '../modules/index.js';\nexport * from './make_cloudinary.js';\n"
  },
  {
    "path": "packages/vulcan-cloudinary/lib/server/make_cloudinary.js",
    "content": "import { CloudinaryUtils } from '../server/cloudinary.js';\nimport { getSetting, addCallback } from 'meteor/vulcan:core';\nimport { addCustomFields } from '../modules/index.js';\n\nconst cloudinarySettings = getSetting('cloudinary');\n\nexport const CloudinaryCollections = [];\n\nexport const makeCloudinary = ({collection, fieldName}) => {\n\n  addCustomFields(collection);\n\n  // post submit callback\n  function cacheImageOnNew (document) {\n    if (cloudinarySettings) {\n      if (document[fieldName]) {\n\n        const data = CloudinaryUtils.uploadImage(document[fieldName]);\n        if (data) {\n          document.cloudinaryId = data.cloudinaryId;\n          document.cloudinaryUrls = data.urls;\n        }\n\n      }\n    }\n    return document;\n  }\n  addCallback(`${collection.options.collectionName.toLowerCase()}.new.sync`, cacheImageOnNew);\n\n  function cacheImageOnEdit (modifier, oldDocument) {\n    if (cloudinarySettings) {\n      if (modifier.$set[fieldName] && modifier.$set[fieldName] !== oldDocument[fieldName]) {\n        const data = CloudinaryUtils.uploadImage(modifier.$set[fieldName]);\n        modifier.$set.cloudinaryId = data.cloudinaryId;\n        modifier.$set.cloudinaryUrls = data.urls;\n      }\n    }\n    return modifier;\n  }\n  addCallback(`${collection.options.collectionName.toLowerCase()}.edit.sync`, cacheImageOnEdit);\n\n};"
  },
  {
    "path": "packages/vulcan-cloudinary/package.js",
    "content": "Package.describe({\n  name: 'vulcan:cloudinary',\n  summary: 'Vulcan file upload package.',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9']);\n\n  api.mainModule('lib/client/main.js', 'client');\n  api.mainModule('lib/server/main.js', 'server');\n});\n"
  },
  {
    "path": "packages/vulcan-core/README.md",
    "content": "Vulcan core package, used internally. "
  },
  {
    "path": "packages/vulcan-core/lib/client/components/AppGenerator.jsx",
    "content": "/**\n * The App + relevant wrappers\n */\nimport React from 'react';\nimport { ApolloProvider } from '@apollo/client';\nimport { runCallbacks } from '../../modules';\n\nimport { Components } from 'meteor/vulcan:lib';\nimport { CookiesProvider } from 'react-cookie';\nimport { BrowserRouter } from 'react-router-dom';\n\nconst AppGenerator = ({ apolloClient }) => {\n  const App = (\n    <ApolloProvider client={apolloClient}>\n        <CookiesProvider>\n            <BrowserRouter>\n                <Components.App />\n            </BrowserRouter>\n        </CookiesProvider>\n    </ApolloProvider>\n  );\n  // run user registered callbacks to wrap the app\n  const WrappedApp = runCallbacks({\n    name: 'router.client.wrapper', \n    iterator: App, \n    properties: { apolloClient }\n  });\n  return WrappedApp;\n};\nexport default AppGenerator;"
  },
  {
    "path": "packages/vulcan-core/lib/client/main.js",
    "content": "export * from '../modules/index.js';\n\nexport * from './start.jsx';\n"
  },
  {
    "path": "packages/vulcan-core/lib/client/start.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { onPageLoad } from 'meteor/server-render';\nimport AppGenerator from './components/AppGenerator';\nimport { InjectData } from 'meteor/vulcan:lib';\n\nimport {\n  createApolloClient,\n  populateComponentsApp,\n  populateRoutesApp,\n  initializeFragments,\n  getSetting,\n  runCallbacks,\n} from 'meteor/vulcan:lib';\n\nconst disableSsr = getSetting('apolloSsr.disable', false);\n\nMeteor.startup(() => {\n  // run functions that must be called before populating components or routes\n  runCallbacks('populate.before');\n  // init the application components and routes, including components & routes from 3rd-party packages\n  initializeFragments();\n  populateComponentsApp();\n  populateRoutesApp();\n  const apolloClient = createApolloClient();\n\n  // Create the root element\n  const rootElement = document.createElement('div');\n  rootElement.id = 'react-app';\n  document.body.appendChild(rootElement);\n\n  const Main = () => <AppGenerator apolloClient={apolloClient} />;\n\n  if (!disableSsr) {\n    onPageLoad(() => {\n      const ssrUrl = InjectData.getDataSync('url');\n      // in localhost hostname is null\n      if (ssrUrl && ssrUrl.hostname && ssrUrl.hostname !== window.location.hostname) {\n        console.warn(\n          `Mismatch between the browser hostname (${\n            window.location.hostname\n          }) and the hostname used during SSR (${\n            ssrUrl.hostname\n          }). Will prevent full rehydration of the React DOM.`\n        );\n      } else {\n        ReactDOM.hydrate(<Main />, document.getElementById('react-app'));\n      }\n    });\n  } else {\n    ReactDOM.render(<Main />, document.getElementById('react-app'));\n  }\n});\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/callbacks.js",
    "content": "import { registerCallback } from 'meteor/vulcan:lib';\n\nregisterCallback({\n    name: 'populate.before',\n    description: 'Run before Vulcan objects are populated. Use if you need to add routes dynamically on startup for example.',\n    arguments: [],\n    runs: 'sync',\n    returns: 'nothing',\n});"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/AccessControl.jsx",
    "content": "import React from 'react';\nimport { Components, registerComponent } from 'meteor/vulcan:lib';\nimport { useCurrentUser } from '../containers/currentUser';\nimport Users from 'meteor/vulcan:users';\nimport { useHistory } from 'react-router-dom';\nimport { withMessages } from '../containers/withMessages';\nimport { intlShape } from 'meteor/vulcan:i18n';\n\nconst AccessControl = ({ currentRoute, children, flash }, { intl }) => {\n  const { loading, currentUser } = useCurrentUser();\n  const { access } = currentRoute;\n  const history = useHistory();\n\n  if (!access) {\n    return children;\n  }\n\n  const { groups, redirect, redirectMessage, check } = access;\n\n  if (loading) {\n    return <Components.Loading />;\n  } else if (!currentUser) {\n    if (redirect) {\n      history.push(redirect);\n      flash(\n        redirectMessage ? redirectMessage : intl.formatMessage({ id: 'app.please_sign_up_log_in', defaultMessage: 'Please log in first.' })\n      );\n      return null;\n    } else {\n      return <Components.DefaultLogInFailureComponent {...access} />;\n    }\n  } else {\n    const canAccess = check ? check(currentUser, currentRoute) : groups ? Users.isMemberOf(currentUser, groups) : true;\n    return canAccess ? children : <FailureComponent {...access} />;\n  }\n};\n\nAccessControl.displayName = 'AccessControl';\n\nAccessControl.contextTypes = {\n  intl: intlShape,\n};\n\nregisterComponent({ name: 'AccessControl', component: AccessControl, hocs: [withMessages] });\n\nconst FailureComponent = props => {\n  const { failureComponentName, failureComponent, ...rest } = props;\n  if (failureComponentName) {\n    const FailureComponent = Components[failureComponentName];\n    return <FailureComponent {...rest} />;\n  } else if (failureComponent) {\n    const FailureComponent = failureComponent; // necesary because jsx components must be uppercase\n    return <FailureComponent {...rest} />;\n  } else return <Components.DefaultPermissionFailureComponent {...rest} />;\n};\n\nconst DefaultLogInFailureComponent = () => (\n  <Components.Alert className=\"access-control-failure\" variant=\"danger\">\n    <Components.FormattedMessage id=\"app.please_sign_up_log_in\" defaultMessage=\"Please log in first.\" />\n  </Components.Alert>\n);\n\nregisterComponent({ name: 'DefaultLogInFailureComponent', component: DefaultLogInFailureComponent });\n\nconst DefaultPermissionFailureComponent = () => (\n  <Components.Alert className=\"access-control-failure\" variant=\"danger\">\n    <Components.FormattedMessage id=\"app.no_access_permissions\" defaultMessage=\"Sorry, you are not allowed to access this page.\" />\n  </Components.Alert>\n);\n\nregisterComponent({ name: 'DefaultPermissionFailureComponent', component: DefaultPermissionFailureComponent });\n\nexport default AccessControl;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/App.jsx",
    "content": "import { Components, registerComponent, Strings, runCallbacks, hasIntlFields, Routes, getLocale, getStrings } from 'meteor/vulcan:lib';\nimport React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { IntlProvider, intlShape, IntlContext } from 'meteor/vulcan:i18n';\nimport withCurrentUser from '../containers/currentUser.js';\nimport withUpdate from '../containers/update.js';\nimport withSiteData from '../containers/siteData.js';\nimport { withLocaleData, withLocales } from '../containers/localeData.js';\nimport { withApollo } from '@apollo/client/react/hoc';\nimport { withCookies } from 'react-cookie';\nimport moment from 'moment';\nimport { Switch, Route } from 'react-router-dom';\nimport { withRouter } from 'react-router';\nimport get from 'lodash/get';\nimport merge from 'lodash/merge';\nimport { SSRProvider } from '@react-aria/ssr';\n\n// see https://stackoverflow.com/questions/42862028/react-router-v4-with-multiple-layouts\nconst RouteWithLayout = ({ layoutComponent, layoutName, component, currentRoute, ...rest }) => {\n  // if defined, use ErrorCatcher component to wrap layout contents\n  const ErrorCatcher = Components.ErrorCatcher ? Components.ErrorCatcher : Components.Dummy;\n\n  return (\n    <Route\n      // NOTE: Switch ignores the \"exact\" prop of components that\n      // are not its direct children\n      // Since the render tree is now Switch > RouteWithLayout > Route\n      // (instead of just Switch > Route), we must write <RouteWithLayout exact ... />\n      //exact\n      {...rest}\n      render={props => {\n        const layoutProps = { ...props, currentRoute };\n        const childComponentProps = { ...props, currentRoute };\n        // Use layoutComponent, or else registered layout component; or else default layout\n        const layout = layoutComponent ? layoutComponent : layoutName ? Components[layoutName] : Components.Layout;\n        const children = (\n          <ErrorCatcher>\n            <Components.RouterHook currentRoute={currentRoute} />\n            <Components.AccessControl currentRoute={currentRoute}>\n              {React.createElement(component, childComponentProps)}\n            </Components.AccessControl>\n          </ErrorCatcher>\n        );\n        return React.createElement(layout, layoutProps, children);\n      }}\n    />\n  );\n};\n\nclass App extends PureComponent {\n  constructor(props) {\n    super(props);\n    const { currentUser, locale } = props;\n    if (currentUser) {\n      runCallbacks('events.identify', currentUser);\n    }\n\n    // get translation strings loaded dynamically\n    const loadedStrings = get(props.locale, 'data.locale.strings');\n    // get translation strings bundled statically\n    const bundledStrings = Strings[locale.id];\n\n    this.state = {\n      locale: {\n        id: locale.id,\n        rtl: locale.rtl ?? false,\n        method: locale.method,\n        loading: false,\n        strings: merge({}, loadedStrings, bundledStrings),\n      },\n    };\n\n    moment.locale(locale.id);\n  }\n\n  componentDidMount = async () => {\n    runCallbacks('app.mounted', this.props);\n  };\n\n  // actually returns an id, not a locale\n  getLocale = () => {\n    return this.state.locale.id;\n  };\n\n  setLocale = async localeId => {\n    // note: this is the getLocale in intl.js, not this.getLocale()!\n    const localeObject = getLocale(localeId);\n    const { cookies, updateUser, client, currentUser } = this.props;\n    let localeStrings;\n\n    // if this is a dynamic locale, fetch its data from the server\n    if (localeObject.dynamic) {\n      this.setState({ locale: { ...this.state.locale, loading: true, rtl: localeObject?.rtl ?? false } });\n      localeStrings = await this.loadLocaleStrings(localeId);\n    } else {\n      localeStrings = getStrings(localeId);\n    }\n\n    // before removing the loading we have to change the rtl class on HTML tag if it exists\n    if (document && typeof document.getElementsByTagName === 'function' && document.getElementsByTagName('html')) {\n      const htmlTag = document.getElementsByTagName('html');\n      if (htmlTag && htmlTag.length === 1) {\n        // change in locale didn't change the html lang as well, which is fixed by this PR\n        htmlTag[0].lang = localeId;\n        if (localeObject?.rtl === true) {\n          htmlTag[0].classList.add('rtl');\n        } else {\n          htmlTag[0].classList.remove('rtl');\n        }\n      }\n    }\n    this.setState({\n      locale: { ...this.state.locale, loading: false, id: localeId, rtl: localeObject?.rtl ?? false, strings: localeStrings },\n    });\n\n    cookies.remove('locale', { path: '/' });\n    cookies.set('locale', localeId, { path: '/' });\n    // if user is logged in, change their `locale` profile property\n    if (currentUser) {\n      await updateUser({\n        selector: { documentId: currentUser._id },\n        data: { locale: localeId },\n      });\n    }\n    moment.locale(localeId);\n    if (hasIntlFields) {\n      client.resetStore();\n    }\n  };\n\n  /*\n\n  Load a locale by triggering the refetch() method passed down by\n  withLocalData HoC\n\n  */\n  loadLocaleStrings = async localeId => {\n    const result = await this.props.locale.refetch({ localeId });\n    const fetchedLocaleStrings = get(result, 'data.locale.strings', []);\n    const localeStrings = merge({}, this.state.localeStrings, fetchedLocaleStrings);\n    return localeStrings;\n  };\n\n  getChildContext() {\n    return {\n      getLocale: this.getLocale,\n      setLocale: this.setLocale,\n    };\n  }\n\n  componentDidUpdate(nextProps) {\n    const currentUser = this.props.currentUser;\n    const nextUser = nextProps.currentUser;\n    if (nextUser && (!currentUser || currentUser._id !== nextUser._id)) {\n      runCallbacks('events.identify', nextUser);\n    }\n  }\n\n  render() {\n    const routeNames = Object.keys(Routes);\n    const localeId = this.state.locale.id;\n    //const LayoutComponent = currentRoute.layoutName ? Components[currentRoute.layoutName] : Components.Layout;\n\n    const intlObject = {\n      locale: localeId,\n      key: localeId,\n      messages: this.state.locale.strings,\n    };\n\n    // keep IntlProvider for now for backwards compatibility with legacy Context API\n    return (\n      <SSRProvider>\n        <IntlProvider {...intlObject}>\n          <IntlContext.Provider value={intlObject}>\n            <Components.ScrollToTop />\n            <div className={`locale-${localeId}`}>\n              <Components.HeadTags />\n              {this.props.currentUserLoading ? (\n                <div className=\"app-initial-loading\">\n                  <Components.Loading />\n                </div>\n              ) : routeNames.length ? (\n                <Switch>\n                  {routeNames.map(key => (\n                    // NOTE: if we want the exact props to be taken into account\n                    // we have to pass it to the RouteWithLayout, not the underlying Route,\n                    // because it is the direct child of Switch\n                    <RouteWithLayout exact currentRoute={Routes[key]} siteData={this.props.siteData} key={key} {...Routes[key]} />\n                  ))}\n                  <RouteWithLayout siteData={this.props.siteData} currentRoute={{ name: '404' }} component={Components.Error404} />\n                  {/* <Route component={Components.Error404} />  */}\n                </Switch>\n              ) : (\n                <Components.Welcome />\n              )}\n              {this.state.locale.loading && (\n                <div className=\"app-secondary-loading\">\n                  <Components.Loading />\n                </div>\n              )}\n            </div>\n          </IntlContext.Provider>\n        </IntlProvider>\n      </SSRProvider>\n    );\n  }\n}\n\nApp.propTypes = {\n  currentUserLoading: PropTypes.bool,\n};\n\nApp.childContextTypes = {\n  intl: intlShape,\n  setLocale: PropTypes.func,\n  getLocale: PropTypes.func,\n};\n\nApp.displayName = 'App';\n\nconst updateOptions = {\n  collectionName: 'Users',\n  fragmentName: 'UsersCurrent',\n};\n\nregisterComponent(\n  'App',\n  App,\n  withCurrentUser,\n  withSiteData,\n  // withLocales,\n  withLocaleData,\n  [withUpdate, updateOptions],\n  withApollo,\n  withCookies,\n  withRouter\n);\n\nexport default App;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Avatar.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport { getDisplayName, avatar as avatarUtility, getProfileUrl } from 'meteor/vulcan:users';\nimport { Link } from 'react-router-dom';\nimport classNames from 'classnames';\n\nconst Avatar = ({ className, user, size, gutter, link, fallback }) => {\n\n  const avatarClassNames = classNames('avatar', `size-${size}`, `gutter-${gutter}`, className);\n\n  if (!user) {\n    return <div className={avatarClassNames}>{fallback}</div>;\n  }\n  const avatarUrl = user.avatarUrl || avatarUtility.getUrl(user);\n\n  const img = <img alt={getDisplayName(user)} className=\"avatar-image\" src={avatarUrl} title={user.username} />;\n  const initials = <span className=\"avatar-initials\"><span>{avatarUtility.getInitials(user)}</span></span>;\n\n  const avatar = avatarUrl ? img : initials;\n\n  return (\n    <div className={avatarClassNames}>\n      {link ?\n        <Link to={getProfileUrl(user)}>\n          <span>{avatar}</span>\n        </Link>\n        : <span>{avatar}</span>\n      }\n    </div>\n  );\n\n};\n\nAvatar.propTypes = {\n  user: PropTypes.object,\n  size: PropTypes.oneOf(['xsmall', 'small', 'medium', 'large', 'profile']),\n  gutter: PropTypes.oneOf(['bottom', 'left', 'right', 'sides', 'all', 'none']),\n  link: PropTypes.bool\n};\n\nAvatar.defaultProps = {\n  size: 'medium',\n  gutter: 'none',\n  link: true\n};\n\nAvatar.displayName = 'Avatar';\n\nregisterComponent('Avatar', Avatar);\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/Card.jsx",
    "content": "import { registerComponent, Components, formatLabel } from 'meteor/vulcan:lib';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\nimport without from 'lodash/without';\nimport withComponents from '../../containers/withComponents.js';\nimport Users from 'meteor/vulcan:users';\nimport get from 'lodash/get';\n\n/*\n\nHelpers\n\n*/\nconst getLabel = (field, fieldName, collection, intl) => {\n  const schema = collection && collection.simpleSchema()._schema;\n  return formatLabel({\n    intl,\n    fieldName: fieldName,\n    collectionName: collection && collection._name,\n    schema: schema,\n  });\n};\n\n// Main component\n\nconst CardItem = ({ label, value, typeName, Components, fieldName, collection }) => (\n  <tr>\n    <td className=\"datacard-label\">\n      <strong>{label}</strong>\n    </td>\n    <td className=\"datacard-value\">\n      <Components.CardItemSwitcher\n        value={value}\n        typeName={typeName}\n        Components={Components}\n        fieldName={fieldName}\n        collection={collection}\n      />\n    </td>\n  </tr>\n);\n\nconst CardEdit = (props, context) => (\n  <tr>\n    <td colSpan=\"2\">\n      <Components.ModalTrigger\n        label={context.intl.formatMessage({ id: 'cards.edit' })}\n        component={\n          <Components.Button variant=\"info\">\n            <Components.FormattedMessage id=\"cards.edit\" />\n          </Components.Button>\n        }>\n        <CardEditForm {...props} />\n      </Components.ModalTrigger>\n    </td>\n  </tr>\n);\n\nCardEdit.contextTypes = { intl: intlShape };\n\nconst CardEditForm = ({ collection, document, closeModal, ...editFormProps }) => (\n  <Components.SmartForm\n    collection={collection}\n    documentId={document._id}\n    showRemove={true}\n    successCallback={document => {\n      closeModal();\n    }}\n    {...editFormProps}\n  />\n);\n\nconst Card = ({ title, className, collection, document, currentUser, fields, showEdit = true, Components, ...editFormProps }, { intl }) => {\n  \n  if (!document) {\n    return (\n      <div>\n        <Components.FormattedMessage id=\"error.no_document\" defaultMessage=\"No document\" />\n      </div>\n    );\n  }\n\n  const fieldNames = fields ? fields : without(Object.keys(document), '__typename');\n\n  let canUpdate = false;\n\n  // new APIs\n  const permissionCheck = get(collection, 'options.permissions.canUpdate');\n  // openCRUD backwards compatibility\n  const check = get(collection, 'options.mutations.edit.check') || get(collection, 'options.mutations.update.check');\n\n  if (Users.isAdmin(currentUser)) {\n    canUpdate = true;\n  } else if (permissionCheck) {\n    canUpdate = Users.permissionCheck({\n      check: permissionCheck,\n      user: currentUser,\n      context: { Users },\n      operationName: 'update',\n      document,\n    });\n  } else if (check) {\n    canUpdate = check && check(currentUser, document, { Users });\n  }\n\n  const typeName = collection && collection.typeName.toLowerCase();\n  const semantizedClassName = classNames(\n    className,\n    'datacard',\n    typeName && `datacard-${typeName}`,\n    document && document._id && `datacard-${document._id}`\n  );\n\n  return (\n    <div className={semantizedClassName}>\n      {title && <div className=\"datacard-title\">{title}</div>}\n      <table className=\"table table-bordered\" style={{ maxWidth: '100%' }}>\n        <tbody>\n          {showEdit && canUpdate ? <CardEdit collection={collection} document={document} {...editFormProps} /> : null}\n          {fieldNames.map((fieldName, index) => (\n            <CardItem\n              key={index}\n              value={document[fieldName]}\n              fieldName={fieldName}\n              collection={collection}\n              label={getLabel(document[fieldName], fieldName, collection, intl)}\n              Components={Components}\n              document={document}\n            />\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n};\n\nCard.displayName = 'Card';\n\nCard.propTypes = {\n  className: PropTypes.string,\n  collection: PropTypes.object,\n  document: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),\n  currentUser: PropTypes.object,\n  fields: PropTypes.array,\n  showEdit: PropTypes.bool,\n  editFormProps: PropTypes.object,\n};\n\nCard.contextTypes = {\n  intl: intlShape,\n};\n\nregisterComponent({\n  name: 'Card',\n  component: Card,\n  hocs: [withComponents],\n});\n\nexport default Card;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemArray.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\n// Array\nconst CardItemArray = ({ nestingLevel, value, Components }) => (\n  <ol className=\"contents-array\">\n    {value.map((item, index) => (\n      <li key={index}>\n        {\n          <Components.CardItemSwitcher\n            value={item}\n            typeName={typeof item}\n            Components={Components}\n            nestingLevel={nestingLevel}\n          />\n        }\n      </li>\n    ))}\n  </ol>\n);\nregisterComponent({ name: 'CardItemArray', component: CardItemArray });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemDate.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport moment from 'moment';\n\n// Date\nconst CardItemDate = ({ value }) => (\n  <span className=\"contents-date\">{moment(new Date(value)).format('YYYY/MM/DD, hh:mm')}</span>\n);\nregisterComponent({ name: 'CardItemDate', component: CardItemDate });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemDefault.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\n// Default\nconst CardItemDefault = ({ value }) => <span>{value.toString()}</span>;\nregisterComponent({ name: 'CardItemDefault', component: CardItemDefault });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemHTML.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\n// HTML\nconst CardItemHTML = ({ value }) => (\n  <div className=\"contents-html\" dangerouslySetInnerHTML={{ __html: value }} />\n);\nregisterComponent({ name: 'CardItemHTML', component: CardItemHTML });"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemImage.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\n/*\n\nCard Item Components\n\n*/\n\n// Image\nconst CardItemImage = ({ value, force = false, Components }) => {\n  const isImage =\n    ['.png', '.jpg', '.gif'].indexOf(value.substr(-4)) !== -1 ||\n    ['.webp', '.jpeg'].indexOf(value.substr(-5)) !== -1;\n  return isImage || force ? (\n    <img\n      className=\"contents-image\"\n      style={{ width: '100%', minWidth: 80, maxWidth: 200, display: 'block' }}\n      src={value}\n      alt={value}\n    />\n  ) : (\n    <Components.CardItemURL value={value} Components={Components} />\n  );\n};\nregisterComponent({ name: 'CardItemImage', component: CardItemImage });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemNumber.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\n// Number\nconst CardItemNumber = ({ value }) => <code className=\"contents-number\">{value.toString()}</code>;\nregisterComponent({ name: 'CardItemNumber', component: CardItemNumber });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemObject.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport { Link } from 'react-router-dom';\nimport without from 'lodash/without';\n\n// Object\nconst CardItemObject = props => {\n  const { nestingLevel, value, Components, showExpand } = props;\n  const showExpandControl = showExpand || nestingLevel > 1;\n  if (value.__typename === 'User') {\n    const user = value;\n\n    return (\n      <div className=\"dashboard-user\" style={{ whiteSpace: 'nowrap' }}>\n        <Components.Avatar size=\"small\" user={user} link />\n        {user.pagePath ? <Link to={user.pagePath}>{user.displayName}</Link> : <span>{user.displayName}</span>}\n      </div>\n    );\n  } else {\n    return (\n      <div className=\"card-item-details\">\n        {showExpandControl ? (\n          <details>\n            <summary>Expand</summary>\n            <CardItemObjectContents {...props} />\n          </details>\n        ) : (\n          <CardItemObjectContents {...props} />\n        )}\n      </div>\n    );\n  }\n};\n\nconst CardItemObjectContents = ({ nestingLevel, value: object, Components }) => (\n  <table className=\"table table-bordered\">\n    <tbody>\n      {without(Object.keys(object), '__typename').map(key => (\n        <tr key={key}>\n          <td>\n            <strong>{key}</strong>\n          </td>\n          <td>\n            <Components.CardItemSwitcher\n              nestingLevel={nestingLevel}\n              value={object[key]}\n              typeName={typeof object[key]}\n              Components={Components}\n            />\n          </td>\n        </tr>\n      ))}\n    </tbody>\n  </table>\n);\n\nregisterComponent({ name: 'CardItemObject', component: CardItemObject });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemRelationHasMany.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\n// HasMany Relation\nconst CardItemRelationHasMany = ({ relatedDocument: relatedDocuments, Components, ...rest }) => (\n  <div className=\"contents-hasmany\">\n    {relatedDocuments.map(relatedDocument => (\n      <Components.CardItemRelationItem\n        key={relatedDocument._id}\n        relatedDocument={relatedDocument}\n        Components={Components}\n        {...rest}\n      />\n    ))}\n  </div>\n);\nregisterComponent({ name: 'CardItemRelationHasMany', component: CardItemRelationHasMany });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemRelationHasOne.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\n// HasOne Relation\nconst CardItemRelationHasOne = ({ Components, ...rest }) => (\n  <div className=\"contents-hasone\">\n    {<Components.CardItemRelationItem Components={Components} {...rest} />}\n  </div>\n);\nregisterComponent({ name: 'CardItemRelationHasOne', component: CardItemRelationHasOne });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemRelationItem.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport { Link } from 'react-router-dom';\n\n/*\n\nTokens are components used to display an invidual element like a user name, \nlink to a post, category name, etc. \n\nThe naming convention is Type+Token, e.g. UserToken, PostToken, CategoryToken…\n\n*/\n\n// Relation Item\nconst CardItemRelationItem = ({ relatedDocument, relatedCollection, Components }) => {\n  const label = relatedCollection.options.getLabel\n    ? relatedCollection.options.getLabel(relatedDocument)\n    : relatedDocument._id;\n  const typeName = relatedDocument.__typename;\n  const Cell = Components[`${typeName}Cell`];\n  return Cell ? (\n    <Cell document={relatedDocument} label={label} Components={Components} />\n  ) : (\n    <Components.DefaultCell document={relatedDocument} label={label} Components={Components} />\n  );\n};\nregisterComponent({ name: 'CardItemRelationItem', component: CardItemRelationItem });\n\n// Default Cell\nconst DefaultCell = ({ document, label }) => (\n  <li className=\"relation-default-cell\">\n    {document.pagePath ? <Link to={document.pagePath}>{label}</Link> : <span>{label}</span>}\n  </li>\n);\nregisterComponent({ name: 'DefaultCell', component: DefaultCell });\n\n// User Token\nconst UserCell = ({ document, Components }) => (\n  <div className=\"contents-user user-item\">\n    <Components.Avatar size=\"small\" user={document} />\n    {document.pagePath ? (\n      <Link className=\"user-item-name\" to={document.pagePath}>\n        {document.displayName}\n      </Link>\n    ) : (\n      <span className=\"user-item-name\">{document.displayName}</span>\n    )}\n  </div>\n);\nregisterComponent({ name: 'UserCell', component: UserCell });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemString.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\n// String\nconst CardItemString = ({ string }) => (\n  <div className=\"contents-string\">\n    {string.indexOf(' ') === -1 && string.length > 30 ? (\n      <span title={string}>{string.substr(0, 30)}…</span>\n    ) : (\n      <span>{string}</span>\n    )}\n  </div>\n);\nregisterComponent({ name: 'CardItemString', component: CardItemString });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemSwitcher.jsx",
    "content": "import { getCollectionByTypeName, registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\nconst getTypeName = (value, fieldName, collection) => {\n  const schema = collection && collection.simpleSchema()._schema;\n  const fieldSchema = schema && schema[fieldName];\n  if (fieldSchema) {\n    const type = fieldSchema.type.singleType;\n    const typeName = typeof type === 'function' ? type.name : type;\n    return typeName;\n  } else {\n    return typeof value;\n  }\n};\n\nconst getFieldSchema = (fieldName, collection) => {\n  const schema = collection && collection.simpleSchema()._schema;\n  const fieldSchema = schema && schema[fieldName];\n  return fieldSchema;\n};\n\nconst CardItemSwitcher = props => {\n  // if typeName is not provided, default to typeof value\n  // note: contents provides additional clues about the contents (image, video, etc.)\n\n  let { nestingLevel = 0, value, typeName, contents, Components, fieldName, collection, document } = props;\n\n  const fieldSchema = getFieldSchema(fieldName, collection);\n\n  if (!typeName) {\n    if (collection) {\n      typeName = getTypeName(value, fieldName, collection);\n    } else {\n      typeName = typeof value;\n    }\n  }\n\n  const itemProps = { nestingLevel: nestingLevel + 1, value, Components, document, fieldName, collection, fieldSchema };\n\n  // no value; we return an empty string\n  if (typeof value === 'undefined' || value === null) {\n    return '';\n  }\n\n  // JSX element\n  if (React.isValidElement(value)) {\n    return value;\n  }\n\n  // Relation\n  if (fieldSchema && fieldSchema.resolveAs && fieldSchema.resolveAs.relation) {\n    itemProps.relatedFieldName = fieldSchema.resolveAs.fieldName || fieldName;\n    itemProps.relatedDocument = document[itemProps.relatedFieldName];\n    itemProps.relatedCollection = getCollectionByTypeName(fieldSchema.resolveAs.typeName || fieldSchema.resolveAs.type);\n\n    if (!itemProps.relatedDocument) {\n      return (\n        <span>\n          Missing data for sub-document <code>{value}</code> of type <code>{typeName}</code> (<code>{itemProps.relatedFieldName}</code>)\n        </span>\n      );\n    }\n\n    switch (fieldSchema.resolveAs.relation) {\n      case 'hasOne':\n        return <Components.CardItemRelationHasOne {...itemProps} />;\n\n      case 'hasMany':\n        return <Components.CardItemRelationHasMany {...itemProps} />;\n\n      default:\n        return <Components.CardItemDefault {...itemProps} />;\n    }\n  }\n\n  // Array\n  if (Array.isArray(value)) {\n    typeName = 'Array';\n  }\n\n  switch (typeName) {\n    case 'Boolean':\n    case 'boolean':\n    case 'Number':\n    case 'number':\n    case 'SimpleSchema.Integer':\n      return <Components.CardItemNumber {...itemProps} />;\n\n    case 'Array':\n      return <Components.CardItemArray {...itemProps} />;\n\n    case 'Object':\n    case 'object':\n      return <Components.CardItemObject {...itemProps} />;\n\n    case 'Date':\n      return <Components.CardItemDate {...itemProps} />;\n\n    case 'String':\n    case 'string':\n      switch (contents) {\n        case 'html':\n          return <Components.CardItemHTML {...itemProps} />;\n\n        case 'date':\n          return <Components.CardItemDate {...itemProps} />;\n\n        case 'image':\n          return <Components.CardItemImage {...itemProps} force={true} />;\n\n        case 'url':\n          return <Components.CardItemURL {...itemProps} force={true} />;\n\n        default:\n          // still attempt to parse string as an image or URL if possible\n          return <Components.CardItemImage {...itemProps} />;\n      }\n\n    default:\n      return <Components.CardItemDefault {...itemProps} />;\n  }\n};\nregisterComponent({ name: 'CardItemSwitcher', component: CardItemSwitcher });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/CardItemURL.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\n// URL\nconst CardItemURL = ({ value, force, Components }) => {\n  return force || value.slice(0, 4) === 'http' ? (\n    <a className=\"contents-link\" href={value} target=\"_blank\" rel=\"noopener noreferrer\">\n      <Components.CardItemString string={value} />\n    </a>\n  ) : (\n    <Components.CardItemString string={value} />\n  );\n};\nregisterComponent({ name: 'CardItemURL', component: CardItemURL });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Card/index.js",
    "content": "import './CardItemArray';\nimport './CardItemDate';\nimport './CardItemDefault';\nimport './CardItemHTML';\nimport './CardItemImage';\nimport './CardItemURL';\nimport './CardItemNumber';\nimport './CardItemObject';\nimport './CardItemString';\nimport './CardItemRelationItem';\nimport './CardItemRelationHasMany';\nimport './CardItemRelationHasOne';\nimport './CardItemSwitcher';\nimport './CardItemURL';\nexport { default } from './Card';\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Datatable/Datatable.jsx",
    "content": "import { Utils, registerComponent, getCollection } from 'meteor/vulcan:lib';\nimport React, { PureComponent, memo } from 'react';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport qs from 'qs';\nimport { withRouter } from 'react-router';\nimport { compose } from 'meteor/vulcan:lib';\nimport _isEmpty from 'lodash/isEmpty';\nimport _set from 'lodash/set';\nimport _cloneDeep from 'lodash/cloneDeep';\nimport withCurrentUser from '../../containers/currentUser.js';\nimport withComponents from '../../containers/withComponents';\nimport withMulti from '../../containers/multi2.js';\nimport Users from 'meteor/vulcan:users';\nimport _get from 'lodash/get';\n\nconst ascSortOperator = 'asc';\nconst descSortOperator = 'desc';\n\nconst convertToBoolean = s => (s === 'true' ? true : false);\n\n/*\n\nDatatable Component\n\n*/\n\n// see: http://stackoverflow.com/questions/1909441/jquery-keyup-delay\nconst delay = (function() {\n  var timer = 0;\n  return function(callback, ms) {\n    clearTimeout(timer);\n    timer = setTimeout(callback, ms);\n  };\n})();\n\nclass Datatable extends PureComponent {\n  constructor(props) {\n    super(props);\n\n    const { initialState, useUrlState } = props;\n\n    let initState = {\n      searchValue: '',\n      search: '',\n      currentSort: {},\n      currentFilters: {},\n      selectedItems: [],\n    };\n\n    // initial state can be defined via props\n    // note: this prop-originating initial state will *not* be reflected in the URL\n    if (initialState) {\n      if (initialState.search) {\n        initState.searchValue = initialState.search;\n        initState.search = initialState.search;\n      }\n      if (initialState.sort) {\n        initState.currentSort = initialState.sort;\n      }\n      if (initialState.filter) {\n        initState.currentFilters = initialState.filter;\n      }\n    }\n\n    // only load urlState if useUrlState is enabled\n    if (useUrlState) {\n      const urlState = this.getUrlState(props);\n      if (urlState.search) {\n        initState.searchValue = urlState.search;\n        initState.search = urlState.search;\n      }\n      if (urlState.sort) {\n        const [sortKey, sortValue] = urlState.sort.split('|');\n        initState.currentSort = { [sortKey]: sortValue };\n      }\n      if (urlState.filter) {\n        // all URL values are stored as strings, so convert them back to numbers if needed\n        initState.currentFilters = this.convertToNumbers(urlState.filter, props);\n      }\n    }\n\n    this.state = initState;\n  }\n\n  /*\n\n  Take a complex filter object and convert its \"leaves\" to numbers or booleans when needed\n\n  */\n  convertToNumbers = (urlStateFilters, props) => {\n    const convertedFilters = _cloneDeep(urlStateFilters);\n    const p = props || this.props;\n    const { collection } = p;\n\n    // only try to convert when we have a collection schema\n    if (collection) {\n      const schema = collection.simpleSchema()._schema;\n\n      Object.keys(urlStateFilters).forEach(fieldName => {\n        const field = schema[fieldName];\n        const fieldType = Utils.getFieldType(field);\n        const filter = urlStateFilters[fieldName];\n        // the \"operator\" can be _in, _eq, _gte, etc.\n        const [operator] = Object.keys(filter);\n        const value = urlStateFilters[fieldName][operator];\n        let convertedValue = value;\n        // for each field, check if it's supposed to be a number\n        if (fieldType === Number) {\n          // value can be a single value or an array, depending on filter type\n          convertedValue = Array.isArray(value) ? value.map(parseFloat) : parseFloat(value);\n        } else if (fieldType === Boolean) {\n          // value can be a single value or an array, depending on filter type\n          convertedValue = Array.isArray(value) ? value.map(convertToBoolean) : convertToBoolean(value);\n        }\n        _set(convertedFilters, `${fieldName}.${operator}`, convertedValue);\n      });\n    }\n    return convertedFilters;\n  };\n\n  getUrlState = props => {\n    const p = props || this.props;\n    return qs.parse(p.location.search, { ignoreQueryPrefix: true });\n  };\n\n  /*\n\n  If useUrlState is not enabled, do nothing\n\n  */\n  updateQueryParameter = (key, value) => {\n    if (this.props.useUrlState) {\n      const urlState = this.getUrlState();\n\n      if (value === null || value === '') {\n        // when value is null or empty, remove key from URL state\n        delete urlState[key];\n      } else {\n        urlState[key] = value;\n      }\n      const queryString = qs.stringify(urlState);\n      this.props.history.push({\n        search: `?${queryString}`,\n      });\n    }\n  };\n\n  /*\n\n  Note: when state is asc, toggling goes to desc;\n  but when state is desc toggling again removes sort.\n\n  */\n  toggleSort = column => {\n    let currentSort;\n    let urlValue;\n    if (!this.state.currentSort[column]) {\n      currentSort = { [column]: ascSortOperator };\n      urlValue = `${column}|${ascSortOperator}`;\n    } else if (this.state.currentSort[column] === ascSortOperator) {\n      currentSort = { [column]: descSortOperator };\n      urlValue = `${column}|${descSortOperator}`;\n    } else {\n      currentSort = {};\n      urlValue = null;\n    }\n    this.setState({ currentSort });\n    this.updateQueryParameter('sort', urlValue);\n  };\n\n  submitFilters = ({ name, filters }) => {\n    // clone state filters object\n    let newFilters = Object.assign({}, this.state.currentFilters);\n    if (_isEmpty(filters)) {\n      // if there are no filter options, remove column filter from state altogether\n      delete newFilters[name];\n    } else {\n      // else, update filters\n      newFilters[name] = filters;\n    }\n    this.setState({ currentFilters: newFilters });\n    this.updateQueryParameter('filter', _isEmpty(newFilters) ? null : newFilters);\n  };\n\n  updateSearch = e => {\n    e.persist();\n    e.preventDefault();\n    const searchValue = e.target.value;\n    this.setState({\n      searchValue,\n    });\n    delay(() => {\n      this.setState({\n        search: searchValue,\n      });\n      this.updateQueryParameter('search', searchValue);\n    }, 700);\n  };\n\n  toggleItem = id => {\n    const { selectedItems } = this.state;\n    const newSelectedItems = selectedItems.includes(id) ? selectedItems.filter(x => x !== id) : [...selectedItems, id];\n    this.setState({\n      selectedItems: newSelectedItems,\n    });\n  };\n\n  render() {\n    const { Components, modalProps, data, currentUser, onSubmitSelected } = this.props;\n\n    if (this.props.data) {\n      // static JSON datatable\n\n      return (\n        <Components.DatatableContents\n          Components={Components}\n          {...this.props}\n          datatableData={data}\n          results={this.props.data}\n          showEdit={false}\n          showDelete={false}\n          showNew={false}\n          modalProps={modalProps}\n        />\n      );\n    } else {\n      // dynamic datatable with data loading\n\n      const collection = this.props.collection || getCollection(this.props.collectionName);\n      const options = {\n        collection,\n        ...this.props.options,\n      };\n\n      const DatatableWithMulti = compose(withMulti(options))(Components.DatatableContents);\n\n      let canCreate = false;\n\n      // new APIs\n      const permissionCheck = _get(collection, 'options.permissions.canCreate');\n\n      // openCRUD backwards compatibility\n      const check = _get(collection, 'options.mutations.new.check') || _get(collection, 'options.mutations.create.check');\n\n      if (Users.isAdmin(currentUser)) {\n        canCreate = true;\n      } else if (permissionCheck) {\n        canCreate = Users.permissionCheck({\n          check: permissionCheck,\n          user: currentUser,\n          context: { Users },\n          operationName: 'create',\n        });\n      } else if (check) {\n        canCreate = check && check(currentUser, {}, { Users });\n      }\n\n      const input = {};\n      if (!_isEmpty(this.state.search)) {\n        input.search = this.state.search;\n      }\n      if (!_isEmpty(this.state.currentSort)) {\n        input.sort = this.state.currentSort;\n      }\n      if (!_isEmpty(this.state.currentFilters)) {\n        input.filter = this.state.currentFilters;\n      }\n\n      return (\n        <Components.DatatableLayout Components={Components} collectionName={collection.options.collectionName}>\n          <Components.DatatableAbove\n            Components={Components}\n            {...this.props}\n            collection={collection}\n            canInsert={canCreate}\n            canCreate={canCreate}\n            searchValue={this.state.searchValue}\n            updateSearch={this.updateSearch}\n            selectedItems={this.state.selectedItems}\n            onSubmitSelected={onSubmitSelected}\n          />\n          <DatatableWithMulti\n            Components={Components}\n            {...this.props}\n            collection={collection}\n            input={input}\n            currentUser={this.props.currentUser}\n            toggleSort={this.toggleSort}\n            currentSort={this.state.currentSort}\n            submitFilters={this.submitFilters}\n            currentFilters={this.state.currentFilters}\n            toggleItem={this.toggleItem}\n            selectedItems={this.state.selectedItems}\n          />\n        </Components.DatatableLayout>\n      );\n    }\n  }\n}\n\nDatatable.propTypes = {\n  title: PropTypes.string,\n  collection: PropTypes.object,\n  columns: PropTypes.array,\n  data: PropTypes.array,\n  options: PropTypes.object,\n  showEdit: PropTypes.bool,\n  showDelete: PropTypes.bool,\n  showNew: PropTypes.bool,\n  showSearch: PropTypes.bool,\n  newFormProps: PropTypes.object,\n  editFormProps: PropTypes.object,\n  newFormOptions: PropTypes.object, // backwards compatibility\n  editFormOptions: PropTypes.object, // backwards compatibility\n  Components: PropTypes.object.isRequired,\n  location: PropTypes.shape({ search: PropTypes.string }).isRequired,\n  rowClass: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),\n};\n\nDatatable.defaultProps = {\n  showNew: true,\n  showEdit: true,\n  showDelete: false,\n  showSearch: true,\n  useUrlState: true,\n};\nregisterComponent({\n  name: 'Datatable',\n  component: Datatable,\n  hocs: [withCurrentUser, withComponents, withRouter, memo],\n});\nexport default Datatable;\n\nconst DatatableLayout = ({ collectionName, children }) => (\n  <div className={`datatable datatable-${collectionName.toLowerCase()}`}>{children}</div>\n);\nregisterComponent({ name: 'DatatableLayout', component: DatatableLayout, hocs: [memo] });\n\n/*\n\nDatatableAbove Component\n\n*/\nconst DatatableAbove = props => {\n  const { Components } = props;\n\n  return (\n    <Components.DatatableAboveLayout>\n      <Components.DatatableAboveLeft {...props} />\n      <Components.DatatableAboveRight {...props} />\n    </Components.DatatableAboveLayout>\n  );\n};\nDatatableAbove.propTypes = {\n  Components: PropTypes.object.isRequired,\n};\nregisterComponent({ name: 'DatatableAbove', component: DatatableAbove, hocs: [memo] });\n\nconst DatatableAboveLeft = (props, { intl }) => {\n  const { showSearch, searchValue, updateSearch, Components } = props;\n  return (\n    <div className=\"datatable-above-left\">\n      {showSearch && (\n        <Components.DatatableAboveSearchInput\n          className=\"datatable-search form-control\"\n          inputProperties={{\n            path: 'datatableSearchQuery',\n            placeholder: `${intl.formatMessage({\n              id: 'datatable.search',\n              defaultMessage: 'Search',\n            })}…`,\n            value: searchValue,\n            onChange: updateSearch,\n          }}\n          Components={Components}\n        />\n      )}\n    </div>\n  );\n};\n\nDatatableAboveLeft.contextTypes = {\n  intl: intlShape,\n};\n\nregisterComponent({ name: 'DatatableAboveLeft', component: DatatableAboveLeft, hocs: [memo] });\n\nconst DatatableAboveRight = props => {\n  const {\n    collection,\n    currentUser,\n    showNew,\n    canInsert,\n    options,\n    newFormOptions,\n    newFormProps,\n    Components,\n    showSelect,\n    selectedItems,\n    onSubmitSelected,\n  } = props;\n  return (\n    <div className=\"datatable-above-right\">\n      {showSelect && selectedItems.length > 0 && (\n        <div className=\"datatable-above-item\">\n          <Components.DatatableSubmitSelected selectedItems={selectedItems} onSubmitSelected={onSubmitSelected} />\n        </div>\n      )}\n      {showNew && canInsert && (\n        <div className=\"datatable-above-item\">\n          <Components.NewButton\n            collection={collection}\n            currentUser={currentUser}\n            mutationFragmentName={options && options.fragmentName}\n            {...newFormOptions}\n            {...newFormProps}\n          />\n        </div>\n      )}\n    </div>\n  );\n};\n\nregisterComponent({ name: 'DatatableAboveRight', component: DatatableAboveRight, hocs: [memo] });\n\nconst DatatableAboveSearchInput = props => {\n  const { Components } = props;\n  return (\n    <div className=\"datatable-above-search-input\">\n      <Components.FormComponentText {...props} />\n    </div>\n  );\n};\nregisterComponent({ name: 'DatatableAboveSearchInput', component: DatatableAboveSearchInput, hocs: [memo] });\n\nconst DatatableAboveLayout = ({ children }) => <div className=\"datatable-above\">{children}</div>;\nregisterComponent({ name: 'DatatableAboveLayout', component: DatatableAboveLayout, hocs: [memo] });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Datatable/DatatableCell.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:lib';\nimport React, { memo } from 'react';\nimport PropTypes from 'prop-types';\n\n/*\n\nDatatableCell Component\n\n*/\nconst DatatableCell = ({ column, document, currentUser, Components, collection }) => {\n  const Component = column.component || (column.componentName && Components[column.componentName]) || Components.DatatableDefaultCell;\n  const columnName = column.label || column.name;\n\n  return (\n    <Components.DatatableCellLayout className={`datatable-item-${columnName.toLowerCase().replace(/\\s/g, '-')}`}>\n      <Component column={column} document={document} currentUser={currentUser} Components={Components} collection={collection} />\n    </Components.DatatableCellLayout>\n  );\n};\nDatatableCell.propTypes = {\n  Components: PropTypes.object.isRequired,\n};\nregisterComponent({ name: 'DatatableCell', component: DatatableCell, hocs: [memo] });\n\nconst DatatableCellLayout = ({ children, ...otherProps }) => (\n  <td {...otherProps}>\n    <div className=\"cell-contents\">{children}</div>\n  </td>\n);\nregisterComponent({ name: 'DatatableCellLayout', component: DatatableCellLayout, hocs: [memo] });\n\n/*\n\nDatatableDefaultCell Component\n\n*/\nconst DatatableDefaultCell = ({ column, document, ...rest }) => (\n  <Components.CardItemSwitcher value={document[column.name]} document={document} fieldName={column.name} {...column} {...rest} />\n);\n\nregisterComponent({ name: 'DatatableDefaultCell', component: DatatableDefaultCell, hocs: [memo] });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Datatable/DatatableContents.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:lib';\nimport React, { memo } from 'react';\nimport PropTypes from 'prop-types';\nimport _sortBy from 'lodash/sortBy';\n\nconst wrapColumns = c => ({ name: c });\n\nconst getColumns = (columns, results, data) => {\n  if (columns) {\n    // convert all columns to objects\n    const convertedColums = columns.map(column => (typeof column === 'object' ? column : { name: column }));\n    const sortedColumns = _sortBy(convertedColums, column => column.order);\n    return sortedColumns;\n  } else if (results && results.length > 0) {\n    // if no columns are provided, default to using keys of first array item\n    return Object.keys(results[0])\n      .filter(k => k !== '__typename')\n      .map(wrapColumns);\n  } else if (data) {\n    // note: withMulti HoC also passes a prop named data, but in this case\n    // data should be the prop passed to the Datatable\n    return Object.keys(data[0]).map(wrapColumns);\n  }\n  return [];\n};\n\n/*\n\nDatatableContents Component\n\n*/\n\nconst DatatableContents = props => {\n  let {\n    title,\n    collection,\n    datatableData,\n    results = [],\n    columns,\n    loading,\n    loadMore,\n    count,\n    totalCount,\n    networkStatus,\n    showEdit,\n    showDelete,\n    currentUser,\n    toggleSort,\n    currentSort,\n    submitFilters,\n    currentFilters,\n    modalProps,\n    Components,\n    error,\n    showSelect,\n  } = props;\n\n  if (loading) {\n    return (\n      <div className=\"datatable-list datatable-list-loading\">\n        <Components.Loading />\n      </div>\n    );\n  }\n\n  const isLoadingMore = networkStatus === 2;\n  const hasMore = results && totalCount > results.length;\n\n  const sortedColumns = getColumns(columns, results, datatableData);\n\n  return (\n    <Components.DatatableContentsLayout>\n      {/* note: we want to be able to show potential errors while still showing the data below */}\n      {error && <Components.Alert variant=\"danger\">{error.message}</Components.Alert>}\n      {title && <Components.DatatableTitle title={title} />}\n      <Components.DatatableContentsInnerLayout>\n        <Components.DatatableContentsHeadLayout>\n          {showSelect && <th />}\n          {sortedColumns.map((column, index) => (\n            <Components.DatatableHeader\n              Components={Components}\n              key={index}\n              collection={collection}\n              column={column}\n              toggleSort={toggleSort}\n              currentSort={currentSort}\n              submitFilters={submitFilters}\n              currentFilters={currentFilters}\n            />\n          ))}\n          {showEdit ? (\n            <th>\n              <Components.FormattedMessage id=\"datatable.edit\" defaultMessage=\"Edit\" />\n            </th>\n          ) : null}\n          {showDelete ? (\n            <th>\n              <Components.FormattedMessage id=\"datatable.delete\" defaultMessage=\"Delete\" />\n            </th>\n          ) : null}\n        </Components.DatatableContentsHeadLayout>\n        <Components.DatatableContentsBodyLayout>\n          {results && results.length ? (\n            results.map((document, index) => (\n              <Components.DatatableRow\n                {...props}\n                collection={collection}\n                columns={sortedColumns}\n                document={document}\n                key={index}\n                showEdit={showEdit}\n                showDelete={showDelete}\n                currentUser={currentUser}\n                modalProps={modalProps}\n              />\n            ))\n          ) : (\n            <Components.DatatableEmpty />\n          )}\n        </Components.DatatableContentsBodyLayout>\n      </Components.DatatableContentsInnerLayout>\n      {hasMore && (\n        <Components.DatatableContentsMoreLayout>\n          {isLoadingMore ? (\n            <Components.Loading />\n          ) : (\n            <Components.DatatableLoadMoreButton\n              Components={Components}\n              onClick={e => {\n                e.preventDefault();\n                loadMore();\n              }}>\n              Load More ({count}/{totalCount})\n            </Components.DatatableLoadMoreButton>\n          )}\n        </Components.DatatableContentsMoreLayout>\n      )}\n    </Components.DatatableContentsLayout>\n  );\n};\nDatatableContents.propTypes = {\n  Components: PropTypes.object.isRequired,\n};\nregisterComponent({ name: 'DatatableContents', component: DatatableContents, hocs: [memo] });\n\nconst DatatableContentsLayout = ({ children }) => <div className=\"datatable-list\">{children}</div>;\nregisterComponent({ name: 'DatatableContentsLayout', component: DatatableContentsLayout, hocs: [memo] });\n\nconst DatatableContentsInnerLayout = ({ children }) => <table className=\"table\">{children}</table>;\nregisterComponent({\n  name: 'DatatableContentsInnerLayout',\n  component: DatatableContentsInnerLayout,\n  hocs: [memo],\n});\n\nconst DatatableContentsHeadLayout = ({ children }) => (\n  <thead>\n    <tr>{children}</tr>\n  </thead>\n);\n\nregisterComponent({ name: 'DatatableContentsHeadLayout', component: DatatableContentsHeadLayout, hocs: [memo] });\n\nconst DatatableContentsBodyLayout = ({ children }) => <tbody>{children}</tbody>;\n\nregisterComponent({ name: 'DatatableContentsBodyLayout', component: DatatableContentsBodyLayout, hocs: [memo] });\n\nconst DatatableContentsMoreLayout = ({ children }) => <div className=\"datatable-list-load-more\">{children}</div>;\n\nregisterComponent({ name: 'DatatableContentsMoreLayout', component: DatatableContentsMoreLayout, hocs: [memo] });\n\nconst DatatableLoadMoreButton = ({ count, totalCount, Components, children, ...otherProps }) => (\n  <Components.Button variant=\"primary\" {...otherProps}>\n    {children}\n  </Components.Button>\n);\nregisterComponent({ name: 'DatatableLoadMoreButton', component: DatatableLoadMoreButton, hocs: [memo] });\n\n/*\n\nDatatableTitle Component\n\n*/\nconst DatatableTitle = ({ title }) => <div className=\"datatable-title\">{title}</div>;\n\nregisterComponent({ name: 'DatatableTitle', component: DatatableTitle, hocs: [memo] });\n\n/*\n\nDatatableEmpty Component\n\n*/\nconst DatatableEmpty = () => (\n  <tr>\n    <td colSpan=\"99\">\n      <div style={{ textAlign: 'center', padding: 10 }}>\n        <Components.FormattedMessage id=\"datatable.empty\" defaultMessage=\"No items to display.\" />\n      </div>\n    </td>\n  </tr>\n);\n\nregisterComponent({ name: 'DatatableEmpty', component: DatatableEmpty, hocs: [memo] });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Datatable/DatatableFilter.jsx",
    "content": "import { Components, registerComponent, Utils, expandQueryFragments } from 'meteor/vulcan:lib';\nimport React, { useState } from 'react';\nimport gql from 'graphql-tag';\nimport { useQuery } from '@apollo/client';\nimport moment from 'moment';\nimport isEmpty from 'lodash/isEmpty';\n\nconst getCount = columnFilters => {\n  if (!columnFilters) {\n    return 0;\n  } else if (Array.isArray(columnFilters._in)) {\n    return columnFilters._in.length;\n  } else if (columnFilters._gte || columnFilters._lte) {\n    if (columnFilters._gte && columnFilters._lte) {\n      return 2;\n    } else {\n      return 1;\n    }\n  }\n  return 0;\n};\n\nconst Filter = ({ count }) => (\n  <svg width=\"16\" height=\"16\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 448 512\">\n    <path\n      fill=\"#000\"\n      d=\"M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-6 400H54c-3.3 0-6-2.7-6-6V86c0-3.3 2.7-6 6-6h340c3.3 0 6 2.7 6 6v340c0 3.3-2.7 6-6 6z\"\n      fillOpacity={count ? 0.8 : 0.3}\n    />\n    {count ? (\n      <text x=\"50%\" y=\"55%\" fill=\"#000\" fontSize=\"300px\" textAnchor=\"middle\" alignmentBaseline=\"middle\" fillOpacity={0.8}>\n        {count}\n      </text>\n    ) : (\n      <path\n        fill=\"#000\"\n        d=\"M224 200v-16c0-13.3-10.7-24-24-24h-24v-20c0-6.6-5.4-12-12-12h-8c-6.6 0-12 5.4-12 12v20h-24c-13.3 0-24 10.7-24 24v16c0 13.3 10.7 24 24 24h24v148c0 6.6 5.4 12 12 12h8c6.6 0 12-5.4 12-12V224h24c13.3 0 24-10.7 24-24zM352 328v-16c0-13.3-10.7-24-24-24h-24V140c0-6.6-5.4-12-12-12h-8c-6.6 0-12 5.4-12 12v148h-24c-13.3 0-24 10.7-24 24v16c0 13.3 10.7 24 24 24h24v20c0 6.6 5.4 12 12 12h8c6.6 0 12-5.4 12-12v-20h24c13.3 0 24-10.7 24-24z\"\n        fillOpacity={0.3}\n      />\n    )}\n  </svg>\n);\n\nconst DatatableFilter = props => {\n  const { columnFilters, label, query, Components } = props;\n  return (\n    <span className=\"datatable-filter\">\n      <Components.ModalTrigger\n        title={<Components.FormattedMessage id=\"datatable.filter_column\" values={{ label }} defaultMessage={`Filter “${label}”`} />}\n        size=\"small\"\n        trigger={<Filter count={getCount(columnFilters)} />}>\n        {query ? <Components.DatatableFilterContentsWithData {...props} /> : <Components.DatatableFilterContents {...props} />}\n      </Components.ModalTrigger>\n    </span>\n  );\n};\n\nregisterComponent('DatatableFilter', DatatableFilter);\n\n/*\n\nDatatableFilterContents Components\n\n*/\n\nconst DatatableFilterContentsWithData = props => {\n  const { query, options } = props;\n\n  // if query is a function, execute it\n  const queryText = typeof query === 'function' ? query({ mode: 'static' }) : query;\n  const filterQuery = gql(expandQueryFragments(queryText));\n\n  const { loading, error, data } = useQuery(filterQuery);\n\n  if (loading) {\n    return <Components.Loading />;\n  } else if (error) {\n    return <p>error</p>;\n  } else {\n    // note: options function expects the entire props object\n    const queryOptions = options({ data });\n    return <Components.DatatableFilterContents {...props} options={queryOptions} />;\n  }\n};\n\nregisterComponent('DatatableFilterContentsWithData', DatatableFilterContentsWithData);\n\nconst DatatableFilterContents = props => {\n  const { Components, name, field, options, columnFilters, submitFilters, filterComponent } = props;\n  const fieldType = Utils.getFieldType(field);\n\n  const [filters, setFilters] = useState(columnFilters);\n\n  const filterProps = { ...props, filters, setFilters };\n\n  let contents;\n\n  if (filterComponent) {\n    const CustomFilter = filterComponent;\n    contents = <CustomFilter {...filterProps} />;\n  } else if (options) {\n    contents = <Components.DatatableFilterCheckboxes {...filterProps} />;\n  } else {\n    switch (fieldType) {\n      case Date:\n        contents = <Components.DatatableFilterDates {...filterProps} />;\n        break;\n\n      case Number:\n        contents = <Components.DatatableFilterNumbers {...filterProps} />;\n        break;\n\n      case Boolean:\n        contents = <Components.DatatableFilterBooleans {...filterProps} />;\n        break;\n\n      default:\n        contents = (\n          <p>\n            <Components.FormattedMessage\n              id=\"datatable.specify_option\"\n              defaultMessage=\"Please specify an options property on your schema field.\"\n            />\n          </p>\n        );\n    }\n  }\n\n  return (\n    <form>\n      {contents}\n      <Components.Button\n        variant=\"link\"\n        style={{ display: 'inline-block', marginRight: 10 }}\n        className=\"datatable_filter_clear\"\n        onClick={() => {\n          setFilters(undefined);\n        }}>\n        <Components.FormattedMessage id=\"datatable.clear_all\" defaultMessage=\"Clear All\" />\n      </Components.Button>\n      <Components.Button\n        type=\"submit\"\n        className=\"datatable_filter_submit\"\n        onClick={() => {\n          submitFilters({ name, filters });\n        }}>\n        <Components.FormattedMessage id=\"datatable.submit\" defaultMessage=\"Submit\" />\n      </Components.Button>\n    </form>\n  );\n};\n\nregisterComponent('DatatableFilterContents', DatatableFilterContents);\n\n/*\n\nFilter Types Components\n\nNote: the operators used here should match the ones handled server-side by\nthe filtering API (_in, _gte, _lte, etc.)\n\n*/\n\n/*\n\nCheckboxes\n\nOperator: _in\n\n*/\nconst checkboxOperator = '_in';\nconst DatatableFilterCheckboxes = ({ Components, options, filters = { [checkboxOperator]: [] }, setFilters }) => (\n  <Components.FormComponentCheckboxGroup\n    path=\"filter\"\n    itemProperties={{ layout: 'inputOnly' }}\n    inputProperties={{ options }}\n    value={filters[checkboxOperator]}\n    updateCurrentValues={({ filter: newValues }) => {\n      if (isEmpty(newValues)) {\n        setFilters(undefined);\n      } else {\n        setFilters({ [checkboxOperator]: newValues });\n      }\n    }}\n  />\n);\n\nregisterComponent('DatatableFilterCheckboxes', DatatableFilterCheckboxes);\n\n/*\n\nBooleans\n\n*/\nconst booleanOptions = [{ label: 'True', value: true }, { label: 'False', value: false }];\nconst DatatableFilterBooleans = ({ filters = { _eq: [] }, setFilters }) => (\n  <Components.FormComponentRadioGroup\n    path=\"filter\"\n    itemProperties={{ layout: 'inputOnly' }}\n    inputProperties={{\n      options: booleanOptions,\n      value: filters['_eq'],\n      onChange: e => {\n        const value = e.target.value; // note: this will be a string\n        setFilters({ _eq: value === 'true' ? true : false });\n      },\n    }}\n  />\n);\n\nregisterComponent('DatatableFilterBooleans', DatatableFilterBooleans);\n\n/*\n\nDates\n\nOperators: _gte and _lte\n\n*/\nconst DatatableFilterDates = ({ filters, setFilters }) => (\n  <div>\n    <Components.FormComponentDate\n      path=\"_gte\"\n      itemProperties={{\n        label: <Components.FormattedMessage id=\"datatable.after\" defaultMessage=\"After\" />,\n        layout: 'horizontal',\n      }}\n      inputProperties={{}}\n      value={filters && moment(filters._gte, 'YYYY-MM-DD')}\n      updateCurrentValues={newValues => {\n        if (!newValues._gte || newValues._gte === '') {\n          const newFilters = Object.assign({}, filters);\n          delete newFilters._gte;\n          setFilters(newFilters);\n        } else {\n          setFilters({ ...filters, _gte: newValues._gte.format('YYYY-MM-DD') });\n        }\n      }}\n    />\n    <Components.FormComponentDate\n      path=\"_lte\"\n      itemProperties={{\n        label: <Components.FormattedMessage id=\"datatable.before\" defaultMessage=\"Before\" />,\n        layout: 'horizontal',\n      }}\n      inputProperties={{}}\n      value={filters && moment(filters._lte, 'YYYY-MM-DD')}\n      updateCurrentValues={newValues => {\n        if (!newValues._lte || newValues._lte === '') {\n          const newFilters = Object.assign({}, filters);\n          delete newFilters._lte;\n          setFilters(newFilters);\n        } else {\n          setFilters({ ...filters, _lte: newValues._lte.format('YYYY-MM-DD') });\n        }\n      }}\n    />\n  </div>\n);\n\nregisterComponent('DatatableFilterDates', DatatableFilterDates);\n\n/*\n\nNumbers\n\nOperators: _gte and _lte\n\n*/\nconst DatatableFilterNumbers = ({ filters, setFilters }) => (\n  <div>\n    <Components.FormComponentNumber\n      path=\"_gte\"\n      itemProperties={{\n        label: <Components.FormattedMessage id=\"datatable.greater_than\" defaultMessage=\"Greater than\" />,\n        layout: 'horizontal',\n      }}\n      inputProperties={{\n        onChange: event => {\n          const value = event.target.value;\n          if (!value || value === '') {\n            const newFilters = Object.assign({}, filters);\n            delete newFilters._gte;\n            setFilters(newFilters);\n          } else {\n            setFilters({ ...filters, _gte: value });\n          }\n        },\n        value: filters && parseFloat(filters._gte),\n      }}\n    />\n    <Components.FormComponentNumber\n      path=\"_lte\"\n      itemProperties={{\n        label: <Components.FormattedMessage id=\"datatable.lower_than\" defaultMessage=\"Lower than\" />,\n        layout: 'horizontal',\n      }}\n      inputProperties={{\n        onChange: event => {\n          const value = event.target.value;\n          if (!value) {\n            const newFilters = Object.assign({}, filters);\n            delete newFilters._lte;\n            setFilters(newFilters);\n          } else {\n            setFilters({ ...filters, _lte: value });\n          }\n        },\n        value: filters && parseFloat(filters._lte),\n      }}\n    />\n  </div>\n);\n\nregisterComponent('DatatableFilterNumbers', DatatableFilterNumbers);\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Datatable/DatatableHeader.jsx",
    "content": "import { registerComponent, formatLabel } from 'meteor/vulcan:lib';\nimport React, { memo } from 'react';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport PropTypes from 'prop-types';\n\n/*\n\nDatatableHeader Component\n\n*/\nconst DatatableHeader = ({ collection, column, toggleSort, currentSort, submitFilters, currentFilters, Components }, { intl }) => {\n  // column label\n  let formattedLabel;\n\n  if (collection) {\n    const schema = collection.simpleSchema()._schema;\n    const field = schema[column.name];\n\n    if (column.label) {\n      formattedLabel = column.label;\n    } else {\n      /*\n  \n      use either:\n  \n      1. the column name translation : `${collectionName}.${columnName}`, `global.${columnName}`, columnName\n      2. the column name label in the schema (if the column name matches a schema field)\n      3. the raw column name.\n  \n      */\n      formattedLabel = formatLabel({\n        intl,\n        fieldName: column.name,\n        collectionName: collection._name,\n        schema: schema,\n      });\n    }\n\n    const fieldOptions = field && field.options;\n\n    // for filter options, use either column.options or else the options property defined on the schema field\n    const filterOptions = column.options ? column.options : fieldOptions;\n    const filterQuery = field && field.staticQuery;\n\n    return (\n      <Components.DatatableHeaderCellLayout className={`datatable-header-${column.name}`}>\n        <span className=\"datatable-header-cell-label\">{formattedLabel}</span>\n        {column.sortable && (\n          <Components.DatatableSorter\n            collection={collection}\n            field={field}\n            name={column.name}\n            label={formattedLabel}\n            toggleSort={toggleSort}\n            currentSort={currentSort}\n            sortable={column.sortable}\n            Components={Components}\n          />\n        )}\n        {column.filterable && (\n          <Components.DatatableFilter\n            collection={collection}\n            field={field}\n            name={column.name}\n            label={formattedLabel}\n            query={filterQuery}\n            options={filterOptions}\n            submitFilters={submitFilters}\n            columnFilters={currentFilters[column.name]}\n            filterComponent={column.filterComponent}\n            Components={Components}\n          />\n        )}\n      </Components.DatatableHeaderCellLayout>\n    );\n  } else {\n    const formattedLabel = column.label || intl.formatMessage({ id: column.name, defaultMessage: column.name });\n    return (\n      <Components.DatatableHeaderCellLayout className={`datatable-th-${formattedLabel.toLowerCase().replace(/\\s/g, '-')}`}>\n        {formattedLabel}\n      </Components.DatatableHeaderCellLayout>\n    );\n  }\n};\nDatatableHeader.contextTypes = {\n  intl: intlShape,\n};\nDatatableHeader.propTypes = {\n  Components: PropTypes.object.isRequired,\n};\nregisterComponent({ name: 'DatatableHeader', component: DatatableHeader, hocs: [memo] });\n\nconst DatatableHeaderCellLayout = ({ children, ...otherProps }) => (\n  <th {...otherProps}>\n    <div className=\"datatable-header-cell-inner\">{children}</div>\n  </th>\n);\nregisterComponent({ name: 'DatatableHeaderCellLayout', component: DatatableHeaderCellLayout, hocs: [memo] });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Datatable/DatatableRow.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:lib';\nimport React, { memo } from 'react';\nimport _isFunction from 'lodash/isFunction';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport Users from 'meteor/vulcan:users';\nimport get from 'lodash/get';\n\n/*\n\nDatatableRow Component\n\n*/\nconst DatatableRow = (props, { intl }) => {\n  const {\n    collection,\n    columns,\n    document,\n    showEdit,\n    showDelete,\n    currentUser,\n    options,\n    editFormOptions,\n    editFormProps,\n    rowClass,\n    Components,\n    showSelect,\n    toggleItem,\n    selectedItems,\n  } = props;\n\n  let canUpdate = false;\n\n  // new APIs\n  const permissionCheck = get(collection, 'options.permissions.canUpdate');\n  // openCRUD backwards compatibility\n  const check = get(collection, 'options.mutations.edit.check') || get(collection, 'options.mutations.update.check');\n\n  if (Users.isAdmin(currentUser)) {\n    canUpdate = true;\n  } else if (permissionCheck) {\n    canUpdate = Users.permissionCheck({\n      check: permissionCheck,\n      user: currentUser,\n      document,\n      context: { Users },\n      operationName: 'update',\n    });\n  } else if (check) {\n    canUpdate = check && check(currentUser, document, { Users });\n  }\n\n  const row = typeof rowClass === 'function' ? rowClass(document) : rowClass || '';\n  const { modalProps = {} } = props;\n  const defaultModalProps = { title: <code>{document._id}</code> };\n  const customModalProps = {\n    ...defaultModalProps,\n    ...(_isFunction(modalProps) ? modalProps(document) : modalProps),\n  };\n\n  const isSelected = selectedItems && selectedItems.includes(document._id);\n\n  return (\n    <Components.DatatableRowLayout className={`datatable-item ${row} ${isSelected ? 'datatable-item-selected' : ''}`}>\n      {showSelect && (\n        <Components.DatatableSelect\n          Components={Components}\n          document={document}\n          currentUser={currentUser}\n          collection={collection}\n          toggleItem={toggleItem}\n          selectedItems={selectedItems}\n        />\n      )}\n      {columns.map((column, index) => (\n        <Components.DatatableCell\n          key={index}\n          Components={Components}\n          column={column}\n          document={document}\n          currentUser={currentUser}\n          collection={collection}\n        />\n      ))}\n      {showEdit && canUpdate ? ( // openCRUD backwards compatibility\n        <Components.DatatableCellLayout className=\"datatable-edit\">\n          <Components.EditButton\n            collection={collection}\n            documentId={document._id}\n            currentUser={currentUser}\n            mutationFragmentName={options && options.fragmentName}\n            modalProps={customModalProps}\n            formProps={editFormOptions || editFormProps }\n          />\n        </Components.DatatableCellLayout>\n      ) : null}\n      {showDelete && canUpdate ? ( // openCRUD backwards compatibility\n        <Components.DatatableCellLayout className=\"datatable-delete\">\n          <Components.DeleteButton\n            collection={collection}\n            documentId={document._id}\n            currentUser={currentUser}\n            mutationFragmentName={options && options.fragmentName}\n          />\n        </Components.DatatableCellLayout>\n      ) : null}\n    </Components.DatatableRowLayout>\n  );\n};\nDatatableRow.propTypes = {\n  Components: PropTypes.object.isRequired,\n};\nregisterComponent({ name: 'DatatableRow', component: DatatableRow, hocs: [memo] });\n\nDatatableRow.contextTypes = {\n  intl: intlShape,\n};\nconst DatatableRowLayout = ({ children, ...otherProps }) => <tr {...otherProps}>{children}</tr>;\nregisterComponent({ name: 'DatatableRowLayout', component: DatatableRowLayout, hocs: [memo] });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Datatable/DatatableSelect.jsx",
    "content": "import { registerComponent, Components } from 'meteor/vulcan:lib';\nimport React, { memo } from 'react';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport PropTypes from 'prop-types';\n\n/*\n\nDatatableSelect Component\n\n*/\nconst DatatableSelect = ({ toggleItem, selectedItems, document, Components }) => {\n  const value = selectedItems.includes(document._id);\n  const onChange = e => {\n    toggleItem(document._id);\n  };\n  return (\n    <Components.DatatableCellLayout className=\"datatable-check\">\n      <Components.FormComponentCheckbox inputProperties={{ value, onChange }} itemProperties={{ layout: 'elementOnly' }} />\n    </Components.DatatableCellLayout>\n  );\n};\n\nDatatableSelect.contextTypes = {\n  intl: intlShape,\n};\nDatatableSelect.propTypes = {\n  Components: PropTypes.object.isRequired,\n};\nregisterComponent({ name: 'DatatableSelect', component: DatatableSelect, hocs: [memo] });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Datatable/DatatableSorter.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\nconst SortNone = () => (\n  <svg width=\"16\" height=\"16\" viewBox=\"0 0 438 438\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      d=\"M25.7368 247.243H280.263C303.149 247.243 314.592 274.958 298.444 291.116L171.18 418.456C161.128 428.515 144.872 428.515 134.926 418.456L7.55631 291.116C-8.59221 274.958 2.85078 247.243 25.7368 247.243ZM298.444 134.884L171.18 7.54408C161.128 -2.51469 144.872 -2.51469 134.926 7.54408L7.55631 134.884C-8.59221 151.042 2.85078 178.757 25.7368 178.757H280.263C303.149 178.757 314.592 151.042 298.444 134.884Z\"\n      transform=\"translate(66 6)\"\n      fill=\"#000\"\n      fillOpacity=\"0.2\"\n    />\n  </svg>\n);\n\nconst SortDesc = () => (\n  <svg width=\"16\" height=\"16\" viewBox=\"0 0 438 438\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      d=\"M25.7368 0H280.263C303.149 0 314.592 27.7151 298.444 43.8734L171.18 171.213C161.128 181.272 144.872 181.272 134.926 171.213L7.55631 43.8734C-8.59221 27.7151 2.85078 0 25.7368 0Z\"\n      transform=\"translate(66 253.243)\"\n      fill=\"black\"\n      fillOpacity=\"0.7\"\n    />\n    <path\n      d=\"M171.18 7.54408L298.444 134.884C314.592 151.042 303.149 178.757 280.263 178.757H25.7368C2.85078 178.757 -8.59221 151.042 7.55631 134.884L134.926 7.54408C144.872 -2.51469 161.128 -2.51469 171.18 7.54408Z\"\n      transform=\"translate(66 6)\"\n      fill=\"black\"\n      fillOpacity=\"0.2\"\n    />\n  </svg>\n);\n\nconst SortAsc = () => (\n  <svg width=\"16\" height=\"16\" viewBox=\"0 0 438 438\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      d=\"M298.444 134.884L171.18 7.54408C161.128 -2.51469 144.872 -2.51469 134.926 7.54408L7.55631 134.884C-8.59221 151.042 2.85078 178.757 25.7368 178.757H280.263C303.149 178.757 314.592 151.042 298.444 134.884Z\"\n      transform=\"translate(66 6)\"\n      fill=\"black\"\n      fillOpacity=\"0.7\"\n    />\n    <path\n      d=\"M280.263 0H25.7368C2.85078 0 -8.59221 27.7151 7.55631 43.8734L134.926 171.213C144.872 181.272 161.128 181.272 171.18 171.213L298.444 43.8734C314.592 27.7151 303.149 0 280.263 0Z\"\n      transform=\"translate(66 253.243)\"\n      fill=\"black\"\n      fillOpacity=\"0.2\"\n    />\n  </svg>\n);\n\nconst DatatableSorter = ({ name, label, toggleSort, currentSort }) => (\n  <span\n    className=\"datatable-sorter\"\n    onClick={() => {\n      toggleSort(name);\n    }}>\n    {!currentSort[name] ? <SortNone /> : currentSort[name] === 'asc' ? <SortAsc /> : <SortDesc />}\n  </span>\n);\n\nregisterComponent('DatatableSorter', DatatableSorter);\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Datatable/DatatableSubmitSelected.jsx",
    "content": "import { registerComponent, Components } from 'meteor/vulcan:lib';\nimport React, { memo } from 'react';\nimport PropTypes from 'prop-types';\n\n/*\n\nDatatableSelect Component\n\n*/\nconst DatatableSubmitSelected = ({ selectedItems, onSubmitSelected }) => (\n  <Components.Button\n    className=\"datatable-submit-selected\"\n    onClick={e => {\n      e.preventDefault();\n      onSubmitSelected(selectedItems);\n    }}>\n    <Components.FormattedMessage id=\"datatable.submit\" />\n  </Components.Button>\n);\n\nregisterComponent({ name: 'DatatableSubmitSelected', component: DatatableSubmitSelected, hocs: [memo] });\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Datatable/index.js",
    "content": "import './Datatable.jsx';\nimport './DatatableSorter.jsx';\nimport './DatatableFilter.jsx';\nimport './DatatableCell.jsx';\nimport './DatatableContents.jsx';\nimport './DatatableHeader.jsx';\nimport './DatatableRow.jsx';\nimport './DatatableSelect.jsx';\nimport './DatatableSubmitSelected.jsx';\nexport { default } from './Datatable.jsx';\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/DeleteButton.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport { useDelete2 } from '../containers/delete2';\n\nconst DeleteButton = (props) => {\n  const { label, collection, collectionName, fragment, fragmentName, documentId, mutationOptions, currentUser, mutationFragmentName, ...rest } = props;\n  const [deleteFunction, { loading }] = useDelete2({\n    collection,\n    collectionName,\n    fragment,\n    fragmentName,\n    mutationOptions,\n  });\n\n  return (\n    <Components.LoadingButton\n      loading={loading}\n      onClick={() => {\n        deleteFunction({ input: { id: documentId } });\n      }}\n      label={label || <Components.FormattedMessage id=\"datatable.delete\" defaultMessage=\"Delete\" />}\n      {...rest}\n    />\n  );\n};\n\nregisterComponent('DeleteButton', DeleteButton);\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Dummy.jsx",
    "content": "import React from 'react';\nimport {registerComponent} from 'meteor/vulcan:lib';\n\nfunction Dummy({children}) {\n  return children;\n}\nDummy.displayName = 'Dummy';\n\nregisterComponent({name: 'Dummy', component: Dummy});\nexport default Dummy;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/DynamicLoading.jsx",
    "content": "import React from 'react';\nimport { Components, registerComponent } from 'meteor/vulcan:lib';\n\nconst DynamicLoading = ({ isLoading, pastDelay, error }) => {\n  if (isLoading && pastDelay) {\n    return <Components.Loading/>;\n  } else if (error && !isLoading) {\n    // eslint-disable-next-line no-console\n    console.log(error);\n    return <p>Error!</p>;\n  } else {\n    return null;\n  }\n};\n\nregisterComponent('DynamicLoading', DynamicLoading);\n\nexport default DynamicLoading;"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/EditButton.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport { intlShape } from 'meteor/vulcan:i18n';\n\nconst EditButton = (\n  { style = 'primary', variant, label, size, showId, modalProps, formProps, component, mutationFragmentName, ...props },\n  { intl }\n) => (\n  <Components.ModalTrigger\n    label={label || intl.formatMessage({ id: 'datatable.edit', defaultMessage: 'Edit' })}\n    component={\n      component ? (\n        component\n      ) : (\n        <Components.Button size={size} variant={variant || style}>\n          {label || <Components.FormattedMessage id=\"datatable.edit\" defaultMessage=\"Edit\" />}\n        </Components.Button>\n      )\n    }\n    modalProps={modalProps}>\n    <Components.EditForm {...props} formProps={{ mutationFragmentName, ...formProps }} />\n  </Components.ModalTrigger>\n);\n\nEditButton.contextTypes = {\n  intl: intlShape,\n};\n\nEditButton.displayName = 'EditButton';\n\nregisterComponent('EditButton', EditButton);\n\n/*\n\nEditForm Component\n\n*/\nconst EditForm = props => {\n  const { closeModal, successCallback, removeSuccessCallback, formProps, ...rest } = props;\n  const success = successCallback\n    ? document => {\n        successCallback(document);\n        closeModal();\n      }\n    : () => {\n        closeModal();\n      };\n\n  const remove = removeSuccessCallback\n    ? document => {\n        removeSuccessCallback(document);\n        closeModal();\n      }\n    : () => {\n        closeModal();\n      };\n\n  return <Components.SmartForm successCallback={success} removeSuccessCallback={remove} {...formProps} {...rest} />;\n};\nregisterComponent('EditForm', EditForm);\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Error404.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\nconst Error404 = () => {\n  return (\n    <div className=\"error404\">\n      <h3>\n        <Components.FormattedMessage id=\"app.404\" defaultMessage=\"Sorry, we couldn't find what you were looking for.\" />\n      </h3>\n    </div>\n  );\n};\n\nError404.displayName = 'Error404';\n\nregisterComponent('Error404', Error404);\n\nexport default Error404;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Flash.jsx",
    "content": "import {Components, registerComponent} from 'meteor/vulcan:lib';\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport {intlShape} from 'meteor/vulcan:i18n';\n\nconst Flash = (props) => {\n  const {message, type} = props.message;\n\n  const dismissFlash = (e) => {\n    e.preventDefault();\n    props.dismissFlash(props.message._id);\n  };\n\n  return (\n      <Components.Alert className=\"flash-message\"\n                        variant={type}\n                        onClose={dismissFlash}>\n        <span dangerouslySetInnerHTML={{__html: message}}/>\n      </Components.Alert>\n  );\n};\n\nFlash.propTypes = {\n  message: PropTypes.object.isRequired,\n  dismissFlash: PropTypes.func.isRequired,\n};\n\nFlash.contextTypes = {\n  intl: intlShape\n};\n\nregisterComponent('Flash', Flash);\n\nexport default Flash;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/FlashMessages.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent } from 'meteor/vulcan:lib';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { useMessages } from '../containers/withMessages.js';\nimport { useReactiveVar } from '@apollo/client';\n\nconst FlashMessages = ({ className }, { intl }) => {\n  const { messagesState, ...flashActions } = useMessages(intl);\n  const messages = useReactiveVar(messagesState.reactiveVar).messages;\n\n  return (\n    <div className={`flash-messages ${className}`}>\n      {\n        messages.map((message, i) =>\n          <Components.Flash key={i} message={message} {...flashActions} />)\n      }\n    </div>\n  );\n};\n\nFlashMessages.propTypes = {\n  className: PropTypes.string,\n};\nFlashMessages.contextTypes = {\n  intl: intlShape.isRequired,\n};\nFlashMessages.displayName = 'FlashMessages';\n\nregisterComponent('FlashMessages', FlashMessages);\n\nexport default FlashMessages;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/HeadTags.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { Helmet } from 'react-helmet';\nimport { registerComponent, Utils, getSetting, registerSetting, Head } from 'meteor/vulcan:lib';\nimport { compose } from 'meteor/vulcan:lib';\n\nregisterSetting('logoUrl', null, 'Absolute URL for the logo image');\nregisterSetting('title', 'My App', 'App title');\nregisterSetting('tagline', null, 'App tagline');\nregisterSetting('description');\nregisterSetting('siteImage', null, 'An image used to represent the site on social media');\nregisterSetting('faviconUrl', '/img/favicon.ico', 'Favicon absolute URL');\n\nclass HeadTags extends PureComponent {\n  render() {\n\n    const url = this.props.url || Utils.getSiteUrl();\n    const title = this.props.title || getSetting('title', 'My App');\n    const description = this.props.description || getSetting('tagline') || getSetting('description');\n\n    // default image meta: logo url, else site image defined in settings\n    let image = !!getSetting('siteImage') ? getSetting('siteImage'): getSetting('logoUrl');\n\n    // overwrite default image if one is passed as props \n    if (!!this.props.image) {\n      image = this.props.image; \n    }\n\n    // add site url base if the image is stored locally\n    if (!!image && image.indexOf('//') === -1) {\n      // remove starting slash from image path if needed\n      if (image.charAt(0) === '/') {\n        image = image.slice(1);\n      }\n      image = Utils.getSiteUrl() + image;\n    }\n\n    return (\n      <div>\n        <Helmet>\n          \n          <title>{title}</title>\n\n          <meta charSet='utf-8'/>\n          <meta name='description' content={description}/>\n          <meta name='viewport' content='width=device-width, initial-scale=1'/>\n\n          {/* facebook */}\n          <meta property='og:type' content='article'/>\n          <meta property='og:url' content={url}/>\n          <meta property='og:image' content={image}/>\n          <meta property='og:title' content={title}/>\n          <meta property='og:description' content={description}/>\n\n          {/* twitter */}\n          <meta name='twitter:card' content='summary'/>\n          <meta name='twitter:image:src' content={image}/>\n          <meta name='twitter:title' content={title}/>\n          <meta name='twitter:description' content={description}/>\n\n          <link rel='canonical' href={url}/>\n          <link name='favicon' rel='shortcut icon' href={getSetting('faviconUrl', '/img/favicon.ico')}/>\n\n          {Head.meta.map((tag, index) => <meta key={index} {...tag}/>)}\n          {Head.link.map((tag, index) => <link key={index} {...tag}/>)}\n          {Head.script.map((tag, index) => <script key={index} {...tag}>{tag.contents}</script>)}\n\n        </Helmet>\n\n        {Head.components.map((componentOrArray, index) => {\n          let HeadComponent;\n          if (Array.isArray(componentOrArray)) {\n            const [component, ...hocs] = componentOrArray;\n            HeadComponent = compose(...hocs)(component);\n          } else {\n            HeadComponent = componentOrArray;\n          }\n          return <HeadComponent key={index} />;\n        })}\n        \n      </div>\n    );\n  }\n}\n\nHeadTags.propTypes = {\n  url: PropTypes.string,\n  title: PropTypes.string,\n  description: PropTypes.string,\n  image: PropTypes.string,\n};\n\nregisterComponent('HeadTags', HeadTags);\n\nexport default HeadTags;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/HelloWorld.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nconst wrapper = {\n  fontFamily: '\"Source Sans\", \"Helvetica\", sans-serif', \n  background: '#F7F6F5',\n  position: 'fixed',\n  top: 0,\n  right: 0,\n  left: 0,\n  bottom: 0,\n  display: 'flex',\n  alignItems: 'center',\n  justifyContent: 'center',\n};\n\nconst header = {\n  textAlign: 'center',\n};\n\nconst code = {\n  border: '1px solid #ccc',\n  borderRadius: 3,\n  padding: '10px 20px',\n  background: 'white',\n};\n\nfunction escapeHtml(unsafe) {\n  return unsafe\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&#039;');\n }\n\nconst HelloWorld = props => \n  <div style={wrapper}>\n\n    <div>\n      <h3 style={header}>Well Done! Now replace this with your own component</h3>\n\n      <p>1. Create a new <code>components/Home.jsx</code> file.</p>\n\n      <p>2. Import it into your custom package.</p>\n\n      <p>3. Add the following code:</p>\n\n      <pre style={code}>\n        <code dangerouslySetInnerHTML={{__html: escapeHtml(`\nimport { registerComponent } from 'meteor/vulcan:core';\nimport React from 'react';\n\nconst Home = props => \n  <div>\n    <h3>Welcome Home!</h3>\n  </div>\n\nregisterComponent('Home', Home);\n        `)}}/>\n      </pre>\n\n      <p>4. Update your route:</p>\n\n      <pre style={code}>\n        <code dangerouslySetInnerHTML={{__html: `\nimport { addRoute } from 'meteor/vulcan:core';\n\naddRoute({ name: 'home', path: '/', componentName: 'Home' });\n        `}}/>\n      </pre>\n\n    </div>\n\n  </div>;\n\nHelloWorld.displayName = 'HelloWorld';\n\nregisterComponent('HelloWorld', HelloWorld);\n\nexport default HelloWorld;"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Icon.jsx",
    "content": "import { registerComponent, Utils } from 'meteor/vulcan:lib';\nimport React from 'react';\n\nconst Icon = ({ name, iconClass, onClick }) => {\n  const icons = Utils.icons;\n  const iconCode = !!icons[name] ? icons[name] : name;\n  iconClass = (typeof iconClass === 'string') ? ' '+iconClass : '';\n  const c = 'icon fa fa-fw fa-' + iconCode + ' icon-' + name + iconClass;\n  return <i onClick={onClick} className={c} aria-hidden=\"true\"></i>;\n};\n\nIcon.displayName = 'Icon';\n\nregisterComponent('Icon', Icon);\n\nexport default Icon;"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Layout.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\nconst Layout = ({children}) =>\n  <div className=\"wrapper\" id=\"wrapper\">{children}</div>;\n\nLayout.displayName = 'Layout';\n\nregisterComponent('Layout', Layout);\n\nexport default Layout;"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Loading.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\nconst Loading = props => {\n  return (\n    <div className={`spinner ${props.className}`}>\n      <div className=\"bounce1\" />\n      <div className=\"bounce2\" />\n      <div className=\"bounce3\" />\n    </div>\n  );\n};\n\nLoading.displayName = 'Loading';\n\nregisterComponent('Loading', Loading);\n\nexport default Loading;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/LoadingButton.jsx",
    "content": "import React from 'react';\nimport { Components, registerComponent } from 'meteor/vulcan:lib';\n\nconst LoadingButton = ({ loading, label, onClick, children, className = '', ...rest }) => {\n\n  const wrapperStyle = {\n    position: 'relative',\n  };\n\n  const labelStyle = loading ? { opacity: 0.5 } : {};\n\n  const loadingStyle = loading ? {\n    position: 'absolute',\n    top: 0,\n    bottom: 0,\n    left: 0,\n    right: 0,\n    display: 'flex',\n    justifyContent: 'center',\n    alignItems: 'center',\n  } : { display: 'none'};\n\n  return (\n    <Components.Button className={`${loading ? 'loading-button-loading' : 'loading-button-notloading'} ${className}`} onClick={onClick} {...rest}>\n      <span style={wrapperStyle}>\n        <span style={labelStyle}>{label || children}</span>\n        <span style={loadingStyle}><Components.Loading/></span>\n      </span>\n    </Components.Button>\n  );\n};\n\nregisterComponent('LoadingButton', LoadingButton);\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/MutationButton.jsx",
    "content": "/*\n\nExample Usage\n\n<Components.MutationButton\n  label=\"Cancel Subscription\"\n  variant=\"primary\"\n  mutationOptions={{\n    name: 'cancelSubscription',\n    args: { bookingId: 'String' },\n    fragmentName: 'BookingsStripeDataFragment',\n  }}\n  mutationArguments={{ bookingId: booking._id }}\n  submitCallback={() => {}}\n  successCallback={result => { console.log(result) }}\n/>\n\n*/\nimport React, { PureComponent } from 'react';\nimport { Components, registerComponent } from 'meteor/vulcan:lib';\nimport withMutation from '../containers/registeredMutation';\n\nclass MutationButton extends PureComponent {\n  constructor(props) {\n    super(props);\n    this.button = withMutation(props.mutationOptions)(MutationButtonInner);\n  }\n\n  render() {\n    const Component = this.button;\n    return <Component {...this.props} />;\n  }\n}\n\nclass MutationButtonInner extends PureComponent {\n  state = {\n    loading: false,\n    error: null,\n  };\n\n  handleClick = async e => {\n    e.preventDefault();\n    this.setState({ loading: true, error: null });\n    const { mutationOptions, submitCallback, successCallback, errorCallback } = this.props;\n    let { mutationArguments } = this.props;\n    const mutationName = mutationOptions.name;\n    const mutation = this.props[mutationName];\n\n    try {\n      if (submitCallback) {\n        const callbackReturn = await submitCallback(mutationArguments);\n        if (callbackReturn?.mutationArguments) {\n          mutationArguments = callbackReturn.mutationArguments;\n        }\n      }\n      const result = await mutation(mutationArguments);\n      this.setState({ loading: false });\n      if (successCallback) {\n        await successCallback(result);\n      }\n    } catch (error) {\n      this.setState({ loading: false, error });\n      if (errorCallback) {\n        await errorCallback(error);\n      }\n    }\n\n    // mutation(mutationArguments)\n    //   .then(result => {\n    //     this.setState({ loading: false });\n    //     if (successCallback) {\n    //       successCallback(result);\n    //     }\n    //   })\n    //   .catch(error => {\n    //     this.setState({ loading: false });\n    //     if (errorCallback) {\n    //       errorCallback(error);\n    //     }\n    //   });\n  };\n\n  render() {\n    const { loading, error } = this.state;\n    const mutationName = this.props.mutationOptions.name;\n\n    const { label, ...rest } = this.props;\n    delete rest[mutationName];\n    delete rest.mutationOptions;\n    delete rest.mutationArguments;\n    delete rest.successCallback;\n    delete rest.errorCallback;\n    delete rest.submitCallback;\n\n    const loadingButton = <Components.LoadingButton loading={loading} onClick={this.handleClick} label={label} {...rest} />;\n\n    // note: the div wrapping trigger is needed so that the tooltip coordinates\n    // can be properly calculated\n\n    if (error) {\n      return (\n        <Components.TooltipTrigger trigger={<div style={{ display: 'inline-block' }}>{loadingButton}</div>} show={true} defaultShow={true}>\n          {error.message.replace('GraphQL error: ', '')}\n        </Components.TooltipTrigger>\n      );\n    }\n    return loadingButton;\n  }\n}\n\nregisterComponent('MutationButton', MutationButton);\nexport default MutationButton;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/NewButton.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport { intlShape } from 'meteor/vulcan:i18n';\n\nconst NewButton = ({ collection, size, label, style = 'primary', formProps, ...props }, { intl }) => (\n  <Components.ModalTrigger\n    label={label || intl.formatMessage({ id: 'datatable.new' })}\n    title={label || intl.formatMessage({ id: 'datatable.new' })}\n    component={\n      <Components.Button variant={style} size={size}>\n       {label || <Components.FormattedMessage id=\"datatable.new\" />}\n      </Components.Button>\n    }\n  >\n    <Components.NewForm collection={collection} formProps={formProps} {...props} />\n  </Components.ModalTrigger>\n);\n\nNewButton.contextTypes = {\n  intl: intlShape,\n};\n\nNewButton.displayName = 'NewButton';\n\nregisterComponent('NewButton', NewButton);\n\n/*\n\nNewForm Component\n\n*/\nconst NewForm = ({ closeModal, successCallback, formProps, ...props }) => {\n\n  const success = successCallback\n    ? document => {\n        successCallback(document);\n        closeModal();\n      }\n    : () => {\n         closeModal();\n      };\n\n  return <Components.SmartForm successCallback={success} {...formProps} {...props} />;\n};\nregisterComponent('NewForm', NewForm);\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/PaginatedList/PaginatedList.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport withComponents from '../../containers/withComponents';\nimport { useMulti2 } from '../../containers/multi2';\n\nexport const PaginatedList = ({ className, options, Components }) => {\n  const useMultiResults = useMulti2(options);\n\n  const {\n    results = [],\n    loadingInitial,\n    loadingMore,\n    count,\n    totalCount,\n    showLoadMore = true,\n    networkError,\n    graphQLErrors,\n  } = useMultiResults;\n\n  // error handling\n  let errors = [];\n  if (networkError) {\n    errors = [networkError];\n  }\n  if (graphQLErrors) {\n    errors = [...errors, ...graphQLErrors];\n  }\n\n  const props = {\n    className,\n    options,\n    Components,\n    errors,\n    ...useMultiResults,\n  };\n\n  // console.log(Components)\n\n  return (\n    <Components.PaginatedListLayout {...props}>\n      {errors.length > 0 ? (\n        <Components.PaginatedListErrors {...props} />\n      ) : loadingInitial ? (\n        <Components.PaginatedListLoadingInitial {...props} />\n      ) : (\n        <Components.PaginatedListContentLayout>\n          {results.length ? <Components.PaginatedListResults {...props} /> : <Components.PaginatedListNoResults {...props} />}\n          {totalCount > count &&\n            (loadingMore ? (\n              <Components.PaginatedListLoadingMore {...props} />\n            ) : (\n              showLoadMore && <Components.PaginatedListLoadMore {...props} />\n            ))}\n        </Components.PaginatedListContentLayout>\n      )}\n    </Components.PaginatedListLayout>\n  );\n};\n\nregisterComponent({ name: 'PaginatedList', component: PaginatedList, hocs: [withComponents] });\n\nconst PaginatedListLayout = ({ className, children }) => <div className={`${className} list-container`}>{children}</div>;\n\nregisterComponent('PaginatedListLayout', PaginatedListLayout);\n\nconst PaginatedListContentLayout = ({ children }) => <div className=\"list-contents\">{children}</div>;\n\nregisterComponent('PaginatedListContentLayout', PaginatedListContentLayout);\n\nconst PaginatedListErrors = ({ Components, errors }) => (\n  <div className=\"list-errors\">\n    {errors.map(error => (\n      <Components.PaginatedListError key={error.message} Components={Components} error={error} />\n    ))}\n  </div>\n);\nregisterComponent('PaginatedListErrors', PaginatedListErrors);\n\nconst PaginatedListError = ({ Components, error }) => <Components.Alert variant=\"danger\">{error.message}</Components.Alert>;\n\nregisterComponent('PaginatedListError', PaginatedListError);\n\nconst PaginatedListLoadingInitial = ({ Components }) => <Components.Loading />;\n\nregisterComponent('PaginatedListLoadingInitial', PaginatedListLoadingInitial);\n\nconst PaginatedListResults = ({ Components, results }) => (\n  <div className=\"list-results\">\n    {results.map(\n      (document, i) => document && <Components.PaginatedListItem Components={Components} key={document._id || i} document={document} />\n    )}\n  </div>\n);\n\nregisterComponent('PaginatedListResults', PaginatedListResults);\n\nconst PaginatedListItem = ({ Components, document }) => <Components.Card document={document} />;\n\nregisterComponent('PaginatedListItem', PaginatedListItem);\n\nconst PaginatedListNoResults = () => (\n  <p className=\"list-noresults\">\n    <Components.FormattedMessage id=\"paginatedlist.no_results\" />\n  </p>\n);\n\nregisterComponent('PaginatedListNoResults', PaginatedListNoResults);\n\nconst PaginatedListLoadingMore = ({ Components }) => <Components.Loading />;\n\nregisterComponent('PaginatedListLoadingMore', PaginatedListLoadingMore);\n\nconst PaginatedListLoadMore = ({ Components, loadMore, count, totalCount }) => (\n  <Components.Button\n    className=\"list-loadmore\"\n    onClick={e => {\n      e.preventDefault();\n      loadMore();\n    }}>\n    <Components.FormattedMessage id=\"paginatedlist.load_more\" defaultMessage=\"Load More\" />\n    &nbsp;{' '}\n    <span className=\"list-loadedcount\">\n      ({count}/{totalCount})\n    </span>\n  </Components.Button>\n);\n\nregisterComponent('PaginatedListLoadMore', PaginatedListLoadMore);\n\nexport default PaginatedList;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/PaginatedList/index.js",
    "content": "export { default } from './PaginatedList.jsx';"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/RouterHook.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent, runCallbacks, runCallbacksAsync } from 'meteor/vulcan:lib';\nimport { withApollo } from '@apollo/client/react/hoc';\n\nclass RouterHook extends PureComponent {\n  constructor(props) {\n    super(props);\n    this.runOnUpdateCallback(props);\n  }\n\n  componentDidUpdate(nextProps) {\n    this.runOnUpdateCallback(this.props, nextProps);\n  }\n\n  runOnUpdateCallback = (props, nextProps = {}) => {\n    const { currentRoute, client } = props;\n    // the first argument is an item to iterate on, needed by vulcan:lib/callbacks\n    // note: this item is not used in this specific callback: router.onUpdate\n    runCallbacks('router.onUpdate', {}, currentRoute, client.store, client);\n\n    runCallbacksAsync('router.onupdate.async', props, nextProps);\n  };\n\n  render() {\n    return null;\n  }\n}\n\nRouterHook.propTypes = {\n  currentRoute: PropTypes.object,\n  client: PropTypes.object,\n};\n\nRouterHook.displayName = 'RouterHook';\n\nregisterComponent('RouterHook', RouterHook, withApollo);\nexport default RouterHook;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/ScrollToTop.jsx",
    "content": "import React, {Component} from 'react';\nimport {withRouter} from 'react-router';\nimport {registerComponent} from 'meteor/vulcan:lib';\n\n// Scroll restoration based on https://reacttraining.com/react-router/web/guides/scroll-restoration.\nexport default class ScrollToTop extends Component {\n  componentDidUpdate(prevProps) {\n    if (this.props.location.pathname !== prevProps.location.pathname) {\n      window.scrollTo(0, 0);\n    }\n  }\n\n  render() {\n    return null;\n  }\n}\n\nregisterComponent('ScrollToTop', ScrollToTop, withRouter);\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/ShowIf.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport withCurrentUser from '../containers/currentUser.js';\n\nconst ShowIf = props => {\n  const { check, document, failureComponent = null, currentUser, children } = props;\n  return check(currentUser, document) ? children : failureComponent;\n};\n\nShowIf.propTypes = {\n  check: PropTypes.func.isRequired,\n  currentUser: PropTypes.object,\n  document: PropTypes.object,\n  failureComponent: PropTypes.object,\n};\n\nShowIf.displayName = 'ShowIf';\n\nregisterComponent('ShowIf', ShowIf, withCurrentUser);\nexport default ShowIf;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/VerticalMenuLayout/MenuLayout.jsx",
    "content": ""
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/VerticalMenuLayout/VerticalMenuLayout.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\nconst VerticalMenuLayout = ({menu}) => {\n  return (\n    <div className=\"verticalMenuLayout\" >\n      {menu}\n    </div>\n  );\n};\n\n\nregisterComponent('VerticalMenuLayout', VerticalMenuLayout);\n\nexport default VerticalMenuLayout;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components/Welcome.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\n\nconst wrapper = {\n  fontFamily: '\"Source Sans\", \"Helvetica\", sans-serif', \n  background: '#F7F6F5',\n  position: 'fixed',\n  top: 0,\n  right: 0,\n  left: 0,\n  bottom: 0,\n  display: 'flex',\n  alignItems: 'center',\n  justifyContent: 'center',\n};\n\nconst header = {\n  textAlign: 'center',\n};\n\nconst code = {\n  border: '1px solid #ccc',\n  borderRadius: 3,\n  padding: '10px 20px',\n  background: 'white',\n};\n\nconst Welcome = props => \n  <div style={wrapper}>\n\n    <div>\n      <h3 style={header}>Welcome to VulcanJS! Create a new index route to get started.</h3>\n\n      <p>1. Create a new <code>route.js</code> file.</p>\n\n      <p>2. Import it into your custom package.</p>\n\n      <p>3. Add the following code:</p>\n\n      <pre style={code}>\n        <code dangerouslySetInnerHTML={{__html: `\nimport { addRoute } from 'meteor/vulcan:core';\n\naddRoute({ name: 'home', path: '/', componentName: 'HelloWorld' });\n        `}}/>\n      </pre>\n\n    </div>\n\n  </div>;\n\nWelcome.displayName = 'Welcome';\n\nregisterComponent('Welcome', Welcome);\n\nexport default Welcome;"
  },
  {
    "path": "packages/vulcan-core/lib/modules/components.js",
    "content": "/*\n\nAlso imported by Storybook\n\n*/\n\nimport './components/Avatar.jsx';\nimport './components/Loading.jsx';\nimport './components/LoadingButton.jsx';\nimport './components/NewButton.jsx';\nimport './components/EditButton.jsx';\nimport './components/DeleteButton.jsx';\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/cacheUpdate.js",
    "content": "\n/**\n *  Optimistic cache updates\n */\nimport Mingo from 'mingo';\n\n/**\n * Safe getter\n * Must returns null if the document is absent (eg in case of validation failure)\n * TODO: use this getter\n * @param {*} mutation \n * @param {*} mutationName \n */\nexport const getDocumentFromMutation = (mutation, mutationName) => {\n    const mutationData = (mutation.result.data[mutationName] || {});\n    const document = mutationData.data;\n    return document;\n};\n\n// When using multi queries, we can't track all parameters, which are sadly needed\n// by cache.readQuery for optimistic updates.\n// This function can get a list of queries based on their name and should solve this issue\n// @see https://gist.github.com/ngryman/6856c7eb8f9a15b1095032a6ba478c5c\n// @see https://github.com/apollographql/react-apollo/issues/708#issuecomment-506975142\n// @see https://github.com/apollographql/apollo-client/issues/3505\n// @see https://github.com/apollographql/apollo-client/issues/3505#issuecomment-535388194\nexport const getVariablesListFromCache = (proxy, queryName) => {\n    //const queryName = query.definitions[0].name.value;\n    const rootQuery = proxy.data.data.ROOT_QUERY;\n\n    // XXX: When using `optimisticResponse`, `proxy.data.data` resolves to\n    // another cache that doesn't contain the root query.\n    if (!rootQuery) return [];\n\n    // Customer(*) will be matched but no customer. This last one would cause an error in\n    // JSON.parse. If wanted to be treated, parseQueryNameToVariables should be adapted\n    const matchQueryReducer = (names, name) => {\n        if (name.startsWith(queryName + '(')) {\n            names.push(name);\n        }\n        return names;\n    };\n\n    const parseQueryNameToVariables = (name) =>\n        JSON.parse((name.match(/{.*}/))[0]);\n\n    return Object.keys(rootQuery)\n        .reduce(matchQueryReducer, [])\n        .map(parseQueryNameToVariables);\n};\n\n\n/*\n\nTest if a document is already in a result set\n\n*/\nexport const isInData = ({ queryResult, multiResolverName, document }) => positionInSet(queryResult[multiResolverName].results, document) === -1;\n\nexport const positionInSet = (results, document) => results.findIndex(item => item._id && (item._id === document._id));\n\n\n/*\n\nReorder results according to a sort\n\n*/\nexport const reorderSet = (results, sort, selector) => {\n    const mingoQuery = new Mingo.Query(selector);\n    const cursor = mingoQuery.find(results);\n    const sortedResults = cursor.sort(sort).all();\n    return sortedResults;\n};\n\n/**\n * Add to data\n * @param {*} queryData \n * @param {*} document \n */\nexport const addToData = ({ queryResult, multiResolverName, document, sort, selector }) => {\n    const queryData = queryResult[multiResolverName];\n    let { results, totalCount } = queryData;\n    const idx = positionInSet(results, document);\n    let newResults = [...results];\n    if (idx !== -1) {\n        // doc has already been added, eg after an optimistic response\n        // update it\n        newResults[idx] = document;\n    } else {\n        // add to list\n        newResults.unshift(document);\n        totalCount = totalCount + 1;\n    }\n    // sort if necessary\n    if (sort) {\n        newResults = reorderSet(newResults, sort, selector);\n    }\n    return {\n        ...queryResult,\n        [multiResolverName]: {\n            ...queryData,\n            // TODO: check order using mingo\n            results: newResults,\n            totalCount\n        }\n    };\n};\n\n\nexport const removeFromData = ({ queryResult, multiResolverName, document }) => {\n    const queryData = queryResult[multiResolverName];\n    return {\n        ...queryResult,\n        [multiResolverName]: {\n            ...queryData,\n            results: queryData.results.filter(item => item._id !== document._id),\n            totalCount: Math.max(0, queryData.totalCount - 1)\n        }\n    };\n};\n\n/*\n\nTest if a document is matched by a given selector\n\n*/\nexport const matchSelector = (document, selector) => {\n    const mingoQuery = new Mingo.Query(selector);\n    return mingoQuery.test(document);\n};\n\n\n/*\n\nAdd a document to a set of results\n\n*/\n// export const addToSet = (queryData, document) => {\n//   const newData = {\n//     ...queryData,\n//     results: [...queryData.results, document],\n//     totalCount: queryData.totalCount + 1,\n//   };\n//   return newData;\n// };\n\n/*\n\nUpdate a document in a set of results\n\n*/\n// TODO: legacy, not used anymore because Apollo handles it out of the box\n/* export const updateInSet = (queryData, document) => {\n  const oldDocument = queryData.results.find(item => item._id === document._id);\n  const newDocument = { ...oldDocument, ...document };\n  const index = queryData.results.findIndex(item => item._id === document._id);\n  const newData = { ...queryData, results: [...queryData.results] }; // clone\n  newData.results[index] = newDocument;\n  return newData;\n};\n*/\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/create.js",
    "content": "/*\n\nGeneric mutation wrapper to insert a new document in a collection and update\na related query on the client with the new item and a new total item count. \n\nSample mutation: \n\n  mutation createMovie($data: CreateMovieData) {\n    createMovie(data: $data) {\n      data {\n        _id\n        name\n        __typename\n      }\n      __typename\n    }\n  }\n\nArguments: \n\n  - data: the document to insert\n\nChild Props:\n\n  - createMovie({ data })\n    \n*/\n\nimport React from 'react';\nimport gql from 'graphql-tag';\nimport { createClientTemplate } from 'meteor/vulcan:core';\nimport { extractCollectionInfo, extractFragmentInfo, filterFunction } from 'meteor/vulcan:lib';\nimport { useMutation } from '@apollo/client';\nimport { buildMultiQuery } from './multi';\nimport { addToData, getVariablesListFromCache, matchSelector } from './cacheUpdate';\n\nexport const buildCreateQuery = ({ typeName, fragmentName, fragment }) => {\n  const query = gql`\n    ${createClientTemplate({ typeName, fragmentName })}\n    ${fragment}\n  `;\n  return query;\n};\n\n/**\n * Update cached list of data after a document creation\n */\nexport const multiQueryUpdater = ({\n  typeName,\n  fragment,\n  fragmentName,\n  collection,\n  resolverName\n}) => (cache, { data }) => {\n  const multiResolverName = collection.options.multiResolverName;\n  // update multi queries\n  const multiQuery = buildMultiQuery({ typeName, fragmentName, fragment });\n  const newDoc = data?.[resolverName]?.data;\n  // get all the resolvers that match\n  const variablesList = getVariablesListFromCache(cache, multiResolverName);\n  variablesList.forEach(async variables => {\n    try {\n      const queryResult = cache.readQuery({ query: multiQuery, variables });\n      // get mongo selector and options objects based on current terms\n      const terms = variables.input.terms;\n      const parameters = terms ? collection.getParameters(terms) : await filterFunction(collection, variables.input, {});\n      const { selector, options: paramOptions } = parameters;\n      const { sort } = paramOptions;\n      // check if the document should be included in this query, given the query filters\n      if (matchSelector(newDoc, selector)) {\n        // TODO: handle order using the selector\n        const newData = addToData({ queryResult, multiResolverName, document: newDoc, sort, selector });\n        cache.writeQuery({ query: multiQuery, variables, data: newData });\n      }\n    } catch (err) {\n      // could not find the query\n      // TODO: be smarter about the error cases and check only for cache mismatch\n      console.log(err);\n    }\n  });\n};\n\nexport const useCreate = (options) => {\n  const { mutationOptions = {} } = options;\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n\n  const typeName = collection.options.typeName;\n  const resolverName = `create${typeName}`;\n\n  const query = buildCreateQuery({ typeName, fragmentName, fragment });\n  const [createFunc, ...rest] = useMutation(query, {\n    update: multiQueryUpdater({ typeName, fragment, fragmentName, collection, resolverName }),\n    ...mutationOptions\n  });\n\n  const extendedCreateFunc = {\n    [resolverName]: (args) => createFunc({ variables: { input: args.input, data: args.data } })\n  };\n  return [extendedCreateFunc[resolverName], ...rest];\n};\n\nexport const withCreate = options => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n  const funcName = `create${typeName}`;\n  const legacyError = () => {\n    throw new Error(`newMutation function has been removed. Use ${funcName} instead.`);\n  };\n  const Wrapper = props => {\n    const [createFunc] = useCreate(options);\n    return <C\n      {...props}\n      {...{ [funcName]: createFunc }}\n      newMutation={legacyError}\n    />;\n  };\n\n  Wrapper.displayName = `withCreate${typeName}`;\n  return Wrapper;\n};\n\nexport default withCreate;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/create2.js",
    "content": "/*\n\nGeneric mutation wrapper to insert a new document in a collection and update\na related query on the client with the new item and a new total item count. \n\nSample mutation: \n\n  mutation createMovie($data: CreateMovieData) {\n    createMovie(data: $data) {\n      data {\n        _id\n        name\n        __typename\n      }\n      __typename\n    }\n  }\n\nArguments: \n\n  - data: the document to insert\n\nChild Props:\n\n  - createMovie({ data })\n    \n*/\n\nimport React from 'react';\nimport gql from 'graphql-tag';\nimport { createClientTemplate } from 'meteor/vulcan:core';\nimport { extractCollectionInfo, extractFragmentInfo, filterFunction, getApolloClient } from 'meteor/vulcan:lib';\nimport { useMutation } from '@apollo/client';\nimport { buildMultiQuery } from './multi';\nimport { addToData, getVariablesListFromCache, matchSelector } from './cacheUpdate';\n\nexport const buildCreateQuery = ({ typeName, fragmentName, fragment }) => {\n  const query = gql`\n    ${createClientTemplate({ typeName, fragmentName })}\n    ${fragment}\n  `;\n  return query;\n};\n\n/**\n * Update cached list of data after a document creation\n */\nexport const multiQueryUpdater = ({\n  typeName,\n  fragment,\n  fragmentName,\n  collection,\n  resolverName,\n}) => async (cache, { data }) => {\n  const multiResolverName = collection.options.multiResolverName;\n  // update multi queries\n  const multiQuery = buildMultiQuery({ typeName, fragmentName, fragment });\n  const newDoc = data?.[resolverName]?.data;\n  // get all the resolvers that match\n  const client = getApolloClient();\n  const variablesList = getVariablesListFromCache(cache, multiResolverName);\n  // compute all necessary updates\n  const multiQueryUpdates = (await Promise.all(\n    variablesList\n      .map(async variables => {\n        try {\n          const queryResult = cache.readQuery({ query: multiQuery, variables });\n          // get mongo selector and options objects based on current terms\n          const multiInput = variables.input;\n          // TODO: the 3rd argument is the context, not available here\n          // Maybe we could pass the currentUser? The context is passed to custom filters function\n          const filter = await filterFunction(collection, multiInput, {});\n          const { selector, options: paramOptions } = filter;\n          const { sort } = paramOptions;\n          // check if the document should be included in this query, given the query filters\n          if (matchSelector(newDoc, selector)) {\n            // TODO: handle order using the selector\n            const newData = addToData({ queryResult, multiResolverName, document: newDoc, sort, selector });\n            // memorize updates just in case\n            return { query: multiQuery, variables, data: newData };\n          }\n        } catch (err) {\n          // could not find the query\n          // TODO: be smarter about the error cases and check only for cache mismatch\n          console.log(err);\n        }\n      })\n  )\n  ).filter(x => !!x); // filter out null values\n  // apply updates to the client\n  multiQueryUpdates.forEach((update) => {\n    client.writeQuery(update);\n  });\n  // return for potential chainging\n  return multiQueryUpdates;\n};\n\nconst buildResult = (options, resolverName, executionResult) => {\n  const { data } = executionResult;\n  const propertyName = options.propertyName || 'document';\n  const props = {\n    ...executionResult,\n    [propertyName]: data && data[resolverName] && data[resolverName].data,\n  };\n  return props;\n};\n\nexport const useCreate2 = (options) => {\n  const { mutationOptions = {} } = options;\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n\n  const typeName = collection.options.typeName;\n\n  const query = buildCreateQuery({ typeName, fragmentName, fragment });\n\n  const resolverName = `create${typeName}`;\n\n  const [createFunc, ...rest] = useMutation(query, {\n    update: multiQueryUpdater({ typeName, fragment, fragmentName, collection, resolverName }),\n    ...mutationOptions\n  });\n\n  const extendedCreateFunc = async (args) => {\n    const executionResult = await createFunc({\n      variables: { input: args.input, data: args.data },\n    });\n    return buildResult(options, resolverName, executionResult);\n  };\n  return [extendedCreateFunc, ...rest];\n};\n\nexport const withCreate2 = options => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n  const funcName = `create${typeName}`;\n  const metaName = `update${typeName}Meta`;\n\n  const legacyError = () => {\n    throw new Error(`newMutation function has been removed. Use ${funcName} instead.`);\n  };\n  const Wrapper = props => {\n    const [createFunc, createMeta] = useCreate2(options);\n    return <C\n      {...props}\n      {...{ [funcName]: createFunc }}\n      {...{ [metaName]: createMeta }}\n      newMutation={legacyError}\n    />;\n  };\n\n  Wrapper.displayName = `withCreate${typeName}`;\n  return Wrapper;\n};\n\nexport default withCreate2;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/currentUser.js",
    "content": "import React from 'react';\nimport { getFragment } from 'meteor/vulcan:lib';\nimport { useQuery } from '@apollo/client';\nimport gql from 'graphql-tag';\nimport get from 'lodash/get';\n\n// NOTE: this needs to be a function to avoid fragment registration issue at build time\nexport const buildCurrentUserQuery = () => gql`\n  query getCurrentUser {\n    currentUser {\n      ...UsersCurrent\n    }\n  }\n  ${getFragment('UsersCurrent')}\n`;\n\nexport const useCurrentUser = () => {\n  const result = useQuery(buildCurrentUserQuery());\n  return {\n    ...result,\n    currentUser: get(result, 'data.currentUser'),\n  };\n};\n\nexport const withCurrentUser = C => {\n  const Wrapped = props => {\n    const res = useCurrentUser();\n    const { loading, data, refetch } = res;\n    const namedRes = {\n      currentUserLoading: loading,\n      currentUser: data && data.currentUser,\n      currentUserData: data,\n      currentUserRefetch: refetch,\n    };\n    return <C {...props} {...namedRes} />;\n  };\n  Wrapped.displayName = 'withCurrentUser';\n  return Wrapped;\n};\n\n// legacy export\nexport default withCurrentUser;\n\n// previous implementation\n/*\n  return graphql(\n    gql`\n      query getCurrentUser {\n        currentUser {\n          ...UsersCurrent\n        }\n      }\n      ${getFragment('UsersCurrent')}\n    `, {\n      alias: 'withCurrentUser',\n\n      props(props) {\n        const { data } = props;\n        return {\n          currentUserLoading: data.loading,\n          currentUser: data.currentUser,\n          currentUserData: data,\n        };\n      },\n    }\n  )(component);\n};\n\nexport default withCurrentUser;\n*/\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/delete.js",
    "content": "/*\n\nGeneric mutation wrapper to remove a document from a collection. \n\nSample mutation: \n\n  mutation deleteMovie($input: DeleteMovieInput) {\n    deleteMovie(input: $input) {\n      data {\n        _id\n        name\n        __typename\n      }\n      __typename\n    }\n  }\n\nArguments: \n\n  - input\n    - input.selector: the id of the document to remove\n\nChild Props:\n\n  - deleteMovie({ selector })\n  \n*/\n\nimport React from 'react';\nimport gql from 'graphql-tag';\nimport { useMutation } from '@apollo/client';\nimport { deleteClientTemplate } from 'meteor/vulcan:core';\nimport { extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';\nimport { buildMultiQuery } from './multi';\nimport { getVariablesListFromCache, removeFromData } from './cacheUpdate';\n\nexport const buildDeleteQuery = ({ typeName, fragmentName, fragment }) => (\n  gql`\n    ${deleteClientTemplate({ typeName, fragmentName })}\n    ${fragment}\n  `\n);\n\n// remove value from the cached lists\nconst multiQueryUpdater = ({ collection, typeName, fragmentName, fragment }) => {\n  const multiResolverName = collection.options.multiResolverName;\n  const deleteResolverName = `delete${typeName}`;\n  return (cache, { data }) => {\n    // update multi queries\n    const multiQuery = buildMultiQuery({ typeName, fragmentName, fragment });\n    const removedDoc = data[deleteResolverName].data;\n    // get all the resolvers that match\n    const variablesList = getVariablesListFromCache(cache, multiResolverName);\n    variablesList.forEach(variables => {\n      try {\n        const queryResult = cache.readQuery({ query: multiQuery, variables });\n        const newData = removeFromData({ queryResult, multiResolverName, document: removedDoc });\n        cache.writeQuery({ query: multiQuery, variables, data: newData });\n      } catch (err) {\n        // could not find the query\n        // TODO: be smarter about the error cases and check only for cache mismatch\n        console.log(err);\n      }\n    });\n  };\n};\n\nexport const useDelete = (options) => {\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n  const typeName = collection.options.typeName;\n  const resolverName = `delete${typeName}`;\n  const { mutationOptions = {} } = options;\n\n  const query = buildDeleteQuery({\n    fragment,\n    fragmentName, typeName\n  });\n\n  const [deleteFunc, ...rest] = useMutation(query, {\n    // optimistic update\n    update: multiQueryUpdater({ collection, typeName, fragment, fragmentName }),\n    ...mutationOptions\n  });\n\n  const extendedDeleteFunc = {\n    [resolverName]: (args) => {\n      // support legacy syntax mistake\n      // @see https://github.com/VulcanJS/Vulcan/issues/2417\n      const selector = (args && args.selector) || args;\n      return deleteFunc({ variables: { selector } });\n    }\n  }\n  return [extendedDeleteFunc[resolverName], ...rest];\n};\n\nexport const withDelete = options => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n  const funcName = `delete${typeName}`;\n  const legacyError = () => {\n    throw new Error(`removeMutation function has been removed. Use ${funcName} function instead.`);\n  };\n\n  const Wrapper = (props) => {\n    const [deleteFunc] = useDelete(options);\n    return (\n      <C {...props} {...{ [funcName]: deleteFunc }} removeMutation={legacyError} />\n    );\n  };\n  Wrapper.displayName = `withDelete${typeName}`;\n  return Wrapper;\n};\n\nexport default withDelete;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/delete2.js",
    "content": "/*\n\nGeneric mutation wrapper to remove a document from a collection. \n\nSample mutation: \n\n  mutation deleteMovie($input: DeleteMovieInput) {\n    deleteMovie(input: $input) {\n      data {\n        _id\n        name\n        __typename\n      }\n      __typename\n    }\n  }\n\nArguments: \n\n  - input\n    - input.selector: the id of the document to remove\n\nChild Props:\n\n  - deleteMovie({ selector })\n  \n*/\n\nimport React from 'react';\nimport gql from 'graphql-tag';\nimport { useMutation } from '@apollo/client';\nimport { deleteClientTemplate } from 'meteor/vulcan:core';\nimport { extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';\nimport { buildMultiQuery } from './multi';\nimport { getVariablesListFromCache, removeFromData } from './cacheUpdate';\nimport { computeQueryVariables } from './variables';\n\nexport const buildDeleteQuery = ({ typeName, fragmentName, fragment }) =>\n  gql`\n    ${deleteClientTemplate({ typeName, fragmentName })}\n    ${fragment}\n  `;\n\n// remove value from the cached lists\nconst multiQueryUpdater = ({ collection, typeName, fragmentName, fragment }) => {\n  const multiResolverName = collection.options.multiResolverName;\n  const deleteResolverName = `delete${typeName}`;\n  return (cache, { data }) => {\n    // update multi queries\n    const multiQuery = buildMultiQuery({ typeName, fragmentName, fragment });\n    const removedDoc = data[deleteResolverName].data;\n    // get all the resolvers that match\n    const variablesList = getVariablesListFromCache(cache, multiResolverName);\n    variablesList.forEach(variables => {\n      try {\n        const queryResult = cache.readQuery({ query: multiQuery, variables });\n        const newData = removeFromData({ queryResult, multiResolverName, document: removedDoc });\n        cache.writeQuery({ query: multiQuery, variables, data: newData });\n      } catch (err) {\n        // could not find the query\n        // TODO: be smarter about the error cases and check only for cache mismatch\n        console.log(err);\n      }\n    });\n  };\n};\n\nexport const useDelete2 = options => {\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n  const typeName = collection.options.typeName;\n  const {\n    //input: optionsInput,\n    //_id: optionsId,\n    mutationOptions = {},\n  } = options;\n\n  const query = buildDeleteQuery({\n    fragment,\n    fragmentName,\n    typeName,\n  });\n\n  const [deleteFunc, ...rest] = useMutation(query, {\n    // optimistic update\n    update: multiQueryUpdater({ collection, typeName, fragment, fragmentName }),\n    ...mutationOptions,\n  });\n  const extendedDeleteFunc = (args /*{ input: argsInput, _id: argsId }*/) => {\n    return deleteFunc({\n      variables: {\n        ...computeQueryVariables(options, args),\n      },\n    });\n  };\n  return [extendedDeleteFunc, ...rest];\n};\n\nexport const withDelete2 = options => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n  const funcName = `delete${typeName}`;\n  const metaName = `delete${typeName}Meta`;\n\n  const legacyError = () => {\n    throw new Error(`removeMutation function has been removed. Use ${funcName} function instead.`);\n  };\n\n  const Wrapper = props => {\n    const [deleteFunc, deleteMeta] = useDelete2(options);\n    return <C {...props} {...{ [funcName]: deleteFunc }} {...{ [metaName]: deleteMeta }} removeMutation={legacyError} />;\n  };\n  Wrapper.displayName = `withDelete${typeName}`;\n  return Wrapper;\n};\n\nexport default withDelete2;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/index.js",
    "content": "export * from './localeData';"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/localeData.js",
    "content": "import React from 'react';\nimport { useQuery } from '@apollo/client';\nimport gql from 'graphql-tag';\nimport { initLocale } from 'meteor/vulcan:lib';\nimport { useCurrentUser } from './currentUser';\nimport { useCookies } from 'react-cookie';\n\n/*\n\nQuery to load strings for a specific locale from the server\n\n*/\nexport const localeDataQuery = gql`\n  query LocaleData($localeId: String) {\n    locale(localeId: $localeId) {\n      id\n      strings\n    }\n  }\n`;\n\n\n/*\n\nHook\n\n*/\nexport const useLocaleData = props => {\n  const [cookies] = useCookies(['locale']);\n  const { currentUser } = useCurrentUser();\n  const init = initLocale({ currentUser, cookies, locale: props.locale, dynamicLocales: props?.locales?.data?.locales });\n  const queryResult = useQuery(localeDataQuery, { variables: { localeId: init.id } });\n  return { ...queryResult, ...init };\n};\n\n/*\n\nHoC\n\n*/\nexport const withLocaleData = C => {\n  const Wrapped = props => {\n    const response = useLocaleData(props);\n    return <C {...props} locale={response} />;\n  };\n  Wrapped.displayName = 'withLocaleData';\n  return Wrapped;\n};\n\n\n/*\n\nQuery to load all locales metadata from the server\n\n*/\nexport const localesQuery = gql`\n  query LocalesQuery {\n    locales {\n      id\n      label\n    }\n  }\n`;\n\n\n/*\n\nHook\n\n*/\nexport const useLocales = props => {\n  const queryResult = useQuery(localesQuery);\n  return queryResult;\n};\n\n/*\n\nHoC\n\n*/\nexport const withLocales = C => {\n  const Wrapped = props => {\n    const response = useLocales(props);\n    return <C {...props} locales={response} />;\n  };\n  Wrapped.displayName = 'withLocales';\n  return Wrapped;\n};\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/multi.js",
    "content": "/*\n\n### withMulti\n\nPaginated items container\n\nOptions:\n\n  - collection: the collection to fetch the documents from\n  - fragment: the fragment that defines which properties to fetch\n  - fragmentName: the name of the fragment, passed to getFragment\n  - limit: the number of documents to show initially\n  - pollInterval: how often the data should be updated, in ms (set to 0 to disable polling)\n  - terms: an object that defines which documents to fetch\n\nProps Received:\n\n  - terms: an object that defines which documents to fetch\n\nTerms object can have the following properties:\n\n  - view: String\n  - userId: String\n  - cat: String\n  - date: String\n  - after: String\n  - before: String\n  - enableTotal: Boolean\n  - enableCache: Boolean\n  - listId: String\n  - query: String # search query\n  - postId: String\n  - limit: String\n  \n*/\n\nimport React from 'react';\nimport { useQuery } from '@apollo/client';\nimport { useState } from 'react';\nimport gql from 'graphql-tag';\nimport {\n  getSetting,\n  multiClientTemplate,\n  extractCollectionInfo,\n  extractFragmentInfo,\n  deprecate\n} from 'meteor/vulcan:lib';\n\nexport const buildMultiQuery = ({\n  typeName, fragmentName, extraQueries, fragment\n}) => (gql`\n    ${multiClientTemplate({ typeName, fragmentName, extraQueries })}\n    ${fragment}\n  `\n  );\n\nconst defaultPaginationTerms = ({ limit = 10 }, props) => {\n  // get initial limit from props, or else options\n  const paginationLimit = (props.terms && props.terms.limit) || limit;\n  const paginationTerms = {\n    limit: paginationLimit,\n    itemsPerPage: paginationLimit,\n  };\n  return paginationTerms;\n};\n\n\n/**\n * Build the graphQL query options\n * @param {*} options\n * @param {*} state\n * @param {*} props\n */\nconst buildQueryOptions = (options, { paginationTerms }, { terms }) => {\n  let {\n    pollInterval = getSetting('pollInterval', 20000),\n    enableTotal = true,\n    enableCache = false,\n    // generic graphQL options\n    queryOptions = {}\n  } = options;\n  // if this is the SSR process, set pollInterval to null\n  // see https://github.com/apollographql/apollo-client/issues/1704#issuecomment-322995855\n  pollInterval = typeof window === 'undefined' ? null : pollInterval;\n\n  // get terms from options, then props, then pagination\n  const mergedTerms = { ...options.terms, ...terms, ...paginationTerms };\n\n  const graphQLOptions = {\n    variables: {\n      input: {\n        terms: mergedTerms,\n        enableCache,\n        enableTotal,\n      },\n    },\n    // note: pollInterval can be set to 0 to disable polling (20s by default)\n    pollInterval,\n  };\n\n  if (options.fetchPolicy) {\n    deprecate('1.13.3', 'use the \"queryOptions\" object to pass options to the underlying Apollo hooks (hook: useMulti, option: fetchPolicy)');\n    graphQLOptions.fetchPolicy = options.fetchPolicy;\n  }\n  if (typeof options.pollInterval !== 'undefined') {\n    deprecate('1.13.3', 'use the \"queryOptions\" object to pass options to the underlying Apollo hooks (hook: useMulti, option: pollInterval)');\n  }\n  // set to true if running into https://github.com/apollographql/apollo-client/issues/1186\n  if (options.notifyOnNetworkStatusChange) {\n    deprecate('1.13.3', 'use the \"queryOptions\" object to pass options to the underlying Apollo hooks (hook: useMulti, option: notifyOnNetworkStatusChange)');\n    graphQLOptions.notifyOnNetworkStatusChange = options.notifyOnNetworkStatusChange;\n  }\n\n  // see https://www.apollographql.com/docs/react/features/error-handling/#error-policies\n  graphQLOptions.errorPolicy = 'all';\n\n\n  return {\n    ...graphQLOptions,\n    ...queryOptions // allow overriding options\n  };\n};\n\nconst buildResult = (options, { fragmentName, fragment, resolverName }, { setPaginationTerms, paginationTerms }, returnedProps) => {\n  //console.log('returnedProps', returnedProps);\n  const { refetch, networkStatus, error, fetchMore, data } = returnedProps;\n  // results = Utils.convertDates(collection, props.data[listResolverName]),\n  const results = data && data[resolverName] && data[resolverName].results;\n  const totalCount = data && data[resolverName] && data[resolverName].totalCount;\n  // see https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/networkStatus.ts\n  const loadingInitial = networkStatus === 1;\n  const loading = networkStatus === 1;\n  const loadingMore = networkStatus === 3;\n  const propertyName = options.propertyName || 'results';\n\n  if (error) {\n    // eslint-disable-next-line no-console\n    console.log(error);\n  }\n\n  return {\n    // see https://github.com/apollostack/apollo-client/blob/master/src/queries/store.ts#L28-L36\n    // note: loading will propably change soon https://github.com/apollostack/apollo-client/issues/831\n    ...returnedProps,\n    loading,\n    loadingInitial,\n    loadingMore,\n    [propertyName]: results,\n    totalCount,\n    refetch,\n    networkStatus,\n    error,\n    count: results && results.length,\n\n    // regular load more (reload everything)\n    loadMore(providedTerms) {\n      // if new terms are provided by presentational component use them, else default to incrementing current limit once\n      const newTerms =\n        typeof providedTerms === 'undefined'\n          ? {\n            /*...props.ownProps.terms,*/\n            ...paginationTerms,\n            limit: results.length + paginationTerms.itemsPerPage,\n          }\n          : providedTerms;\n      setPaginationTerms(newTerms);\n    },\n\n    // incremental loading version (only load new content)\n    // note: not compatible with polling\n    loadMoreInc(providedTerms) {\n      // get terms passed as argument or else just default to incrementing the offset\n\n      const newTerms =\n        typeof providedTerms === 'undefined'\n          ? {\n            ...paginationTerms,\n            offset: results.length,\n          }\n          : providedTerms;\n\n      return fetchMore({\n        variables: { input: { terms: newTerms } }, // ??? not sure about 'terms: newTerms'\n        updateQuery(previousResults, { fetchMoreResult }) {\n          // no more post to fetch\n          if (!(\n            fetchMoreResult[resolverName]\n            && fetchMoreResult[resolverName].results\n            && fetchMoreResult[resolverName].results.length\n          )) {\n            return previousResults;\n          }\n          const newResults = {\n            ...previousResults,\n            [resolverName]: { ...previousResults[resolverName] }\n          }; // TODO: should we clone this object? => yes\n          newResults[resolverName].results = [\n            ...previousResults[resolverName].results,\n            ...fetchMoreResult[resolverName].results,\n          ];\n          return newResults;\n        },\n      });\n    },\n\n    fragmentName,\n    fragment,\n    data,\n  };\n};\n\nexport const useMulti = (options, props) => {\n  const [paginationTerms, setPaginationTerms] = useState(defaultPaginationTerms(options, props));\n\n  let {\n    extraQueries,\n  } = options;\n\n\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n\n  const typeName = collection.options.typeName;\n  const resolverName = collection.options.multiResolverName;\n\n  // build graphql query from options\n  const query = buildMultiQuery({ typeName, fragmentName, extraQueries, fragment });\n\n  const queryOptions = buildQueryOptions(options, { paginationTerms }, props);\n  const queryRes = useQuery(query, queryOptions);\n\n  const result = buildResult(\n    options,\n    { fragment, fragmentName, resolverName },\n    { setPaginationTerms, paginationTerms },\n    queryRes\n  );\n\n  return result;\n\n};\n\nexport const withMulti = (options) => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n  const Wrapped = props => {\n    const res = useMulti(options, props);\n    return <C {...props} {...res} />;\n  };\n  Wrapped.displayName = `with${typeName}`;\n  return Wrapped;\n};\n\n// legacy\nexport default withMulti;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/multi2.js",
    "content": "/*\n\n### withMulti\n\nPaginated items container\n\nOptions:\n\n  - collection: the collection to fetch the documents from\n  - fragment: the fragment that defines which properties to fetch\n  - fragmentName: the name of the fragment, passed to getFragment\n  - limit: the number of documents to show initially\n  - pollInterval: how often the data should be updated, in ms (set to 0 to disable polling)\n  - input: the initial query input\n    - filter\n    - sort\n    - search\n    - offset\n    - limit\n    \n*/\n\nimport React from 'react';\nimport { useQuery } from '@apollo/client';\nimport { useState } from 'react';\nimport gql from 'graphql-tag';\nimport {\n  getSetting,\n  multiClientTemplate,\n  extractCollectionInfo,\n  extractFragmentInfo,\n} from 'meteor/vulcan:lib';\nimport merge from 'lodash/merge';\nimport get from 'lodash/get';\n\n// default query input object\nconst defaultInput = {\n  limit: 20,\n  enableTotal: true,\n  enableCache: false,\n};\n\nexport const buildMultiQuery = ({ typeName, fragmentName, extraQueries, fragment }) => gql`\n  ${multiClientTemplate({ typeName, fragmentName, extraQueries })}\n  ${fragment}\n`;\n\nconst getInitialPaginationInput = (options, props) => {\n  // get initial limit from props, or else options, or else default value\n  const limit = (props.input && props.input.limit) || (options.input && options.input.limit) || options.limit || defaultInput.limit;\n  const paginationInput = {\n    limit,\n  };\n  return paginationInput;\n};\n\n/**\n * Build the graphQL query options\n * @param {*} options\n * @param {*} state\n * @param {*} props\n */\nconst buildQueryOptions = (options, paginationInput = {}, props) => {\n\n  let {\n    input: optionsInput,\n    pollInterval = getSetting('pollInterval', 20000),\n    // generic graphQL options\n    queryOptions = {},\n  } = options;\n\n  // get dynamic input from props\n  const { input: propsInput = {} } = props;\n\n  // merge static and dynamic inputs\n  const input = merge({}, optionsInput, propsInput);\n\n  // if this is the SSR process, set pollInterval to null\n  // see https://github.com/apollographql/apollo-client/issues/1704#issuecomment-322995855\n  pollInterval = typeof window === 'undefined' ? null : pollInterval;\n\n  // get input from options, then props, then pagination\n  // TODO: should be done during the merge with lodash\n  const mergedInput = { ...defaultInput, ...options.input, ...input, ...paginationInput };\n\n  const graphQLOptions = {\n    variables: {\n      input: mergedInput,\n    },\n    // note: pollInterval can be set to 0 to disable polling (20s by default)\n    pollInterval,\n  };\n\n  // see https://www.apollographql.com/docs/react/features/error-handling/#error-policies\n  queryOptions.errorPolicy = 'all';\n\n  return {\n    ...graphQLOptions,\n    ...queryOptions, // allow overriding options\n  };\n};\n\nconst buildResult = (\n  options,\n  { fragmentName, fragment, resolverName },\n  { setPaginationInput, paginationInput, initialPaginationInput },\n  returnedProps\n) => {\n  //console.log('returnedProps', returnedProps);\n  const { refetch, networkStatus, error, fetchMore, data, previousData, graphQLErrors } = returnedProps;\n  // Note: Scalar types like Dates are NOT converted. It should be done at the UI level.\n  const bestAvailableData = data ?? previousData;\n  const results = bestAvailableData && bestAvailableData[resolverName] && bestAvailableData[resolverName].results;\n  const totalCount = bestAvailableData && bestAvailableData[resolverName] && bestAvailableData[resolverName].totalCount;\n  // see https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/networkStatus.ts\n  const loadingInitial = networkStatus === 1;\n  const loading = networkStatus === 1;\n  const loadingMore = networkStatus === 3 || networkStatus === 2;\n  const propertyName = options.propertyName || 'results';\n\n  if (error) {\n    // eslint-disable-next-line no-console\n    console.log(error);\n  }\n\n  return {\n    // see https://github.com/apollostack/apollo-client/blob/master/src/queries/store.ts#L28-L36\n    // note: loading will propably change soon https://github.com/apollostack/apollo-client/issues/831\n    ...returnedProps,\n    loading,\n    loadingInitial,\n    loadingMore,\n    [propertyName]: results,\n    totalCount,\n    refetch,\n    networkStatus,\n    error,\n    networkError: error && error.networkError,\n    graphQLErrors,\n    count: results && results.length,\n\n    // regular load more (reload everything)\n    loadMore(providedInput) {\n      // if new terms are provided by presentational component use them, else default to incrementing current limit once\n      const newInput = providedInput || {\n        ...paginationInput,\n        limit: results.length + initialPaginationInput.limit,\n      };\n      setPaginationInput(newInput);\n    },\n\n    // incremental loading version (only load new content)\n    // note: not compatible with polling\n    // TODO\n    loadMoreInc(providedInput) {\n      // get terms passed as argument or else just default to incrementing the offset\n\n      const newInput = providedInput || {\n        ...paginationInput,\n        offset: results.length,\n      };\n\n      return fetchMore({\n        variables: { input: newInput },\n        updateQuery(previousResults, { fetchMoreResult }) {\n          // no more post to fetch\n          if (\n            !(\n              fetchMoreResult[resolverName] &&\n              fetchMoreResult[resolverName].results &&\n              fetchMoreResult[resolverName].results.length\n            )\n          ) {\n            return previousResults;\n          }\n          const newResults = {\n            ...previousResults,\n            [resolverName]: { ...previousResults[resolverName] },\n          }; // TODO: should we clone this object? => yes\n          newResults[resolverName].results = [\n            ...previousResults[resolverName].results,\n            ...fetchMoreResult[resolverName].results,\n          ];\n          return newResults;\n        },\n      });\n    },\n\n    fragmentName,\n    fragment,\n    data,\n  };\n};\n\nexport const useMulti = (options, props = {}) => {\n  const initialPaginationInput = getInitialPaginationInput(options, props);\n  const [paginationInput, setPaginationInput] = useState(initialPaginationInput);\n\n  let { extraQueries } = options;\n\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n\n  const typeName = collection.options.typeName;\n  const resolverName = collection.options.multiResolverName;\n\n  // build graphql query from options\n  const query = buildMultiQuery({ typeName, fragmentName, extraQueries, fragment });\n\n  const queryOptions = buildQueryOptions(options, paginationInput, props);\n  const queryRes = useQuery(query, queryOptions);\n\n  // workaround for https://github.com/apollographql/apollo-client/issues/2810\n  queryRes.graphQLErrors = get(queryRes, 'error.networkError.result.errors');\n  \n  const result = buildResult(\n    options,\n    { fragment, fragmentName, resolverName },\n    { setPaginationInput, paginationInput, initialPaginationInput },\n    queryRes\n  );\n\n  return result;\n};\n\nexport const withMulti = options => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n  const Wrapped = props => {\n    const res = useMulti(options, props);\n    return <C {...props} {...res} />;\n  };\n  Wrapped.displayName = `with${typeName}`;\n  return Wrapped;\n};\n\nexport const useMulti2 = useMulti;\nexport const withMulti2 = withMulti;\n\n// legacy\nexport default withMulti;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/registeredMutation.js",
    "content": "/*\n\nHoC that provides a simple mutation that expects a single JSON object in return\n\nExample usage:\n\nexport default withMutation({\n  name: 'getEmbedData',\n  args: {url: 'String'},\n})(EmbedURL);\n\n*/\n\nimport React from 'react';\nimport { useMutation } from '@apollo/client';\nimport gql from 'graphql-tag';\nimport { expandQueryFragments } from 'meteor/vulcan:lib';\nimport isEmpty from 'lodash/isEmpty';\nimport map from 'lodash/map';\n\nexport const useRegisteredMutation = (options) => {\n  const { name, args, fragmentText, fragmentName, mutationOptions = {} } = options;\n  let mutation, fragmentBlock = '';\n\n  if (fragmentName) {\n    fragmentBlock = `{\n      ...${fragmentName}\n    }`;\n  } else if (fragmentText) {\n    fragmentBlock = `{\n      ${fragmentText}\n    }`;\n  }\n\n  if (args && !isEmpty(args)) {\n    const args1 = map(args, (type, name) => `$${name}: ${type}`); // e.g. $url: String\n    const args2 = map(args, (type, name) => `${name}: $${name}`); // e.g. url: $url\n    mutation = `\n      mutation ${name}(${args1}) {\n        ${name}(${args2})${fragmentBlock}\n      }\n    `;\n  } else {\n    mutation = `\n      mutation ${name} {\n        ${name}${fragmentBlock}\n      }\n    `;\n  }\n  const query = gql(expandQueryFragments(mutation));\n\n  const [mutateFunc] = useMutation(query, mutationOptions);\n  const extendedMutateFunc = vars => mutateFunc({ variables: vars });\n  return extendedMutateFunc;\n};\n\nexport const withMutation = (options) => C => {\n  const Wrapper = props => {\n    const mutation = useRegisteredMutation(options);\n    return (\n      <C {...props} {...{ [options.name]: mutation }} />\n    );\n  };\n  Wrapper.displayName = 'withMutation';\n  return Wrapper;\n};\n\nexport default withMutation;"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/single.js",
    "content": "import React from 'react';\nimport { useQuery } from '@apollo/client';\nimport gql from 'graphql-tag';\nimport {\n  getSetting, singleClientTemplate, Utils, extractCollectionInfo,\n  extractFragmentInfo, deprecate\n} from 'meteor/vulcan:lib';\n\nexport const singleQuery = ({\n  typeName,\n  fragmentName,\n  fragment,\n  extraQueries,\n}) => {\n  const query = gql`\n    ${singleClientTemplate({ typeName, fragmentName, extraQueries })}\n    ${fragment}\n  `;\n  // debug\n  //const { print } = require('graphql/language/printer');\n  //console.log('****');\n  //console.log(print(query));\n  //console.log('****');\n\n  return query;\n};\n\n/**\n * Create GraphQL useQuery options and variables based on props and provided options\n * @param {*} options \n * @param {*} props\n */\nconst buildQueryOptions = (options, { documentId, slug, selector = { documentId, slug } }) => {\n  let { pollInterval = getSetting('pollInterval', 20000), enableCache = false, fetchPolicy, queryOptions = {} } = options;\n  // if this is the SSR process, set pollInterval to null\n  // see https://github.com/apollographql/apollo-client/issues/1704#issuecomment-322995855\n  pollInterval = typeof window === 'undefined' ? null : pollInterval;\n\n  // OpenCrud backwards compatibility\n  const graphQLOptions = {\n    variables: {\n      input: {\n        selector,\n        enableCache\n      }\n    },\n    pollInterval // note: pollInterval can be set to 0 to disable polling (20s by default)\n  };\n\n  if (fetchPolicy) {\n    deprecate('1.13.3', 'use the \"queryOptions\" object to pass options to the underlying Apollo hooks (hook: useSingle, option: fetchPolicy)');\n    graphQLOptions.fetchPolicy = fetchPolicy;\n  }\n  if (typeof options.pollInterval !== 'undefined') {\n    deprecate('1.13.3', 'use the \"queryOptions\" object to pass options to the underlying Apollo hooks (hook: useMulti, option: pollInterval)');\n  }\n\n  // see https://www.apollographql.com/docs/react/features/error-handling/#error-policies\n  graphQLOptions.errorPolicy = 'all';\n\n  return {\n    ...graphQLOptions,\n    ...queryOptions\n  };\n};\n\nconst buildResult = (\n  options,\n  { fragmentName, fragment, resolverName },\n  returnedProps,\n) => {\n  const { /* ownProps, */ data, error } = returnedProps;\n  const propertyName = options.propertyName || 'document';\n  const props = {\n    ...returnedProps,\n    // document: Utils.convertDates(collection, data[singleResolverName]),\n    [propertyName]: data && data[resolverName] && data[resolverName].result,\n    fragmentName,\n    fragment,\n    data\n  };\n  if (error) {\n    // get graphQL error (see https://github.com/thebigredgeek/apollo-errors/issues/12)\n    props.error = error.graphQLErrors[0];\n  }\n  return props;\n};\n\nexport const useSingle = (options, props) => {\n  const { extraQueries } = options;\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n  const typeName = collection.options.typeName;\n  const resolverName = Utils.camelCaseify(typeName);\n\n  const query = singleQuery({\n    typeName, fragmentName, fragment, extraQueries\n  });\n\n  const queryRes = useQuery(\n    query,\n    buildQueryOptions(options, props)\n  );\n  const result = buildResult(\n    options,\n    { fragment, fragmentName, resolverName },\n    queryRes,\n  );\n  return result;\n};\n\nexport const withSingle = (options) => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n  const Wrapped = props => {\n    const res = useSingle(options, props);\n    return <C {...props} {...res} />;\n  };\n  Wrapped.displayName = `with${typeName}`;\n  return Wrapped;\n};\n\n// legacy default export\nexport default withSingle;"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/single2.js",
    "content": "import React from 'react';\nimport { useQuery } from '@apollo/client';\nimport gql from 'graphql-tag';\nimport {\n  getSetting, singleClientTemplate, Utils, extractCollectionInfo,\n  extractFragmentInfo,\n} from 'meteor/vulcan:lib';\nimport _merge from 'lodash/merge';\nimport { computeQueryVariables } from './variables';\n\nconst defaultInput = {\n  enableCache: false,\n  allowNull: false\n};\n\nexport const singleQuery = ({\n  typeName,\n  fragmentName,\n  fragment,\n  extraQueries,\n}) => {\n  const query = gql`\n    ${singleClientTemplate({ typeName, fragmentName, extraQueries })}\n    ${fragment}\n  `;\n  // debug\n  //const { print } = require('graphql/language/printer');\n  //console.log('****');\n  //console.log(print(query));\n  //console.log('****');\n\n  return query;\n};\n\n/**\n * Create GraphQL useQuery options and variables based on props and provided options\n * @param {*} options \n * @param {*} props\n */\nconst buildQueryOptions = (options, props) => {\n  let {\n    pollInterval = getSetting('pollInterval', 20000),\n    // generic apollo graphQL options\n    queryOptions = {}\n  } = options;\n\n\n  // if this is the SSR process, set pollInterval to null\n  // see https://github.com/apollographql/apollo-client/issues/1704#issuecomment-322995855\n  pollInterval = typeof window === 'undefined' ? null : pollInterval;\n\n  // OpenCrud backwards compatibility\n  const graphQLOptions = {\n    variables: {\n      ...computeQueryVariables(\n        { ...options, input: _merge({}, defaultInput, options.input || {}) }, // needed to merge in defaultInput, could be improved\n        props\n      )\n    },\n    pollInterval // note: pollInterval can be set to 0 to disable polling (20s by default)\n  };\n\n  // see https://www.apollographql.com/docs/react/features/error-handling/#error-policies\n  graphQLOptions.errorPolicy = 'all';\n\n  return {\n    ...graphQLOptions,\n    ...queryOptions\n  };\n};\n\nconst buildResult = (\n  options,\n  { fragmentName, fragment, resolverName },\n  returnedProps,\n) => {\n  const { /* ownProps, */ data, error } = returnedProps;\n  const propertyName = options.propertyName || 'document';\n  const props = {\n    ...returnedProps,\n    // Note: Scalar types like Dates are NOT converted. It should be done at the UI level.\n    [propertyName]: data && data[resolverName] && data[resolverName].result,\n    fragmentName,\n    fragment,\n    data,\n    error\n  };\n  if (error) {\n    // eslint-disable-next-line no-console\n    console.log(error);\n  }\n  return props;\n};\n\nexport const useSingle2 = (options, props = {}) => {\n  const { extraQueries } = options;\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n  const typeName = collection.options.typeName;\n  const resolverName = Utils.camelCaseify(typeName);\n\n  const query = singleQuery({\n    typeName, fragmentName, fragment, extraQueries\n  });\n\n  const queryRes = useQuery(\n    query,\n    buildQueryOptions(options, props)\n  );\n  const result = buildResult(\n    options,\n    { fragment, fragmentName, resolverName },\n    queryRes,\n  );\n  return result;\n};\n\nexport const withSingle2 = (options) => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n  const Wrapped = props => {\n    const res = useSingle2(options, props);\n    return <C {...props} {...res} />;\n  };\n  Wrapped.displayName = `with${typeName}`;\n  return Wrapped;\n};\n\n// legacy default export\nexport default withSingle2;"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/siteData.js",
    "content": "import React from 'react';\nimport { useQuery } from '@apollo/client';\nimport gql from 'graphql-tag';\n\nconst siteDataQuery = gql`\n      query getSiteData {\n        siteData {\n          url\n          title\n          sourceVersion\n          logoUrl\n        }\n      }\n    `;\nexport const useSiteData = () => (\n  useQuery(siteDataQuery)\n);\n\nexport const withSiteData = C => {\n  const Wrapped = (props) => {\n    const res = useSiteData();\n    const { loading, data } = res;\n    const namedRes =\n    {\n      siteDataLoading: loading,\n      siteData: data && data.SiteData,\n      siteDataData: data,\n    };\n    return <C {...props} {...namedRes} />;\n  };\n  Wrapped.displayName = 'withSiteData';\n  return Wrapped;\n};\n\nexport default withSiteData;\n/*\nreturn graphql(\n    , {\n    alias: 'withSiteData',\n\n    props(props) {\n      const { data } = props;\n      return {\n        siteDataLoading: data.loading,\n        siteData: data.siteData,\n        siteDataData: data,\n      };\n    },\n  }\n)(component);\n};\n*/\n\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/update.js",
    "content": "/*\n\nGeneric mutation wrapper to update a document in a collection. \n\nSample mutation: \n\n  mutation updateMovie($input: UpdateMovieInput) {\n    updateMovie(input: $input) {\n      data {\n        _id\n        name\n        __typename\n      }\n      __typename\n    }\n  }\n\nArguments: \n\n  - input\n    - input.selector: a selector to indicate the document to update\n    - input.data: the document (set a field to `null` to delete it)\n\nChild Props:\n\n  - updateMovie({ selector, data })\n  \n*/\n\nimport React from 'react';\nimport { useMutation } from '@apollo/client';\nimport gql from 'graphql-tag';\nimport {\n  updateClientTemplate,\n  extractCollectionInfo,\n  extractFragmentInfo,\n} from 'meteor/vulcan:lib';\n\nexport const buildUpdateQuery = ({ typeName, fragmentName, fragment }) => (\n  gql`\n    ${updateClientTemplate({ typeName, fragmentName })}\n    ${fragment}\n  `\n);\n\nexport const useUpdate = (options) => {\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n  const { mutationOptions = {} } = options;\n\n  const typeName = collection.options.typeName;\n  const resolverName = `update${typeName}`;\n  const query = buildUpdateQuery({ typeName, fragmentName, fragment });\n\n  const [updateFunc, ...rest] = useMutation(query, {\n    // see https://www.apollographql.com/docs/react/features/error-handling/#error-policies\n    errorPolicy: 'all',\n    ...mutationOptions\n  }\n  );\n\n  const extendedUpdateFunc = {\n    [resolverName]: ({ data, selector }) => updateFunc({\n      variables: { data, selector },\n    })\n  }\n  return [extendedUpdateFunc[resolverName], ...rest];\n};\n\nexport const withUpdate = options => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n  const funcName = `update${typeName}`;\n\n  const legacyError = () => {\n    throw new Error(`editMutation function has been removed. Use ${funcName} function instead.`);\n  };\n  const Wrapper = props => {\n    const [updateFunc] = useUpdate(options);\n    return <C\n      {...props}\n      {...{ [funcName]: updateFunc }}\n      editMutation={legacyError}\n    />;\n  };\n  Wrapper.displayName = `withUpdate${typeName}`;\n  return Wrapper;\n};\n\nexport default withUpdate;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/update2.js",
    "content": "/*\n\nGeneric mutation wrapper to update a document in a collection. \n\nSample mutation: \n\n  mutation updateMovie($input: UpdateMovieInput) {\n    updateMovie(input: $input) {\n      data {\n        _id\n        name\n        __typename\n      }\n      __typename\n    }\n  }\n\nArguments: \n\n  - input\n    - input.selector: a selector to indicate the document to update\n    - input.data: the document (set a field to `null` to delete it)\n\nChild Props:\n\n  - updateMovie({ selector, data })\n  \n*/\n\nimport React from 'react';\nimport { useMutation } from '@apollo/client';\nimport gql from 'graphql-tag';\nimport {\n  updateClientTemplate,\n  extractCollectionInfo,\n  extractFragmentInfo,\n} from 'meteor/vulcan:lib';\nimport { computeQueryVariables } from './variables';\n\nimport { multiQueryUpdater } from './create';\n\nexport const buildUpdateQuery = ({ typeName, fragmentName, fragment }) => (\n  gql`\n    ${updateClientTemplate({ typeName, fragmentName })}\n    ${fragment}\n  `\n);\n\nexport const useUpdate2 = (options) => {\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n  const {\n    mutationOptions = {}\n  } = options;\n\n  const typeName = collection.options.typeName;\n\n  const query = buildUpdateQuery({ typeName, fragmentName, fragment });\n\n  const [updateFunc, ...rest] = useMutation(query, {\n    // see https://www.apollographql.com/docs/react/features/error-handling/#error-policies\n    errorPolicy: 'all',\n    update: multiQueryUpdater({ typeName, fragment, fragmentName, collection, resolverName: `update${typeName}` }),\n    ...mutationOptions\n  });\n\n  const extendedUpdateFunc = ({ data, ...args }) => {\n    return updateFunc({\n      variables: {\n        data,\n        ...computeQueryVariables(options, args)\n      },\n    });\n  };\n  return [extendedUpdateFunc, ...rest];\n};\n\nexport const withUpdate2 = options => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n  const funcName = `update${typeName}`;\n  const metaName = `update${typeName}Meta`;\n\n  const legacyError = () => {\n    throw new Error(`editMutation function has been removed. Use ${funcName} function instead.`);\n  };\n  const Wrapper = props => {\n    const [updateFunc, updateMeta] = useUpdate2(options);\n    return <C\n      {...props}\n      {...{ [funcName]: updateFunc }}\n      {...{ [metaName]: updateMeta }}\n      editMutation={legacyError}\n    />;\n  };\n  Wrapper.displayName = `withUpdate${typeName}`;\n  return Wrapper;\n};\n\nexport default withUpdate2;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/upsert.js",
    "content": "/*\n\nGeneric mutation wrapper to upsert a document in a collection. \n\nSample mutation: \n\n  mutation upsertMovie($input: UpsertMovieInput) {\n    upsertMovie(input: $input) {\n      data {\n        _id\n        name\n        __typename\n      }\n      __typename\n    }\n  }\n\nArguments: \n\n  - input\n    - input.selector: a selector to indicate the document to update\n    - input.data: the document (set a field to `null` to delete it)\n\nChild Props:\n\n  - upsertMovie({ selector, data })\n  \n*/\n\nimport React from 'react';\nimport { useMutation } from '@apollo/client';\nimport gql from 'graphql-tag';\nimport { upsertClientTemplate } from 'meteor/vulcan:core';\n\nimport { extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';\n\nimport { multiQueryUpdater } from './create';\n\nexport const buildUpsertQuery = ({ typeName, fragment, fragmentName }) => (\n  gql`\n    ${upsertClientTemplate({ typeName, fragmentName })}\n    ${fragment}\n  `\n);\nexport const useUpsert = options => {\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n  const typeName = collection.options.typeName;\n  const { mutationOptions = {} } = options;\n\n  const query = buildUpsertQuery({ typeName, fragmentName, fragment });\n\n  const [upsertFunc, ...rest] = useMutation(query, {\n    errorPolicy: 'all',\n    // we reuse the update function create, which should actually support\n    // upserting\n    update: multiQueryUpdater({ typeName, fragment, fragmentName, collection, resolverName: `upsert${typeName}` }),\n    ...mutationOptions\n  });\n\n  const extendedUpsertFunc = ({ data, selector }) => upsertFunc({ variables: { data, selector } });\n\n  return [extendedUpsertFunc, ...rest];\n};\n\nexport const withUpsert = options => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n\n  const funcName = `upsert${typeName}`;\n  const legacyError = () => {\n    throw new Error(`upsertMutation function has been removed. Use ${funcName} function instead.`);\n  };\n\n  const Wrapper = props => {\n    const [upsertFunc] = useUpsert(options);\n    return (\n      <C {...props} {...{ [funcName]: upsertFunc }} upsertMutation={legacyError} />\n\n    );\n\n  };\n\n  Wrapper.displayName = `withUpsert${typeName}`;\n  return Wrapper;\n};\n\nexport default withUpsert;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/upsert2.js",
    "content": "/*\n\nGeneric mutation wrapper to upsert a document in a collection. \n\nSample mutation: \n\n  mutation upsertMovie($input: UpsertMovieInput) {\n    upsertMovie(input: $input) {\n      data {\n        _id\n        name\n        __typename\n      }\n      __typename\n    }\n  }\n\nArguments: \n\n  - input\n    - input.selector: a selector to indicate the document to update\n    - input.data: the document (set a field to `null` to delete it)\n\nChild Props:\n\n  - upsertMovie({ selector, data })\n  \n*/\n\nimport React from 'react';\nimport { useMutation } from '@apollo/client';\nimport gql from 'graphql-tag';\nimport { upsertClientTemplate } from 'meteor/vulcan:core';\n\nimport { extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';\n\nimport { multiQueryUpdater } from './create';\nimport { computeQueryVariables } from './variables';\n\nexport const buildUpsertQuery = ({ typeName, fragment, fragmentName }) => (\n  gql`\n    ${upsertClientTemplate({ typeName, fragmentName })}\n    ${fragment}\n  `\n);\nexport const useUpsert2 = options => {\n  const { collectionName, collection } = extractCollectionInfo(options);\n  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);\n  const typeName = collection.options.typeName;\n  const {\n    mutationOptions = {}\n  } = options;\n\n  const query = buildUpsertQuery({ typeName, fragmentName, fragment });\n  const resolverName = `upsert${typeName}`;\n  const [upsertFunc, ...rest] = useMutation(query, {\n    errorPolicy: 'all',\n    // we reuse the update function create, which should actually support\n    // upserting\n    update: multiQueryUpdater({ typeName, fragment, fragmentName, collection, resolverName }),\n    ...mutationOptions\n  });\n\n  const extendedUpsertFunc = ({ data, ...args }) => {\n    return upsertFunc({\n      variables: {\n        data,\n        ...computeQueryVariables(options, args)\n      }\n    });\n  };\n\n  return [extendedUpsertFunc, ...rest];\n};\n\nexport const withUpsert2 = options => C => {\n  const { collection } = extractCollectionInfo(options);\n  const typeName = collection.options.typeName;\n\n  const funcName = `upsert${typeName}`;\n  const legacyError = () => {\n    throw new Error(`upsertMutation function has been removed. Use ${funcName} function instead.`);\n  };\n\n  const Wrapper = props => {\n    const [upsertFunc] = useUpsert2(options);\n    return (\n      <C {...props} {...{ [funcName]: upsertFunc }} upsertMutation={legacyError} />\n\n    );\n\n  };\n\n  Wrapper.displayName = `withUpsert${typeName}`;\n  return Wrapper;\n};\n\nexport default withUpsert2;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/variables.js",
    "content": "import _merge from 'lodash/merge';\n/**\n * Compute the _id or input based on default options of the hooks\n * + dynamic props (for single) or dynamic arguments (for update)\n * @param {*} options \n * @param {*} argsOrProps \n */\nexport const computeQueryVariables = (options, argsOrProps) => {\n    const { _id: optionsId, input: optionsInput = {} } = options;\n    const { _id: argsId, input: argsInput = {} } = argsOrProps;\n    const _id = argsId || optionsId || undefined; // use dynamic _id in priority, default _id otherwise\n    const input = !_id ? _merge({}, optionsInput, argsInput) : undefined; // if _id is defined ignore input, else use dynamic input in priority\n    return { _id, input };\n};"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/withAccess.js",
    "content": "import React, { PureComponent } from 'react';\nimport { Components } from 'meteor/vulcan:lib';\nimport withCurrentUser from './currentUser';\nimport { withRouter } from 'react-router';\nimport Users from 'meteor/vulcan:users';\nimport { withMessages } from './withMessages.js';\n\n/**\n * withAccess - description\n *\n * @param  {Object}    options                      the options that define the hoc\n * @param  {string[]}  options.groups               the groups that have access to this component\n * @param  {string}    options.redirect             the link to redirect to in case the access is not granted (optional)\n * @param  {string}    options.failureComponentName the name of a component to display if access is not granted (optional)\n * @param  {Component} options.failureComponent     the component to display if access is not granted (optional)\n * @return {PureComponent}                          a React component that will display only if the acces is granted\n */\n\nexport default function withAccess(options) {\n  const { groups = [], redirect = null, failureComponent = null, failureComponentName = null, message } = options;\n\n  // we return a function that takes a component and itself returns a component\n  return WrappedComponent => {\n    class AccessComponent extends PureComponent {\n      // if there are any groups defined check if user belongs, else just check if user exists\n      canAccess = currentUser => {\n        return groups ? Users.isMemberOf(currentUser, groups) : currentUser;\n      };\n\n      // redirect on constructor if user cannot access\n      constructor(props) {\n        super(props);\n        const { currentUser, history, flash } = props;\n        if (!this.canAccess(currentUser) && typeof redirect === 'string') {\n          history.push(redirect);\n          if (message) {\n            flash(message);\n          }\n        }\n      }\n\n      renderFailureComponent() {\n        if (failureComponentName) {\n          const FailureComponent = Components[failureComponentName];\n          return <FailureComponent {...this.props} />;\n        } else if (failureComponent) {\n          const FailureComponent = failureComponent; // necesary because jsx components must be uppercase\n          return <FailureComponent {...this.props} />;\n        } else return null;\n      }\n\n      render() {\n        return this.canAccess(this.props.currentUser) ? <WrappedComponent {...this.props} /> : this.renderFailureComponent();\n      }\n    }\n\n    AccessComponent.displayName = `withAccess(${WrappedComponent.displayName})`;\n\n    return withMessages(withRouter(withCurrentUser(AccessComponent)));\n  };\n}\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/withComponents.js",
    "content": "/**\n * This HOC will load the global Components.\n * If a \"components\" prop is passed, it will be merged with the global Components.\n * \n * This allow local replacement of global components, for example if \n * you want a specific submit button but only for one specific form.\n */\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport { mergeWithComponents } from 'meteor/vulcan:lib';\n\nconst withComponents = C => {\n    const WrappedComponent = ({ components, formComponents, ...otherProps }) => {\n        //if (formComponents){\n        //    console.warn('\"formComponents\" prop is deprecated, use \"components\" prop instead (same behaviour)');\n        //}\n        const Components = mergeWithComponents(components || formComponents);\n        return <C Components={Components} {...otherProps} />;\n    };\n    WrappedComponent.displayName = `withComponents(${C.displayName})`;\n    WrappedComponent.propTypes = {\n        formComponents: PropTypes.object,\n        components: PropTypes.object\n    };\n    return WrappedComponent;\n};\n\nexport default withComponents;"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/withMessages-state-link.js",
    "content": "/*\n\nHoC that provides access to flash messages stored in Redux state and actions to operate on them\n\n     NOTE: the code is voluntary a bit verbose, to provide an example \n     of the apollo-link-state mutation patterns\n*/\n\nimport React from 'react';\nimport { registerStateLinkMutation, registerStateLinkDefault } from 'meteor/vulcan:lib';\nimport { graphql } from '@apollo/client/react/hoc';\nimport gql from 'graphql-tag';\nimport { compose } from 'meteor/vulcan:lib';\n\n// 1. Define the queries\n// the @client tag tells graphQL that we fetch data from the cache\n\n// read (equivalent to selectors)\nconst getMessagesQuery = gql`\n  query FlashMessage {\n    flashMessages @client\n  }\n`;\n// write (equivalent to actions)\nconst flashQuery = gql`\n  mutation flashMessagesFlash($content: JSON) {\n    flashMessagesFlash(content: $content) @client\n  }\n`;\nconst markAsSeenQuery = gql`\n  mutation markAsSeenQuery($i: Number) {\n    markAsSeenQuery(i: $i) @client\n  }\n`;\nconst clearSeenQuery = gql`\n  mutation clearSeenQuery {\n    clearSeenQuery @client\n  }\n`;\nconst clearQuery = gql`\n  mutation clearQuery($i: Number) {\n    clearQuery(i: $i) @client\n  }\n`;\n\n// init the flash message state\nregisterStateLinkDefault({\n  name: 'flashMessages',\n  defaultValue: [],\n});\n// mutations (equivalent to reducers)\nregisterStateLinkMutation({\n  name: 'flashMessagesFlash',\n  mutation: (obj, args, context, info) => {\n    // get relevant values from args\n    const { cache } = context;\n    const { content } = args;\n    // retrieve current state\n    const currentFlashMessages = cache.readQuery({ query: getMessagesQuery }).flashMessages;\n    // transform content\n    const flashType = content && typeof content.type !== 'undefined' ? content.type : 'error';\n    const _id = currentFlashMessages.length;\n    const flashMessage = {\n      __typename: 'FlashMessage',\n      _id,\n      ...content,\n      type: flashType,\n      seen: false,\n      show: true,\n    };\n    // const { } = obj  // the obj param is generally ignored in apollo-state-link\n    // const { } = info // barely needed (external info about the query)\n    // get the current messages\n    // push data\n    const data = {\n      flashMessages: [...currentFlashMessages, flashMessage],\n    };\n\n    cache.writeQuery({\n      query: gql`\n        query GetFlashMessages {\n          flashMessages\n        }\n      `,\n      data,\n    });\n    return null;\n  },\n});\nregisterStateLinkMutation({\n  name: 'flashMessagesMarkAsSeen',\n  mutation: (obj, args, context) => {\n    const { cache } = context;\n    const { i } = args;\n    const currentFlashMessages = cache.readQuery({ query: getMessagesQuery });\n    currentFlashMessages[i] = { ...currentFlashMessages[i], seen: true };\n    const data = {\n      flashMessages: currentFlashMessages,\n    };\n    cache.writeQuery({\n      query: gql`\n        query GetFlashMessages {\n          flashMessages\n        }\n      `,\n      data,\n    });\n    return null;\n  },\n});\nregisterStateLinkMutation({\n  name: 'flashMessagesClear',\n  mutation: (obj, args, context) => {\n    const { cache } = context;\n    const { i } = args;\n    const currentFlashMessages = cache.readQuery({ query: getMessagesQuery });\n    currentFlashMessages[i] = { ...currentFlashMessages[i], show: false };\n    const data = {\n      flashMessages: currentFlashMessages,\n    };\n\n    cache.writeQuery({\n      query: gql`\n        query GetFlashMessages {\n          flashMessages\n        }\n      `,\n      data,\n    });\n    return null;\n  },\n});\nregisterStateLinkMutation({\n  name: 'flashMessagesClearSeen',\n  mutation: (obj, args, context) => {\n    const { cache } = context;\n    const currentFlashMessages = cache.readQuery({ query: getMessagesQuery });\n    const newValue = currentFlashMessages.map(message => (message.seen ? { ...message, show: false } : message));\n    const data = {\n      flashMessages: newValue,\n    };\n\n    cache.writeQuery({\n      query: gql`\n        query GetFlashMessages {\n          flashMessages\n        }\n      `,\n      data,\n    });\n    return null;\n  },\n});\n\nconst withMessages = compose(\n  // equivalent to mapDispatchToProps (map the state-link to the component props, so it can access the mutations)\n  graphql(flashQuery, {\n    name: 'flash', // name in the props\n  }),\n  graphql(markAsSeenQuery, {\n    name: 'markAsSeen',\n  }),\n  graphql(clearQuery, {\n    name: 'clear',\n  }),\n  graphql(clearSeenQuery, {\n    name: 'clearSeen',\n  }),\n\n  // equivalent to mapStateToProps (map the graphql query to the component props)\n  graphql(getMessagesQuery, {\n    props: ({ ownProps, data /*: { flashMessages }*/ }) => {\n      const { flashMessages = [] } = data;\n      return { ...ownProps, messages: flashMessages };\n    },\n  })\n);\n\nexport default withMessages;\n\n// Equivalent in Redux (code used with Apollo v1):\n// addAction({\n//   messages: {\n//     flash(content) {\n//       return {\n//         type: 'FLASH',\n//         content,\n//       };\n//     },\n//     clear(i) {\n//       return {\n//         type: 'CLEAR',\n//         i,\n//       };\n//     },\n//     markAsSeen(i) {\n//       return {\n//         type: 'MARK_AS_SEEN',\n//         i,\n//       };\n//     },\n//     clearSeen() {\n//       return {\n//         type: 'CLEAR_SEEN'\n//       };\n//     },\n//   }\n// });\n\n// /*\n\n//   Messages reducers\n\n// */\n\n// addReducer({\n//   messages: (state = [], action) => {\n//     // default values\n//     const flashType = action.content && typeof action.content.type !== 'undefined' ? action.content.type : 'error';\n//     const currentMsg = typeof action.i === 'undefined' ? {} : state[action.i];\n\n//     switch(action.type) {\n//       case 'FLASH':\n//         return [\n//           ...state,\n//           {\n//             _id: state.length,\n//             ...action.content,\n//             type: flashType,\n//             seen: false,\n//             show: true,\n//           },\n//         ];\n//       case 'MARK_AS_SEEN':\n//         return [\n//           ...state.slice(0, action.i),\n//           { ...currentMsg, seen: true },\n//           ...state.slice(action.i + 1),\n//         ];\n//       case 'CLEAR':\n//         return [\n//           ...state.slice(0, action.i),\n//           { ...currentMsg, show: false },\n//           ...state.slice(action.i + 1),\n//         ];\n//       case 'CLEAR_SEEN':\n//         return state.map(message => message.seen ? { ...message, show: false } : message);\n//       default:\n//         return state;\n//     }\n//   },\n// });\n\n// /*\n\n//   withMessages HOC\n\n// */\n\n// const mapStateToProps = state => ({ messages: state.messages, });\n// const mapDispatchToProps = dispatch => bindActionCreators(getActions().messages, dispatch);\n\n// const withMessages = component => connect(mapStateToProps, mapDispatchToProps)(component);\n\n// export default withMessages;\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/containers/withMessages.js",
    "content": "/*\n\nHook and HoC that provides access to flash messages stored in reactive state\n\n*/\nimport React from 'react';\nimport { createReactiveState, Random } from 'meteor/vulcan:lib';\nimport { intlShape, useIntl } from 'meteor/vulcan:i18n';\n\nconst messagesSchema = {\n  messages: {\n    type: Array,\n    arrayItem: {\n      type: Object,\n      blackbox: true,\n    },\n    defaultValue: [],\n  },\n};\n\nconst messagesState = createReactiveState({ stateKey: 'messagesState', schema: messagesSchema });\n\nconst normalizeMessage = (messageObject, intl) => {\n  if (typeof messageObject === 'string') {\n    // if error is a string, use it as message\n    return {\n      message: messageObject,\n      type: 'error',\n    };\n  } else {\n    // else return full error object after internationalizing message\n    const { id = 'error', type, message, properties } = messageObject;\n    const translatedMessage = intl.formatMessage({ id, defaultMessage: message }, properties);\n\n    const transformedType = type === 'error' ? 'danger' : !['danger', 'success', 'warning'].includes(type) ? 'info' : type;\n\n    return {\n      ...messageObject,\n      message: translatedMessage,\n      type: transformedType,\n      _id: Random.id(),\n    };\n  }\n};\n\nexport const useMessages = legacyContextIntl => {\n  const intl = legacyContextIntl;\n\n  // doen't work properly yet, once it does the legacyContextIntl argument\n  // can be removed:\n  // const newContextIntl = useIntl();\n\n  const messagesProps = {\n    messagesState,\n\n    messages: messagesState().messages,\n\n    flash: message => {\n      message = normalizeMessage(message, intl);\n      messagesState(state => {\n        state.messages.push(message);\n        return state;\n      });\n    },\n\n    dismissFlash: _id => {\n      // mark message as dismissed\n      const messages = messagesState().messages;\n      const message = messages.find(message => message._id === _id);\n      if (message) {\n        message.dismissed = true;\n      }\n\n      // if all messages are dismissed, empty the messages array\n      const hasUnDismissed = messages.find(message => !message.dismissed);\n      if (!hasUnDismissed) {\n        messagesState({ messages: [] });\n      }\n    },\n\n    dismissAllFlash: () => {\n      messagesState({ messages: [] });\n    },\n  };\n\n  return messagesProps;\n};\n\nexport const withMessages = WrappedComponent => {\n  const MessagesComponent = (props, { intl }) => {\n    const legacyContextIntl = intl;\n\n    const messagesProps = useMessages(legacyContextIntl);\n    return <WrappedComponent {...props} {...messagesProps} />;\n  };\n\n  MessagesComponent.contextTypes = {\n    intl: intlShape,\n  };\n\n  return MessagesComponent;\n};\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/decorators/autocomplete.js",
    "content": "import {\n  getCollectionByTypeName,\n  fieldDynamicQueryTemplate,\n  fieldStaticQueryTemplate,\n  autocompleteQueryTemplate,\n} from 'meteor/vulcan:core';\nimport get from 'lodash/get';\n\nconst getQueryResolverName = field => {\n  const isRelation = field.relation || get(field, 'resolveAs.relation');\n  if (isRelation) {\n    const typeName = get(field, 'relation.typeName') || get(field, 'resolveAs.typeName');\n    const collection = getCollectionByTypeName(typeName);\n    return get(collection, 'options.multiResolverName');\n  } else {\n    throw new Error('Could not guess query resolver name, please specify a queryResolverName option for the makeAutocomplete decorator.');\n  }\n};\n\n// note: the following decorator function is called both for autocomplete and autocompletemultiple\nexport const makeAutocomplete = (field = {}, options = {}) => {\n  /*\n\n  - queryResolverName: the name of the query resolver used to fetch the list of autocomplete suggestions\n  - autocompletePropertyName: the name of the property used as the label for each item\n  - fragmentName: the name of the fragment to use to fetch additional data besides autocompletePropertyName\n  - valuePropertyName: the name of the property to return (defaults to `_id`)\n\n  */\n  const { autocompletePropertyName, fragmentName, valuePropertyName = '_id', multi } = options;\n\n  if (!autocompletePropertyName) {\n    throw new Error('makeAutocomplete decorator is missing an autocompletePropertyName option.');\n  }\n\n  // if field stores an array, use multi autocomplete\n  const isMultiple = multi || field.type === Array;\n\n  // define this as a function to run later as some variables may not yet be available\n  // at init time\n  const getQueryProps = () => {\n    const queryResolverName = options.queryResolverName || getQueryResolverName(field);\n    return { queryResolverName, autocompletePropertyName, valuePropertyName, fragmentName };\n  };\n\n  // define query to load extra data for input values\n\n  // to load only some items based on a key\n  const dynamicQuery = () => {\n    return fieldDynamicQueryTemplate(getQueryProps());\n  };\n\n  // to load all possible items\n  const staticQuery = () => {\n    return fieldStaticQueryTemplate(getQueryProps());\n  };\n\n  // query to load autocomplete suggestions\n  const autocompleteQuery = () => {\n    return autocompleteQueryTemplate(getQueryProps());\n  };\n\n  // define a function that takes the options returned by the queries\n  // and transforms them into { value, label } pairs.\n  const optionsFunction = props => {\n    const queryResolverName = options.queryResolverName || getQueryResolverName(field);\n\n    return get(props, `data.${queryResolverName}.results`, []).map(document => ({\n      ...document,\n      value: document[valuePropertyName],\n      label: document[autocompletePropertyName],\n    }));\n  };\n\n  const acField = {\n    dynamicQuery,\n    staticQuery, // not currently used?\n    query: dynamicQuery, // backwards-compatibility\n    autocompleteQuery,\n    queryWaitsForValue: true,\n    options: optionsFunction,\n    input: isMultiple ? 'multiautocomplete' : 'autocomplete',\n    ...field, // add field last to allow manual override of properties in field definition\n  };\n\n  return acField;\n};\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/decorators/checkboxgroup.js",
    "content": "import get from 'lodash/get';\n\nexport const makeCheckboxgroup = (field = {}) => {\n  const hasOther = !!get(field, 'itemProperties.showOther');\n\n  if (!field.options) {\n    throw new Error(`Checkboxgroup fields need an 'options' property`);\n  }\n\n  // add additional field object properties\n  const cbgField = {\n    ...field,\n    type: Array,\n    input: 'checkboxgroup',\n  };\n\n  // if field doesn't allow \"other\" responses, limit it to whitelist of allowed values\n  if (!hasOther) {\n    cbgField.arrayItem = { ...cbgField.arrayItem, allowedValues: field.options.map(({ value }) => value) };\n  }\n\n  return cbgField;\n};\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/decorators/index.js",
    "content": "export * from './likert';\nexport * from './checkboxgroup';\nexport * from './radiogroup';\nexport * from './autocomplete';\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/decorators/likert.js",
    "content": "import SimpleSchema from 'simpl-schema';\n\nexport const makeLikert = (field = {}) => {\n  // get typeName from fieldName unless it's already specified in field object\n  const { canRead, canCreate, canUpdate } = field;\n  const fieldOptions = field.options;\n\n  if (!fieldOptions) {\n    throw new Error(`Likert fields need an 'options' property`);\n  }\n\n  // build SimpleSchema type object for validation\n  const typeObject = {};\n  fieldOptions.forEach(({ value }) => {\n    typeObject[value] = {\n      type: SimpleSchema.Integer,\n      canRead,\n      canCreate,\n      canUpdate,\n    };\n  });\n\n  // add additional field object properties\n  const likertField = {\n    ...field,\n    type: new SimpleSchema(typeObject),\n    input: 'likert',\n  };\n\n  return likertField;\n};\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/decorators/radiogroup.js",
    "content": "import get from 'lodash/get';\n\nexport const makeRadiogroup = (field = {}) => {\n  const hasOther = !!get(field, 'itemProperties.showOther');\n\n  if (!field.options) {\n    throw new Error(`Radiogroup fields need an 'options' property`);\n  }\n\n  const rgField = {\n    ...field,\n    type: Array,\n    input: 'radiogroup',\n  };\n\n  // if field doesn't allow \"other\" responses, limit it to whitelist of allowed values\n  if (!hasOther) {\n    rgField.arrayItem = {...rgField.arrayItem, allowedValues: field.options.map(({value}) => value)};\n  }\n\n  return rgField;\n};\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/index.js",
    "content": "// import and re-export\nimport './callbacks';\nexport * from 'meteor/vulcan:lib';\n\nexport * from './containers';\nexport * from './components.js';\n\nexport { default as App } from './components/App.jsx';\nexport { default as AccessControl } from './components/AccessControl.jsx';\nexport { default as Card } from './components/Card';\nexport { default as Datatable } from './components/Datatable';\nexport { default as Dummy } from './components/Dummy.jsx';\nexport { default as DynamicLoading } from './components/DynamicLoading.jsx';\nexport { default as Error404 } from './components/Error404.jsx';\nexport { default as Flash } from './components/Flash.jsx';\nexport { default as FlashMessages } from './components/FlashMessages.jsx';\nexport { default as HeadTags } from './components/HeadTags.jsx';\nexport { default as HelloWorld } from './components/HelloWorld.jsx';\nexport { default as Icon } from './components/Icon.jsx';\nexport { default as Layout } from './components/Layout.jsx';\nexport { default as Loading } from './components/Loading';\nexport { default as MutationButton } from './components/MutationButton.jsx';\nexport { default as RouterHook } from './components/RouterHook.jsx';\nexport { default as ScrollToTop } from './components/ScrollToTop.jsx';\nexport { default as ShowIf } from './components/ShowIf.jsx';\nexport { default as Welcome } from './components/Welcome.jsx';\nexport { default as VerticalMenuLayout } from './components/VerticalMenuLayout/VerticalMenuLayout.jsx';\nexport * from './components/PaginatedList/index';\n\nexport { default as withAccess } from './containers/withAccess.js';\nexport { withMessages, useMessages } from './containers/withMessages.js';\nexport { withMulti, useMulti } from './containers/multi.js';\nexport { withMulti2, useMulti2 } from './containers/multi2.js';\nexport { withSingle, useSingle } from './containers/single.js';\nexport { withSingle2, useSingle2 } from './containers/single2.js';\nexport { withCreate, useCreate } from './containers/create.js';\nexport { withCreate2, useCreate2 } from './containers/create2.js';\nexport { withUpdate, useUpdate } from './containers/update.js';\nexport { withUpdate2, useUpdate2 } from './containers/update2.js';\nexport { withUpsert, useUpsert } from './containers/upsert.js';\nexport { withUpsert2, useUpsert2 } from './containers/upsert2.js';\nexport { withDelete, useDelete } from './containers/delete.js';\nexport { withDelete2, useDelete2 } from './containers/delete2.js';\nexport { withCurrentUser, useCurrentUser } from './containers/currentUser.js';\nexport { withMutation, useRegisteredMutation } from './containers/registeredMutation.js';\nexport { withSiteData, useSiteData } from './containers/siteData.js';\n\nexport * from './decorators';\n\nexport { default as withComponents } from './containers/withComponents.js';\n\n// OpenCRUD backwards compatibility\nexport { default as withNew } from './containers/create.js';\nexport { default as withEdit } from './containers/update.js';\nexport { default as withRemove } from './containers/delete.js';\nexport { default as withList } from './containers/multi.js';\nexport { default as withDocument } from './containers/single.js';\n\nexport * from './menu.js';\n"
  },
  {
    "path": "packages/vulcan-core/lib/modules/menu.js",
    "content": "/**\n * Menu configuration is a map\n * {\n *  defaultMenu: {\n *    item1: {\n *      ...\n *  }\n *  adminMenu: {\n *    some-item: {\n *      ...\n *    }\n *  }\n *  shortMenu: { ... }\n *  ...\n * }\n */\nimport values from 'lodash/values';\nimport Users from 'meteor/vulcan:users';\nimport PropTypes from 'prop-types';\n\nexport const menuItemProps = {\n  name: PropTypes.string.isRequired,\n  label: PropTypes.string,\n  labelToken: PropTypes.string, // TODO: one of label or labelToken must be defined\n  path: PropTypes.string,\n  onClick: PropTypes.func,\n  LeftComponent: PropTypes.any, //React component @see https://github.com/facebook/prop-types/issues/200\n  RightComponent: PropTypes.any,\n  groups: PropTypes.arrayOf(PropTypes.string), // groups that can see the item\n  menuGroup: PropTypes.string, // submenu name (facultative for main menu)\n};\n\nconst defaultMenuGroup = 'defaultMenu';\nconst Menus = {\n  [defaultMenuGroup]: {},\n};\n\n// only for testing\nexport const resetMenus = () => {\n  Object.keys(Menus).forEach(key => {\n    delete Menus[key];\n  });\n  Menus[defaultMenuGroup] = {};\n};\n/**\n * \n * @param {*} config \n */\nexport const addMenuItem = config => {\n  const { menuGroup = defaultMenuGroup, name, ...otherConfig } = config;\n  if (!Menus[menuGroup]) {\n    Menus[menuGroup] = {};\n  }\n  Menus[menuGroup][name] = { name, menuGroup, ...otherConfig };\n};\n\nexport const removeMenuItem = (itemId, menuGroup = defaultMenuGroup) => {\n  delete Menus[menuGroup][itemId];\n  if (menuGroup !== defaultMenuGroup && Object.isEmpty(Menus[menuGroup])) {\n    delete Menus[menuGroup];\n  }\n};\n\n// should not be needed\nexport const getMenuItemsConfig = (menuGroup = defaultMenuGroup) => Menus[menuGroup];\nexport const getAllMenuItemsConfig = () => Menus;\n\nconst filterAuthorized = (currentUser, menuItems) =>\n  menuItems.filter(({ groups }) => {\n    // items without groups are visible by guests too\n    if (!groups) return true;\n    return Users.isMemberOf(currentUser, groups);\n  });\n\n// same as getMenuItems but filter out unauthorized items\nexport const getAuthorizedMenuItems = (currentUser, ...args) =>\n  filterAuthorized(currentUser, getMenuItems(...args));\n\n// get menu items as an array\nexport const getMenuItems = (menuGroup = defaultMenuGroup) => {\n  const menu = Menus[menuGroup];\n  if (!menu) {\n    console.warn(\n      `Warning: Menu group ${menuGroup} unknown. Menu groups available: ${Object.keys(Menus)}`\n    );\n    return [];\n  }\n  return values(menu);\n};\n\n// { admin: [menuItem1, menuItem2, ...], defaultMenu: [...]}\nexport const getMenuItemsMap = () =>\n  Object.keys(Menus).reduce((res, menuGroup) => ({\n    ...res,\n    [menuGroup]: getMenuItems(menuGroup),\n  }));\n"
  },
  {
    "path": "packages/vulcan-core/lib/server/main.js",
    "content": "import './start.js';\n\nexport * from '../modules/index.js';\n"
  },
  {
    "path": "packages/vulcan-core/lib/server/start.js",
    "content": "import { SyncedCron } from 'meteor/littledata:synced-cron';\nimport { getSetting, registerSetting } from 'meteor/vulcan:lib';\n\nregisterSetting('mailUrl', null, 'The SMTP URL used to send out email');\n\nif (getSetting('mailUrl')) {\n  process.env.MAIL_URL = getSetting('mailUrl');\n}\n\nMeteor.startup(function() {\n  if (process.env.NODE_ENV === 'development') {\n    // eslint-disable-next-line no-undef\n    Vulcan.getGraphQLSchema();\n  }\n  if (typeof SyncedCron !== 'undefined') {\n    SyncedCron.start();\n  }\n});\n"
  },
  {
    "path": "packages/vulcan-core/package.js",
    "content": "const version = '1.16.9';\n\nPackage.describe({\n  name: 'vulcan:core',\n  summary: 'Vulcan core package',\n  version,\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use([`vulcan:lib@=${version}`, `vulcan:i18n@=${version}`, `vulcan:users@=${version}`]);\n\n  api.use([`vulcan:i18n@=${version}`], ['server', 'client'], { weak: true });\n\n  api.imply([`vulcan:lib@=${version}`]);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n\nPackage.onTest(function(api) {\n  api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:core', 'vulcan:test', 'vulcan:users']);\n  api.mainModule('./test/server/index.js', ['server']);\n  api.mainModule('./test/client/index.js', ['client']);\n});\n"
  },
  {
    "path": "packages/vulcan-core/test/client/index.js",
    "content": "import '../index';\n\nimport './mutations2.test';\n"
  },
  {
    "path": "packages/vulcan-core/test/client/mutations2.test.js",
    "content": "import { multiQueryUpdater } from '../../lib/modules/containers/create2';\nimport { createDummyCollection } from 'meteor/vulcan:test';\nimport expect from 'expect';\nimport sinon from 'sinon';\n\nconst test = it;\n\ndescribe('vulcan:core/container/mutations2', () => {\n  const typeName = 'Foo';\n  const Foo = createDummyCollection({\n    options: {\n      collectionName: 'Foos',\n      typeName,\n      multiResolverName: 'foos',\n    },\n    schema: { val: { type: Number, canRead: ['guests'] } },\n  });\n  const fragmentName = 'FoosDefaultFragment';\n  const fragment = {\n    definitions: [\n      {\n        name: {\n          value: fragmentName,\n        },\n      },\n    ],\n    toString: () => `fragment FoosDefaultFragment on Foo { \n        _id\n        hello\n        __typename\n      }`,\n  };\n\n  const foo = { _id: 1, hello: 'world', __typename: 'Foo' };\n\n  describe('multiQuery update after mutations', () => {\n    describe('update after a document creation', () => {\n      const defaultOptions = {\n        typeName,\n        fragment,\n        fragmentName,\n        collection: Foo,\n      };\n      const defaultCacheContent = {\n        foos: {\n          results: [],\n          totalCount: 0,\n        },\n      };\n      const makeCacheData = (vars = { input: { filter: {} } }) => ({\n        data: {\n          ROOT_QUERY: {\n            // variables are contained in the query name\n            [`foos(${JSON.stringify(vars)})`]: {},\n          },\n        },\n      });\n      const defaultCacheData = makeCacheData({});\n\n      beforeEach(() => {\n        Foo.getParameters = terms => ({\n          selector: {},\n          options: {},\n        });\n      });\n\n      test('add document to multi query after a creation', async () => {\n        const update = multiQueryUpdater({ ...defaultOptions, resolverName: 'createFoo' });\n        const cache = {\n          readQuery: () => defaultCacheContent,\n          //writeQuery, // we write to the client instead\n          data: defaultCacheData,\n        };\n        const updates = await update(cache, {\n          data: {\n            createFoo: {\n              data: foo,\n            },\n          },\n        });\n        expect(updates).toHaveLength(1);\n        expect(updates[0].data).toEqual({\n          foos: { results: [foo], totalCount: 1 },\n        });\n      });\n      test('update document if already there', async () => {\n        const update = multiQueryUpdater({ ...defaultOptions, resolverName: 'createFoo' });\n\n        const cache = {\n          readQuery: () => ({\n            ...defaultCacheContent,\n            foos: {\n              results: [foo],\n              totalCount: 1,\n            },\n          }),\n          data: defaultCacheData,\n        };\n        const updateFoo = { ...foo, UPDATED: true };\n        const updates = await update(cache, {\n          data: {\n            createFoo: {\n              data: updateFoo,\n            },\n          },\n        });\n        expect(updates).toHaveLength(1);\n        expect(updates[0].data).toMatchObject({\n          foos: { results: [updateFoo], totalCount: 1 },\n        });\n      });\n      test('do not add document if it does not match the mongo selector of the query', async () => {\n        const update = multiQueryUpdater({ ...defaultOptions, resolverName: 'createFoo' });\n\n        const cache = {\n          readQuery: () => defaultCacheContent,\n          data: makeCacheData({\n            input: {\n              filter: { val: { _gt: 42 } },\n            },\n          }),\n        };\n        const newFoo = { ...foo, val: 41 };\n        const updates = await update(cache, {\n          data: {\n            createFoo: {\n              data: newFoo,\n            },\n          },\n        });\n        expect(updates).toHaveLength(0);\n      });\n      test('add document if it does match the mongo selector', async () => {\n        const update = multiQueryUpdater({ ...defaultOptions, resolverName: 'createFoo' });\n\n        const cache = {\n          readQuery: () => defaultCacheContent,\n          data: makeCacheData({\n            input: {\n              filter: { val: { _gt: 42 } },\n            },\n          }),\n        };\n        const newFoo = { ...foo, val: 46 };\n        const updates = await update(cache, {\n          data: {\n            createFoo: {\n              data: newFoo,\n            },\n          },\n        });\n        expect(updates).toHaveLength(1);\n      });\n      test('sort documents', async () => {\n        const update = multiQueryUpdater({ ...defaultOptions, resolverName: 'createFoo' });\n\n        const cache = {\n          readQuery: () => ({\n            foos: {\n              results: [{ val: 40 }, { val: 43 }],\n              totalCount: 2,\n            },\n          }),\n          data: makeCacheData({\n            input: {\n              sort: {\n                val: 'asc',\n              },\n            },\n          }),\n        };\n        const newFoo = { ...foo, val: 42 };\n        const updates = await update(cache, {\n          data: {\n            createFoo: {\n              data: newFoo,\n            },\n          },\n        });\n        expect(updates).toHaveLength(1);\n        expect(updates[0].data.foos.results).toHaveLength(3);\n        expect(updates[0].data.foos.results[1]).toEqual(newFoo);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-core/test/components.test.js",
    "content": "import React from 'react';\nimport expect from 'expect';\nimport { mount, shallow } from 'enzyme';\nimport { Components } from 'meteor/vulcan:core';\nimport { initComponentTest } from 'meteor/vulcan:test';\n\n\n// we must import all the other components, so that \"registerComponent\" is called\nimport '../lib/modules';\nimport Datatable from '../lib/modules/components/Datatable';\n// stub collection\nimport { createCollection, getDefaultResolvers, getDefaultMutations, registerFragment } from 'meteor/vulcan:core';\nconst createDummyCollection = (typeName, schema) => {\n    return createCollection({\n        collectionName: typeName + 's',\n        typeName,\n        schema,\n        resolvers: getDefaultResolvers(typeName + 's'),\n        mutations: getDefaultMutations(typeName + 's')\n    });\n};\nconst Articles = createDummyCollection('Article', {\n    name: {\n        type: String,\n        canRead: ['members']\n    }\n});\nregisterFragment(`\n   fragment ArticlesDefaultFragment on Article {\n       name\n   }\n`);\n\n// setup Vulcan (load components, initialize fragments)\ninitComponentTest();\n\n\ndescribe('vulcan-core/components', function () {\n    describe('DataTable', function () {\n        it('shallow renders DataTable', function () {\n            const wrapper = shallow(<Datatable\n                Components={Components}\n                collection={Articles}\n                location={{\n                    search: null\n                }}\n            />);\n            expect(wrapper).toBeDefined();\n        });\n        it('render a static version', function () {\n            const wrapper = shallow(<Datatable\n                Components={Components}\n                data={[{ name: 'foo' }, { name: 'bar' }]}\n                location={{\n                    search: null\n                }}\n            />);\n            const content = wrapper.find('DatatableContents').first();\n            expect(content).toBeDefined();\n        });\n        const context = {\n            intl: {\n                formatMessage: () => { },\n                formatDate: () => { },\n                formatTime: () => { },\n                formatRelative: () => { },\n                formatNumber: () => { },\n                formatPlural: () => { },\n                formatHTMLMessage: () => { },\n                now: () => { }\n            }\n        };\n        it.skip('mounts a static version', function () {\n            const wrapper = mount(\n                <Datatable\n                    Components={Components}\n                    data={[{ name: 'foo' }, { name: 'bar' }]}\n                />\n                , {\n                    context,\n                    childContextTypes: context\n                });\n            expect(wrapper).toBeDefined();\n            //const content = wrapper.find('DatatableContents').first();\n            //expect(content).toBeDefined();\n        });\n    });\n});"
  },
  {
    "path": "packages/vulcan-core/test/containers/mutations.test.js",
    "content": "import { OperationNameMockLink } from 'operation-name-mock-link';\nimport React from 'react';\nimport {\n  withCreate,\n  withUpdate,\n  withUpsert,\n  withDelete,\n  withMutation,\n  useCreate,\n  useUpdate,\n  useUpsert,\n  useDelete,\n} from '../../lib/modules';\nimport { multiQueryUpdater, buildCreateQuery } from '../../lib/modules/containers/create';\nimport { buildUpdateQuery } from '../../lib/modules/containers/update';\nimport { buildUpsertQuery } from '../../lib/modules/containers/upsert';\nimport { buildDeleteQuery } from '../../lib/modules/containers/delete';\nimport { MockedProvider } from 'meteor/vulcan:test';\nimport { mount } from 'enzyme';\nimport expect from 'expect';\nimport gql from 'graphql-tag';\nimport sinon from 'sinon';\nimport { getVariablesListFromCache } from '../../lib/modules/containers/cacheUpdate';\nconst test = it;\n\ndescribe('vulcan:core/container/mutations', () => {\n  const typeName = 'Foo';\n  const Foo = {\n    options: {\n      collectionName: 'Foos',\n      typeName,\n      multiResolverName: 'foos',\n    },\n  };\n  const fragmentName = 'FoosDefaultFragment';\n  const fragment = {\n    definitions: [\n      {\n        name: {\n          value: fragmentName,\n        },\n      },\n    ],\n    toString: () => `fragment FoosDefaultFragment on Foo { \n        _id\n        hello\n        __typename\n      }`,\n  };\n\n  const rawFoo = { hello: 'world' };\n  const fooUpdate = { _id: 1, hello: 'world' };\n  const foo = { _id: 1, hello: 'world', __typename: 'Foo' };\n  const TestComponent = () => 'test';\n  const defaultOptions = {\n    collection: Foo,\n    fragmentName: fragmentName,\n    fragment,\n  };\n  describe('similar queries in cache', () => {\n    test('return from the cache only the variables which match exactly the query', async () => {\n      const queryName = 'myCustomQuery';\n      const cacheQueryName = queryName + '({\"correct\":\"variables\"})';\n      const cacheSimilarQueryName = queryName + 'Foo({\"foo\":\"bar\"})';\n      const cacheObject = {\n        data: {\n          data: {\n            ROOT_QUERY: {\n              [cacheQueryName]: { foo: 'bar' },\n              [cacheSimilarQueryName]: { foo: 'bar' },\n            },\n          },\n        },\n      };\n      const variables = await getVariablesListFromCache(cacheObject, queryName);\n      expect(variables).toHaveLength(1);\n      expect(variables[0].correct).toBe('variables');\n    });\n    test('ignore the queries from the cache not including variables', async () => {\n      const queryName = 'myCustomQuery';\n      const cacheQueryName = queryName + '({\"correct\":\"variables\"})';\n      const cacheObject = {\n        data: {\n          data: {\n            ROOT_QUERY: {\n              [queryName]: { foo: 'bar' },\n              [cacheQueryName]: { foo: 'bar' },\n            },\n          },\n        },\n      };\n      const variables = await getVariablesListFromCache(cacheObject, queryName);\n      expect(variables).toHaveLength(1);\n      expect(variables[0].correct).toBe('variables');\n    });\n  });\n  describe('common', () => {\n    test('export hooks and hocs', () => {\n      expect(useCreate).toBeInstanceOf(Function);\n      expect(useUpdate).toBeInstanceOf(Function);\n      expect(useUpsert).toBeInstanceOf(Function);\n      expect(useDelete).toBeInstanceOf(Function);\n      expect(withCreate).toBeInstanceOf(Function);\n      expect(withUpdate).toBeInstanceOf(Function);\n      expect(withUpsert).toBeInstanceOf(Function);\n      expect(withDelete).toBeInstanceOf(Function);\n    });\n    test('pass down props', () => {\n      const CreateComponent = withCreate(defaultOptions)(TestComponent);\n      const UpdateComponent = withUpdate(defaultOptions)(TestComponent);\n      const UpsertComponent = withUpsert(defaultOptions)(TestComponent);\n      const DeleteComponent = withDelete(defaultOptions)(TestComponent);\n      [CreateComponent, UpdateComponent, UpsertComponent, DeleteComponent].forEach(C => {\n        const wrapper = mount(\n          <MockedProvider mocks={[]}>\n            <C foo=\"bar\" />\n          </MockedProvider>\n        );\n        expect({\n          res: wrapper.find('TestComponent').prop('foo'),\n          C: C.displayName,\n        }).toEqual({ res: 'bar', C: C.displayName });\n      });\n    });\n  });\n  describe('withCreate', () => {\n    // NOT passing for no reason...\n    // @see https://github.com/apollographql/react-apollo/issues/3478\n    test('run a create mutation', async () => {\n      const CreateComponent = withCreate(defaultOptions)(TestComponent);\n      const responses = [\n        {\n          request: {\n            operationName: 'createFoo',\n            query: buildCreateQuery({ fragmentName, fragment, typeName }),\n            // variables: {\n            //     data: rawFoo\n            // }\n          },\n          result: {\n            data: {\n              createFoo: {\n                data: foo,\n                __typename: 'Foo',\n              },\n            },\n          },\n        },\n      ];\n      const wrapper = mount(\n        <MockedProvider /*mocks={responses}*/ link={new OperationNameMockLink(responses, false)}>\n          <CreateComponent />\n        </MockedProvider>\n      );\n      // trigger the query\n      expect(wrapper.find(TestComponent).prop('createFoo')).toBeInstanceOf(Function);\n      const res = await wrapper.find(TestComponent).prop('createFoo')({\n        data: rawFoo,\n      });\n      expect(res).toEqual({ data: { createFoo: { data: foo, __typename: 'Foo' } } });\n    });\n\n    describe('multiQuery update after create mutation for optimistic UI', () => {\n      const defaultOptions = {\n        typeName,\n        fragment,\n        fragmentName,\n        collection: Foo,\n      };\n      const defaultCacheContent = {\n        foos: {\n          results: [],\n          totalCount: 0,\n        },\n      };\n      const defaultCacheData = {\n        data: {\n          ROOT_QUERY: {\n            // variables are contained in the query name\n            [`foos(${JSON.stringify({\n              input: {\n                terms: {},\n              },\n            })})`]: {},\n          },\n        },\n      };\n\n      beforeEach(() => {\n        Foo.getParameters = terms => ({\n          selector: {},\n          options: {},\n        });\n      });\n      // TODO: tests not passing but I am not sure why, the spy should have been called...\n      test('add document to multi query after a creation', async () => {\n        const update = multiQueryUpdater({ ...defaultOptions, resolverName: 'createFoo' });\n        const writeQuery = sinon.spy();\n        const cache = {\n          readQuery: () => defaultCacheContent,\n          writeQuery,\n          data: defaultCacheData,\n        };\n        await update(cache, {\n          data: {\n            createFoo: {\n              data: foo,\n            },\n          },\n        });\n        expect(writeQuery.calledOnce).toBe(true);\n      });\n      test('update document if already there', async () => {\n        const update = multiQueryUpdater({ ...defaultOptions, resolverName: 'createFoo' });\n        const writeQuery = sinon.spy();\n        const cache = {\n          readQuery: () => ({\n            ...defaultCacheContent,\n            foos: {\n              results: [foo],\n              totalCount: 1,\n            },\n          }),\n          writeQuery,\n          data: defaultCacheData,\n        };\n        const updateFoo = { ...foo, UPDATED: true };\n        await update(cache, {\n          data: {\n            createFoo: {\n              data: updateFoo,\n            },\n          },\n        });\n        expect(writeQuery.calledOnce).toBe(true);\n        expect(writeQuery.getCall(0).args[0]).toMatchObject({\n          data: { foos: { results: [updateFoo], totalCount: 1 } },\n        });\n      });\n      test('do not add document if it does not match the mongo selector', async () => {\n        const update = multiQueryUpdater({ ...defaultOptions, resolverName: 'createFoo' });\n        const writeQuery = sinon.spy();\n        const cache = {\n          readQuery: () => defaultCacheContent,\n          writeQuery,\n          data: defaultCacheData,\n        };\n        const newFoo = { ...foo, val: 41 };\n        Foo.getParameters = () => ({\n          selector: {\n            val: { $gt: 42 },\n          },\n          options: {},\n        });\n        await update(cache, {\n          data: {\n            createFoo: {\n              data: newFoo,\n            },\n          },\n        });\n        expect(writeQuery.notCalled).toBe(true);\n      });\n      test('add document if it does match the mongo selector', async () => {\n        const update = multiQueryUpdater({ ...defaultOptions, resolverName: 'createFoo' });\n        const writeQuery = sinon.spy();\n        const cache = {\n          readQuery: () => defaultCacheContent,\n          writeQuery,\n          data: defaultCacheData,\n        };\n        const newFoo = { ...foo, val: 46 };\n        Foo.getParameters = () => ({\n          selector: {\n            val: { $gt: 42 },\n          },\n          options: {},\n        });\n        await update(cache, {\n          data: {\n            createFoo: {\n              data: newFoo,\n            },\n          },\n        });\n        expect(writeQuery.calledOnce).toBe(true);\n      });\n      test('sort documents', async () => {\n        const update = multiQueryUpdater({ ...defaultOptions, resolverName: 'createFoo' });\n        const writeQuery = sinon.spy();\n        const cache = {\n          readQuery: () => ({\n            foos: {\n              results: [{ val: 40 }, { val: 43 }],\n              totalCount: 2,\n            },\n          }),\n          writeQuery,\n          data: defaultCacheData,\n        };\n        const newFoo = { ...foo, val: 42 };\n        Foo.getParameters = () => ({\n          selector: {},\n          options: {\n            sort: {\n              val: 1,\n            },\n          },\n        });\n        await update(cache, {\n          data: {\n            createFoo: {\n              data: newFoo,\n            },\n          },\n        });\n        const res = writeQuery.getCall(0).args[0].data.foos.results;\n        expect(res).toHaveLength(3);\n        expect(res[1]).toEqual(newFoo);\n      });\n    });\n  });\n\n  describe('update', () => {\n    test('run update mutation', async () => {\n      const UpdateComponent = withUpdate(defaultOptions)(TestComponent);\n      const responses = [\n        {\n          request: {\n            query: buildUpdateQuery({ typeName, fragmentName, fragment }),\n            operationName: 'updateFoo',\n            //variables: {\n            //  //selector: { documentId: foo._id },\n            //  data: fooUpdate,\n            //},\n          },\n          result: {\n            data: {\n              updateFoo: {\n                data: foo,\n                __typename: 'Foo',\n              },\n            },\n          },\n        },\n      ];\n      const wrapper = mount(\n        <MockedProvider /*mocks={responses}*/ link={new OperationNameMockLink(responses, false)}>\n          <UpdateComponent />\n        </MockedProvider>\n      );\n      expect(wrapper.find(TestComponent).prop('updateFoo')).toBeInstanceOf(Function);\n      const res = await wrapper.find(TestComponent).prop('updateFoo')({\n        data: fooUpdate,\n      });\n      expect(res).toEqual({ data: { updateFoo: { data: foo, __typename: 'Foo' } } });\n    });\n  });\n  describe('upsert', () => {\n    test('run upsert mutation', async () => {\n      const UpsertComponent = withUpsert(defaultOptions)(TestComponent);\n      const responses = [\n        {\n          request: {\n            query: buildUpsertQuery({ typeName, fragmentName, fragment }),\n            operationName: 'upsertFoo',\n            //variables: {\n            //  data: fooUpdate,\n            //},\n          },\n          result: {\n            data: {\n              upsertFoo: {\n                data: foo,\n                __typename: 'Foo',\n              },\n            },\n          },\n        },\n      ];\n      const wrapper = mount(\n        <MockedProvider /*mocks={responses}*/ link={new OperationNameMockLink(responses, false)}>\n          <UpsertComponent />\n        </MockedProvider>\n      );\n      expect(wrapper.find(TestComponent).prop('upsertFoo')).toBeInstanceOf(Function);\n      const res = await wrapper.find(TestComponent).prop('upsertFoo')({\n        data: fooUpdate,\n      });\n      expect(res).toEqual({ data: { upsertFoo: { data: foo, __typename: 'Foo' } } });\n    });\n  });\n  describe('delete', () => {\n    test('run delete mutations', async () => {\n      const DeleteComponent = withDelete(defaultOptions)(TestComponent);\n      const responses = [\n        {\n          request: {\n            query: buildDeleteQuery({ typeName, fragment, fragmentName }),\n            operationName: 'deleteFoo',\n            //variables: {\n            //  selector: {\n            //    documentId: '42',\n            //  },\n            //},\n          },\n          result: {\n            data: {\n              deleteFoo: {\n                data: foo,\n                __typename: 'Foo',\n              },\n            },\n          },\n        },\n      ];\n      const wrapper = mount(\n        <MockedProvider /*mocks={responses}*/ link={new OperationNameMockLink(responses, false)}>\n          <DeleteComponent />\n        </MockedProvider>\n      );\n      expect(wrapper.find(TestComponent).prop('deleteFoo')).toBeInstanceOf(Function);\n      const res = await wrapper.find(TestComponent).prop('deleteFoo')({\n        documentId: '42',\n      });\n      expect(res).toEqual({ data: { deleteFoo: { data: foo, __typename: 'Foo' } } });\n    });\n  });\n\n  describe('custom mutation', () => {\n    test('return a component even if fragment is not yet registered', () => {\n      const MutationComponent = withMutation({ name: 'whatever', fragmentName: 'foobar' })(TestComponent);\n      expect(MutationComponent).toBeInstanceOf(Function);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-core/test/containers/queries.test.js",
    "content": "import React from 'react';\nimport expect from 'expect';\nimport { mount } from 'enzyme';\n//import gql from 'graphql-tag';\nimport { initComponentTest } from 'meteor/vulcan:test';\nimport {\n  withSingle,\n  withMulti,\n  withCurrentUser,\n  withSiteData,\n  useCurrentUser,\n  useMulti,\n  useSingle,\n  useSiteData\n} from '../../lib/modules';\nimport {\n  singleQuery\n} from '../../lib/modules/containers/single';\nimport {\n  buildMultiQuery\n} from '../../lib/modules/containers/multi';\n\n\nimport wait from 'waait';\n\nimport { MockedProvider } from 'meteor/vulcan:test';\n\n\nconst test = it;\n\n// we must import all the other components, so that \"registerComponent\" is called\nimport '../../lib/modules';\n// setup Vulcan (load components, initialize fragments)\ninitComponentTest();\n\ndescribe('vulcan:core/queries', function () {\n  // increase timeout\n  this.timeout(5000);\n\n  const typeName = 'Foo';\n  const Foo = {\n    options: {\n      collectionName: 'Foos',\n      typeName,\n      multiResolverName: 'foos'\n    }\n  };\n  const fragmentName = 'FoosDefaultFragment';\n  const fragment = {\n    definitions: [{\n      name: {\n        value: fragmentName\n      }\n    }],\n    toString: () => `fragment FoosDefaultFragment on Foo { \n        id\n        hello\n        __typename\n      }`\n  };\n  const foo = { id: 1, hello: 'world', __typename: 'Foo' };\n  const TestComponent = (props) => {\n    return <div>test</div>;\n  };\n\n  describe('exports', () => {\n    expect(useSingle).toBeDefined();\n    expect(useMulti).toBeDefined();\n    expect(useCurrentUser).toBeDefined();\n    expect(useSiteData).toBeDefined();\n    expect(withSingle).toBeDefined();\n    expect(withMulti).toBeDefined();\n    expect(withCurrentUser).toBeDefined();\n    expect(withSiteData).toBeDefined();\n  });\n\n  describe('withSingle', () => {\n    test('returns a graphql component', () => {\n      const wrapper = withSingle({\n        collection: Foo,\n        fragment\n      });\n      expect(wrapper).toBeDefined();\n      expect(wrapper).toBeInstanceOf(Function);\n    });\n    test('query single document', async () => {\n      const mock = {\n        request: {\n          query: singleQuery({ typeName, fragmentName, fragment }),\n          variables: {\n            // variables must absolutely match with the emitted request,\n            // including undefined values\n            'input': { 'selector': { documentId: undefined, slug: undefined, }, 'enableCache': false }\n          }\n        },\n        result: {\n          data: {\n            foo: { result: foo, __typename: 'Foo' }\n          },\n        },\n      };\n      const mocks = [\n        mock,\n      ]; // need multiple mocks, one per query\n      const SingleComponent = withSingle({\n        collection: Foo,\n        queryOptions: {\n          pollInterval: 0, // disable polling otherwise it will fail (we need 1 mock per request)\n        },\n        fragment\n      })(TestComponent);\n      const wrapper = mount(\n        <MockedProvider removeTypename mocks={mocks} >\n          <SingleComponent />\n        </MockedProvider>\n      );\n\n      const loadingRes = wrapper.find(TestComponent).first();\n      expect(loadingRes.prop('loading')).toBe(true);\n      // @see https://www.apollographql.com/docs/react/recipes/testing/#testing-final-state\n      //await new Promise(resolve => setTimeout(resolve));\n      await wait(0);\n      wrapper.update(); // rerender\n\n      const finalRes = wrapper.find(TestComponent).first();\n      expect(finalRes.prop('loading')).toBe(false);\n      expect(finalRes.prop('data').error).toBeFalsy();\n      expect(finalRes.prop('document')).toEqual(foo);\n    });\n    test('send new request if props are updated', async () => {\n      const query = singleQuery({ typeName, fragmentName, fragment });\n      const firstRequest = {\n        request: {\n          query,\n          variables: {\n            // variables must absolutely match with the emitted request,\n            // including undefined values\n            'input': { 'selector': { documentId: undefined, slug: undefined, }, 'enableCache': false }\n          }\n        },\n        result: {\n          data: {\n            foo: { result: null, __typename: 'Foo' }\n          },\n        },\n      };\n      const documentIdRequest = {\n        request: {\n          query,\n          variables: {\n            input: { selector: { documentId: '42', slug: undefined }, 'enableCache': false }\n          }\n        },\n        result: {\n          data: {\n            foo: { result: foo, __typename: 'Foo' }\n          }\n        }\n      };\n      const mocks = [\n        firstRequest,\n        documentIdRequest\n      ]; // need multiple mocks, one per query\n      const SingleComponent = withSingle({\n        collection: Foo,\n        queryOptions: {\n          pollInterval: 0, // disable polling otherwise it will fail (we need 1 mock per request)\n        },\n        fragment\n      })(TestComponent);\n      const wrapper = mount(\n        <MockedProvider mocks={mocks} >\n          <SingleComponent />\n        </MockedProvider>\n      );\n      // @see https://www.apollographql.com/docs/react/recipes/testing/#testing-final-state\n      //await new Promise(resolve => setTimeout(resolve));\n      await wait(0);\n      wrapper.update(); // rerender\n      const intermediateRes = wrapper.find(TestComponent).first();\n      expect(intermediateRes.prop('loading')).toBe(false);\n      expect(intermediateRes.prop('data').error).toBeFalsy();\n      expect(intermediateRes.prop('document')).toEqual(null);\n      // change props (MockedProvider will pass childProps down)\n      wrapper.setProps({ childProps: { documentId: '42' } });\n      await wait(0);\n      wrapper.update();\n      const finalRes = wrapper.find(TestComponent).first();\n      expect(finalRes.prop('loading')).toBe(false);\n      expect(finalRes.prop('data').error).toBeFalsy();\n      expect(finalRes.prop('document')).toEqual(foo);\n\n    });\n    test('work if fragment is not yet defined', () => {\n      const hoc = withSingle({\n        collection: Foo,\n        fragmentName: 'NotRegisteredYetFragment'\n      });\n      expect(hoc).toBeDefined();\n      expect(hoc).toBeInstanceOf(Function);\n    });\n    test('add extra queries', async () => {\n      const mock = {\n        request: {\n          query: singleQuery({ typeName, fragmentName, fragment, extraQueries: 'extra { foo }' }),\n          variables: {\n            // variables must absolutely match with the emitted request,\n            // including undefined values\n            'input': { 'selector': { documentId: undefined, slug: undefined, }, 'enableCache': false }\n          }\n        },\n        result: {\n          data: {\n            foo: { result: foo, __typename: 'Foo' },\n            extra: { foo: 'bar', __typename: 'Foo' }\n          },\n        },\n      };\n      const mocks = [\n        mock,\n      ]; // need multiple mocks, one per query\n      const SingleComponent = withSingle({\n        collection: Foo,\n        queryOptions: {\n          pollInterval: 0, // disable polling otherwise it will fail (we need 1 mock per request)\n        },\n        fragment,\n        extraQueries: 'extra { foo }'\n      })(TestComponent);\n      const wrapper = mount(\n        <MockedProvider removeTypename mocks={mocks} >\n          <SingleComponent />\n        </MockedProvider>\n      );\n      await wait(0);\n      wrapper.update(); // rerender\n      const finalRes = wrapper.find(TestComponent).first();\n      expect(finalRes.prop('loading')).toBe(false);\n      expect(finalRes.prop('data').error).toBeFalsy();\n      expect(finalRes.prop('document')).toEqual(foo);\n    });\n  });\n\n  describe('withMulti', () => {\n    const defaultQuery = buildMultiQuery({\n      fragment,\n      typeName,\n      fragmentName\n    });\n    const defaultVariables = {\n      'input': {\n        'terms': {\n          'limit': 10,\n          'itemsPerPage': 10\n        },\n        'enableCache': false,\n        'enableTotal': true\n      }\n    };\n    const defaultOptions = {\n      collection: Foo,\n      fragment,\n      queryOptions: {\n        pollInterval: 0,\n        notifyOnNetworkStatusChange: true // necessary for loadMoreInc\n      }\n    };\n    test('returns a graphql component', () => {\n      const wrapper = withMulti(defaultOptions);\n      expect(wrapper).toBeDefined();\n      expect(wrapper).toBeInstanceOf(Function);\n    });\n    test('query multiple documents', async () => {\n\n      const response = {\n        request: {\n          query: defaultQuery,\n          variables: defaultVariables\n        },\n        result: {\n          data: {\n            foos: {\n              results: [foo],\n              totalCount: 10,\n              __typename: '[Foo]'\n            },\n          }\n        }\n      };\n      const mocks = [response];\n      const MultiComponent = withMulti({\n        collection: Foo,\n        fragment,\n        queryOptions: {\n          pollInterval: 0,\n        }\n      })(TestComponent);\n\n      const wrapper = mount(\n        <MockedProvider mocks={mocks}>\n          <MultiComponent\n            terms={{}}\n          />\n        </MockedProvider>\n      );\n      const loadingRes = wrapper.find(TestComponent);\n      expect(loadingRes.prop('loading')).toEqual(true);\n      expect(loadingRes.prop('error')).toBeFalsy();\n      // pass loading\n      await wait(0);\n      wrapper.update();\n      const finalRes = wrapper.find(TestComponent);\n      expect(finalRes.prop('loading')).toEqual(false);\n      expect(finalRes.prop('error')).toBeFalsy();\n      expect(finalRes.prop('results')).toEqual([foo]);\n      expect(finalRes.prop('count')).toEqual(1);\n    });\n\n    test('load more increase the limit', async () => {\n      // @see https://stackoverflow.com/questions/49064334/invoke-a-function-with-enzyme-when-function-is-passed-down-as-prop-react\n      const responses = [\n        // first request\n        {\n          request: {\n            query: defaultQuery,\n            variables: {\n              input: {\n                ...defaultVariables.input,\n                terms: {\n                  limit: 1,\n                  itemsPerPage: 1 // = first limit\n                }\n              }\n            }\n          },\n          result: {\n            data: {\n              foos: {\n                results: [foo],\n                totalCount: 10,\n                __typename: '[Foo]'\n              }\n            }\n          }\n        },\n        // calling loadMore / loadMoreInc will send new requests with updated terms\n        // loadMore\n        {\n          request: {\n            query: defaultQuery,\n            variables: {\n              input: {\n                ...defaultVariables.input,\n                terms: {\n                  limit: 2, // limit is increased by load more\n                  itemsPerPage: 1\n                }\n              }\n            }\n          },\n          result: {\n            data: {\n              foos: {\n                results: [foo, foo, foo],\n                totalCount: 10,\n                __typename: '[Foo]'\n              }\n            }\n          }\n        },\n      ];\n\n      const MultiComponent = withMulti(defaultOptions)(TestComponent);\n\n      const wrapper = mount(\n        <MockedProvider mocks={responses}>\n          <MultiComponent terms={{ limit: 1 }} />\n        </MockedProvider>\n      );\n      // get data\n      await wait(0);\n      wrapper.update();\n\n      // call load more\n      expect(wrapper.find(TestComponent).prop('loadMore')).toBeInstanceOf(Function);\n      wrapper.find(TestComponent).prop('loadMore')();\n      await wait(0);\n      wrapper.update();\n      const loadMoreRes = wrapper.find(TestComponent);\n      expect(loadMoreRes.prop('error')).toBeFalsy();\n      expect(loadMoreRes.prop('results')).toHaveLength(3);\n    });\n\n    // FIXME: sometimes this test does not pass\n    test.skip('loadMoreInc get more data', async () => {\n      // @see https://stackoverflow.com/questions/49064334/invoke-a-function-with-enzyme-when-function-is-passed-down-as-prop-react\n      const responses = [\n        // first request\n        {\n          request: {\n            query: defaultQuery,\n            variables: {\n              input: {\n                ...defaultVariables.input,\n                terms: {\n                  limit: 1,\n                  itemsPerPage: 1 // = first limit\n                }\n              }\n            }\n          },\n          result: {\n            data: {\n              foos: {\n                results: [foo],\n                totalCount: 10,\n                __typename: '[Foo]'\n              }\n            }\n          }\n        },\n        // loadmoreInc\n        {\n          request: {\n            query: defaultQuery,\n            variables: {\n              // get an offset to load only relevant data\n              input: { terms: { limit: 1, itemsPerPage: 1, offset: 1 } }\n            }\n          },\n          result: {\n            data: {\n              foos: {\n                results: [foo],\n                totalCount: 10,\n                __typename: '[Foo]'\n              }\n            }\n          }\n        }\n      ];\n\n      const MultiComponent = withMulti(defaultOptions)(TestComponent);\n\n      const wrapper = mount(\n        <MockedProvider mocks={responses}>\n          <MultiComponent terms={{ limit: 1 }} />\n        </MockedProvider>\n      );\n      // get data\n      await wait(0);\n      wrapper.update();\n      // call load more incremental\n      // TODO: weird behaviour\n      expect(wrapper.find(TestComponent).prop('loadMoreInc')).toBeInstanceOf(Function);\n      wrapper.find(TestComponent).prop('loadMoreInc')();\n      await wait();\n      wrapper.update();\n      // NOTE: this can sometimes fail for no reason... rerun the tests to debug\n      if (Meteor.isServer) {\n        // in the client call is instantaneous... don't know why\n        const loadMoreIncLoading = wrapper.find(TestComponent);\n        expect(loadMoreIncLoading.prop('loadingMore')).toBe(true);\n        await wait();\n        wrapper.update();\n      }\n      const loadMoreIncRes = wrapper.find(TestComponent);\n      expect(loadMoreIncRes.prop('loadingMore')).toEqual(false);\n      expect(loadMoreIncRes.prop('error')).toBeFalsy();\n      expect(loadMoreIncRes.prop('results')).toHaveLength(2);\n\n    });\n    test('work if fragment is not yet defined', () => {\n      const hoc = withMulti({\n        collection: Foo,\n        fragmentName: 'NotRegisteredYetFragment'\n      });\n      expect(hoc).toBeDefined();\n      expect(hoc).toBeInstanceOf(Function);\n    });\n  });\n\n  describe('withCurrentUser', () => {\n    test('return a valid component', () => {\n      const CurrentUserComponent = withCurrentUser(TestComponent);\n      expect(CurrentUserComponent).toBeDefined();\n    });\n  });\n  describe('withSiteData', () => {\n    test('return a valid component', () => {\n      const SiteDataComponent = withSiteData(TestComponent);\n      expect(SiteDataComponent).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-core/test/containers2/mutations.test.js",
    "content": "import React from 'react';\nimport {\n  withCreate2,\n  withUpdate2,\n  withUpsert2,\n  withDelete2,\n  withMutation,\n  useCreate2,\n  useUpdate2,\n  useUpsert2,\n  useDelete2,\n} from '../../lib/modules';\nimport { /* multiQueryUpdater*/ buildCreateQuery } from '../../lib/modules/containers/create2';\nimport { buildUpdateQuery } from '../../lib/modules/containers/update2';\nimport { buildUpsertQuery } from '../../lib/modules/containers/upsert2';\nimport { buildDeleteQuery } from '../../lib/modules/containers/delete2';\nimport { MockedProvider, createDummyCollection } from 'meteor/vulcan:test';\nimport { OperationNameMockLink } from 'operation-name-mock-link';\nimport { mount } from 'enzyme';\nimport expect from 'expect';\n// import gql from 'graphql-tag';\n\nconst test = it;\n\ndescribe('vulcan:core/container/mutations2', () => {\n  const typeName = 'Foo';\n  const Foo = createDummyCollection({\n    options: {\n      collectionName: 'Foos',\n      typeName,\n      multiResolverName: 'foos',\n    },\n    schema: { val: { type: Number, canRead: ['guests'] } },\n  });\n  const fragmentName = 'FoosDefaultFragment';\n  const fragment = {\n    definitions: [\n      {\n        name: {\n          value: fragmentName,\n        },\n      },\n    ],\n    toString: () => `fragment FoosDefaultFragment on Foo { \n        _id\n        hello\n        __typename\n      }`,\n  };\n\n  const rawFoo = { hello: 'world' };\n  const fooUpdate = { _id: 1, hello: 'world' };\n  const foo = { _id: 1, hello: 'world', __typename: 'Foo' };\n  const TestComponent = () => 'test';\n  const defaultOptions = {\n    collection: Foo,\n    fragmentName: fragmentName,\n    fragment,\n  };\n  describe('common', () => {\n    test('export hooks and hocs', () => {\n      expect(useCreate2).toBeInstanceOf(Function);\n      expect(useUpdate2).toBeInstanceOf(Function);\n      expect(useUpsert2).toBeInstanceOf(Function);\n      expect(useDelete2).toBeInstanceOf(Function);\n      expect(withCreate2).toBeInstanceOf(Function);\n      expect(withUpdate2).toBeInstanceOf(Function);\n      expect(withUpsert2).toBeInstanceOf(Function);\n      expect(withDelete2).toBeInstanceOf(Function);\n    });\n    test('pass down props', () => {\n      const CreateComponent = withCreate2(defaultOptions)(TestComponent);\n      const UpdateComponent = withUpdate2(defaultOptions)(TestComponent);\n      const UpsertComponent = withUpsert2(defaultOptions)(TestComponent);\n      const DeleteComponent = withDelete2(defaultOptions)(TestComponent);\n      [CreateComponent, UpdateComponent, UpsertComponent, DeleteComponent].forEach(C => {\n        const wrapper = mount(\n          <MockedProvider mocks={[]}>\n            <C foo=\"bar\" />\n          </MockedProvider>\n        );\n        expect({\n          res: wrapper.find('TestComponent').prop('foo'),\n          C: C.displayName,\n        }).toEqual({ res: 'bar', C: C.displayName });\n      });\n    });\n  });\n\n  describe('create', () => {\n    test('run a create mutation', async () => {\n      const CreateComponent = withCreate2(defaultOptions)(TestComponent);\n      const responses = [\n        {\n          request: {\n            operationName: 'createFoo',\n            query: buildCreateQuery({ fragmentName, fragment, typeName }),\n            // For matching using MockedProvider, we need the exact variables\n            // Instead we use a more flexible mock link based on operation name\n            //   variables: {\n            //     data: rawFoo,\n          },\n          result: {\n            data: {\n              createFoo: {\n                data: foo,\n                __typename: 'Foo',\n              },\n            },\n          },\n        },\n      ];\n      const wrapper = mount(\n        <MockedProvider /*mocks={responses}*/ link={new OperationNameMockLink(responses, false)}>\n          <CreateComponent />\n        </MockedProvider>\n      );\n      // trigger the query\n      expect(wrapper.find(TestComponent).prop('createFoo')).toBeInstanceOf(Function);\n      const res = await wrapper.find(TestComponent).prop('createFoo')({\n        data: rawFoo,\n      });\n      expect(res).toMatchObject({ data: { createFoo: { data: foo, __typename: 'Foo' } } });\n    });\n  });\n\n  describe('update', () => {\n    test('run update mutation', async () => {\n      const UpdateComponent = withUpdate2(defaultOptions)(TestComponent);\n      const responses = [\n        {\n          request: {\n            query: buildUpdateQuery({ typeName, fragmentName, fragment }),\n            operationName: 'updateFoo',\n            //variables: {\n            //  //selector: { documentId: foo._id },\n            //  data: fooUpdate,\n            //  input: {},\n            //},\n          },\n          result: {\n            data: {\n              updateFoo: {\n                data: foo,\n                __typename: 'Foo',\n              },\n            },\n          },\n        },\n      ];\n      const wrapper = mount(\n        <MockedProvider /*mocks={responses}*/ link={new OperationNameMockLink(responses, false)}>\n          <UpdateComponent />\n        </MockedProvider>\n      );\n      expect(wrapper.find(TestComponent).prop('updateFoo')).toBeInstanceOf(Function);\n      const res = await wrapper.find(TestComponent).prop('updateFoo')({\n        data: fooUpdate,\n      });\n      expect(res).toEqual({ data: { updateFoo: { data: foo, __typename: 'Foo' } } });\n    });\n  });\n  describe('upsert', () => {\n    test('run upsert mutation', async () => {\n      const UpsertComponent = withUpsert2(defaultOptions)(TestComponent);\n      const responses = [\n        {\n          request: {\n            query: buildUpsertQuery({ typeName, fragmentName, fragment }),\n            operationName: 'upsertFoo',\n            // variables: {\n            //   data: fooUpdate,\n            //   input: {},\n            // },\n          },\n          result: {\n            data: {\n              upsertFoo: {\n                data: foo,\n                __typename: 'Foo',\n              },\n            },\n          },\n        },\n      ];\n      const wrapper = mount(\n        <MockedProvider /*mocks={responses}*/ link={new OperationNameMockLink(responses, false)}>\n          <UpsertComponent />\n        </MockedProvider>\n      );\n      expect(wrapper.find(TestComponent).prop('upsertFoo')).toBeInstanceOf(Function);\n      const res = await wrapper.find(TestComponent).prop('upsertFoo')({\n        data: fooUpdate,\n      });\n      expect(res).toEqual({ data: { upsertFoo: { data: foo, __typename: 'Foo' } } });\n    });\n  });\n  describe('delete', () => {\n    test('run delete mutations', async () => {\n      const DeleteComponent = withDelete2(defaultOptions)(TestComponent);\n      const responses = [\n        {\n          request: {\n            operationName: 'deleteFoo',\n            query: buildDeleteQuery({ typeName, fragment, fragmentName }),\n            // variables: {\n            //   input: {\n            //     _id: '42',\n            //   },\n            // },\n          },\n          result: {\n            data: {\n              deleteFoo: {\n                data: foo,\n                __typename: 'Foo',\n              },\n            },\n          },\n        },\n      ];\n      const wrapper = mount(\n        <MockedProvider /*mocks={responses}*/ link={new OperationNameMockLink(responses, false)}>\n          <DeleteComponent />\n        </MockedProvider>\n      );\n      expect(wrapper.find(TestComponent).prop('deleteFoo')).toBeInstanceOf(Function);\n      const res = await wrapper.find(TestComponent).prop('deleteFoo')({\n        input: {\n          _id: '42',\n        },\n      });\n      expect(res).toEqual({ data: { deleteFoo: { data: foo, __typename: 'Foo' } } });\n    });\n  });\n\n  describe('custom mutation', () => {\n    test('return a component even if fragment is not yet registered', () => {\n      const MutationComponent = withMutation({ name: 'whatever', fragmentName: 'foobar' })(TestComponent);\n      expect(MutationComponent).toBeInstanceOf(Function);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-core/test/containers2/queries.test.js",
    "content": "import React from 'react';\nimport expect from 'expect';\nimport { mount } from 'enzyme';\n//import gql from 'graphql-tag';\nimport { initComponentTest } from 'meteor/vulcan:test';\nimport {\n  withSingle,\n  withMulti,\n  withCurrentUser,\n  withSiteData,\n  useCurrentUser,\n  useMulti,\n  useSingle,\n  useSiteData\n} from '../../lib/modules';\nimport {\n  singleQuery\n} from '../../lib/modules/containers/single';\nimport {\n  buildMultiQuery\n} from '../../lib/modules/containers/multi';\n\n\nimport wait from 'waait';\n\nimport { MockedProvider } from 'meteor/vulcan:test';\n\n\nconst test = it;\n\n// we must import all the other components, so that \"registerComponent\" is called\nimport '../../lib/modules';\n// setup Vulcan (load components, initialize fragments)\ninitComponentTest();\n\ndescribe('vulcan:core/queries2', function () {\n  // increase timeout\n  this.timeout(5000);\n\n  const typeName = 'Foo';\n  const Foo = {\n    options: {\n      collectionName: 'Foos',\n      typeName,\n      multiResolverName: 'foos'\n    }\n  };\n  const fragmentName = 'FoosDefaultFragment';\n  const fragment = {\n    definitions: [{\n      name: {\n        value: fragmentName\n      }\n    }],\n    toString: () => `fragment FoosDefaultFragment on Foo { \n        id\n        hello\n        __typename\n      }`\n  };\n  const foo = { id: 1, hello: 'world', __typename: 'Foo' };\n  const TestComponent = (props) => {\n    return <div>test</div>;\n  };\n\n  describe('exports', () => {\n    expect(useSingle).toBeDefined();\n    expect(useMulti).toBeDefined();\n    expect(useCurrentUser).toBeDefined();\n    expect(useSiteData).toBeDefined();\n    expect(withSingle).toBeDefined();\n    expect(withMulti).toBeDefined();\n    expect(withCurrentUser).toBeDefined();\n    expect(withSiteData).toBeDefined();\n  });\n\n  describe('withSingle', () => {\n    test('returns a graphql component', () => {\n      const wrapper = withSingle({\n        collection: Foo,\n        fragment\n      });\n      expect(wrapper).toBeDefined();\n      expect(wrapper).toBeInstanceOf(Function);\n    });\n    test('query single document', async () => {\n      const mock = {\n        request: {\n          query: singleQuery({ typeName, fragmentName, fragment }),\n          variables: {\n            // variables must absolutely match with the emitted request,\n            // including undefined values\n            'input': { 'selector': { documentId: undefined, slug: undefined, }, 'enableCache': false }\n          }\n        },\n        result: {\n          data: {\n            foo: { result: foo, __typename: 'Foo' }\n          },\n        },\n      };\n      const mocks = [\n        mock,\n      ]; // need multiple mocks, one per query\n      const SingleComponent = withSingle({\n        collection: Foo,\n        queryOptions: {\n          pollInterval: 0, // disable polling otherwise it will fail (we need 1 mock per request)\n        },\n        fragment\n      })(TestComponent);\n      const wrapper = mount(\n        <MockedProvider removeTypename mocks={mocks} >\n          <SingleComponent />\n        </MockedProvider>\n      );\n\n      const loadingRes = wrapper.find(TestComponent).first();\n      expect(loadingRes.prop('loading')).toBe(true);\n      // @see https://www.apollographql.com/docs/react/recipes/testing/#testing-final-state\n      //await new Promise(resolve => setTimeout(resolve));\n      await wait(0);\n      wrapper.update(); // rerender\n\n      const finalRes = wrapper.find(TestComponent).first();\n      expect(finalRes.prop('loading')).toBe(false);\n      expect(finalRes.prop('data').error).toBeFalsy();\n      expect(finalRes.prop('document')).toEqual(foo);\n    });\n    test('send new request if props are updated', async () => {\n      const query = singleQuery({ typeName, fragmentName, fragment });\n      const firstRequest = {\n        request: {\n          query,\n          variables: {\n            // variables must absolutely match with the emitted request,\n            // including undefined values\n            'input': { 'selector': { documentId: undefined, slug: undefined, }, 'enableCache': false }\n          }\n        },\n        result: {\n          data: {\n            foo: { result: null, __typename: 'Foo' }\n          },\n        },\n      };\n      const documentIdRequest = {\n        request: {\n          query,\n          variables: {\n            input: { selector: { documentId: '42', slug: undefined }, 'enableCache': false }\n          }\n        },\n        result: {\n          data: {\n            foo: { result: foo, __typename: 'Foo' }\n          }\n        }\n      };\n      const mocks = [\n        firstRequest,\n        documentIdRequest\n      ]; // need multiple mocks, one per query\n      const SingleComponent = withSingle({\n        collection: Foo,\n        queryOptions: {\n          pollInterval: 0, // disable polling otherwise it will fail (we need 1 mock per request)\n        },\n        fragment\n      })(TestComponent);\n      const wrapper = mount(\n        <MockedProvider mocks={mocks} >\n          <SingleComponent />\n        </MockedProvider>\n      );\n      // @see https://www.apollographql.com/docs/react/recipes/testing/#testing-final-state\n      //await new Promise(resolve => setTimeout(resolve));\n      await wait(0);\n      wrapper.update(); // rerender\n      const intermediateRes = wrapper.find(TestComponent).first();\n      expect(intermediateRes.prop('loading')).toBe(false);\n      expect(intermediateRes.prop('data').error).toBeFalsy();\n      expect(intermediateRes.prop('document')).toEqual(null);\n      // change props (MockedProvider will pass childProps down)\n      wrapper.setProps({ childProps: { documentId: '42' } });\n      await wait(0);\n      wrapper.update();\n      const finalRes = wrapper.find(TestComponent).first();\n      expect(finalRes.prop('loading')).toBe(false);\n      expect(finalRes.prop('data').error).toBeFalsy();\n      expect(finalRes.prop('document')).toEqual(foo);\n\n    });\n    test('work if fragment is not yet defined', () => {\n      const hoc = withSingle({\n        collection: Foo,\n        fragmentName: 'NotRegisteredYetFragment'\n      });\n      expect(hoc).toBeDefined();\n      expect(hoc).toBeInstanceOf(Function);\n    });\n    test('add extra queries', async () => {\n      const mock = {\n        request: {\n          query: singleQuery({ typeName, fragmentName, fragment, extraQueries: 'extra { foo }' }),\n          variables: {\n            // variables must absolutely match with the emitted request,\n            // including undefined values\n            'input': { 'selector': { documentId: undefined, slug: undefined, }, 'enableCache': false }\n          }\n        },\n        result: {\n          data: {\n            foo: { result: foo, __typename: 'Foo' },\n            extra: { foo: 'bar', __typename: 'Foo' }\n          },\n        },\n      };\n      const mocks = [\n        mock,\n      ]; // need multiple mocks, one per query\n      const SingleComponent = withSingle({\n        collection: Foo,\n        queryOptions: {\n          pollInterval: 0, // disable polling otherwise it will fail (we need 1 mock per request)\n        },\n        fragment,\n        extraQueries: 'extra { foo }'\n      })(TestComponent);\n      const wrapper = mount(\n        <MockedProvider removeTypename mocks={mocks} >\n          <SingleComponent />\n        </MockedProvider>\n      );\n      await wait(0);\n      wrapper.update(); // rerender\n      const finalRes = wrapper.find(TestComponent).first();\n      expect(finalRes.prop('loading')).toBe(false);\n      expect(finalRes.prop('data').error).toBeFalsy();\n      expect(finalRes.prop('document')).toEqual(foo);\n    });\n  });\n\n  describe('withMulti', () => {\n    const defaultQuery = buildMultiQuery({\n      fragment,\n      typeName,\n      fragmentName\n    });\n    const defaultVariables = {\n      'input': {\n        'terms': {\n          'limit': 10,\n          'itemsPerPage': 10\n        },\n        'enableCache': false,\n        'enableTotal': true\n      }\n    };\n    const defaultOptions = {\n      collection: Foo,\n      fragment,\n      queryOptions: {\n        pollInterval: 0,\n        notifyOnNetworkStatusChange: true // necessary for loadMoreInc\n      }\n    };\n    test('returns a graphql component', () => {\n      const wrapper = withMulti(defaultOptions);\n      expect(wrapper).toBeDefined();\n      expect(wrapper).toBeInstanceOf(Function);\n    });\n    test('query multiple documents', async () => {\n\n      const response = {\n        request: {\n          query: defaultQuery,\n          variables: defaultVariables\n        },\n        result: {\n          data: {\n            foos: {\n              results: [foo],\n              totalCount: 10,\n              __typename: '[Foo]'\n            },\n          }\n        }\n      };\n      const mocks = [response];\n      const MultiComponent = withMulti({\n        collection: Foo,\n        fragment,\n        queryOptions: {\n          pollInterval: 0,\n        }\n      })(TestComponent);\n\n      const wrapper = mount(\n        <MockedProvider mocks={mocks}>\n          <MultiComponent\n            terms={{}}\n          />\n        </MockedProvider>\n      );\n      const loadingRes = wrapper.find(TestComponent);\n      expect(loadingRes.prop('loading')).toEqual(true);\n      expect(loadingRes.prop('error')).toBeFalsy();\n      // pass loading\n      await wait(0);\n      wrapper.update();\n      const finalRes = wrapper.find(TestComponent);\n      expect(finalRes.prop('loading')).toEqual(false);\n      expect(finalRes.prop('error')).toBeFalsy();\n      expect(finalRes.prop('results')).toEqual([foo]);\n      expect(finalRes.prop('count')).toEqual(1);\n    });\n\n    test('load more increase the limit', async () => {\n      // @see https://stackoverflow.com/questions/49064334/invoke-a-function-with-enzyme-when-function-is-passed-down-as-prop-react\n      const responses = [\n        // first request\n        {\n          request: {\n            query: defaultQuery,\n            variables: {\n              input: {\n                ...defaultVariables.input,\n                terms: {\n                  limit: 1,\n                  itemsPerPage: 1 // = first limit\n                }\n              }\n            }\n          },\n          result: {\n            data: {\n              foos: {\n                results: [foo],\n                totalCount: 10,\n                __typename: '[Foo]'\n              }\n            }\n          }\n        },\n        // calling loadMore / loadMoreInc will send new requests with updated terms\n        // loadMore\n        {\n          request: {\n            query: defaultQuery,\n            variables: {\n              input: {\n                ...defaultVariables.input,\n                terms: {\n                  limit: 2, // limit is increased by load more\n                  itemsPerPage: 1\n                }\n              }\n            }\n          },\n          result: {\n            data: {\n              foos: {\n                results: [foo, foo, foo],\n                totalCount: 10,\n                __typename: '[Foo]'\n              }\n            }\n          }\n        },\n      ];\n\n      const MultiComponent = withMulti(defaultOptions)(TestComponent);\n\n      const wrapper = mount(\n        <MockedProvider mocks={responses}>\n          <MultiComponent terms={{ limit: 1 }} />\n        </MockedProvider>\n      );\n      // get data\n      await wait(0);\n      wrapper.update();\n\n      // call load more\n      expect(wrapper.find(TestComponent).prop('loadMore')).toBeInstanceOf(Function);\n      wrapper.find(TestComponent).prop('loadMore')();\n      await wait(0);\n      wrapper.update();\n      const loadMoreRes = wrapper.find(TestComponent);\n      expect(loadMoreRes.prop('error')).toBeFalsy();\n      expect(loadMoreRes.prop('results')).toHaveLength(3);\n    });\n\n    // FIXME: sometimes this test does not pass\n    test.skip('loadMoreInc get more data', async () => {\n      // @see https://stackoverflow.com/questions/49064334/invoke-a-function-with-enzyme-when-function-is-passed-down-as-prop-react\n      const responses = [\n        // first request\n        {\n          request: {\n            query: defaultQuery,\n            variables: {\n              input: {\n                ...defaultVariables.input,\n                terms: {\n                  limit: 1,\n                  itemsPerPage: 1 // = first limit\n                }\n              }\n            }\n          },\n          result: {\n            data: {\n              foos: {\n                results: [foo],\n                totalCount: 10,\n                __typename: '[Foo]'\n              }\n            }\n          }\n        },\n        // loadmoreInc\n        {\n          request: {\n            query: defaultQuery,\n            variables: {\n              // get an offset to load only relevant data\n              input: { terms: { limit: 1, itemsPerPage: 1, offset: 1 } }\n            }\n          },\n          result: {\n            data: {\n              foos: {\n                results: [foo],\n                totalCount: 10,\n                __typename: '[Foo]'\n              }\n            }\n          }\n        }\n      ];\n\n      const MultiComponent = withMulti(defaultOptions)(TestComponent);\n\n      const wrapper = mount(\n        <MockedProvider mocks={responses}>\n          <MultiComponent terms={{ limit: 1 }} />\n        </MockedProvider>\n      );\n      // get data\n      await wait(0);\n      wrapper.update();\n      // call load more incremental\n      // TODO: weird behaviour\n      expect(wrapper.find(TestComponent).prop('loadMoreInc')).toBeInstanceOf(Function);\n      wrapper.find(TestComponent).prop('loadMoreInc')();\n      await wait();\n      wrapper.update();\n      // NOTE: this can sometimes fail for no reason... rerun the tests to debug\n      if (Meteor.isServer) {\n        // in the client call is instantaneous... don't know why\n        const loadMoreIncLoading = wrapper.find(TestComponent);\n        expect(loadMoreIncLoading.prop('loadingMore')).toBe(true);\n        await wait();\n        wrapper.update();\n      }\n      const loadMoreIncRes = wrapper.find(TestComponent);\n      expect(loadMoreIncRes.prop('loadingMore')).toEqual(false);\n      expect(loadMoreIncRes.prop('error')).toBeFalsy();\n      expect(loadMoreIncRes.prop('results')).toHaveLength(2);\n\n    });\n    test('work if fragment is not yet defined', () => {\n      const hoc = withMulti({\n        collection: Foo,\n        fragmentName: 'NotRegisteredYetFragment'\n      });\n      expect(hoc).toBeDefined();\n      expect(hoc).toBeInstanceOf(Function);\n    });\n  });\n\n  describe('withCurrentUser', () => {\n    test('return a valid component', () => {\n      const CurrentUserComponent = withCurrentUser(TestComponent);\n      expect(CurrentUserComponent).toBeDefined();\n    });\n  });\n  describe('withSiteData', () => {\n    test('return a valid component', () => {\n      const SiteDataComponent = withSiteData(TestComponent);\n      expect(SiteDataComponent).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-core/test/index.js",
    "content": "import './components.test';\nimport './containers/queries.test';\nimport './containers/mutations.test';\nimport './containers2/queries.test';\nimport './containers2/mutations.test';\nimport './withComponents.test';\nimport './menu.test';\n"
  },
  {
    "path": "packages/vulcan-core/test/menu.test.js",
    "content": "import expect from 'expect'\nimport { addMenuItem, getMenuItems, getAuthorizedMenuItems, resetMenus } from '../lib/modules/menu'\ndescribe('vulcan:lib/menu', () => {\n    beforeEach(resetMenus)\n\n    it('add a memu item in the default group', () => {\n        const menuItem = {\n            name: 'home',\n            label: 'Home',\n            path: '/home',\n            groups: ['guests']\n\n        }\n        addMenuItem(menuItem)\n        const defaultItems = getMenuItems()\n        expect(defaultItems).toHaveLength(1)\n        expect(defaultItems[0]).toMatchObject(menuItem)\n\n    })\n    it('add a menu item in a specific group', () => {\n        const menuItem = {\n            name: 'home',\n            label: 'Home',\n            path: '/home',\n            groups: ['admin'],\n            menuGroup: 'admin'\n        }\n        addMenuItem(menuItem)\n        const defaultItems = getMenuItems()\n        const adminItems = getMenuItems('admin')\n        expect(defaultItems).toHaveLength(0)\n        expect(adminItems[0]).toMatchObject(menuItem)\n    })\n    it('filter out non authorized menu items', () => {\n        const allowedMenuItem = {\n            name: 'home',\n            label: 'Home',\n            path: '/home',\n            // no groups is equivalent to guests\n        }\n        const nonAllowedMenuItem = {\n            name: 'private',\n            label: 'private',\n            path: '/private',\n            groups: ['members'],\n        }\n        addMenuItem(allowedMenuItem)\n        addMenuItem(nonAllowedMenuItem)\n        const defaultItems = getAuthorizedMenuItems(null)\n        expect(defaultItems).toHaveLength(1)\n        expect(defaultItems[0]).toMatchObject(allowedMenuItem)\n\n    })\n})"
  },
  {
    "path": "packages/vulcan-core/test/server/index.js",
    "content": "import '../index';"
  },
  {
    "path": "packages/vulcan-core/test/withComponents.test.js",
    "content": "import React from 'react';\nimport expect from 'expect';\nimport { shallow, mount } from 'enzyme';\nimport { Components } from 'meteor/vulcan:core';\nimport { initComponentTest } from 'meteor/vulcan:test';\nimport {\n    withComponents,\n} from '../lib/modules';\n\n\nconst test = it;\n\n// we must import all the other components, so that \"registerComponent\" is called\nimport '../lib/modules';\n// setup Vulcan (load components, initialize fragments)\ninitComponentTest();\n\ndescribe('vulcan:core/containers/withComponents', function () {\n    it('should override components', function () {\n        // replace any component for testing purpose\n        const firstComponentName = Components[Object.keys(Components)[0]];\n        const FooComponent = () => 'FOO';\n        const components = { [firstComponentName]: FooComponent };\n        const MyComponent = withComponents(({ Components }) => Components[firstComponentName]());\n        const wrapper = shallow(<MyComponent components={components} />);\n        expect(wrapper.prop('Components')).toBeDefined();\n        expect(wrapper.prop('Components')[firstComponentName]).toEqual(FooComponent);\n        expect(wrapper.html()).toEqual('FOO');\n    });\n});\n"
  },
  {
    "path": "packages/vulcan-debug/README.md",
    "content": "Vulcan debug package. "
  },
  {
    "path": "packages/vulcan-debug/lib/client/main.js",
    "content": "export * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-debug/lib/components/Callbacks.jsx",
    "content": "import React from 'react';\nimport { registerComponent, Components } from 'meteor/vulcan:lib';\nimport Callbacks from '../modules/callbacks/collection.js';\n\nconst CallbacksName = ({ document }) => \n  <strong>{document.name}</strong>;\n\nconst CallbacksDashboard = props => \n  <div className=\"settings\">\n    <Components.Datatable\n      showSearch={false}\n      showEdit={false}\n      collection={Callbacks} \n      options={{\n        fragmentName: 'CallbacksFragment'        \n      }}\n      columns={[\n        { name: 'name', component: CallbacksName }, \n        'iterator', \n        'properties', \n        'runs', \n        'description',\n        'hooks',\n      ]}\n    />\n  </div>;\n\nregisterComponent('Callbacks', CallbacksDashboard);\n\nexport default Callbacks;"
  },
  {
    "path": "packages/vulcan-debug/lib/components/Components.jsx",
    "content": "import React from 'react';\nimport {\n  registerComponent,\n  Components,\n  ComponentsTable\n} from 'meteor/vulcan:lib';\n\nconst ComponentHOCs = ({ document }) => (\n  <div>\n    <ul>\n      {document.hocs.map((hoc, i) => (\n        <li key={i}>{typeof hoc.name === 'string' ? hoc.name : hoc[0].name}</li>\n      ))}\n    </ul>\n  </div>\n);\n\nconst ComponentsDashboard = props => (\n  <div className=\"components\">\n    <Components.Datatable\n      showSearch={false}\n      showNew={false}\n      showEdit={false}\n      data={Object.values(ComponentsTable)}\n      columns={[\n        'name',\n        {\n          name: 'hocs',\n          component: ComponentHOCs\n        }\n      ]}\n    />\n  </div>\n);\n\nregisterComponent('Components', ComponentsDashboard);\n"
  },
  {
    "path": "packages/vulcan-debug/lib/components/Dashboard.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:lib';\nimport { Link } from 'react-router-dom';\n\nfunction Dashboard() {\n  return (\n    <div>\n      <h3>Debug Dashboard</h3>\n      <ul>\n        <li key=\"Callbacks\">\n          <Link to=\"/debug/callbacks\">Callbacks</Link>\n        </li>\n        <li key=\"Components\">\n          <Link to=\"/debug/components\">Components</Link>\n        </li>\n        <li key=\"Emails\">\n          <Link to=\"/debug/emails\">Emails</Link>\n        </li>\n        <li key=\"Groups\">\n          <Link to=\"/debug/groups\">Groups</Link>\n        </li>\n        <li key=\"I18n\">\n          <Link to=\"/debug/i18n\">I18n</Link>\n        </li>\n        <li key=\"Routes\">\n          <Link to=\"/debug/routes\">Routes</Link>\n        </li>\n        <li key=\"Settings\">\n          <Link to=\"/debug/settings\">Settings</Link>\n        </li>\n      </ul>\n    </div>\n  );\n}\n\nregisterComponent({ name: 'DebugDashboard', component: Dashboard, hocs: [] });\n"
  },
  {
    "path": "packages/vulcan-debug/lib/components/Database.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:lib';\nimport React, { useState } from 'react';\nimport { useQuery } from '@apollo/client';\nimport get from 'lodash/get';\nimport gql from 'graphql-tag';\n\nconst databaseObjectQuery = `\nquery DatabaseObjectQuery($id: String){\n  getDatabaseObject(id: $id)\n}\n`;\n\nconst DebugDatabase = () => {\n  const [id, setId] = useState('');\n  const { loading, data = {} } = useQuery(gql(databaseObjectQuery), { variables: { id } });\n\n  const inputProperties = {\n    value: id,\n    placeholder: 'Enter a document _id…',\n    onChange: event => {\n      setId(event.target.value);\n    },\n  };\n\n  return (\n    <div className=\"debug-database\">\n      <h1>Database</h1>\n      <Components.FormComponentText inputProperties={inputProperties} />\n      {loading ? (\n        <Components.Loading />\n      ) : (\n        <div>\n          <h3>{get(data, 'getDatabaseObject.collectionName')}</h3>\n          <pre>\n            <code>{JSON.stringify(get(data, 'getDatabaseObject.document'), '', 2)}</code>\n          </pre>\n        </div>\n      )}\n    </div>\n  );\n};\n\nregisterComponent('DebugDatabase', DebugDatabase);\n\nexport default DebugDatabase;\n"
  },
  {
    "path": "packages/vulcan-debug/lib/components/DebugLayout.jsx",
    "content": "import React from 'react';\nimport { Components, registerComponent } from 'meteor/vulcan:lib';\n\nconst debugStyles = {\n  padding: '20px'\n};\n\nconst DebugLayout = props => <div className=\"debug-layout\" style={debugStyles}>{props.children}</div>;\n\nregisterComponent('DebugLayout', DebugLayout);\n"
  },
  {
    "path": "packages/vulcan-debug/lib/components/Emails.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport Emails from '../modules/emails/collection.js';\nimport get from 'lodash/get';\n\nconst Template = ({ document: email }) => (\n  <a href={'/email/template/' + email.template} target=\"_blank\" rel=\"noopener noreferrer\">\n    {email.template}\n  </a>\n);\n\nconst HTMLPreview = ({ document: email }) => (\n  <a href={email.testPath && email.testPath.replace(':_id?', '').replace(':documentId?', '')} target=\"_blank\" rel=\"noopener noreferrer\">\n    {email.testPath}\n  </a>\n);\n\nconst Test = ({ document: email }) => (\n  <Components.MutationButton\n    label=\"Send Test\"\n    variant=\"primary\"\n    mutationOptions={{\n      name: 'testEmail',\n      args: { emailName: 'String' },\n      fragmentName: 'EmailFragment',\n    }}\n    mutationArguments={{ emailName: email.name }}\n    successCallback={result => {\n      const email = get(result, 'data.testEmail');\n      const { to, subject } = email;\n      alert(`Email “${subject}” sent to ${to}`);\n    }}\n  />\n);\n\nconst EmailsDashboard = () => {\n  return (\n    <div className=\"emails\">\n      <h2 className=\"dashboard-heading dashboard-heading-emails\">Emails</h2>\n\n      <Components.Datatable\n        collection={Emails}\n        columns={[\n          'name',\n          { name: 'template', component: Template },\n          { name: 'subject' },\n          {\n            label: 'HTML Preview',\n            component: HTMLPreview,\n          },\n          {\n            label: 'Send Test',\n            component: Test,\n          },\n        ]}\n        showEdit={false}\n        showNew={false}\n        showSearch={false}\n      />\n      {/* <div className=\"emails-wrapper\">\n\n        <table className=\"table\">\n          <thead>\n            <tr>\n              <td>Name</td>\n              <td>Template</td>\n              <td>Subject</td>\n              <td>HTML Preview</td>\n              <td>Send Test</td>\n            </tr>\n          </thead>\n          <tbody>\n            {_.map(emails, (email, key) => <Email key={key} email={email} name={key}/>)}\n          </tbody>\n        </table>\n\n      </div> */}\n    </div>\n  );\n};\n\nregisterComponent('Emails', EmailsDashboard);\n\nexport default EmailsDashboard;\n"
  },
  {
    "path": "packages/vulcan-debug/lib/components/ErrorCatcherContents.jsx",
    "content": "import React from 'react';\nimport { Components, replaceComponent } from 'meteor/vulcan:lib';\n\nconst ErrorCatcherContents = ({ error, message }) => (\n  <div className=\"error-catcher\">\n    <Components.Flash message={{ message, properties: { error } }} />\n    <div className=\"error-catcher-help\">\n      <p>Here are some suggestions to help you fix this issue:</p>\n      <ol>\n        <li>\n          Open your browser devtools <strong>Console</strong> tab to inspect the full error\n        </li>\n        <li>\n          If this seems like a GraphQL-related issue, you can inspect the GraphQL request in your browser devtools <strong>Network</strong>{' '}\n          tab to see what exactly is being sent to the server. You can then paste the query or mutation into{' '}\n          <a href=\"/graphiql\" target=\"_blank\" rel=\"noopener noreferrer\">\n            GraphiQL\n          </a>{' '}\n          to debug it.\n        </li>\n      </ol>\n      <p>Note: these instructions will only appear during local development.</p>\n    </div>\n  </div>\n);\n\nreplaceComponent('ErrorCatcherContents', ErrorCatcherContents);\n"
  },
  {
    "path": "packages/vulcan-debug/lib/components/Groups.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:lib';\nimport Users from 'meteor/vulcan:users';\n\nconst Group = ({name, actions}) => {\n  return (\n    <tr>\n      <td>{name}</td>\n      <td><ul>{actions.map((action, index) => <li key={index}><code>{action}</code></li>)}</ul></td>\n    </tr>\n  );\n};\n\nconst Groups = props => {\n  return (\n    <div className=\"groups\">\n      <h1>Groups</h1>\n\n      <div className=\"groups-wrapper\">\n\n        <table className=\"table\">\n          <thead>\n            <tr>\n              <td><strong>Name</strong></td>\n              <td><strong>Actions</strong></td>\n            </tr>\n          </thead>\n          <tbody>\n            {_.map(Users.groups, (group, key) => <Group key={key} name={key} actions={group.actions} />)}\n          </tbody>\n        </table>\n\n      </div>\n\n    </div>\n  );\n};\n\nregisterComponent('Groups', Groups);\n\nexport default Groups;"
  },
  {
    "path": "packages/vulcan-debug/lib/components/I18n.jsx",
    "content": "import React from 'react';\nimport { registerComponent, Components, Strings, Locales } from 'meteor/vulcan:lib';\nimport PropTypes from 'prop-types';\nimport sortedUniq from 'lodash/sortedUniq';\n\n/**\n * Internationalization debugging page\n * Note: for non-dynamically-loaded locales only\n *\n **/\nfunction LocaleSwitcher(props, context) {\n  return (\n    <div>\n      <span>Switch locales :</span>\n      {Locales.map(localeObj => (\n        <Components.Button key={localeObj.id} onClick={() => context.setLocale(localeObj.id)}>\n          {localeObj.label}\n        </Components.Button>\n      ))}\n    </div>\n  );\n}\nLocaleSwitcher.contextTypes = {\n  getLocale: PropTypes.func,\n  setLocale: PropTypes.func,\n};\n\nexport const I18n = (props, context) => {\n  // translations holds all the translations ids\n  let translations = [];\n  let columns = [\n    {\n      name: 'id',\n      component: function({ document }) {\n        return document;\n      },\n    },\n  ];\n\n  // reunite all the ids in a single array (translations) and create the columns for each language\n  Object.keys(Strings).forEach(language => {\n    translations.push(...Object.keys(Strings[language]));\n    columns.push({\n      name: language,\n      component: function({ document }) {\n        return Strings[language][document] || null;\n      },\n    });\n  });\n\n  //sort the array\n  translations.sort();\n  //remove duplicates\n  let translationsUniq = sortedUniq(translations);\n\n  return (\n    <div>\n      <h3>{'Your current locale: ' + context.getLocale()}</h3>\n      <LocaleSwitcher />\n      <Components.Datatable showSearch={false} showNew={false} showEdit={false} data={translationsUniq} columns={columns} />\n    </div>\n  );\n};\n\nI18n.contextTypes = {\n  getLocale: PropTypes.func,\n  setLocale: PropTypes.func,\n};\n\nregisterComponent({ name: 'I18n', component: I18n, hocs: [] });\n"
  },
  {
    "path": "packages/vulcan-debug/lib/components/Routes.jsx",
    "content": "import React from 'react';\nimport { registerComponent, Components, Routes } from 'meteor/vulcan:lib';\nimport { Link } from 'react-router-dom';\n\nconst RoutePath = ({document}) => (\n  <Link to={document.path}>{document.path}</Link>\n);\n\nconst RoutesDashboard = props => {\n  return (\n    <div className=\"routes\">\n      <Components.Datatable\n        showSearch={false}\n        showNew={false}\n        showEdit={false}\n        data={Object.values(Routes)}\n        columns={[\n          'name',\n          {\n            name: 'path',\n            component: RoutePath,\n          },\n          'componentName',\n          'layoutName'\n        ]}\n      />\n    </div>\n  );\n};\n\nregisterComponent('Routes', RoutesDashboard);\n"
  },
  {
    "path": "packages/vulcan-debug/lib/components/Settings.jsx",
    "content": "import React from 'react';\nimport { registerComponent, Components } from 'meteor/vulcan:lib';\nimport Settings from '../modules/settings/collection.js';\n\nconst ObjectAsStr = ({ document, column: { name } }) => {\n  const value = document[name];\n  return typeof value === 'string' ? value : JSON.stringify(value);\n};\nconst SettingName = ({ document }) => <strong>{document.name}</strong>;\n\nconst SettingsDashboard = props => (\n  <div className=\"settings\">\n    <Components.Datatable\n      showSearch={false}\n      showEdit={false}\n      collection={Settings}\n      columns={[\n        { name: 'name', component: SettingName },\n        { name: 'value', component: ObjectAsStr },\n        { name: 'defaultValue', component: ObjectAsStr },\n        'isPublic',\n        'description',\n        'serverOnly',\n      ]}\n    />\n  </div>\n);\n\nregisterComponent('Settings', SettingsDashboard);\n\nexport default Settings;\n"
  },
  {
    "path": "packages/vulcan-debug/lib/modules/callbacks/collection.js",
    "content": "import { createCollection } from 'meteor/vulcan:lib';\nimport schema from './schema.js';\nimport './fragments.js';\n\nconst Callbacks = createCollection({\n\n  collectionName: 'Callbacks',\n\n  typeName: 'Callback',\n\n  schema,\n  \n  resolvers: null,\n\n  mutations: null,\n\n});\n\n\nexport default Callbacks;\n"
  },
  {
    "path": "packages/vulcan-debug/lib/modules/callbacks/fragments.js",
    "content": "import { registerFragment } from 'meteor/vulcan:lib';\n\nregisterFragment(`\n  fragment CallbacksFragment on Callback {\n    name\n    iterator\n    properties\n    runs\n    returns\n    description\n    hooks\n  }\n`);\n"
  },
  {
    "path": "packages/vulcan-debug/lib/modules/callbacks/schema.js",
    "content": "import { Callbacks } from 'meteor/vulcan:lib';\nimport { readPermissions } from \"../permissions\";\n\nconst schema = {\n  name: {\n    type: String,\n    canRead: readPermissions,\n  },\n\n  iterator: {\n    type: Object,\n    canRead: readPermissions,\n  },\n\n  properties: {\n    type: Array,\n    canRead: readPermissions,\n  },\n\n  'properties.$': {\n    type: Object,\n    canRead: readPermissions,\n  },\n\n  // iterator: {\n  //   label: 'Iterator',\n  //   type: String,\n  //   canRead: readPermissions,\n  // },\n\n  // options: {\n  //   label: 'Options',\n  //   type: Array,\n  //   canRead: readPermissions,\n  // },\n\n  // 'options.$': {\n  //   type: Object,\n  //   canRead: readPermissions,\n  // },\n\n  runs: {\n    type: String,\n    canRead: readPermissions,\n  },\n\n  newSyntax: {\n    label: 'New Syntax',\n    type: Boolean,\n    canRead: readPermissions,\n  },\n\n  returns: {\n    label: 'Should Return',\n    type: String,\n    canRead: readPermissions,\n  },\n\n  description: {\n    type: String,\n    canRead: readPermissions,\n  },\n\n  hooks: {\n    type: Array,\n    canRead: readPermissions,\n    resolveAs: {\n      type: '[String]',\n      resolver: callback => {\n        if (Callbacks[callback.name]) {\n          const callbacks = Callbacks[callback.name].map(f => f.name);\n          return callbacks;\n        } else {\n          return [];\n        }\n      },\n    },\n  },\n  \n  'hooks.$': {\n    type: Object,\n  }\n};\n\nexport default schema;\n"
  },
  {
    "path": "packages/vulcan-debug/lib/modules/components.js",
    "content": "import '../components/DebugLayout.jsx';\n\nimport '../components/Emails.jsx';\nimport '../components/Groups.jsx';\nimport '../components/Settings.jsx';\nimport '../components/Callbacks.jsx';\nimport '../components/Routes.jsx';\nimport '../components/Components.jsx';\nimport '../components/I18n.jsx';\nimport '../components/Dashboard.jsx';\nimport '../components/Database.jsx';\nimport '../components/ErrorCatcherContents.jsx';\n"
  },
  {
    "path": "packages/vulcan-debug/lib/modules/emails/collection.js",
    "content": "import { createCollection } from 'meteor/vulcan:lib';\nimport schema from './schema.js';\n\nconst Emails = createCollection({\n\n  collectionName: 'Emails',\n\n  typeName: 'Email',\n\n  schema,\n  \n  resolvers: null,\n\n  mutations: null,\n\n});\n\n\nexport default Emails;\n"
  },
  {
    "path": "packages/vulcan-debug/lib/modules/emails/schema.js",
    "content": "import { readPermissions } from \"../permissions\";\n\nconst schema = {\n\n  name: {\n    type: String,\n    canRead: readPermissions,\n  },\n\n  template: {\n    type: String,\n    canRead: readPermissions,\n  },\n\n  subject: {\n    type: String,\n    canRead: readPermissions,\n  },\n\n  testPath: {\n    type: String,\n    canRead: readPermissions,\n  },\n\n};\n\nexport default schema;\n"
  },
  {
    "path": "packages/vulcan-debug/lib/modules/index.js",
    "content": "import './components.js';\nimport './routes.js';\n\nimport './settings/collection.js';\n\nimport './emails/collection.js';\n\n"
  },
  {
    "path": "packages/vulcan-debug/lib/modules/permissions.js",
    "content": "export const readPermissions = ['guests', 'members'];\n"
  },
  {
    "path": "packages/vulcan-debug/lib/modules/routes.js",
    "content": "import {addRoute, getDynamicComponent} from 'meteor/vulcan:lib';\n\naddRoute([\n  // {name: 'cheatsheet', path: '/cheatsheet', component: import('./components/Cheatsheet.jsx')},\n  {\n    name: 'debug',\n    path: '/debug',\n    componentName: 'DebugDashboard',\n    layoutName: 'DebugLayout',\n  },\n  {\n    name: 'debugGroups',\n    path: '/debug/groups',\n    component: () => getDynamicComponent(import('../components/Groups.jsx')),\n    layoutName: 'DebugLayout',\n  },\n  {\n    name: 'debugSettings',\n    path: '/debug/settings',\n    componentName: 'Settings',\n    layoutName: 'DebugLayout',\n  },\n  {\n    name: 'debugCallbacks',\n    path: '/debug/callbacks',\n    componentName: 'Callbacks',\n    layoutName: 'DebugLayout',\n  },\n  // {name: 'emails', path: '/emails', component: () => getDynamicComponent(import('./components/Emails.jsx'))},\n  {\n    name: 'debugEmails',\n    path: '/debug/emails',\n    componentName: 'Emails',\n    layoutName: 'DebugLayout',\n  },\n  {\n    name: 'debugRoutes',\n    path: '/debug/routes',\n    componentName: 'Routes',\n    layoutName: 'DebugLayout',\n  },\n  {\n    name: 'debugComponents',\n    path: '/debug/components',\n    componentName: 'Components',\n    layoutName: 'DebugLayout',\n  },\n  {\n    name: 'debugI18n',\n    path: '/debug/i18n',\n    componentName: 'I18n',\n    layoutName: 'DebugLayout',\n  },\n  {\n    name: 'debugDatabase',\n    path: '/debug/database',\n    componentName: 'DebugDatabase',\n    layoutName: 'DebugLayout',\n  },\n]);\n"
  },
  {
    "path": "packages/vulcan-debug/lib/modules/settings/collection.js",
    "content": "import { createCollection } from 'meteor/vulcan:lib';\nimport schema from './schema.js';\n\nconst Settings = createCollection({\n\n  collectionName: 'Settings',\n\n  typeName: 'Setting',\n\n  schema,\n  \n  resolvers: null,\n\n  mutations: null,\n\n});\n\n\nexport default Settings;\n"
  },
  {
    "path": "packages/vulcan-debug/lib/modules/settings/schema.js",
    "content": "import { readPermissions } from \"../permissions\";\n\nconst schema = {\n\n  name: {\n    label: 'Name',\n    type: String,\n    canRead: readPermissions,\n  },\n\n  value: {\n    label: 'Value',\n    type: Object,\n    canRead: readPermissions,\n  },\n\n  defaultValue: {\n    label: 'Default Value',\n    type: Object,\n    canRead: readPermissions,\n  },\n\n  isPublic: {\n    label: 'Public',\n    type: Boolean,\n    canRead: readPermissions,\n  },\n\n  description: {\n    label: 'Description',\n    type: String,\n    canRead: readPermissions,\n  },\n\n};\n\nexport default schema;\n"
  },
  {
    "path": "packages/vulcan-debug/lib/server/callbacks/collection.js",
    "content": "import { extendCollection } from 'meteor/vulcan:lib';\nimport Callbacks from '../../modules/callbacks/collection';\nimport resolvers from './resolvers';\n\nextendCollection(Callbacks, {\n  \n  resolvers,\n\n  mutations: null,\n\n});"
  },
  {
    "path": "packages/vulcan-debug/lib/server/callbacks/index.js",
    "content": "export * from './collection';\n"
  },
  {
    "path": "packages/vulcan-debug/lib/server/callbacks/resolvers.js",
    "content": "import { CallbackHooks } from 'meteor/vulcan:lib';\n\nconst resolvers = {\n\n  multi: {\n\n    resolver(root, {terms = {}}, context, info) {\n      return { results: CallbackHooks, totalCount: CallbackHooks.length };\n    },\n\n  },\n\n};\n\nexport default resolvers;"
  },
  {
    "path": "packages/vulcan-debug/lib/server/database/index.js",
    "content": "export * from './queries';\n"
  },
  {
    "path": "packages/vulcan-debug/lib/server/database/queries.js",
    "content": "import { Collections, Connectors, addGraphQLQuery, addGraphQLResolvers } from 'meteor/vulcan:lib';\n\nconst getDatabaseObject = async (root, { id }, context) => {\n  let document;\n  for (const collection of Collections) {\n    try {\n      // eslint-disable-next-line no-await-in-loop\n      document = await Connectors.get(collection, { _id: id });\n      if (document) {\n        return { collectionName: collection.options.collectionName, document };\n      }\n    } catch (error) {\n      // do nothing\n    }\n  }\n  return null;\n};\n\naddGraphQLQuery(`getDatabaseObject(id: String): JSON`);\naddGraphQLResolvers({ Query: { getDatabaseObject } });\n"
  },
  {
    "path": "packages/vulcan-debug/lib/server/emails/collection.js",
    "content": "import { extendCollection } from 'meteor/vulcan:lib';\nimport Emails from '../../modules/emails/collection';\nimport resolvers from './resolvers';\n\nextendCollection(Emails, {\n  \n  resolvers,\n\n  mutations: null,\n\n});\n"
  },
  {
    "path": "packages/vulcan-debug/lib/server/emails/index.js",
    "content": "export * from './collection';\n"
  },
  {
    "path": "packages/vulcan-debug/lib/server/emails/resolvers.js",
    "content": "import VulcanEmail from 'meteor/vulcan:email';\n\nconst resolvers = {\n  multi: {\n    resolver() {\n      const results = Object.keys(VulcanEmail.emails).map(name => {\n        const email = VulcanEmail.emails[name];\n        const subject = typeof email.subject === 'function' ? email.subject({}) : email.subject;\n        return { ...email, subject, name };\n      });\n      return { results, totalCount: results.length };\n    },\n  },\n};\n\nexport default resolvers;\n"
  },
  {
    "path": "packages/vulcan-debug/lib/server/main.js",
    "content": "export * from '../modules/index.js';\nexport * from './callbacks';\nexport * from './settings';\nexport * from './emails';\nexport * from './database';\n"
  },
  {
    "path": "packages/vulcan-debug/lib/server/settings/collection.js",
    "content": "import { extendCollection } from 'meteor/vulcan:lib';\nimport Settings from '../../modules/settings/collection';\nimport resolvers from './resolvers';\n\nextendCollection(Settings, {\n  \n  resolvers,\n\n  mutations: null,\n\n});"
  },
  {
    "path": "packages/vulcan-debug/lib/server/settings/index.js",
    "content": "export * from './collection';\n"
  },
  {
    "path": "packages/vulcan-debug/lib/server/settings/resolvers.js",
    "content": "import { getAllSettings } from 'meteor/vulcan:lib';\n\nconst resolvers = {\n\n  multi: {\n\n    resolver(root, {terms = {}}, context, info) {\n      const settings = getAllSettings();\n      return { results: settings, totalCount: settings.length };\n    },\n\n  },\n\n};\n\nexport default resolvers;"
  },
  {
    "path": "packages/vulcan-debug/lib/stylesheets/debug.scss",
    "content": ".test-email{\n  position: relative;\n  .spinner{\n    position: absolute;\n    top: 0px;\n    width: 100%;\n    height: 100%;\n  }\n}"
  },
  {
    "path": "packages/vulcan-debug/package.js",
    "content": "Package.describe({\n  name: 'vulcan:debug',\n  summary: 'Vulcan debug package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n  debugOnly: true,\n});\n\nPackage.onUse(function(api) {\n  api.use([\n    'vulcan:scss@4.12.0',\n    'dynamic-import@0.1.1',\n\n    // Vulcan packages\n\n    'vulcan:lib@=1.16.9',\n    'vulcan:email@=1.16.9',\n  ]);\n\n  api.use(['vulcan:errors@=1.16.9']), ['server', 'client'], { weak: true };\n\n  api.addFiles(['lib/stylesheets/debug.scss'], ['client']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "packages/vulcan-email/.gitignore",
    "content": ".build*\n"
  },
  {
    "path": "packages/vulcan-email/README.md",
    "content": "Vulcan email package, used internally. "
  },
  {
    "path": "packages/vulcan-email/lib/client/main.js",
    "content": "export * from '../modules/index.js';\nexport { VulcanEmail as default } from '../modules/index.js';\n"
  },
  {
    "path": "packages/vulcan-email/lib/modules/fragments.js",
    "content": "import { registerFragment } from 'meteor/vulcan:lib';\n\nregisterFragment(`\n  fragment EmailFragment on EmailResponse {\n    to\n    from\n    subject\n    success\n    error\n  }\n`);\n"
  },
  {
    "path": "packages/vulcan-email/lib/modules/index.js",
    "content": "export * from './fragments.js';\nexport * from './namespace.js';\n"
  },
  {
    "path": "packages/vulcan-email/lib/modules/namespace.js",
    "content": "/**\n * @summary Vulcan VulcanEmail namespace\n * @namespace VulcanEmail\n */\nexport const VulcanEmail = {};\n\nVulcanEmail.emails = {};\n\nVulcanEmail.addEmails = emails => {\n  // copy over \"path\" to \"testPath\" for backwards compatibility\n  Object.keys(emails).forEach(key => {\n    emails[key].testPath = emails[key].testPath || emails[key].path;\n  });\n  VulcanEmail.emails = Object.assign(VulcanEmail.emails, emails);\n};\n\nexport default VulcanEmail;\n"
  },
  {
    "path": "packages/vulcan-email/lib/server/email.js",
    "content": "/* eslint-disable no-console */\nimport { VulcanEmail } from '../modules/index.js';\nimport Juice from 'juice';\nimport htmlToText from 'html-to-text';\nimport Handlebars from 'handlebars';\nimport { Utils, getSetting, registerSetting, runQuery, Strings, getString } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core is not loaded yet\nimport { Email } from 'meteor/email';\nimport get from 'lodash/get';\n\n/*\n\nGet intl string, accepts a second optional values argument. Usage: {{__ \"posts.create\"}} or {{__ \"posts.create\" postValues}}\n\n*/\nHandlebars.registerHelper('__', function(id, values, context) {\n  if (typeof context === 'undefined') {\n    // if context is undefined, then we only have two arguments and context\n    // should be the second one; and values is undefined\n    context = values;\n    values = undefined;\n  }\n  const s = getString({ id, values, locale: get(context, 'data.root.locale') });\n  return new Handlebars.SafeString(s);\n});\n\nHandlebars.registerHelper('log', function(value) {\n  console.log(JSON.stringify(value, '', 2));\n});\n\nregisterSetting('secondaryColor', '#444444');\nregisterSetting('accentColor', '#DD3416');\nregisterSetting('title', 'My App');\nregisterSetting('tagline');\nregisterSetting('emailFooter');\nregisterSetting('logoUrl');\nregisterSetting('logoReverseUrl');\nregisterSetting('logoHeight');\nregisterSetting('logoWidth');\nregisterSetting('defaultEmail', 'noreply@example.com');\nregisterSetting('title', 'Vulcan');\nregisterSetting('enableDevelopmentEmails', false);\n\nVulcanEmail.templates = {};\n\nexport const addTemplates = templates => {\n  _.extend(VulcanEmail.templates, templates);\n};\nVulcanEmail.addTemplates = addTemplates;\n\nexport const getTemplate = templateName => {\n  if (!VulcanEmail.templates[templateName]) {\n    throw new Error(`Couldn't find email template named  “${templateName}”`);\n  }\n  return Handlebars.compile(VulcanEmail.templates[templateName], { noEscape: true, strict: true });\n};\nVulcanEmail.getTemplate = getTemplate;\n\nexport const buildTemplate = (htmlContent, data = {}, locale) => {\n  const emailProperties = {\n    secondaryColor: getSetting('secondaryColor', '#444444'),\n    accentColor: getSetting('accentColor', '#DD3416'),\n    siteName: getSetting('title', 'My App'),\n    tagline: getSetting('tagline'),\n    siteUrl: Utils.getSiteUrl(),\n    rootUrl: Utils.getRootUrl(),\n    body: htmlContent,\n    unsubscribe: '',\n    accountLink: Utils.getSiteUrl() + 'account',\n    footer: getSetting('emailFooter'),\n    logoUrl: getSetting('logoUrl'),\n    logoReverseUrl: getSetting('logoReverseUrl'),\n    logoHeight: getSetting('logoHeight'),\n    logoWidth: getSetting('logoWidth'),\n    ...data,\n    __: Strings[locale],\n  };\n\n  let emailHTML = htmlContent;\n\n  const wrapper = VulcanEmail.getTemplate('wrapper');\n  if (typeof wrapper === 'function') {\n    try {\n      emailHTML = VulcanEmail.getTemplate('wrapper')(emailProperties);\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.log('// getTemplate error, email wrapper template cannot be used');\n      console.log(error);\n    }\n  }\n\n  const inlinedHTML = Juice(emailHTML, { preserveMediaQueries: true });\n  const doctype = '<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">';\n\n  return doctype + inlinedHTML;\n};\nVulcanEmail.buildTemplate = buildTemplate;\n\nexport const generateTextVersion = html => {\n  return htmlToText.fromString(html, {\n    wordwrap: 130,\n  });\n};\nVulcanEmail.generateTextVersion = generateTextVersion;\n\nexport const send = (to, subject, html, text, throwErrors, cc, bcc, replyTo, headers, attachments, from) => {\n  // TODO: limit who can send emails\n  // TODO: fix this error: Error: getaddrinfo ENOTFOUND\n\n  if (typeof to === 'object') {\n    // eslint-disable-next-line no-redeclare\n    var { to, cc, bcc, replyTo, subject, html, text, throwErrors, headers, attachments, from } = to;\n  }\n\n  const _from = from || getSetting('defaultEmail', 'noreply@example.com');\n  const siteName = getSetting('title', 'Vulcan');\n  subject = subject || '[' + siteName + ']';\n\n  if (typeof text === 'undefined') {\n    // Auto-generate text version if it doesn't exist. Has bugs, but should be good enough.\n    text = VulcanEmail.generateTextVersion(html);\n  }\n\n  // in dev or staging environments, add suffix to email subjects to differentiate them.\n  const environment = getSetting('environment');\n  if (['development', 'staging'].includes(environment)) {\n    subject = `${subject} [${environment}]`;\n  }\n\n  const email = {\n    from: _from,\n    to,\n    cc,\n    bcc,\n    replyTo,\n    subject,\n    headers,\n    text,\n    html,\n    attachments,\n  };\n\n  const shouldSendEmail = process.env.NODE_ENV === 'production' || getSetting('enableDevelopmentEmails', false);\n\n  console.log(`✉️  sending email${shouldSendEmail ? '' : ' (simulation)'}…`); // eslint-disable-line\n  console.log('from: ' + _from); // eslint-disable-line\n  console.log('to: ' + to); // eslint-disable-line\n  console.log('subject: ' + subject); // eslint-disable-line\n  // console.log('cc: ' + cc); // eslint-disable-line\n  // console.log('bcc: ' + bcc); // eslint-disable-line\n  // console.log('replyTo: ' + replyTo); // eslint-disable-line\n  // console.log('headers: ' + JSON.stringify(headers)); // eslint-disable-line\n\n  if (shouldSendEmail) {\n    try {\n      Email.send(email);\n    } catch (error) {\n      console.log('// error while sending email:'); // eslint-disable-line\n      console.log(error); // eslint-disable-line\n      if (throwErrors) throw error;\n    }\n  }\n\n  return email;\n};\nVulcanEmail.send = send;\n\nexport const build = async ({ to: staticTo, emailName, variables, locale }) => {\n  let html, htmlContents;\n  // execute email's GraphQL query\n  const email = VulcanEmail.emails[emailName];\n\n  if (!email) {\n    throw new Error(`Could not find email [${emailName}]`);\n  }\n\n  try {\n    const result = email.query ? await runQuery(email.query, variables, { locale }) : { data: {} };\n\n    // if email has a data() function, merge its return value with results from the query\n    const data = email.data ? { ...result.data, ...email.data({ data: result.data, variables, locale }) } : result.data;\n\n    const subject = typeof email.subject === 'function' ? email.subject({ data, variables, locale }) : email.subject;\n    const to = staticTo || (typeof email.to === 'function' ? email.to({ data, variables, locale }) : email.to);\n\n    data.__ = Strings[locale];\n    data.locale = locale;\n\n    // try generating htmlContents, if it fails default to using templateError template instead\n    try {\n      htmlContents = VulcanEmail.getTemplate(email.template)(data);\n    } catch (error) {\n      htmlContents = VulcanEmail.getTemplate('templateError')({ ...data, email, error: error.message });\n      console.log('// getTemplate error');\n      console.log(error);\n    }\n\n    html = VulcanEmail.buildTemplate(htmlContents, data, locale);\n\n    return { data, subject, html, to };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.log(`Error while building email [${emailName}]`);\n    // eslint-disable-next-line no-console\n    console.log(error);\n  }\n};\nVulcanEmail.build = build;\n\nexport const buildAndSend = async ({\n  to,\n  cc,\n  bcc,\n  replyTo,\n  emailName,\n  variables,\n  locale = getSetting('locale'),\n  headers,\n  attachments,\n  from,\n}) => {\n  try {\n    const email = await build({ to, emailName, variables, locale });\n    return send({ to: email.to, cc, bcc, replyTo, subject: email.subject, html: email.html, headers, attachments, from });\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.log(error);\n  }\n};\nVulcanEmail.buildAndSend = buildAndSend;\n\nexport const buildAndSendHTML = (to, subject, html) => VulcanEmail.send(to, subject, VulcanEmail.buildTemplate(html));\nVulcanEmail.buildAndSendHTML = buildAndSendHTML;\n"
  },
  {
    "path": "packages/vulcan-email/lib/server/main.js",
    "content": "export { VulcanEmail as default } from '../modules/index.js';\n\nexport * from './email.js';\nexport * from './mutations.js';\nexport * from './routes.js';\nexport * from './templates/index.js';\n"
  },
  {
    "path": "packages/vulcan-email/lib/server/mutations.js",
    "content": "import Users from 'meteor/vulcan:users';\nimport { addGraphQLSchema, addGraphQLMutation, addGraphQLResolvers } from 'meteor/vulcan:lib';\nimport { VulcanEmail } from '../modules/index.js';\n\nexport const testEmail = async (root, { emailName }, context) => {\n  if (Users.isAdmin(context.currentUser)) {\n    const email = VulcanEmail.emails[emailName];\n    const response = await VulcanEmail.buildAndSend({ emailName, variables: email.testVariables({}) });\n    return response;\n  } else {\n    throw new Error({ id: 'app.noPermission' });\n  }\n};\n\nconst emailResponseSchema = `type EmailResponse {\n  from: String\n  to: String\n  subject: String\n  success: JSON\n  error: String\n}`;\naddGraphQLSchema(emailResponseSchema);\n\naddGraphQLMutation('testEmail(emailName: String) : EmailResponse');\n\nconst resolver = {\n  Mutation: {\n    testEmail,\n  },\n};\n\naddGraphQLResolvers(resolver);\n"
  },
  {
    "path": "packages/vulcan-email/lib/server/routes.js",
    "content": "import { Picker } from 'meteor/meteorhacks:picker';\nimport { getSetting } from 'meteor/vulcan:lib';\nimport { VulcanEmail } from '../modules/index.js';\nimport pickBy from 'lodash/pickBy';\n\nMeteor.startup(function() {\n  Object.keys(VulcanEmail.emails).forEach(key => {\n    const email = VulcanEmail.emails[key];\n\n    // backwards-compatibility\n    const path = email.testPath || email.path;\n\n    if (path) {\n      // template live preview routes\n      Picker.route(path, async (params, req, res) => {\n        let html;\n        // if email has a custom way of generating test HTML, use it\n        if (typeof email.getTestHTML !== 'undefined') {\n          html = email.getTestHTML.bind(email)(params);\n        } else {\n          const locale = params.query.locale || getSetting('locale');\n          delete params.query;\n\n          // filter out \":foo\" placeholder param segments\n          const cleanedParams = pickBy(params, (value, key) => value && value.charAt(0) !== ':');\n\n          // else get test object (sample post, comment, user, etc.)\n          const testVariables =\n            (typeof email.testVariables === 'function' ? email.testVariables(cleanedParams) : email.testVariables) || {};\n          // delete params.query so we don't pass it to GraphQL query\n          delete params.query;\n\n          const variables = testVariables;\n\n          const { data: emailTestData, subject, to, html: builtHtml } = await VulcanEmail.build({\n            emailName: key,\n            variables,\n            locale,\n          });\n\n          // remove Strings object to avoid echoing it out\n          delete emailTestData.__;\n\n          html = `\n          ${builtHtml}\n          <h4 style=\"margin: 20px;\"><code>Subject: ${subject}</code></h4>\n          <h4 style=\"margin: 20px;\"><code>To: ${to}</code></h4>\n          <h5 style=\"margin: 20px;\">Test Variables:</h5>\n          <div style=\"border: 1px solid #999; padding: 10px 20px; margin: 20px;\">\n            <pre><code>${JSON.stringify(variables, null, 2)}</code></pre>\n          </div>\n          <h5 style=\"margin: 20px;\">Data:</h5>\n          <div style=\"border: 1px solid #999; padding: 10px 20px; margin: 20px; overflow-x: scroll\">\n            <pre><code>${JSON.stringify(emailTestData, null, 2)}</code></pre>\n          </div>\n        `;\n        }\n\n        // return html\n        res.end(html);\n      });\n    }\n\n    // raw template\n    Picker.route('/email/template/:template', (params, req, res) => {\n      res.end(VulcanEmail.templates[params.template]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-email/lib/server/templates/index.js",
    "content": "import { addTemplates } from '../email.js';\n\naddTemplates({\n  templateError: Assets.getText('lib/server/templates/template_error.handlebars'),\n});\n"
  },
  {
    "path": "packages/vulcan-email/lib/server/templates/template_error.handlebars",
    "content": "<h1>Template Error</h1>\n<div>The template engine encountered the following error:</div>\n<pre><code>{{error}}</code></pre>"
  },
  {
    "path": "packages/vulcan-email/package.js",
    "content": "Package.describe({\n  name: 'vulcan:email',\n  summary: 'Vulcan email package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:lib@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n\n  api.addAssets(['lib/server/templates/template_error.handlebars'], ['server']);\n});\n"
  },
  {
    "path": "packages/vulcan-embed/.gitignore",
    "content": ".build*\n"
  },
  {
    "path": "packages/vulcan-embed/README.md",
    "content": "### Built-in\n\nSettings:\n\n```\n\"embedProvider\": \"builtin\",\n```\n\n### Embedly\n\nSettings:\n\n```\n\"embedProvider\": \"embedly\",\n\"embedly\": {\n  \"apiKey\": \"123foo\"\n},\n```\n\n### EmbedAPI\n\nSettings: \n\n```\n\"embedProvider\": \"embedAPI\",\n\"embedAPI\": {\n  \"apiKey\": \"123foo\"\n},\n```"
  },
  {
    "path": "packages/vulcan-embed/lib/client/main.js",
    "content": "export * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-embed/lib/components/EmbedURL.jsx",
    "content": "import { Components, registerComponent, Utils, getSetting } from 'meteor/vulcan:core';\nimport { withMutation } from 'meteor/vulcan:core';\nimport React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\n\nclass EmbedURL extends Component {\n  constructor(props) {\n    super(props);\n\n    this.state = {\n      loading: false,\n      value: props.value || '',\n    };\n  }\n\n  // clean the media property of the document if it exists: this field is handled server-side in an async callback\n  componentDidMount() {\n    try {\n      if (this.props.document && !_.isEmpty(this.props.document.media)) {\n        this.context.updateCurrentValues({ media: {} });\n      }\n    } catch (error) {\n      console.error('Error cleaning \"media\" property', error); // eslint-disable-line\n    }\n  }\n\n  editThumbnail = () => {\n    const newThumbnailUrl = prompt(\n      this.context.intl.formatMessage({ id: 'posts.enter_thumbnail_url' }),\n      this.context.getDocument().thumbnailUrl\n    );\n    if (newThumbnailUrl) {\n      this.context.updateCurrentValues({ thumbnailUrl: newThumbnailUrl });\n    }\n  };\n\n  clearThumbnail = () => {\n    if (confirm(this.context.intl.formatMessage({ id: 'posts.clear_thumbnail?' }))) {\n      this.context.updateCurrentValues({ thumbnailUrl: null });\n    }\n  };\n\n  // called whenever the URL input field loses focus\n  handleBlur = async e => {\n    try {\n      const url = e.target.value;\n\n      // start the mutation only if the input has a value\n      if (url.length) {\n        // notify the user that something happens\n        this.setState({ loading: true });\n\n        // the URL has changed, get new title, body, thumbnail & media for this url\n        const result = await this.props.getEmbedData({ url });\n\n        // uncomment for debug\n        // console.log('Embedly Data', result);\n\n        // extract the relevant data, for easier consumption\n        const {\n          data: {\n            getEmbedData: { title, description, thumbnailUrl },\n          },\n        } = result;\n        const body = description;\n\n        // update the form\n        if (title && !this.context.getDocument().title) {\n          this.context.updateCurrentValues({ title });\n        }\n        if (body && !this.context.getDocument().body) {\n          this.context.updateCurrentValues({ body });\n        }\n        if (thumbnailUrl && !this.context.getDocument().thumbnailUrl) {\n          this.context.updateCurrentValues({ thumbnailUrl });\n        }\n\n        // embedly component is done\n        this.setState({ loading: false });\n\n        // remove errors & keep the current values\n        // TODO: why??\n        // this.context.clearForm({clearErrors: true});\n      }\n    } catch (error) {\n      console.error(error); // eslint-disable-line\n      const errorMessage = error.message.includes('401')\n        ? Utils.encodeIntlError({ id: 'app.embedly_not_authorized' })\n        : error.message;\n\n      // embedly component is done\n      this.setState({ loading: false });\n\n      // something bad happened\n      this.context.throwError({\n        id: 'embedurl.error_fetching_data',\n        data: { name: this.props.path, message: errorMessage },\n      });\n    }\n  };\n\n  getDimensions = () => {\n    const width = getSetting('thumbnailWidth', 80);\n    const height = getSetting('thumbnailHeight', 60);\n    const ratio = width / height;\n    return {\n      width,\n      height,\n      ratio,\n    };\n  };\n\n  getLoadingStyle = () => {\n    const loadingStyle = {\n      display: this.state.loading ? 'block' : 'none'\n    };\n\n    return loadingStyle;\n  };\n\n  renderThumbnail() {\n    return (\n      <div className=\"embedly-thumbnail\">\n        <img\n          className=\"embedly-thumbnail-image\"\n          src={this.context.getDocument().thumbnailUrl}\n          onClick={this.editThumbnail}\n        />\n\n        <div className=\"embedly-url-field-loading\" style={this.getLoadingStyle()}>\n          <Components.Loading />\n        </div>\n        <div className=\"embedly-thumbnail-actions\">\n          <a className=\"thumbnail-edit\" onClick={this.editThumbnail}>\n            <Components.Icon name=\"edit\" /> <Components.FormattedMessage id=\"posts.enter_thumbnail_url\" />\n          </a>\n          <a className=\"thumbnail-clear\" onClick={this.clearThumbnail}>\n            <Components.Icon name=\"delete\" /> <Components.FormattedMessage id=\"posts.clear_thumbnail\" />\n          </a>\n        </div>\n      </div>\n    );\n  }\n\n  renderNoThumbnail() {\n    return (\n      <div className=\"embedly-thumbnail\">\n        <div\n          style={{ width: `${Math.round(60 * this.getDimensions().ratio)}px`, height: '60px' }}\n          onClick={this.editThumbnail}\n          className=\"embedly-thumbnail-placeholder\">\n          <Components.Icon name=\"image\" />\n          <Components.FormattedMessage id=\"posts.enter_thumbnail_url\" />\n        </div>\n\n        <div className=\"embedly-url-field-loading\" style={this.getLoadingStyle()}>\n          <Components.Loading />\n        </div>\n      </div>\n    );\n  }\n\n  render() {\n    const wrapperStyle = {\n      position: 'relative',\n    };\n\n    // see https://facebook.github.io/react/warnings/unknown-prop.html\n    const { document, control, getEmbedData, refFunction, inputProperties } = this.props; // eslint-disable-line\n\n    return (\n      <Components.FormComponentText\n        inputProperties={{\n          ...inputProperties,\n          onBlur: this.handleBlur,\n          type: 'url',\n          layout: 'elementOnly',\n        }}\n        itemProperties={{\n          className: 'embedly-form-item',\n          style: wrapperStyle,\n          afterInput: (\n            <div className=\"embedly-url-after-input\">\n              {this.context.getDocument().thumbnailUrl\n                ? this.renderThumbnail()\n                : this.renderNoThumbnail()}\n            </div>\n          ),\n        }}\n      />\n    );\n  }\n}\n\nEmbedURL.propTypes = {\n  name: PropTypes.string,\n  value: PropTypes.any,\n  label: PropTypes.string,\n};\n\nEmbedURL.contextTypes = {\n  updateCurrentValues: PropTypes.func,\n  addToDeletedValues: PropTypes.func,\n  throwError: PropTypes.func,\n  clearForm: PropTypes.func,\n  getDocument: PropTypes.func,\n  intl: intlShape,\n};\n\nconst options = {\n  name: 'getEmbedData',\n  args: { url: 'String' },\n};\n\nexport default withMutation(options)(EmbedURL);\n\nregisterComponent('EmbedURL', EmbedURL, [withMutation, options]);\n"
  },
  {
    "path": "packages/vulcan-embed/lib/modules/embed.js",
    "content": "import { registerSetting } from 'meteor/vulcan:core';\n\nexport const Embed = {};\n\nregisterSetting('thumbnailWidth', 400, 'Image thumbnails width');\nregisterSetting('thumbnailHeight', 300, 'Image thumbnails height');"
  },
  {
    "path": "packages/vulcan-embed/lib/modules/i18n.js",
    "content": "import { addStrings } from 'meteor/vulcan:core';\n\naddStrings('en', {\n  'embedurl.error_fetching_data': 'Sorry, we were not able to fetch metadata for this URL.'\n});\n"
  },
  {
    "path": "packages/vulcan-embed/lib/modules/index.js",
    "content": "export { EmbedURL } from '../components/EmbedURL.jsx';\n\nimport './i18n';\n\nexport * from './embed.js';"
  },
  {
    "path": "packages/vulcan-embed/lib/server/integrations/builtin.js",
    "content": "/*\n\nEmbed.providerName.getData should return an object\nwith the following properties:\n\n- title\n- description\n- thumbnailUrl\n- sourceName\n- sourceUrl\n- media (object)\n\n*/\n\nimport { Embed } from '../../modules/embed.js';\n// import Metascraper from 'metascraper';\n\nEmbed.builtin = {\n\n  getData(url) {\n\n    // const metadata = await Metascraper.scrapeUrl(url);\n\n    const metadata = extractMeta(url);\n    \n    return {\n      title: metadata.title,\n      description: metadata.description,\n      thumbnailUrl: metadata.image,\n    };\n  }\n\n};\n\n// -------------- //\n// adapted from https://github.com/acemtp/meteor-meta-extractor/blob/master/meta-extractor.js\n\nimport he from 'he';\n\nconst extractMeta = function (params) {\n  var html;\n  var match;\n  var META = {};\n\n  if (params.substr(0, 4) === 'http') {\n    try {\n      var result = HTTP.call('GET', params);\n      if (result.statusCode !== 200) {\n        return META;\n      }\n      html = result.content;\n    } catch (e) {\n      // eslint-disable-next-line no-console\n      console.log('catch error', e);\n      return META;\n    }\n  } else {\n    html = params;\n  }\n\n\n  // search for a <title>\n  var title_regex = /<title>(.*)<\\/title>/gmi;\n\n  while ((match = title_regex.exec(html)) !== null) {\n    if (match.index === title_regex.lastIndex) {\n      title_regex.lastIndex++;\n    }\n    META.title = match[1];\n  }\n\n  // search and parse all <meta>\n  var meta_tag_regex = /<meta.*?(?:name|property|http-equiv)=['\"]([^'\"]*?)['\"][\\w\\W]*?content=['\"]([^'\"]*?)['\"].*?>/gmi;\n\n  var tags = {\n    title: ['title', 'og:title', 'twitter:title'],\n    description: ['description', 'og:description', 'twitter:description'],\n    image: ['image', 'og:image', 'twitter:image'],\n    url: ['url', 'og:url', 'twitter:url']\n  };\n\n  while ((match = meta_tag_regex.exec(html)) !== null) {\n    if (match.index === meta_tag_regex.lastIndex) {\n      meta_tag_regex.lastIndex++;\n    }\n\n    for (var item in tags) {\n      tags[item].forEach(function(prop) {\n\n        if (match[1] === prop) {\n\n          var property = tags[item][0];\n          var content = match[2];\n\n          // Only push content to our 'META' object if 'META' doesn't already\n          // contain content for that property.\n          if (!META[property]) {\n            META[property] = he.decode(content);\n          }\n\n        }\n\n      });\n    }\n  }\n\n  return META;\n};\n\n"
  },
  {
    "path": "packages/vulcan-embed/lib/server/integrations/embedapi.js",
    "content": "import { getSetting, registerSetting } from 'meteor/vulcan:core';\nimport { HTTP } from 'meteor/http';\nimport { Embed } from '../../modules/embed.js';\n\nregisterSetting('embedAPI', null, 'EmbedAPI settings');\n\nconst extractBase = 'https://embedapi.com/api/embed';\nconst settings = getSetting('embedAPI');\nconst thumbnailWidth = getSetting('thumbnailWidth', 400);\nconst thumbnailHeight = getSetting('thumbnailHeight', 300);\n\nif (settings) {\n\n  const {apiKey} = settings;\n\n  if(!apiKey) {\n    // fail silently to still let the post be submitted as usual\n    console.log(\"Couldn't find an EmbedAPI API key! Please add it to your Vulcan settings.\"); // eslint-disable-line\n    return null;\n  }\n\n  Embed.embedAPI = {\n\n    getData(url) {\n\n      try {\n\n        const result = HTTP.get(extractBase, {\n          params: {\n            key: apiKey,\n            url: url,\n            image_width: thumbnailWidth,\n            image_height: thumbnailHeight,\n            image_method: 'crop'\n          }\n        });\n\n        const data = JSON.parse(result.content);\n\n        const embedData = {\n          title: data.title,\n          description: data.description\n        };\n        \n        if (data.pics && data.pics.length > 0) {\n          embedData.thumbnailUrl = data.pics[0];\n        }\n        \n        if (data.media ) {\n          embedData.media = data.media;\n        }\n\n        if (data.authors && data.authors.length > 0) {\n          embedData.sourceName = data.authors[0].name;\n        }\n\n        return embedData;\n\n      } catch (error) {\n        console.log('// EmbedAPI error') // eslint-disable-line\n        console.log(error); // eslint-disable-line\n        throw new Error(error.error_message);\n      }\n    },\n\n  };\n\n}\n\n"
  },
  {
    "path": "packages/vulcan-embed/lib/server/integrations/embedly.js",
    "content": "import { getSetting, registerSetting } from 'meteor/vulcan:core';\nimport { HTTP } from 'meteor/http';\nimport { Embed } from '../../modules/embed.js';\n\nregisterSetting('embedly', null, 'Embedly settings');\n\nconst extractBase = 'http://api.embed.ly/1/extract';\nconst settings = getSetting('embedly');\nconst thumbnailWidth = getSetting('thumbnailWidth', 400);\nconst thumbnailHeight = getSetting('thumbnailHeight', 300);\n\nif (settings) {\n\n  const {apiKey} = settings;\n\n  if(!apiKey) {\n    // fail silently to still let the post be submitted as usual\n    // eslint-disable-next-line no-console\n    console.log(\"Couldn't find an Embedly API key! Please add it to your Vulcan settings.\"); // eslint-disable-line\n    return null;\n  }\n\n  Embed.embedly = {\n\n    getData(url) {\n\n      try {\n\n        const data = HTTP.get(extractBase, {\n          params: {\n            key: apiKey,\n            url: url,\n            image_width: thumbnailWidth,\n            image_height: thumbnailHeight,\n            image_method: 'crop'\n          }\n        }).data;\n\n\n        if (data.images && data.images.length > 0) // there may not always be an image\n          data.thumbnailUrl = data.images[0].url.replace('http:',''); // add thumbnailUrl as its own property\n\n        if (data.authors && data.authors.length > 0) {\n          data.sourceName = data.authors[0].name;\n          data.sourceUrl = data.authors[0].url;\n        }\n\n        const embedlyData = _.pick(data, 'title', 'media', 'description', 'thumbnailUrl', 'sourceName', 'sourceUrl');\n\n        return embedlyData;\n\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.log('// Embedly error');\n        // eslint-disable-next-line no-console\n        console.log(error);\n        // the first 13 characters of the Embedly errors are \"failed [400] \", so remove them and parse the rest\n        const errorObject = JSON.parse(error.message.substring(13));\n        throw new Error(errorObject.error_message);\n      }\n    },\n\n  };\n\n}\n\n"
  },
  {
    "path": "packages/vulcan-embed/lib/server/main.js",
    "content": "export * from '../modules/index.js';\n\nimport './integrations/builtin.js';\nimport './integrations/embedly.js';\nimport './integrations/embedapi.js';\n\nimport './mutations.js';"
  },
  {
    "path": "packages/vulcan-embed/lib/server/methods.js",
    "content": "// import { getSetting } from 'meteor/vulcan:core';\n// import Posts from 'meteor/vulcan:posts';\n\n// Meteor.methods({\n//   testgetEmbedData: function (url) {\n//     check(url, String);\n//     console.log(getEmbedData(url));\n//   },\n//   getEmbedData: function (url) {\n//     check(url, String);\n//     return getEmbedData(url);\n//   },\n//   embedlyKeyExists: function () {\n//     return !!getSetting('embedlyKey');\n//   },\n//   generateThumbnail: function (post) {\n//     check(post, Posts.simpleSchema());\n//     if (Posts.options.mutations.edit.check(Meteor.user(), post)) {\n//       regenerateThumbnail(post);\n//     }\n//   },\n//   generateThumbnails: function (limit = 20, mode = \"generate\") {\n//     // mode = \"generate\" : generate thumbnails only for all posts that don't have one\n//     // mode = \"all\" : regenerate thumbnais for all posts\n      \n//     if (Users.isAdmin(Meteor.user())) {\n      \n//       console.log(\"// Generating thumbnails…\")\n      \n//       const selector = {url: {$exists: true}};\n//       if (mode === \"generate\") {\n//         selector.thumbnailUrl = {$exists: false};\n//       }\n\n//       const posts = Posts.find(selector, {limit: limit, sort: {postedAt: -1}});\n\n//       posts.forEach((post, index) => {\n//         Meteor.setTimeout(function () {\n//           console.log(`// ${index}. fetching thumbnail for “${post.title}” (_id: ${post._id})`);\n//           try {\n//             regenerateThumbnail(post);\n//           } catch (error) {\n//             console.log(error);\n//           }\n//         }, index * 1000);\n//       });\n//     }\n//   }\n// });"
  },
  {
    "path": "packages/vulcan-embed/lib/server/mutations.js",
    "content": "import { addGraphQLMutation, addGraphQLResolvers, getSetting, registerSetting } from 'meteor/vulcan:core';\nimport { Embed } from '../modules/embed.js';\n\nregisterSetting('embedProvider', 'builtin', 'Media embed/metadata provider service');\n\nconst embedProvider = getSetting('embedProvider');\n\naddGraphQLMutation('getEmbedData(url: String) : JSON');\n\nconst resolver = {\n  Mutation: {\n    getEmbedData(root, args, context) {\n      const data = Embed[embedProvider].getData(args.url);\n      // eslint-disable-next-line no-console\n      console.log('// getEmbedData');\n      // eslint-disable-next-line no-console\n      console.log(args);\n      // eslint-disable-next-line no-console\n      console.log(data);\n      return data;\n    },\n  },\n};\naddGraphQLResolvers(resolver);\n\n// Meteor.methods({\n//   testgetEmbedData: function (url) {\n//     check(url, String);\n//     console.log(getEmbedData(url));\n//   },\n//   getEmbedData: function (url) {\n//     check(url, String);\n//     return getEmbedData(url);\n//   },\n//   embedlyKeyExists: function () {\n//     return !!getSetting('embedlyKey');\n//   },\n//   generateThumbnail: function (post) {\n//     check(post, Posts.simpleSchema());\n//     if (Users.canEdit(Meteor.user(), post)) {\n//       regenerateThumbnail(post);\n//     }\n//   },\n//   generateThumbnails: function (limit = 20, mode = \"generate\") {\n//     // mode = \"generate\" : generate thumbnails only for all posts that don't have one\n//     // mode = \"all\" : regenerate thumbnais for all posts\n      \n//     if (Users.isAdmin(Meteor.user())) {\n      \n//       console.log(\"// Generating thumbnails…\")\n      \n//       const selector = {url: {$exists: true}};\n//       if (mode === \"generate\") {\n//         selector.thumbnailUrl = {$exists: false};\n//       }\n\n//       const posts = Posts.find(selector, {limit: limit, sort: {postedAt: -1}});\n\n//       posts.forEach((post, index) => {\n//         Meteor.setTimeout(function () {\n//           console.log(`// ${index}. fetching thumbnail for “${post.title}” (_id: ${post._id})`);\n//           try {\n//             regenerateThumbnail(post);\n//           } catch (error) {\n//             console.log(error);\n//           }\n//         }, index * 1000);\n//       });\n//     }\n//   }\n// });\n"
  },
  {
    "path": "packages/vulcan-embed/lib/stylesheets/embedly.scss",
    "content": ".embedly-form-control{\n  display: flex;\n}\n\n.embedly-url-field{\n  position: relative;\n  flex: 1;\n  margin-right: 10px;\n}\n\n.form-component-EmbedURL{\n  .col-sm-9{\n    display: flex;\n  }\n  input{\n    margin-right: 10px;\n  }\n}\n\n.embedly-thumbnail{\n  position: relative;\n}\n.embedly-thumbnail-placeholder{\n  background: #eee;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n\n  .icon{\n    display: block;\n    color: rgba(0,0,0,0.5);\n  }\n  span{\n    display: none;\n  }\n}\n\n.embedly-thumbnail-image{\n  height: 60px;\n  width: auto;\n  display: block;\n}\n\n.embedly-thumbnail-actions{\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  a{\n    cursor: pointer;\n  }\n  .thumbnail-edit{\n    margin-right: 5px;\n  }\n  span{\n    display: none;\n  }\n}\n\n.embedly-url-field-loading{\n  position: absolute;\n  pointer-events: none;\n  top: 50%;\n  left: 50%;\n  margin-top: -5px;\n  margin-left: -20px;\n}"
  },
  {
    "path": "packages/vulcan-embed/package.js",
    "content": "Package.describe({\n  name: 'vulcan:embed',\n  summary: 'Vulcan Embed package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['http@2.0.0', 'vulcan:core@=1.16.9', 'vulcan:scss@4.12.0']);\n\n  api.addFiles(['lib/stylesheets/embedly.scss'], ['client']);\n\n  api.mainModule('lib/client/main.js', 'client');\n  api.mainModule('lib/server/main.js', 'server');\n});\n"
  },
  {
    "path": "packages/vulcan-errors/README.md",
    "content": "Vulcan error tracking package."
  },
  {
    "path": "packages/vulcan-errors/lib/client/init.js",
    "content": "import { addCallback } from 'meteor/vulcan:core';\nimport { initFunctions } from '../modules/index.js';\n\n// on client, init function will be executed once App is ready\nexport const addInitFunction = fn => {\n  initFunctions.push(fn);\n};\n\nfunction runInitFunctions(props) {\n  initFunctions.forEach(f => {\n    f(props);\n  });\n}\naddCallback('app.mounted', runInitFunctions);\n"
  },
  {
    "path": "packages/vulcan-errors/lib/client/main.js",
    "content": "export * from './init.js';\nexport * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-errors/lib/components/ErrorCatcher.jsx",
    "content": "/*\n\nErrorCatcher\n\nUsage: \n\n  <Components.ErrorCatcher>\n    <YourComponentTree />\n  </Components.ErrorCatcher>\n\n*/\n\nimport { Components, registerComponent, withCurrentUser, withSiteData } from 'meteor/vulcan:core';\nimport { withRouter } from 'react-router';\nimport React, { Component } from 'react';\nimport { Errors } from '../modules/errors.js';\n\nconst getMessage = error => error.message || error.errorMessage;\n\nclass ErrorCatcher extends Component {\n  state = {\n    error: null,\n  };\n\n  componentDidCatch = (error, errorInfo) => {\n    const { currentUser, siteData = {} } = this.props;\n    const { sourceVersion } = siteData;\n    this.setState({ error });\n    Errors.log({\n      message: getMessage(error),\n      error,\n      details: { ...errorInfo, sourceVersion },\n      currentUser,\n    });\n  };\n\n  componentDidUpdate(prevProps) {\n    if (\n      this.props.location &&\n      prevProps.location &&\n      this.props.location.pathname &&\n      prevProps.location.pathname &&\n      prevProps.location.pathname !== this.props.location.pathname\n    ) {\n      // reset the component state when the route changes to re-render the app and avodi blocking the navigation\n      this.setState({ error: null });\n    }\n  }\n\n  render() {\n    const { error } = this.state;\n    return error ? <Components.ErrorCatcherContents error={error} message={getMessage(error)} /> : this.props.children;\n  }\n}\n\nregisterComponent('ErrorCatcher', ErrorCatcher, withCurrentUser, withSiteData, withRouter);\n\nconst ErrorCatcherContents = ({ error, message }) => (\n  <div className=\"error-catcher\">\n    <Components.Flash message={{ message, properties: error }} />\n  </div>\n);\n\nregisterComponent('ErrorCatcherContents', ErrorCatcherContents);\n"
  },
  {
    "path": "packages/vulcan-errors/lib/components/ErrorsUserMonitor.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';\nimport classNames from 'classnames';\nimport { Errors } from 'meteor/vulcan:errors';\n\nclass ErrorsUserMonitor extends PureComponent {\n  constructor(props) {\n    super(props);\n  }\n\n  componentDidMount() {\n    this.checkCurrentUser();\n  }\n\n  componentDidUpdate() {\n    this.checkCurrentUser();\n  }\n\n  checkCurrentUser(prevProps, prevState, snapshot) {\n    const currentUser = this.props.currentUser;\n\n    const currentUserId = currentUser && currentUser._id;\n    const errorsUserId = Errors.currentUser && Errors.currentUser._id;\n\n    if (currentUserId !== errorsUserId) {\n      // const currentUserEmail = currentUser && currentUser.email;\n      // const errorsUserEmail = Errors.currentUser && Errors.currentUser.email;\n      // console.log(`User changed from ${errorsUserEmail} (${errorsUserId}) to ${currentUserEmail} (${currentUserId})`);\n\n      Errors.setCurrentUser(currentUser);\n    }\n  }\n\n  render() {\n    const { className, currentUser } = this.props;\n\n    return (\n      <div\n        className={classNames(\n          'errors-user-monitor',\n          (currentUser && currentUser._id) || 'no-user',\n          currentUser && currentUser.email,\n          className\n        )}\n      />\n    );\n  }\n}\n\nErrorsUserMonitor.propTypes = {\n  className: PropTypes.string,\n  currentUser: PropTypes.object,\n};\n\nErrorsUserMonitor.displayName = 'ErrorsUserMonitor';\n\nregisterComponent('ErrorsUserMonitor', ErrorsUserMonitor, withCurrentUser);\n"
  },
  {
    "path": "packages/vulcan-errors/lib/modules/errors.js",
    "content": "import Users from 'meteor/vulcan:users';\nimport get from 'lodash/get';\nimport isEqual from 'lodash/isEqual';\n\nexport const initFunctions = [];\nexport const logFunctions = [];\nexport const userFunctions = [];\nexport const scrubFields = new Set();\n\nexport const userFields = {\n  id: '_id',\n  email: 'email',\n  username: 'profile.username',\n  isAdmin: 'isAdmin',\n};\n\n/*\n\nMoved to server/client's init.js\n\n*/\n// export const addInitFunction = fn => {\n//   initFunctions.push(fn);\n//   // on server, execute init function as soon as possible\n//   fn();\n// };\n\nexport const addLogFunction = fn => {\n  logFunctions.push(fn);\n};\n\nexport const addUserFunction = fn => {\n  userFunctions.push(fn);\n};\n\nexport const addUserFields = fields => {\n  Object.assign(userFields, fields);\n};\n\nexport const addScrubFields = fields => {\n  fields = Array.isArray(fields) ? fields : [fields];\n  for (const field of fields) {\n    scrubFields.add(field);\n  }\n};\n\nexport const getUserPayload = function(userOrUserId) {\n  try {\n    const user = Users.getUser(userOrUserId);\n    if (!user) return null;\n\n    const userPayload = {};\n\n    for (const field in userFields) {\n      const path = userFields[field];\n      userPayload[field] = get(user, path);\n    }\n\n    return userPayload;\n  } catch (error) {\n    return null;\n  }\n};\n\n// export const getServerHost = function() {\n//   return process.env.GALAXY_CONTAINER_ID\n//     ? process.env.GALAXY_CONTAINER_ID.split('-')[1]\n//     : getSetting('public.environment');\n// };\n\n// export const processApolloErrors = function(err) {\n//   if (!err) return;\n\n//   const apolloErrors =\n//     err.original && err.original.data && err.original.data.errors\n//       ? formatApolloError(err.original, formatMessage, '\\n', '  ApolloError: ')\n//       : err.data && err.data.errors\n//         ? formatApolloError(err, formatMessage, '\\n', '  ApolloError: ')\n//         : '';\n\n//   err.message = err.message + '\\n' + apolloErrors;\n// };\n\n// export const formatApolloError = (err, formatMessage, separator = ', ', prefix = '') => {\n//   let formatted = '';\n\n//   const formatProperties = properties => {\n//     return _isEmpty(properties) ? '' : ' ' + inspect(properties);\n//   };\n\n//   const addError = error => {\n//     let message = '';\n\n//     if (error.id) {\n//       try {\n//         message = formatMessage({ id: error.id }, error.properties);\n//       } catch (err) {\n//         message = error.id + formatProperties(error.properties);\n//       }\n//     } else if (error.message) {\n//       message = error.message + formatProperties(error.properties);\n//     }\n\n//     formatted += formatted ? separator : '';\n//     formatted += prefix + message;\n//   };\n\n//   const graphQLErrors = err.data && err.data.errors ? [err] : err.graphQLErrors;\n\n//   if (graphQLErrors) {\n//     for (let graphQLError of graphQLErrors) {\n//       if (graphQLError.data && graphQLError.data.errors) {\n//         for (let innerError of graphQLError.data.errors) {\n//           if (innerError.data) {\n//             addError(innerError.data);\n//           } else {\n//             addError(innerError);\n//           }\n//         }\n//       } else if (graphQLError.data) {\n//         addError(graphQLError.data);\n//       } else {\n//         addError(graphQLError);\n//       }\n//     }\n//   } else {\n//     let message = err.message;\n//     const graphqlPrefixIsPresent = message.match(/GraphQL error: (.*)/);\n//     addError({ message: graphqlPrefixIsPresent ? graphqlPrefixIsPresent[1] : message });\n//   }\n\n//   return formatted;\n// };\n\nexport const Errors = {\n  currentUser: null,\n\n  setCurrentUser: function(user) {\n    // avoid setting current user multiple times\n    if (isEqual(this.currentUser, user)) return;\n\n    for (const fn of userFunctions) {\n      try {\n        fn(user);\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.log(`// ${fn.name} with ${user && user.email}`);\n        // eslint-disable-next-line no-console\n        console.log(error);\n      }\n    }\n\n    this.currentUser = user;\n  },\n\n  /*rethrow: function (message, err, details, level = 'error') {\n    err = new RethrownError(message, err, { stack: true, remove: 1 });\n    Errors.log({ err, details, level });\n  },*/\n\n  log: function(params) {\n    const { message, err, level = 'error' } = params;\n    // processApolloErrors(err);\n\n    for (const fn of logFunctions) {\n      try {\n        fn(params);\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.log(`// ${fn.name} ${level} error for '${(err && err.message) || message}'`);\n        // eslint-disable-next-line no-console\n        console.log(error);\n      }\n    }\n  },\n\n  /*\n\n  Shortcuts\n\n  */\n  debug: params => Errors.log({ level: 'debug', ...params }),\n\n  info: params => Errors.log({ level: 'info', ...params }),\n\n  warning: params => Errors.log({ level: 'warning', ...params }),\n\n  error: params => Errors.log({ level: 'error', ...params }),\n\n  critical: params => Errors.log({ level: 'info', ...params }),\n\n  // getMethodDetails: function(method) {\n  //   return {\n  //     userId: method.userId,\n  //     headers: method.connection && method.connection.httpHeaders,\n  //   };\n  // },\n};\n"
  },
  {
    "path": "packages/vulcan-errors/lib/modules/extended-NOTUSED.js",
    "content": "// This is experimental and not actually used by vulcan:errors\n\n// ### ExtendedError\n// From https://github.com/deployable/deployable-errors\n\n// Custom errors can extend this\n\nexport default class ExtendedError extends Error {\n  constructor(message, options = {}) {\n    // Make it an error\n    super(message);\n\n    // Standard Error things\n    this.name = this.constructor.name;\n    this.message = message;\n\n    // Get a stack trace where we can\n    /* istanbul ignore else */\n    if (typeof Error.captureStackTrace === 'function') {\n      Error.captureStackTrace(this, this.constructor);\n    } else {\n      this.stack = new Error(message).stack;\n    }\n\n    // A standard place to store a more human readable error message\n    if (options.simple) this.simple = options.simple;\n  }\n\n  // Support `.statusCode` for express\n  get statusCode() {\n    return this.status;\n  }\n\n  set statusCode(val) {\n    this.status = val;\n  }\n\n  // Fix Errors `.toJSON` for our errors\n  toJSON() {\n    let o = {};\n    Object.getOwnPropertyNames(this).forEach(key => (o[key] = this[key]), this);\n    return o;\n  }\n\n  toResponse() {\n    let o = this.toJSON();\n    if (process && process.env && process.env.NODE_ENV !== 'development') delete o.stack;\n    return o;\n  }\n}\n"
  },
  {
    "path": "packages/vulcan-errors/lib/modules/index.js",
    "content": "import '../components/ErrorsUserMonitor';\nimport '../components/ErrorCatcher';\nexport * from './errors.js';"
  },
  {
    "path": "packages/vulcan-errors/lib/modules/rethrown-NOTUSED.js",
    "content": "import ExtendedError from './extended';\n\n// This is experimental and not actually used by vulcan:errors\n\n/**\n * Rethrow an error that you caught in your code, adding an additional message,\n * and preserving the stack trace\n *\n * Based on https://github.com/deployable/deployable-errors\n * See https://stackoverflow.com/questions/42754270/re-throwing-exception-in-nodejs-and-not-losing-stack-trace\n *\n * @example\n * try {\n *   ... some code\n * } catch (error) {\n *   new RethrownError('new error message', error, { stack: true });\n * }\n */\nexport default class RethrownError extends ExtendedError {\n  /**\n   * @param {string} message - An error message\n   * @param {Error} error - An Error caught in a catch block\n   * @param {Object} [options] - The employee who is responsible for the project.\n   * @param {boolean|number} [options.stack] - Enable, disable or set the number of lines of stack output\n   * @param {number} [options.remove] - The number of lines to remove from the beginning of the stack trace\n   */\n  constructor(message, error, options = {}) {\n    super(message);\n    if (!error) throw new Error(`new ${this.name} requires a message and error`);\n\n    let message_lines = (this.message.match(/\\n/g) || []).length + 1;\n    let stack_array = this.stack.split('\\n');\n\n    if (options.remove) {\n      stack_array.splice(message_lines, options.remove);\n    }\n\n    if (options.stack !== true) {\n      stack_array = stack_array.slice(0, message_lines + (options.stack || 0));\n    }\n\n    //this.original = error;\n    this.stack = stack_array.join('\\n') + '\\n' + error.stack;\n  }\n}\n"
  },
  {
    "path": "packages/vulcan-errors/lib/server/init.js",
    "content": "import { initFunctions } from '../modules/index.js';\n\nexport const addInitFunction = fn => {\n  initFunctions.push(fn); // on server, this does nothing\n  // on server, execute init function as soon as possible\n  fn();\n};"
  },
  {
    "path": "packages/vulcan-errors/lib/server/main.js",
    "content": "export * from './init.js';\nexport * from '../modules/index.js';\n"
  },
  {
    "path": "packages/vulcan-errors/package.js",
    "content": "Package.describe({\n  name: 'vulcan:errors',\n  summary: 'Vulcan error tracking package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "packages/vulcan-errors-sentry/README.md",
    "content": "Vulcan error tracking adapter for Sentry."
  },
  {
    "path": "packages/vulcan-errors-sentry/lib/client/main.js",
    "content": "export * from '../modules/index';\nimport './sentry-client.js';"
  },
  {
    "path": "packages/vulcan-errors-sentry/lib/client/sentry-client.js",
    "content": "import { getSetting } from 'meteor/vulcan:core';\nimport { addInitFunction, addLogFunction, addUserFunction } from 'meteor/vulcan:errors';\nimport Sentry from '@sentry/browser';\nimport { clientDSNSetting } from '../modules/settings';\nimport { getUserObject } from '../modules/sentry';\n\nconst clientDSN = getSetting(clientDSNSetting);\nconst environment = getSetting('environment');\n\n/*\n\nInitialize Sentry\n\n*/\nfunction initSentryForClient(props = {}) {\n  Sentry.init({\n    dsn: clientDSN,\n    environment,\n    release: props.siteData && props.siteData.sourceVersion\n  });\n}\naddInitFunction(initSentryForClient);\n\n/*\n\nLog an error, and optionally set current user as well\n\n*/\nfunction logToSentry({ error, details, currentUser }) {\n  Sentry.withScope(scope => {\n    if (currentUser) {\n      scope.setUser(getUserObject(currentUser));\n    }\n    Object.keys(details).forEach(key => {\n      scope.setExtra(key, details[key]);\n    });\n    Sentry.captureException(error);\n  });\n}\naddLogFunction(logToSentry);\n\n/*\n\nSet the current user\n\n*/\nfunction setSentryUser({ user }) {\n  Sentry.configureScope(scope => {\n    scope.setUser(getUserObject(user));\n  });\n}\naddUserFunction(setSentryUser);\n"
  },
  {
    "path": "packages/vulcan-errors-sentry/lib/modules/index.js",
    "content": "export * from './settings';\n// import './logToRollbar';\n"
  },
  {
    "path": "packages/vulcan-errors-sentry/lib/modules/sentry.js",
    "content": "export const getUserObject = currentUser => ({\n  id: currentUser._id,\n  username: currentUser.displayName,\n  email: currentUser.email,\n});"
  },
  {
    "path": "packages/vulcan-errors-sentry/lib/modules/settings.js",
    "content": "import { registerSetting } from 'meteor/vulcan:core';\n\nexport const clientDSNSetting = 'sentry.clientDSN';\nexport const serverDSNSetting = 'sentry.serverDSN';\nexport const tokensUrl = 'https://sentry.io/onboarding/{account}/{project}/configure/node';\n\nregisterSetting(clientDSNSetting, null, `Sentry client DSN access token (from ${tokensUrl})`);\nregisterSetting(serverDSNSetting, null, `Sentry client DSN access token (from ${tokensUrl})`);\n"
  },
  {
    "path": "packages/vulcan-errors-sentry/lib/server/main.js",
    "content": "import './sentry-server.js';\nexport * from '../modules/index';\n"
  },
  {
    "path": "packages/vulcan-errors-sentry/lib/server/sentry-server.js",
    "content": "import { getSetting, getSourceVersion } from 'meteor/vulcan:core';\nimport { addInitFunction, addLogFunction, addUserFunction } from 'meteor/vulcan:errors';\nimport { serverDSNSetting } from '../modules/settings';\nimport Sentry from '@sentry/node';\nimport { getUserObject } from '../modules/sentry';\n\nconst serverDSN = getSetting(serverDSNSetting);\nconst environment = getSetting('environment');\n\n/*\n\nInitialize Sentry\n\n*/\nfunction initSentryForServer() {\n  Sentry.init({\n    dsn: serverDSN,\n    environment,\n    // see https://github.com/zodern/meteor-up/issues/807#issuecomment-346915622\n    release: getSourceVersion(),\n  });\n}\naddInitFunction(initSentryForServer);\n\n/*\n\nLog an error, and optionally set current user as well\n\n*/\nfunction logToSentry({ error, details, currentUser }) {\n  Sentry.withScope(scope => {\n    if (currentUser) {\n      scope.setUser(getUserObject(currentUser));\n    }\n    Object.keys(details).forEach(key => {\n      scope.setExtra(key, details[key]);\n    });\n    Sentry.captureException(error);\n  });\n}\naddLogFunction(logToSentry);\n\n/*\n\nSet the current user\n\n*/\nfunction setSentryUser({ user }) {\n  Sentry.configureScope(scope => {\n    scope.setUser(getUserObject(user));\n  });\n}\naddUserFunction(setSentryUser);\n"
  },
  {
    "path": "packages/vulcan-errors-sentry/package.js",
    "content": "Package.describe({\n  name: 'vulcan:errors-sentry',\n  summary: 'Vulcan Sentry error tracking package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9', 'vulcan:users@=1.16.9', 'vulcan:errors@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "packages/vulcan-events/README.md",
    "content": "Vulcan events package, used internally. "
  },
  {
    "path": "packages/vulcan-events/lib/client/main.js",
    "content": "export * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-events/lib/modules/events.js",
    "content": "import { addCallback } from 'meteor/vulcan:core';\n\nexport const initFunctions = [];\n\nexport const trackFunctions = [];\n\nexport const addInitFunction = f => {\n  initFunctions.push(f);\n  // execute init function as soon as possible\n  f();\n};\n\nexport const addTrackFunction = f => {\n  trackFunctions.push(f);\n};\n\nexport const track = async (eventName, eventProperties, currentUser) => {\n  for (let f of trackFunctions) {\n    try {\n      // eslint-disable-next-line no-await-in-loop\n      await f(eventName, eventProperties, currentUser);\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.log(`// ${f.name} track error for event ${eventName}`);\n      // eslint-disable-next-line no-console\n      console.log(error);\n    }\n  }\n};\n\nexport const addUserFunction = f => {\n  addCallback('user.create.async', f);\n};\n\nexport const addIdentifyFunction = f => {\n  addCallback('events.identify', f);\n};\n\nexport const addPageFunction = f => {\n  const f2 = ({ currentRoute }) => f(currentRoute);\n\n  // rename f2 to same name as f\n  // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty\n  const descriptor = Object.create(null); // no inherited properties\n  descriptor.value = f.name;\n  Object.defineProperty(f2, 'name', descriptor);\n\n  addCallback('router.onupdate.async', f2);\n};\n"
  },
  {
    "path": "packages/vulcan-events/lib/modules/index.js",
    "content": "export * from './events';"
  },
  {
    "path": "packages/vulcan-events/lib/server/main.js",
    "content": "export * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-events/package.js",
    "content": "Package.describe({\n  name: 'vulcan:events',\n  summary: 'Vulcan event tracking package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "packages/vulcan-events-ga/README.md",
    "content": "Vulcan events package, used internally. "
  },
  {
    "path": "packages/vulcan-events-ga/lib/client/ga.js",
    "content": "import { getSetting } from 'meteor/vulcan:core';\nimport { addPageFunction, addInitFunction, addTrackFunction } from 'meteor/vulcan:events';\n\n/*\n\n  We provide a special support for Google Analytics.\n  \n  If you want to enable GA page viewing / tracking, go to\n  your settings file and update the 'public > googleAnalytics > apiKey' \n  field with your GA unique identifier (UA-xxx...).\n\n*/\n\nfunction googleAnaticsTrackPage() {\n  if (window && window.ga) {\n    window.ga('send', 'pageview', {\n      page: window.location.pathname,\n    });\n  }\n  return {};\n}\n// add client-side callback: log a ga request on page view\naddPageFunction(googleAnaticsTrackPage);\n\nfunction googleAnaticsTrackEvent(name, properties, currentUser) {\n  const { category = name, action = name, label = name, value } = properties;\n  if (window && window.ga) {\n    window.ga('send', {\n      hitType: 'event',\n      eventCategory: category,\n      eventAction: action,\n      eventLabel: label,\n      eventValue: value,\n    });\n  }\n  return {};\n}\n// add client-side callback: log a ga request on page view\naddTrackFunction(googleAnaticsTrackEvent);\n\nfunction googleAnalyticsInit() {\n  // get the google analytics id from the settings\n  const googleAnalyticsId = getSetting('googleAnalytics.apiKey');\n\n  // the google analytics id exists & isn't the placeholder from sample_settings.json\n  if (googleAnalyticsId && googleAnalyticsId !== 'foo123') {\n    (function(i, s, o, g, r, a, m) {\n      i['GoogleAnalyticsObject'] = r;\n      (i[r] =\n        i[r] ||\n        function() {\n          (i[r].q = i[r].q || []).push(arguments);\n        }),\n        (i[r].l = 1 * new Date());\n      (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]);\n      a.async = 1;\n      a.src = g;\n      m.parentNode.insertBefore(a, m);\n    })(\n      window,\n      document,\n      'script',\n      '//www.google-analytics.com/analytics.js',\n      'ga'\n    );\n\n    const cookieDomain = document.domain === 'localhost' ? 'none' : 'auto';\n\n    window.ga('create', googleAnalyticsId, cookieDomain);\n\n    // trigger first request once analytics are initialized\n    googleAnaticsTrackPage();\n  }\n}\n\n// init google analytics on the client module\naddInitFunction(googleAnalyticsInit);\n"
  },
  {
    "path": "packages/vulcan-events-ga/lib/client/main.js",
    "content": "import './ga.js';\nexport * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-events-ga/lib/modules/index.js",
    "content": "import { registerSetting } from 'meteor/vulcan:core';\n\nregisterSetting('googleAnalytics.apiKey', null, 'Google Analytics ID');\n"
  },
  {
    "path": "packages/vulcan-events-ga/lib/server/main.js",
    "content": "export * from '../modules/index.js';\n"
  },
  {
    "path": "packages/vulcan-events-ga/package.js",
    "content": "Package.describe({\n  name: 'vulcan:events-ga',\n  summary: 'Vulcan Google Analytics event tracking package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9', 'vulcan:events@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "packages/vulcan-events-intercom/README.md",
    "content": "Intercom package.\n\n### Settings\n\n```\n{\n  \"public\": {\n\n    \"intercom\": {\n      \"appId\": \"123foo\"\n    }\n\n  },\n  \n  \"intercom\": {\n    \"accessToken\": \"456bar\"\n  }\n}\n```\n\nRequires installing the [react-intercom](https://github.com/nhagen/react-intercom) package (`npm install --save react-intercom`)."
  },
  {
    "path": "packages/vulcan-events-intercom/lib/client/intercom-client.js",
    "content": "import { getSetting, /* addCallback, Utils */ } from 'meteor/vulcan:core';\nimport {\n  // addPageFunction,\n  addInitFunction,\n  addIdentifyFunction,\n  // addTrackFunction,\n} from 'meteor/vulcan:events';\n\n/*\n\nIdentify User\n\n*/\nfunction intercomIdentify(currentUser) {\n  // eslint-disable-next-line no-undef\n  intercomSettings = {\n    app_id: getSetting('intercom.appId'),\n    name: currentUser.displayName,\n    email: currentUser.email,\n    created_at: currentUser.createdAt,\n    user_id: currentUser._id,\n    pageUrl: currentUser.pageUrl,\n  };\n  (function() {\n    var w = window;\n    var ic = w.Intercom;\n    if (typeof ic === 'function') {\n      // ic('reattach_activator');\n      // eslint-disable-next-line no-undef\n      ic('update', intercomSettings);\n    } else {\n      intercomInit();\n    }\n  })();\n}\naddIdentifyFunction(intercomIdentify);\n\n/*\n\nTrack Event\n\n*/\n// function segmentTrack(eventName, eventProperties) {\n//   analytics.track(eventName, eventProperties);\n// }\n// addTrackFunction(segmentTrack);\n\n/*\n\nInit Snippet\n\n*/\nfunction intercomInit() {\n  window.intercomSettings = {\n    app_id: getSetting('intercom.appId'),\n  };\n  (function() {\n    var w = window;\n    var ic = w.Intercom;\n    if (typeof ic === 'function') {\n      ic('reattach_activator');\n      // eslint-disable-next-line no-undef\n      ic('update', intercomSettings);\n    } else {\n      var d = document;\n      var i = function() {\n        i.c(arguments);\n      };\n      i.q = [];\n      i.c = function(args) {\n        i.q.push(args);\n      };\n      w.Intercom = i;\n      // eslint-disable-next-line no-inner-declarations\n      function l() {\n        var s = d.createElement('script');\n        s.type = 'text/javascript';\n        s.async = true;\n        s.src = `https://widget.intercom.io/widget/${getSetting('intercom.appId')}`;\n        var x = d.getElementsByTagName('script')[0];\n        x.parentNode.insertBefore(s, x);\n      }\n      if (w.attachEvent) {\n        w.attachEvent('onload', l);\n      } else {\n        w.addEventListener('load', l, false);\n      }\n    }\n  })();\n}\naddInitFunction(intercomInit);\n"
  },
  {
    "path": "packages/vulcan-events-intercom/lib/client/main.js",
    "content": "import './intercom-client.js';\n\nexport * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-events-intercom/lib/modules/index.js",
    "content": ""
  },
  {
    "path": "packages/vulcan-events-intercom/lib/server/intercom-server.js",
    "content": "import Intercom from 'intercom-client';\nimport { Connectors, getSetting /* addCallback, Utils */ } from 'meteor/vulcan:core';\nimport {\n  // addPageFunction,\n  addUserFunction,\n  // addInitFunction,\n  // addIdentifyFunction,\n  addTrackFunction,\n} from 'meteor/vulcan:events';\nimport Users from 'meteor/vulcan:users';\n\nconst token = getSetting('intercom.accessToken');\n\nif (!token) {\n  throw new Error('Please add your Intercom access token as intercom.accessToken in settings.json');\n} else {\n  export const intercomClient = new Intercom.Client({ token });\n\n  const getDate = () =>\n    new Date()\n      .valueOf()\n      .toString()\n      .substr(0, 10);\n\n  /*\n\n  New User\n\n  */\n  // eslint-disable-next-line no-inner-declarations\n  async function intercomNewUser({ user }) {\n    await intercomClient.users.create({\n      email: user.email,\n      user_id: user._id,\n      custom_attributes: {\n        name: user.displayName,\n        profileUrl: Users.getProfileUrl(user, true),\n      },\n    });\n  }\n  addUserFunction(intercomNewUser);\n\n  /*\n\n  Track Event\n\n  */\n  // eslint-disable-next-line no-inner-declarations\n  function intercomTrackServer(eventName, eventProperties, currentUser = {}) {\n    intercomClient.events.create({\n      event_name: eventName,\n      created_at: getDate(),\n      email: currentUser.email,\n      user_id: currentUser._id,\n      metadata: {\n        ...eventProperties,\n      },\n    });\n  }\n  addTrackFunction(intercomTrackServer);\n}\n"
  },
  {
    "path": "packages/vulcan-events-intercom/lib/server/main.js",
    "content": "export * from '../modules/index.js';\nexport * from './intercom-server.js';\n"
  },
  {
    "path": "packages/vulcan-events-intercom/package.js",
    "content": "Package.describe({\n  name: 'vulcan:events-intercom',\n  summary: 'Vulcan Intercom integration package.',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9', 'vulcan:events@=1.16.9']);\n\n  api.mainModule('lib/client/main.js', 'client');\n  api.mainModule('lib/server/main.js', 'server');\n});\n"
  },
  {
    "path": "packages/vulcan-events-internal/README.md",
    "content": "Vulcan events package, used internally. "
  },
  {
    "path": "packages/vulcan-events-internal/lib/client/internal-client.js",
    "content": "import { addTrackFunction } from 'meteor/vulcan:events';\nimport { getApolloClient, getFragment, createClientTemplate } from 'meteor/vulcan:lib';\nimport gql from 'graphql-tag';\n\nfunction trackInternal(eventName, eventProperties) {\n  const apolloClient = getApolloClient();\n\n  const fragmentName = 'AnalyticsEventFragment';\n  const fragment = getFragment(fragmentName);\n\n  const mutation = gql`\n    ${createClientTemplate({ typeName: 'AnalyticsEvent', fragmentName })}\n    ${fragment}\n  `;\n\n  const variables = {\n    data: {\n      name: eventName,\n      properties: eventProperties,\n    },\n  };\n  apolloClient.mutate({ mutation, variables });\n}\n\naddTrackFunction(trackInternal);\n"
  },
  {
    "path": "packages/vulcan-events-internal/lib/client/main.js",
    "content": "import './internal-client.js';\n\nexport {default} from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-events-internal/lib/modules/collection.js",
    "content": "import { createCollection, getDefaultMutations } from 'meteor/vulcan:core';\nimport schema from './schema.js';\nimport Users from 'meteor/vulcan:users';\n\nconst Events = createCollection({\n  collectionName: 'AnalyticsEvents',\n\n  typeName: 'AnalyticsEvent',\n\n  schema,\n\n  mutations: {\n    update: null,\n    upsert: null,\n    delete: null\n  },\n\n  permissions: {\n    canRead: ({ user: currentUser }) => {\n      return Users.isAdmin(currentUser);\n    },\n    canCreate: ['guests'],\n    canUpdate: () => false,\n    canDelete: () => false,\n  },\n});\n\nexport default Events;\n"
  },
  {
    "path": "packages/vulcan-events-internal/lib/modules/fragments.js",
    "content": "import { registerFragment } from 'meteor/vulcan:core';\n\nregisterFragment(/* GraphQL */`\n  fragment AnalyticsEventFragment on AnalyticsEvent {\n    __typename\n    name\n    createdAt\n    userId\n    description\n    unique\n    important\n    properties\n    user {\n      _id\n      displayName\n      pagePath\n      pageUrl\n    }\n  }  \n`);"
  },
  {
    "path": "packages/vulcan-events-internal/lib/modules/index.js",
    "content": "import Events from './collection.js';\n\nimport './fragments';\n\nexport default Events;"
  },
  {
    "path": "packages/vulcan-events-internal/lib/modules/schema.js",
    "content": "const schema = {\n  createdAt: {\n    type: Date,\n    canRead: ['guests'],\n    optional: true,\n    onInsert: () => {\n      return new Date();\n    }\n  },\n  name: {\n    type: String,\n    canRead: ['guests'],\n    canCreate: ['guests'],\n  },\n  userId: {\n    type: String,\n    canRead: ['admins'],\n    optional: true,\n    resolveAs: {\n      fieldName: 'user',\n      typeName: 'User',\n      relation: 'hasOne',\n    },\n  },\n  description: {\n    type: String,\n    canRead: ['admins'],\n    optional: true,\n  },\n  unique: {\n    type: Boolean,\n    canRead: ['admins'],\n    optional: true,\n  },\n  important: {\n    // marking an event as important means it should never be erased\n    type: Boolean,\n    canRead: ['admins'],\n    optional: true,\n  },\n  properties: {\n    type: Object,\n    optional: true,\n    blackbox: true,\n    canRead: ['guests'],\n    canCreate: ['guests'],\n  },\n};\n\nexport default schema;"
  },
  {
    "path": "packages/vulcan-events-internal/lib/server/internal-server.js",
    "content": "import { addTrackFunction } from 'meteor/vulcan:events';\nimport { newMutation } from 'meteor/vulcan:lib';\nimport Events from '../modules/collection';\n\nasync function trackInternalServer(eventName, eventProperties, currentUser) {\n  const document = {\n    name: eventName,\n    properties: eventProperties,\n  };\n  return await newMutation({\n    collection: Events,\n    document,\n    currentUser,\n    validate: false,\n    context: {},\n  });\n}\n\naddTrackFunction(trackInternalServer);\n"
  },
  {
    "path": "packages/vulcan-events-internal/lib/server/main.js",
    "content": "import './internal-server';\n\nexport {default} from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-events-internal/package.js",
    "content": "Package.describe({\n  name: 'vulcan:events-internal',\n  summary: 'Vulcan internal event tracking package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9', 'vulcan:events@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "packages/vulcan-events-segment/README.md",
    "content": "Vulcan events package, used internally. "
  },
  {
    "path": "packages/vulcan-events-segment/lib/client/main.js",
    "content": "export * from '../modules/index';\n\nimport './segment-client.js';"
  },
  {
    "path": "packages/vulcan-events-segment/lib/client/segment-client.js",
    "content": "import {getSetting, Utils} from 'meteor/vulcan:core';\nimport {\n  addPageFunction,\n  addInitFunction,\n  addIdentifyFunction,\n  addTrackFunction,\n} from 'meteor/vulcan:events';\n\nconst segmentClientKey = getSetting('segment.clientKey');\n\nif (segmentClientKey) {\n  /*\n\n  Track Page\n\n  */\n  // eslint-disable-next-line no-inner-declarations\n  function segmentTrackPage(route) {\n    const {name, path} = route;\n    const properties = {\n      url: Utils.getSiteUrl().slice(0, -1) + path,\n      path,\n    };\n    window.analytics.page(null, name, properties);\n    return {};\n  }\n\n  addPageFunction(segmentTrackPage);\n\n  /*\n\n  Identify User\n\n  */\n  // eslint-disable-next-line no-inner-declarations\n  function segmentIdentify(currentUser) {\n    window.analytics.identify(currentUser._id, {\n      email: currentUser.email,\n      pageUrl: currentUser.pageUrl,\n    });\n  }\n\n  addIdentifyFunction(segmentIdentify);\n\n  /*\n\n  Track Event\n\n  */\n  // eslint-disable-next-line no-inner-declarations\n  function segmentTrack(eventName, eventProperties) {\n    window.analytics.track(eventName, eventProperties);\n  }\n\n  addTrackFunction(segmentTrack);\n\n  /*\n\n  Init Snippet\n\n  */\n  // eslint-disable-next-line no-inner-declarations\n  function segmentInit() {\n    !function () {\n      var analytics = window.analytics = window.analytics || [];\n      if (!analytics.initialize)\n        if (analytics.invoked)\n          // eslint-disable-next-line no-console\n          window.console && console.error && console.error('Segment snippet included twice.');\n        else {\n          analytics.invoked = !0;\n          analytics.methods =\n            ['trackSubmit', 'trackClick', 'trackLink', 'trackForm', 'pageview', 'identify',\n              'reset', 'group', 'track', 'ready', 'alias', 'debug', 'page', 'once', 'off', 'on'];\n          analytics.factory = function (t) {\n            return function () {\n              var e = Array.prototype.slice.call(arguments);\n              e.unshift(t);\n              analytics.push(e);\n              return analytics;\n            };\n          };\n          for (var t = 0; t < analytics.methods.length; t++) {\n            var e = analytics.methods[t];\n            analytics[e] = analytics.factory(e);\n          }\n          analytics.load = function (t, e) {\n            var n = document.createElement('script');\n            n.type = 'text/javascript';\n            n.async = !0;\n            n.src = 'https://cdn.segment.com/analytics.js/v1/' + t + '/analytics.min.js';\n            var a = document.getElementsByTagName('script')[0];\n            a.parentNode.insertBefore(n, a);\n            analytics._loadOptions = e;\n          };\n          analytics.SNIPPET_VERSION = '4.1.0';\n          analytics.load(getSetting('segment.clientKey'));\n        }\n    }();\n  }\n\n  addInitFunction(segmentInit);\n}\n"
  },
  {
    "path": "packages/vulcan-events-segment/lib/modules/index.js",
    "content": "import { registerSetting } from 'meteor/vulcan:core';\n\nregisterSetting('segment.clientKey', null, 'Segment client-side API key');\nregisterSetting('segment.serverKey', null, 'Segment server-side API key');\n"
  },
  {
    "path": "packages/vulcan-events-segment/lib/server/main.js",
    "content": "// export * from '../modules/index';\n\nexport * from './segment-server.js';"
  },
  {
    "path": "packages/vulcan-events-segment/lib/server/segment-server.js",
    "content": "import Analytics from 'analytics-node';\nimport { getSetting } from 'meteor/vulcan:core';\nimport { addIdentifyFunction, addTrackFunction } from 'meteor/vulcan:events';\n\nconst segmentWriteKey = getSetting('segment.serverKey');\nlet lastIdentifiedUser = null;\n\nif (segmentWriteKey) {\n\n  const analytics = new Analytics(segmentWriteKey);\n\n  /*\n\n  Identify User\n\n  */\n  // eslint-disable-next-line no-inner-declarations\n  function segmentIdentifyServer(currentUser) {\n    const identifiedUser = {\n      userId: currentUser._id,\n      traits: {\n        email: currentUser.email,\n        pageUrl: currentUser.pageUrl,\n      },\n    };\n    \n    if (!identifiedUser.traits.pageUrl) {\n      return;\n    }\n    \n    if (identifiedUser.userId === (lastIdentifiedUser && lastIdentifiedUser.userId) &&\n      identifiedUser.traits.email === (lastIdentifiedUser && lastIdentifiedUser.traits.email) &&\n      identifiedUser.traits.pageUrl === (lastIdentifiedUser && lastIdentifiedUser.traits.pageUrl)\n    ) {\n      return;\n    }\n    \n    analytics.identify(identifiedUser);\n    lastIdentifiedUser = identifiedUser;\n  }\n  addIdentifyFunction(segmentIdentifyServer);\n\n  /*\n\n  Track Event\n\n  */\n  // eslint-disable-next-line no-inner-declarations\n  function segmentTrackServer(eventName, eventProperties, currentUser) {\n    analytics.track({\n      event: eventName,\n      properties: eventProperties,\n      userId: currentUser && currentUser._id,\n    });\n  }\n  addTrackFunction(segmentTrackServer);\n}\n"
  },
  {
    "path": "packages/vulcan-events-segment/package.js",
    "content": "Package.describe({\n  name: 'vulcan:events-segment',\n  summary: 'Vulcan Segment',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9', 'vulcan:events@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "packages/vulcan-forms/README.md",
    "content": "# Vulcan Forms\n\nThis package provides a `SmartForm` component.\n"
  },
  {
    "path": "packages/vulcan-forms/lib/client/main.js",
    "content": "export * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FieldErrors.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent, Components } from 'meteor/vulcan:core';\n\nconst FieldErrors = ({ errors }) => (\n  <ul className=\"form-input-errors\">\n    {errors.map((error, index) => (\n      <li key={index}>\n        <Components.FormError error={error} errorContext=\"field\" />\n      </li>\n    ))}\n  </ul>\n);\nFieldErrors.propTypes = {\n  errors: PropTypes.array.isRequired\n};\nregisterComponent('FieldErrors', FieldErrors);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/Form.jsx",
    "content": "/*\n\nMain form component.\n\nThis component expects:\n\n### All Forms:\n\n- collection\n- currentUser\n- client (Apollo client)\n\n*/\n\nimport {\n  registerComponent,\n  Components,\n  runCallbacks,\n  getErrors,\n  getSetting,\n  Utils,\n  isIntlField,\n  mergeWithComponents,\n  formatLabel,\n  getIntlLabel,\n  getIntlKeys,\n} from 'meteor/vulcan:core';\nimport React, { Component } from 'react';\nimport SimpleSchema from 'simpl-schema';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport cloneDeep from 'lodash/cloneDeep';\nimport get from 'lodash/get';\nimport set from 'lodash/set';\nimport unset from 'lodash/unset';\nimport compact from 'lodash/compact';\nimport update from 'lodash/update';\nimport merge from 'lodash/merge';\nimport find from 'lodash/find';\nimport pick from 'lodash/pick';\nimport isEqual from 'lodash/isEqual';\nimport isEqualWith from 'lodash/isEqualWith';\nimport uniq from 'lodash/uniq';\nimport uniqBy from 'lodash/uniqBy';\nimport isObject from 'lodash/isObject';\nimport mapValues from 'lodash/mapValues';\nimport pickBy from 'lodash/pickBy';\nimport omit from 'lodash/omit';\nimport without from 'lodash/without';\nimport _filter from 'lodash/filter';\nimport omitBy from 'lodash/omitBy';\n\nimport { convertSchema, formProperties } from '../modules/schema_utils';\nimport { isEmptyValue } from '../modules/utils';\nimport { getParentPath } from '../modules/path_utils';\nimport { getEditableFields, getInsertableFields } from '../modules/schema_utils.js';\nimport withCollectionProps from './withCollectionProps';\nimport { callbackProps } from './propTypes';\n\n// props that should trigger a form reset\nconst RESET_PROPS = [\n  'collection',\n  'collectionName',\n  'typeName',\n  'document',\n  'schema',\n  'currentUser',\n  'fields',\n  'removeFields',\n  'prefilledProps', // TODO: prefilledProps should be merged instead?\n];\n\nconst compactParent = (object, path) => {\n  const parentPath = getParentPath(path);\n\n  // note: we only want to compact arrays, not objects\n  const compactIfArray = x => (Array.isArray(x) ? compact(x) : x);\n\n  update(object, parentPath, compactIfArray);\n};\n\nconst getDefaultValues = convertedSchema => {\n  // TODO: make this work with nested schemas, too\n  return pickBy(mapValues(convertedSchema, field => field.defaultValue), value => value);\n};\n\nconst compactObject = o => omitBy(o, f => f === null || f === undefined);\n\nconst getInitialStateFromProps = nextProps => {\n  const collection = nextProps.collection;\n  const schema = nextProps.schema ? new SimpleSchema(nextProps.schema) : collection.simpleSchema();\n  const convertedSchema = convertSchema(schema);\n  const formType = nextProps.document ? 'edit' : 'new';\n  // for new document forms, add default values to initial document\n  const defaultValues = formType === 'new' ? getDefaultValues(convertedSchema) : {};\n  // note: we remove null/undefined values from the loaded document so they don't overwrite possible prefilledProps\n  const initialDocument = merge({}, defaultValues, nextProps.prefilledProps, compactObject(nextProps.document));\n\n  //if minCount is specified, go ahead and create empty nested documents\n  Object.keys(convertedSchema).forEach(key => {\n    let minCount = convertedSchema[key].minCount;\n    if (minCount) {\n      initialDocument[key] = initialDocument[key] || [];\n      while (initialDocument[key].length < minCount) initialDocument[key].push({});\n    }\n  });\n\n  // remove all instances of the `__typename` property from document\n  Utils.removeProperty(initialDocument, '__typename');\n\n  return {\n    disabled: nextProps.disabled,\n    errors: [],\n    deletedValues: [],\n    currentValues: {},\n    originalSchema: convertSchema(schema, { removeArrays: false }),\n    // convert SimpleSchema schema into JSON object\n    schema: convertedSchema,\n    // Also store all field schemas (including nested schemas) in a flat structure\n    flatSchema: convertSchema(schema, { flatten: true }),\n    // the initial document passed as props\n    initialDocument,\n    // initialize the current document to be the same as the initial document\n    currentDocument: initialDocument,\n  };\n};\n\n/*\n\n1. Constructor\n2. Helpers\n3. Errors\n4. Context\n4. Method & Callback\n5. Render\n\n*/\n\nclass SmartForm extends Component {\n  constructor(props) {\n    super(props);\n    const state = getInitialStateFromProps(props);\n    this.state = {\n      ...state,\n    };\n    if(props.initCallback) props.initCallback(state.currentDocument);\n  }\n\n  defaultValues = {};\n\n  submitFormCallbacks = [];\n  successFormCallbacks = [];\n  failureFormCallbacks = [];\n\n  // --------------------------------------------------------------------- //\n  // ------------------------------- Helpers ----------------------------- //\n  // --------------------------------------------------------------------- //\n\n  /*\n  If a document is being passed, this is an edit form\n  */\n  getFormType = () => {\n    return this.props.document ? 'edit' : 'new';\n  };\n\n  /*\n  Get a list of all insertable fields\n  */\n  getInsertableFields = schema => {\n    return getInsertableFields(schema || this.state.schema, this.props.currentUser);\n  };\n\n  /*\n  Get a list of all editable fields\n  */\n  getEditableFields = schema => {\n    return getEditableFields(schema || this.state.schema, this.props.currentUser, this.state.initialDocument);\n  };\n\n  /*\n\n  Get a list of all mutable (insertable/editable depending on current form type) fields\n\n  */\n  getMutableFields = schema => {\n    return this.getFormType() === 'edit' ? this.getEditableFields(schema) : this.getInsertableFields(schema);\n  };\n\n  /*\n\n  Get the current document\n\n  */\n  getDocument = () => {\n    return this.state.currentDocument;\n  };\n\n  /*\n\n  Like getDocument, but cross-reference with getFieldNames()\n  to only return fields that actually need to be submitted\n\n  Also remove any deleted values.\n\n  */\n  getData = customArgs => {\n    // we want to keep prefilled data even for hidden/removed fields\n    let data = this.props.prefilledProps || {};\n\n    // omit prefilled props for nested fields\n    data = omitBy(data, (value, key) => key.endsWith('.$'));\n\n    const args = {\n      excludeRemovedFields: false,\n      excludeHiddenFields: false,\n      replaceIntlFields: true,\n      addExtraFields: false,\n      ...customArgs,\n    };\n\n    // only keep relevant fields\n    // for intl fields, make sure we look in foo_intl and not foo\n    const fields = this.getFieldNames(args);\n    data = { ...data, ...pick(this.getDocument(), ...fields) };\n\n    // compact deleted values\n    this.state.deletedValues.forEach(path => {\n      if (path.includes('.')) {\n        /*\n\n        If deleted field is a nested field, nested array, or nested array item, try to compact its parent array\n\n        - Nested field: 'address.city'\n        - Nested array: 'addresses.1'\n        - Nested array item: 'addresses.1.city'\n\n        */\n        compactParent(data, path);\n      }\n    });\n\n    // run data object through submitForm callbacks\n    data = runCallbacks({\n      callbacks: this.submitFormCallbacks,\n      iterator: data,\n      properties: { form: this },\n    });\n\n    return data;\n  };\n\n  /*\n\n  Get form components, in case any has been overwritten for this specific form\n\n  */\n  getMergedComponents = () => mergeWithComponents(this.props.components || this.props.formComponents);\n\n  // --------------------------------------------------------------------- //\n  // -------------------------------- Fields ----------------------------- //\n  // --------------------------------------------------------------------- //\n\n  /*\n\n  Get all field groups\n\n  */\n  getFieldGroups = () => {\n    // build fields array by iterating over the list of field names\n    let fields = this.getFieldNames().map(fieldName => {\n      // get schema for the current field\n      return this.createField(fieldName, this.state.schema);\n    });\n\n    fields = _.sortBy(fields, 'order');\n\n    // get list of all unique groups (based on their name) used in current fields\n    let groups = _.compact(uniqBy(_.pluck(fields, 'group'), g => g && g.name));\n\n    // for each group, add relevant fields\n    groups = groups.map(group => {\n      group.label = group.label || this.context.intl.formatMessage({ id: group.name }) || Utils.capitalize(group.name);\n      group.fields = _.filter(fields, field => {\n        return field.group && field.group.name === group.name;\n      });\n      return group;\n    });\n\n    // add default group if necessary\n    const defaultGroupFields = _filter(fields, field => !field.group);\n    if (defaultGroupFields.length) {\n      groups = [\n        {\n          name: 'default',\n          label: 'default',\n          order: 0,\n          fields: defaultGroupFields,\n        },\n      ].concat(groups);\n    }\n\n    // sort by order\n    groups = _.sortBy(groups, 'order');\n\n    // console.log(groups);\n\n    return groups;\n  };\n\n  /*\n\n  Get a list of the fields to be included in the current form\n\n  Note: when submitting the form (getData()), do not include any extra fields.\n\n  */\n  getFieldNames = args => {\n    // we do this to avoid having default values in arrow functions, which breaks MS Edge support. See https://github.com/meteor/meteor/issues/10171\n    let args0 = args || {};\n    const {\n      schema = this.state.schema,\n      excludeHiddenFields = true,\n      excludeRemovedFields = true,\n      replaceIntlFields = false,\n      addExtraFields = true,\n    } = args0;\n\n    const { fields, addFields } = this.props;\n\n    // get all editable/insertable fields (depending on current form type)\n    let relevantFields = this.getMutableFields(schema);\n    const allFields = Object.keys(schema);\n\n    // if \"fields\" prop is specified, restrict list of fields to it\n    if (typeof fields !== 'undefined' && fields.length > 0) {\n      const nonSchemaFields = _.difference(fields, allFields);\n      if (nonSchemaFields.length > 0) {\n        // eslint-disable-next-line no-console\n        console.warn(`Unknown field names in 'fields' prop: ${nonSchemaFields.join(', ')}`);\n      }\n      relevantFields = _.intersection(relevantFields, fields);\n    }\n\n    // if \"hideFields\" prop is specified, remove its fields\n    if (excludeRemovedFields) {\n      // OpenCRUD backwards compatibility\n      const removeFields = this.props.removeFields || this.props.hideFields;\n      if (typeof removeFields !== 'undefined' && removeFields.length > 0) {\n        const nonSchemaFields = _.difference(removeFields, allFields);\n        if (nonSchemaFields.length > 0) {\n        // eslint-disable-next-line no-console\n          console.warn(`Unknown field names in 'removeFields' prop: ${nonSchemaFields.join(', ')}`);\n        }\n        relevantFields = _.difference(relevantFields, removeFields);\n      }\n    }\n\n    // if \"addFields\" prop is specified, add its fields\n    if (addExtraFields && typeof addFields !== 'undefined' && addFields.length > 0) {\n      const nonSchemaFields = _.difference(addFields, allFields);\n      if (nonSchemaFields.length > 0) {\n        // eslint-disable-next-line no-console\n        console.warn(`Unknown field names in 'addFields' prop: ${nonSchemaFields.join(', ')}`);\n      }\n      relevantFields = relevantFields.concat(addFields);\n    }\n\n    // remove all hidden fields\n    if (excludeHiddenFields) {\n      const document = this.getDocument();\n      relevantFields = _.reject(relevantFields, fieldName => {\n        const hidden = schema[fieldName].hidden;\n        return typeof hidden === 'function' ? hidden({ ...this.props, document }) : hidden;\n      });\n    }\n\n    // replace intl fields\n    if (replaceIntlFields) {\n      relevantFields = relevantFields.map(fieldName => (isIntlField(schema[fieldName]) ? `${fieldName}_intl` : fieldName));\n    }\n\n    // remove any duplicates\n    relevantFields = uniq(relevantFields);\n\n    return relevantFields;\n  };\n\n  initField = (fieldName, fieldSchema) => {\n    const isArray = get(fieldSchema, 'type.0.type') === Array;\n\n    // intialize properties\n    let field = {\n      ..._.pick(fieldSchema, formProperties),\n      name: fieldName,\n      datatype: fieldSchema.type,\n      layout: this.props.layout,\n      input: fieldSchema.input || fieldSchema.control,\n    };\n\n    // if this is an array field also store its array item type\n    if (isArray) {\n      const itemFieldSchema = this.state.originalSchema[`${fieldName}.$`];\n      field.itemDatatype = get(itemFieldSchema, 'type.0.type');\n    }\n\n    field.label = this.getLabel(fieldName);\n    field.intlKeys = this.getIntlKeys(fieldName);\n    // // replace value by prefilled value if value is empty\n    // const prefill = fieldSchema.prefill || (fieldSchema.form && fieldSchema.form.prefill);\n    // if (prefill) {\n    //   const prefilledValue = typeof prefill === 'function' ? prefill.call(fieldSchema) : prefill;\n    //   if (!!prefilledValue && !field.value) {\n    //     field.prefilledValue = prefilledValue;\n    //     field.value = prefilledValue;\n    //   }\n    // }\n\n    const document = this.getDocument();\n    field.document = document;\n\n    // internationalize field options labels\n    if (field.options && Array.isArray(field.options)) {\n      field.options = field.options.map(option => ({ ...option, label: this.getOptionLabel(fieldName, option) }));\n    }\n\n    // if this an intl'd field, use a special intlInput\n    if (isIntlField(fieldSchema)) {\n      field.intlInput = true;\n    }\n\n    // add any properties specified in fieldSchema.form as extra props passed on\n    // to the form component, calling them if they are functions\n    const inputProperties = fieldSchema.form || fieldSchema.inputProperties || {};\n    for (const prop in inputProperties) {\n      const property = inputProperties[prop];\n      field[prop] =\n          typeof property === 'function' ?\n              property.call(fieldSchema, { ...this.props, fieldName, document, intl: this.context.intl }) :\n              property;\n    }\n\n    // add description as help prop\n    const description = this.getDescription(fieldName);\n    if (description) {\n      field.help = description;\n    }\n\n    return field;\n  };\n  handleFieldPath = (field, fieldName, parentPath) => {\n    const fieldPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;\n    field.path = fieldPath;\n    if (field.defaultValue) {\n      set(this.defaultValues, fieldPath, field.defaultValue);\n    }\n    return field;\n  };\n  handleFieldParent = (field, parentFieldName) => {\n    // if field has a parent field, pass it on\n    if (parentFieldName) {\n      field.parentFieldName = parentFieldName;\n    }\n\n    return field;\n  };\n  handlePermissions = (field, fieldName, schema) => {\n    // if field is not creatable/updatable, disable it\n    if (!this.getMutableFields(schema).includes(fieldName)) {\n      field.disabled = true;\n    }\n    return field;\n  };\n  handleFieldChildren = (field, fieldName, fieldSchema, schema) => {\n    // array field\n    if (fieldSchema.arrayFieldSchema) {\n      field.arrayFieldSchema = fieldSchema.arrayFieldSchema;\n      // create a field that can be exploited by the form\n      field.arrayField = this.createArraySubField(fieldName, field.arrayFieldSchema, schema);\n      //field.nestedInput = true\n    }\n    // nested fields: set input to \"nested\"\n    if (fieldSchema.schema) {\n      field.nestedSchema = fieldSchema.schema;\n      field.nestedInput = true;\n\n      // get nested schema\n      // for each nested field, get field object by calling createField recursively\n      field.nestedFields = this.getFieldNames({\n        schema: field.nestedSchema,\n        addExtraFields: false,\n      }).map(subFieldName => {\n        return this.createField(subFieldName, field.nestedSchema, fieldName, field.path);\n      });\n    }\n    return field;\n  };\n\n  /*\n  Given a field's name, the containing schema, and parent, create the\n  complete field object to be passed to the component\n\n  */\n  createField = (fieldName, schema, parentFieldName, parentPath) => {\n    const fieldSchema = schema[fieldName];\n    let field = this.initField(fieldName, fieldSchema);\n    field = this.handleFieldPath(field, fieldName, parentPath);\n    field = this.handleFieldParent(field, parentFieldName);\n    field = this.handlePermissions(field, fieldName, schema);\n    field = this.handleFieldChildren(field, fieldName, fieldSchema, schema);\n    return field;\n  };\n  createArraySubField = (fieldName, subFieldSchema, schema) => {\n    const subFieldName = `${fieldName}.$`;\n    let subField = this.initField(subFieldName, subFieldSchema);\n    // array subfield has the same path and permissions as its parent\n    // so we use parent name (fieldName) and not subfieldName\n    subField = this.handleFieldPath(subField, fieldName);\n    subField = this.handlePermissions(subField, fieldName, schema);\n    // we do not allow nesting yet\n    //subField = this.handleFieldChildren(field, fieldSchema)\n    return subField;\n  };\n\n  /*\n\n  Get a field's intl keys (useful for debugging)\n\n  */\n  getIntlKeys = fieldName => {\n    const collectionName = this.props.collectionName.toLowerCase();\n    return getIntlKeys({\n      fieldName: fieldName,\n      collectionName,\n      schema: this.state.flatSchema,\n    });\n  };\n\n  /*\n\n   Get a field's label\n\n   */\n  getLabel = (fieldName, fieldLocale) => {\n    const collectionName = this.props.collectionName.toLowerCase();\n    const label = formatLabel({\n      intl: this.context.intl,\n      fieldName: fieldName,\n      collectionName: collectionName,\n      schema: this.state.flatSchema,\n    });\n    if (fieldLocale) {\n      const intlFieldLocale = this.context.intl.formatMessage({\n        id: `locales.${fieldLocale}`,\n        defaultMessage: fieldLocale,\n      });\n      return `${label} (${intlFieldLocale})`;\n    } else {\n      return label;\n    }\n  };\n\n  /*\n\n   Get a field's description\n\n   (Same as getLabel but pass isDescription: true )\n   */\n  getDescription = (fieldName) => {\n    const collectionName = this.props.collectionName.toLowerCase();\n    const description = getIntlLabel({\n      intl: this.context.intl,\n      fieldName: fieldName,\n      collectionName: collectionName,\n      schema: this.state.flatSchema,\n      isDescription: true,\n    });\n    return description || null;\n  }\n\n  /*\n\n  Get a field option's label\n\n  */\n  getOptionLabel = (fieldName, option) => {\n    const collectionName = this.props.collectionName.toLowerCase();\n    const intlId = option.intlId || `${collectionName}.${fieldName}.${option.value}`;\n    return this.context.intl.formatMessage({\n      id: intlId,\n      defaultMessage: option.label,\n    });\n  };\n\n  // --------------------------------------------------------------------- //\n  // ------------------------------- Errors ------------------------------ //\n  // --------------------------------------------------------------------- //\n\n  /*\n\n  Add error to form state\n\n  Errors can have the following properties:\n    - id: used as an internationalization key, for example `errors.required`\n    - path: for field-specific errors, the path of the field with the issue\n    - properties: additional data. Will be passed to vulcan-i18n as values\n    - message: if id cannot be used as i81n key, message will be used\n\n  */\n  throwError = error => {\n    let formErrors = getErrors(error);\n\n    // eslint-disable-next-line no-console\n    console.log(formErrors);\n\n    // add error(s) to state\n    this.setState(prevState => ({\n      errors: [...prevState.errors, ...formErrors],\n    }));\n  };\n\n  /*\n\n  Clear errors for a field\n\n  */\n  clearFieldErrors = path => {\n    this.setState((prevState) => ({\n      errors: prevState.errors.filter(error => error.path !== path)\n    }));\n  };\n\n  // --------------------------------------------------------------------- //\n  // ------------------------------- Context ----------------------------- //\n  // --------------------------------------------------------------------- //\n\n  // add something to deleted values\n  addToDeletedValues = name => {\n    this.setState(prevState => ({\n      deletedValues: [...prevState.deletedValues, name],\n    }));\n  };\n\n  // add a callback to the form submission\n  addToSubmitForm = callback => {\n    this.submitFormCallbacks.push(callback);\n  };\n\n  // add a callback to form submission success\n  addToSuccessForm = callback => {\n    this.successFormCallbacks.push(callback);\n  };\n\n  // add a callback to form submission failure\n  addToFailureForm = callback => {\n    this.failureFormCallbacks.push(callback);\n  };\n\n  clearFormCallbacks = () => {\n    this.submitFormCallbacks = [];\n    this.successFormCallbacks = [];\n    this.failureFormCallbacks = [];\n  };\n\n  setFormState = fn => {\n    this.setState(fn);\n  };\n\n  submitFormContext = newValues => {\n    // keep the previous ones and extend (with possible replacement) with new ones\n    this.setState(\n      prevState => ({\n        currentValues: {\n          ...prevState.currentValues,\n          ...newValues,\n        }, // Submit form after setState update completed\n      }),\n      () => this.submitForm()\n    );\n  };\n\n  // pass on context to all child components\n  getChildContext = () => {\n    return {\n      throwError: this.throwError,\n      clearForm: this.clearForm,\n      refetchForm: this.refetchForm,\n      isChanged: this.isChanged,\n      submitForm: this.submitFormContext, //Change in name because we already have a function\n      // called submitForm, but no reason for the user to know\n      // about that\n      addToDeletedValues: this.addToDeletedValues,\n      updateCurrentValues: this.updateCurrentValues,\n      getDocument: this.getDocument,\n      getLabel: this.getLabel,\n      initialDocument: this.state.initialDocument,\n      setFormState: this.setFormState,\n      addToSubmitForm: this.addToSubmitForm,\n      addToSuccessForm: this.addToSuccessForm,\n      addToFailureForm: this.addToFailureForm,\n      clearFormCallbacks: this.clearFormCallbacks,\n      errors: this.state.errors,\n      currentValues: this.state.currentValues,\n      deletedValues: this.state.deletedValues,\n    };\n  };\n\n  // --------------------------------------------------------------------- //\n  // ------------------------------ Lifecycle ---------------------------- //\n  // --------------------------------------------------------------------- //\n\n  /*\n\n  When props change, reinitialize the form  state\n  Triggered only for data related props (collection, document, currentUser etc.)\n\n  @see https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html\n\n  */\n  UNSAFE_componentWillReceiveProps(nextProps) {\n    const needReset = !!RESET_PROPS.find(prop => !isEqual(this.props[prop], nextProps[prop]));\n    if (needReset) {\n      const newState = getInitialStateFromProps(nextProps);\n      this.setState(newState);\n      if (nextProps.initCallback) nextProps.initCallback(newState.currentDocument);\n    }\n  }\n\n  /*\n\n  Manually update the current values of one or more fields(i.e. on change or blur).\n\n  */\n  updateCurrentValues = (newValues, options = {}) => {\n    // default to overwriting old value with new\n    const { mode = 'overwrite' } = options;\n    const { changeCallback } = this.props;\n\n    // keep the previous ones and extend (with possible replacement) with new ones\n    this.setState(prevState => {\n      // keep only the relevant properties\n      const newState = {\n        currentValues: cloneDeep(prevState.currentValues),\n        currentDocument: cloneDeep(prevState.currentDocument),\n        deletedValues: cloneDeep(prevState.deletedValues),\n      };\n\n      Object.keys(newValues).forEach(key => {\n        const path = key;\n        let value = newValues[key];\n\n        if (isEmptyValue(value)) {\n          // delete value\n          unset(newState.currentValues, path);\n          set(newState.currentDocument, path, null);\n          newState.deletedValues = [...newState.deletedValues, path];\n        } else {\n          // 1. update currentValues\n          set(newState.currentValues, path, value);\n\n          // 2. update currentDocument\n          // For arrays and objects, give option to merge instead of overwrite\n          if (mode === 'merge' && (Array.isArray(value) || isObject(value))) {\n            const oldValue = get(newState.currentDocument, path);\n            set(newState.currentDocument, path, merge(oldValue, value));\n          } else {\n            set(newState.currentDocument, path, value);\n          }\n\n          // 3. in case value had previously been deleted, \"undelete\" it\n          newState.deletedValues = without(newState.deletedValues, path);\n        }\n      });\n      if (changeCallback) changeCallback(newState.currentDocument);\n      return newState;\n    });\n  };\n\n  /*\n\n  Install a route leave hook to warn the user if there are unsaved changes\n\n  */\n  componentDidMount = () => {\n    this.checkRouteChange();\n    this.checkBrowserClosing();\n  };\n\n  /*\n  Remove the closing browser check on component unmount\n  see https://gist.github.com/mknabe/bfcb6db12ef52323954a28655801792d\n  */\n  componentWillUnmount = () => {\n    if (this.getWarnUnsavedChanges()) {\n      // unblock route change\n      if (this.unblock) {\n        this.unblock();\n      }\n      // unblock browser change\n      window.onbeforeunload = undefined; //undefined instead of null to support IE\n    }\n  };\n\n  // -------------------- Check on form leaving ----- //\n\n  /**\n   * Check if we must warn user on unsaved change\n   */\n  getWarnUnsavedChanges = () => {\n    let warnUnsavedChanges = getSetting('forms.warnUnsavedChanges');\n    if (typeof this.props.warnUnsavedChanges === 'boolean') {\n      warnUnsavedChanges = this.props.warnUnsavedChanges;\n    }\n    return warnUnsavedChanges;\n  };\n\n  // check for route change, prevent form content loss\n  checkRouteChange = () => {\n    // @see https://github.com/ReactTraining/react-router/issues/4635#issuecomment-297828995\n    // @see https://github.com/ReactTraining/history#blocking-transitions\n    if (this.getWarnUnsavedChanges()) {\n      this.unblock = this.props.history.block((location, action) => {\n        // return the message that will pop into a window.confirm alert\n        // if returns nothing, the message won't appear and the user won't be blocked\n        return this.handleRouteLeave();\n\n        /*\n            // React-router 3 implementtion\n            const routes = this.props.router.routes;\n            const currentRoute = routes[routes.length - 1];\n            this.props.router.setRouteLeaveHook(currentRoute, this.handleRouteLeave);\n\n            */\n      });\n    }\n  };\n  // check for browser closing\n  checkBrowserClosing = () => {\n    //check for closing the browser with unsaved changes too\n    window.onbeforeunload = this.handlePageLeave;\n  };\n\n  /*\n  Check if the user has unsaved changes, returns a message if yes\n  and nothing if not\n  */\n  handleRouteLeave = () => {\n    if (this.isChanged()) {\n      const message = this.context.intl.formatMessage({\n        id: 'forms.confirm_discard',\n        defaultMessage: 'Are you sure you want to discard your changes?',\n      });\n      return message;\n    }\n  };\n\n  /**\n   * Same for browser closing\n   *\n   * see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload\n   * the message returned is actually ignored by most browsers and a default message 'Are you sure you want to leave this page? You might have unsaved changes' is displayed. See the Notes section on the mozilla docs above\n   */\n  handlePageLeave = event => {\n    if (this.isChanged()) {\n      const message = this.context.intl.formatMessage({\n        id: 'forms.confirm_discard',\n        defaultMessage: 'Are you sure you want to discard your changes?',\n      });\n      if (event) {\n        event.returnValue = message;\n      }\n\n      return message;\n    }\n  };\n  /*\n\n  Returns true if there are any differences between the initial document and the current one\n\n  */\n  isChanged = () => {\n    const initialDocument = this.state.initialDocument;\n    const changedDocument = this.getDocument();\n\n    const changedValue = find(changedDocument, (value, key, collection) => {\n      return !isEqualWith(value, initialDocument[key], (objValue, othValue) => {\n        if (!objValue && !othValue) return true;\n      });\n    });\n\n    return typeof changedValue !== 'undefined';\n  };\n\n  /*\n\n  Refetch the document from the database (in case it was updated by another process or to reset the form)\n\n  */\n  refetchForm = () => {\n    if (this.props.refetch) {\n      this.props.refetch();\n    }\n  };\n\n  /**\n   * Clears form errors and values.\n   *\n   * @example Clear form\n   *  // form will be fully emptied, with exception of prefilled values\n   *  clearForm({ document: {} });\n   *\n   * @example Reset/revert form\n   *  // form will be reverted to its initial state\n   *  clearForm();\n   *\n   * @example Clear with new values\n   *  // form will be cleared but initialized with the new document\n   *  const document = {\n   *    // ... some values\n   *  };\n   *  clearForm({ document });\n   *\n   * @param {Object=} options\n   * @param {Object=} options.document\n   *  Document to use as new initial document when values are cleared instead of\n   *  the existing one. Note that prefilled props will be merged\n   */\n  clearForm = ({ document } = {}) => {\n    document = document ? merge({}, this.props.prefilledProps, document) : null;\n    this.setState(prevState => ({\n      errors: [],\n      currentValues: {},\n      deletedValues: [],\n      currentDocument: document || prevState.initialDocument,\n      initialDocument: document || prevState.initialDocument,\n      disabled: false,\n    }));\n  };\n\n  newMutationSuccessCallback = result => {\n    this.mutationSuccessCallback(result, 'new');\n  };\n\n  editMutationSuccessCallback = result => {\n    this.mutationSuccessCallback(result, 'edit');\n  };\n\n  mutationSuccessCallback = (result, mutationType) => {\n    this.setState(prevState => ({ disabled: false, success: true }));\n    let document = result.data[Object.keys(result.data)[0]].data; // document is always on first property\n\n    // for new mutation, run refetch function if it exists\n    if (mutationType === 'new' && this.props.refetch) this.props.refetch();\n\n    // call the clear form method (i.e. trigger setState) only if the form has not been unmounted\n    // (we are in an async callback, everything can happen!)\n    if (this.form) {\n      this.clearForm({\n        document: mutationType === 'edit' ? document : undefined,\n      });\n    }\n\n    // run document through mutation success callbacks\n    document = runCallbacks({\n      callbacks: this.successFormCallbacks,\n      iterator: document,\n      properties: { form: this },\n    });\n\n    // run success callback if it exists\n    if (this.props.successCallback) this.props.successCallback(document, { form: this });\n  };\n\n  // catch graphql errors\n  mutationErrorCallback = (document, error) => {\n    this.setState(prevState => ({ disabled: false }));\n\n    // eslint-disable-next-line no-console\n    console.log('// graphQL Error');\n    // eslint-disable-next-line no-console\n    console.log(error);\n\n    // run mutation failure callbacks on error, we do not allow the callbacks to change the error\n    runCallbacks({\n      callbacks: this.failureFormCallbacks,\n      iterator: error,\n      properties: { error, form: this },\n    });\n\n    if (!_.isEmpty(error)) {\n      // add error to state\n      this.throwError(error);\n    }\n\n    // run error callback if it exists\n    if (this.props.errorCallback) this.props.errorCallback(document, error, { form: this });\n\n    // scroll back up to show error messages\n    Utils.scrollIntoView('.flash-message');\n  };\n\n  /*\n\n  Submit form handler\n\n  */\n  submitForm = async event => {\n    event && event.preventDefault();\n    event && event.stopPropagation();\n\n    const { contextName } = this.props;\n\n    // if form is disabled (there is already a submit handler running) don't do anything\n    if (this.state.disabled) {\n      return;\n    }\n\n    // clear errors and disable form while it's submitting\n    this.setState(prevState => ({ errors: [], disabled: true }));\n\n    // complete the data with values from custom components\n    // note: it follows the same logic as SmartForm's getDocument method\n    let data = this.getData({ replaceIntlFields: true, addExtraFields: false });\n\n    // if there's a submit callback, run it\n    if (this.props.submitCallback) {\n      data = this.props.submitCallback(data) || data;\n    }\n\n    if (this.getFormType() === 'new') {\n      // create document form\n      try {\n        const result = await this.props[`create${this.props.typeName}`]({\n          input: {\n            data,\n            contextName,\n          },\n        });\n        const meta = this.props[`create${this.props.typeName}Meta`];\n        // in new versions of Apollo Client errors are no longer thrown/caught\n        // but can instead be provided as props by the useMutation hook\n        if (meta?.error) {\n          this.mutationErrorCallback(document, meta.error);\n        } else {\n          this.newMutationSuccessCallback(result);\n        }\n      } catch (error) {\n        this.mutationErrorCallback(document, error);\n      }\n    } else {\n      // update document form\n      try {\n        const documentId = this.getDocument()._id;\n        const result = await this.props[`update${this.props.typeName}`]({\n          input: {\n            id: documentId,\n            data,\n            contextName,\n          },\n        });\n        const meta = this.props[`update${this.props.typeName}Meta`];\n        // in new versions of Apollo Client errors are no longer thrown/caught\n        // but can instead be provided as props by the useMutation hook\n        if (meta.error) {\n          this.mutationErrorCallback(document, meta.error);\n        } else {\n          this.editMutationSuccessCallback(result);\n        }\n      } catch (error) {\n        this.mutationErrorCallback(document, error);\n      }\n    }\n  };\n\n  /*\n\n  Delete document handler\n\n  */\n  deleteDocument = () => {\n    const document = this.getDocument();\n    const documentId = this.props.document._id;\n    const documentTitle = document.title || document.name || '';\n\n    const deleteDocumentConfirm = this.context.intl.formatMessage({ id: 'forms.delete_confirm' }, { title: documentTitle });\n\n    if (window.confirm(deleteDocumentConfirm)) {\n      this.props[`delete${this.props.typeName}`]({ input: { id: documentId } })\n        .then(mutationResult => {\n          // the mutation result looks like {data:{collectionRemove: null}} if succeeded\n          if (this.props.removeSuccessCallback) this.props.removeSuccessCallback({ documentId, documentTitle });\n          if (this.props.refetch) this.props.refetch();\n        })\n        .catch(error => {\n          // eslint-disable-next-line no-console\n          console.log(error);\n        });\n    }\n  };\n\n  // --------------------------------------------------------------------- //\n  // ------------------------- Props to Pass ----------------------------- //\n  // --------------------------------------------------------------------- //\n\n  getCommonProps = () => {\n    const { errors, currentValues, deletedValues, disabled } = this.state;\n    const { currentUser, prefilledProps, formComponents, itemProperties, contextName } = this.props;\n    return {\n      errors,\n      throwError: this.throwError,\n      document: this.getDocument(),\n      currentValues,\n      updateCurrentValues: this.updateCurrentValues,\n      deletedValues,\n      addToDeletedValues: this.addToDeletedValues,\n      clearFieldErrors: this.clearFieldErrors,\n      formType: this.getFormType(),\n      currentUser,\n      disabled,\n      prefilledProps,\n      formComponents: this.getMergedComponents(),\n      FormComponents: this.getMergedComponents(),\n      itemProperties,\n      submitForm: this.submitForm,\n      contextName,\n    };\n  };\n\n  getFormProps = () => {\n    const docClassName = `document-${this.getFormType()}`;\n    const typeName = this.props.typeName.toLowerCase();\n\n    return {\n      className: `${docClassName} ${docClassName}-${typeName}`,\n      id: this.props.id,\n      onSubmit: this.submitForm,\n      ref: e => {\n        this.form = e;\n      },\n    };\n  };\n\n  getFormLayoutProps = () => {\n    const { formComponents, repeatErrors } = this.props;\n    const FormComponents = this.getMergedComponents();\n\n    return {\n      FormComponents,\n      formProps: this.getFormProps(),\n      errorProps: this.getFormErrorsProps(),\n      repeatErrors: repeatErrors,\n      submitProps: this.getFormSubmitProps(),\n      commonProps: this.getCommonProps(),\n    };\n  };\n\n  getFormErrorsProps = () => ({\n    errors: this.state.errors,\n  });\n\n  getFormGroupProps = group => ({\n    key: group.name,\n    ...group,\n    group: omit(group, ['fields']),\n    ...this.getCommonProps(),\n  });\n\n  getFormSubmitProps = () => {\n    const { submitLabel, cancelLabel, revertLabel, cancelCallback, revertCallback, collectionName } = this.props;\n    const { currentValues, deletedValues, errors } = this.state;\n    return {\n      submitForm: this.submitForm,\n      submitLabel,\n      cancelLabel,\n      revertLabel,\n      cancelCallback,\n      revertCallback,\n      document: this.getDocument(),\n      deleteDocument: (this.getFormType() === 'edit' && (this.props.showRemove && this.props.showDelete) && this.deleteDocument) || null,\n      collectionName,\n      currentValues,\n      deletedValues,\n      errors,\n    };\n  };\n\n  // --------------------------------------------------------------------- //\n  // ----------------------------- Render -------------------------------- //\n  // --------------------------------------------------------------------- //\n\n  render() {\n    const { formComponents, Components, successComponent } = this.props;\n    const FormComponents = this.getMergedComponents();\n\n    return this.state.success && successComponent ? (\n      successComponent\n    ) : (\n      <FormComponents.FormLayout {...this.getFormLayoutProps()}>\n        {this.getFieldGroups().map((group, i) => (\n          <FormComponents.FormGroup key={i} {...this.getFormGroupProps(group)} />\n        ))}\n      </FormComponents.FormLayout>\n    );\n  }\n}\n\nSmartForm.propTypes = {\n  // main options\n  collection: PropTypes.object.isRequired,\n  collectionName: PropTypes.string.isRequired,\n  typeName: PropTypes.string.isRequired,\n\n  document: PropTypes.object, // if a document is passed, this will be an edit form\n  schema: PropTypes.object, // usually not needed\n\n  // graphQL\n  // => now mutations have dynamic names\n  //newMutation: PropTypes.func, // the new mutation\n  //editMutation: PropTypes.func, // the edit mutation\n  //removeMutation: PropTypes.func, // the remove mutation\n\n  // form\n  prefilledProps: PropTypes.object,\n  layout: PropTypes.string,\n  fields: PropTypes.arrayOf(PropTypes.string),\n  addFields: PropTypes.arrayOf(PropTypes.string),\n  removeFields: PropTypes.arrayOf(PropTypes.string),\n  hideFields: PropTypes.arrayOf(PropTypes.string), // OpenCRUD backwards compatibility\n  showRemove: PropTypes.bool,\n  showDelete: PropTypes.bool,\n  submitLabel: PropTypes.node,\n  cancelLabel: PropTypes.node,\n  revertLabel: PropTypes.node,\n  repeatErrors: PropTypes.bool,\n  warnUnsavedChanges: PropTypes.bool,\n  formComponents: PropTypes.object,\n  disabled: PropTypes.bool,\n  itemProperties: PropTypes.object,\n  successComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),\n  contextName: PropTypes.string,\n\n  // callbacks\n  ...callbackProps,\n\n  currentUser: PropTypes.object,\n  client: PropTypes.object,\n};\n\nSmartForm.defaultProps = {\n  layout: 'horizontal',\n  prefilledProps: {},\n  repeatErrors: false,\n  showRemove: true,\n  showDelete: true,\n};\n\nSmartForm.contextTypes = {\n  intl: intlShape,\n};\n\nSmartForm.childContextTypes = {\n  addToDeletedValues: PropTypes.func,\n  deletedValues: PropTypes.array,\n  addToSubmitForm: PropTypes.func,\n  addToFailureForm: PropTypes.func,\n  addToSuccessForm: PropTypes.func,\n  clearFormCallbacks: PropTypes.func,\n  updateCurrentValues: PropTypes.func,\n  setFormState: PropTypes.func,\n  throwError: PropTypes.func,\n  clearForm: PropTypes.func,\n  refetchForm: PropTypes.func,\n  isChanged: PropTypes.func,\n  initialDocument: PropTypes.object,\n  getDocument: PropTypes.func,\n  getLabel: PropTypes.func,\n  submitForm: PropTypes.func,\n  errors: PropTypes.array,\n  currentValues: PropTypes.object,\n};\n\nexport default SmartForm;\n\nregisterComponent({\n  name: 'Form',\n  component: SmartForm,\n  hocs: [withCollectionProps],\n});\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormClear.jsx",
    "content": "import React from 'react';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\n\nconst FormClear = ({ clearField, inputType, disabled }, { intl }) => {\n  if (['date', 'date2', 'datetime', 'time', 'select', 'radiogroup'].includes(inputType) && !disabled) {\n    return (\n      <Components.TooltipTrigger\n        trigger={\n          <button className=\"form-component-clear\" title={intl.formatMessage({ id: 'forms.clear_field' })} onClick={clearField}>\n            <span>✕</span>\n          </button>\n        }>\n        <Components.FormattedMessage id=\"forms.clear_field\" />\n      </Components.TooltipTrigger>\n    );\n  } else {\n    return null;\n  }\n};\n\nFormClear.contextTypes = {\n  intl: intlShape,\n};\n\nregisterComponent('FormClear', FormClear);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormComponent.jsx",
    "content": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent, mergeWithComponents } from 'meteor/vulcan:core';\nimport get from 'lodash/get';\nimport isEqual from 'lodash/isEqual';\nimport SimpleSchema from 'simpl-schema';\nimport { isEmptyValue, getNullValue } from '../modules/utils.js';\n\n// extract this as a pure function so that it can be used inside getDerivedStateFromProps\nconst getCharacterCounts = (value, max) => {\n  const characterCount = value ? value.length : 0;\n  return { charsRemaining: max - characterCount, charsCount: characterCount };\n};\n\n// If this is an intl input, get _intl field instead\n// extract this as a pure function so that it can be used inside getDerivedStateFromProps\nconst getPath = p => {\n  return p.intlInput ? `${p.path}_intl` : p.path;\n};\n\nclass FormComponent extends Component {\n  constructor(props) {\n    super(props);\n\n    this.state = {};\n  }\n\n  static getDerivedStateFromProps(props) {\n    const { document, max } = props;\n    if (!max) {\n      return null;\n    }\n    const path = getPath(props);\n    const intlOrRegularValue = get(document, path);\n    const value = (intlOrRegularValue && typeof intlOrRegularValue === 'object') ? intlOrRegularValue.value : intlOrRegularValue;\n    return getCharacterCounts(value, max);\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    // allow custom controls to determine if they should update\n    if (this.isCustomInput(this.getInputType(nextProps))) {\n      return true;\n    }\n\n    const { document, deletedValues, errors } = nextProps;\n    const path = getPath(this.props);\n\n    // when checking for deleted values, both current path ('foo') and child path ('foo.0.bar') should trigger updates\n    const includesPathOrChildren = deletedValues => deletedValues.some(deletedPath => deletedPath.includes(path));\n\n    const valueChanged = !isEqual(get(document, path), get(this.props.document, path));\n    const errorChanged = !isEqual(this.getErrors(errors), this.getErrors());\n    const deleteChanged = includesPathOrChildren(deletedValues) !== includesPathOrChildren(this.props.deletedValues);\n    const charsChanged = nextState.charsRemaining !== this.state.charsRemaining;\n    const disabledChanged = nextProps.disabled !== this.props.disabled;\n    const helpChanged = nextProps.help !== this.props.help;\n\n    const shouldUpdate = valueChanged || errorChanged || deleteChanged || charsChanged || disabledChanged || helpChanged;\n\n    return shouldUpdate;\n  }\n\n  /*\n\n  Returns true if the passed input type is a custom\n\n  */\n  isCustomInput = inputType => {\n    const isStandardInput = [\n      'nested',\n      'number',\n      'url',\n      'email',\n      'textarea',\n      'checkbox',\n      'checkboxgroup',\n      'radiogroup',\n      'select',\n      'selectmultiple',\n      'datetime',\n      'date',\n      'time',\n      'text',\n      'password',\n    ].includes(inputType);\n    return !isStandardInput;\n  };\n\n  /*\n\n  Function passed to form controls (always controlled) to update their value\n\n  */\n  handleChange = value => {\n    // if value is an empty string, delete the field\n    if (value === '') {\n      value = null;\n    }\n    // if this is a number field, convert value before sending it up to Form\n    if (this.getFieldType() === Number && value != null) {\n      value = Number(value);\n    } else if (this.getFieldType() === SimpleSchema.Integer && value != null) {\n      value = parseInt(value);\n    }\n\n    if (value !== this.getValue()) {\n      const updateValue = this.props.locale ?\n          { locale: this.props.locale, value } :\n          value;\n      this.props.updateCurrentValues({ [getPath(this.props)]: updateValue });\n      this.props.clearFieldErrors(getPath(this.props));\n    }\n\n    // for text fields, update character count on change\n    if (this.showCharsRemaining()) {\n      this.updateCharacterCount(value);\n    }\n  };\n\n  /*\n\n  Updates the state of charsCount and charsRemaining as the users types\n\n  */\n  updateCharacterCount = value => {\n    this.setState(getCharacterCounts(value, this.props.max));\n  };\n\n  /*\n\n  Get value from Form state through document and currentValues props\n\n  */\n  getValue = (props, context) => {\n    const p = props || this.props;\n    const c = context || this.context;\n    const { locale, defaultValue, deletedValues, formType, datatype } = p;\n    const path = locale ? `${getPath(p)}.value` : getPath(p);\n    const currentDocument = c.getDocument();\n    let value = get(currentDocument, path);\n    // note: force intl fields to be treated like strings\n    const nullValue = locale ? '' : getNullValue(datatype);\n\n    // handle deleted & empty value\n    if (deletedValues.includes(path)) {\n      value = nullValue;\n    } else if (isEmptyValue(value)) {\n      // replace empty value by the default value from the schema if it exists – for new forms only\n      value = formType === 'new' && defaultValue ? defaultValue : nullValue;\n    }\n    return value;\n  };\n\n  /*\n\n  Whether to keep track of and show remaining chars\n\n  */\n  showCharsRemaining = props => {\n    const p = props || this.props;\n    return p.max && ['url', 'email', 'textarea', 'text'].includes(this.getInputType(p));\n  };\n\n  /*\n\n  Get errors from Form state through context\n\n  Note: we use `includes` to get all errors from nested components, which have longer paths\n\n  */\n  getErrors = errors => {\n    errors = errors || this.props.errors;\n    const fieldErrors = errors.filter(error => error.path && error.path.includes(this.props.path));\n    return fieldErrors;\n  };\n\n  /*\n\n  Get field field value type\n\n  */\n  getFieldType = props => {\n    const p = props || this.props;\n    return p.datatype && p.datatype[0].type;\n  };\n\n  /*\n\n  Get form input type, either based on input props, or by guessing based on form field type\n\n  */\n  getInputType = props => {\n    const p = props || this.props;\n    const fieldType = this.getFieldType();\n    const autoType =\n      fieldType === Number || fieldType === SimpleSchema.Integer\n        ? 'number'\n        : fieldType === Boolean\n        ? 'checkbox'\n        : fieldType === Date\n        ? 'date'\n        : 'text';\n    return p.input || autoType;\n  };\n\n  /*\n\n  Function passed to form controls to clear their contents (set their value to null)\n\n  */\n  clearField = event => {\n    event.preventDefault();\n    event.stopPropagation();\n    this.props.updateCurrentValues({ [this.props.path]: null });\n    if (this.showCharsRemaining()) {\n      this.updateCharacterCount(null);\n    }\n  };\n\n  /*\n\n  Function passed to FormComponentInner to help with rendering the component\n\n  */\n  getFormInput = () => {\n    const inputType = this.getInputType();\n    const FormComponents = mergeWithComponents(this.props.formComponents);\n\n    // if input is a React component, use it\n    if (typeof this.props.input === 'function') {\n      const InputComponent = this.props.input;\n      return InputComponent;\n    } else {\n      // else pick a predefined component\n\n      switch (inputType) {\n        case 'text':\n          return FormComponents.FormComponentDefault;\n\n        case 'password':\n          return FormComponents.FormComponentPassword;\n\n        case 'number':\n          return FormComponents.FormComponentNumber;\n\n        case 'url':\n          return FormComponents.FormComponentUrl;\n\n        case 'email':\n          return FormComponents.FormComponentEmail;\n\n        case 'textarea':\n          return FormComponents.FormComponentTextarea;\n\n        case 'checkbox':\n          return FormComponents.FormComponentCheckbox;\n\n        case 'checkboxgroup':\n          return FormComponents.FormComponentCheckboxGroup;\n\n        case 'radiogroup':\n          return FormComponents.FormComponentRadioGroup;\n\n        case 'select':\n          return FormComponents.FormComponentSelect;\n\n        case 'selectmultiple':\n          return FormComponents.FormComponentSelectMultiple;\n\n        case 'datetime':\n          return FormComponents.FormComponentDateTime;\n\n        case 'date':\n          return FormComponents.FormComponentDate;\n\n        case 'date2':\n          return FormComponents.FormComponentDate2;\n\n        case 'time':\n          return FormComponents.FormComponentTime;\n\n        case 'statictext':\n          return FormComponents.FormComponentStaticText;\n\n        case 'likert':\n          return FormComponents.FormComponentLikert;\n\n        case 'autocomplete':\n          return FormComponents.FormComponentAutocomplete;\n\n        case 'multiautocomplete':\n          return FormComponents.FormComponentMultiAutocomplete;\n\n        default:\n          const CustomComponent = FormComponents[this.props.input];\n          return CustomComponent ? CustomComponent : FormComponents.FormComponentDefault;\n      }\n    }\n  };\n\n  isArrayField = () => {\n    return this.getFieldType() === Array;\n  };\n\n  isObjectField = () => {\n    return this.getFieldType() instanceof SimpleSchema;\n  };\n\n  render() {\n    const FormComponents = mergeWithComponents(this.props.formComponents);\n\n    if (this.props.intlInput) {\n      return <FormComponents.FormIntl {...this.props} />;\n    } else if (!this.props.input && this.props.nestedInput) {\n      if (this.isArrayField()) {\n        return (\n          <FormComponents.FormNestedArray\n            {...this.props}\n            formComponents={FormComponents}\n            errors={this.getErrors()}\n            value={this.getValue()}\n          />\n        );\n      } else if (this.isObjectField()) {\n        return (\n          <FormComponents.FormNestedObject\n            {...this.props}\n            formComponents={FormComponents}\n            errors={this.getErrors()}\n            value={this.getValue()}\n          />\n        );\n      }\n    }\n\n    const fciProps = {\n      ...this.props,\n      ...this.state,\n      inputType: this.getInputType(),\n      value: this.getValue(),\n      errors: this.getErrors(),\n      document: this.context.getDocument(),\n      showCharsRemaining: !!this.showCharsRemaining(),\n      handleChange: this.handleChange,\n      clearField: this.clearField,\n      formInput: this.getFormInput(),\n      formComponents: FormComponents,\n    };\n\n    // if there is no query, handle options here; otherwise they will be handled by\n    // the FormComponentLoader component\n    if (!this.props.query && typeof this.props.options === 'function') {\n      fciProps.options = this.props.options(fciProps);\n    }\n\n    const fci = <FormComponents.FormComponentInner {...fciProps} />;\n\n    return this.props.query ? <FormComponents.FormComponentLoader {...fciProps}>{fci}</FormComponents.FormComponentLoader> : fci;\n  }\n}\n\nFormComponent.propTypes = {\n  document: PropTypes.object.isRequired,\n  name: PropTypes.string.isRequired,\n  label: PropTypes.string,\n  value: PropTypes.any,\n  placeholder: PropTypes.string,\n  prefilledValue: PropTypes.any,\n  options: PropTypes.any,\n  input: PropTypes.any,\n  datatype: PropTypes.any,\n  path: PropTypes.string.isRequired,\n  disabled: PropTypes.bool,\n  nestedSchema: PropTypes.object,\n  currentValues: PropTypes.object.isRequired,\n  deletedValues: PropTypes.array.isRequired,\n  throwError: PropTypes.func.isRequired,\n  updateCurrentValues: PropTypes.func.isRequired,\n  errors: PropTypes.array.isRequired,\n  addToDeletedValues: PropTypes.func,\n  clearFieldErrors: PropTypes.func.isRequired,\n  currentUser: PropTypes.object,\n  prefilledProps: PropTypes.object,\n};\n\nFormComponent.contextTypes = {\n  getDocument: PropTypes.func.isRequired,\n};\n\n//module.exports = FormComponent;\nexport default FormComponent;\n\nregisterComponent('FormComponent', FormComponent);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormComponentLoader.jsx",
    "content": "import React, { useEffect } from 'react';\nimport { Components, registerComponent, expandQueryFragments } from 'meteor/vulcan:core';\nimport { useLazyQuery } from '@apollo/client';\nimport gql from 'graphql-tag';\nimport isEmpty from 'lodash/isEmpty';\n\nconst FormComponentLoader = props => {\n  const { query, children, options, value, queryWaitsForValue } = props;\n\n  // if query is a function, execute it\n  const queryText = typeof query === 'function' ? query({ value }) : query;\n\n  const [loadFieldQuery, { loading, error, data }] = useLazyQuery(gql(expandQueryFragments(queryText)));\n\n  const valueIsEmpty = isEmpty(value) || (Array.isArray(value) && value.length) === 0;\n\n  useEffect(() => {\n    if (queryWaitsForValue && valueIsEmpty) {\n      // we don't want to run this query until we have a value to pass to it\n      // so do nothing\n    } else {\n      loadFieldQuery({\n        variables: { value },\n      });\n    }\n  }, [valueIsEmpty, value, queryWaitsForValue]);\n\n  if (error) {\n    throw new Error(error);\n  }\n\n  if (loading){\n    return (\n      <div className=\"form-component-loader\">\n        <Components.Loading />\n      </div>\n    );\n  }\n\n  // pass newly loaded data (and options if needed) to child component\n  const extraProps = { data, queryData: data, queryError: error, loading };\n  if (typeof options === 'function') {\n    extraProps.optionsFunction = options;\n    extraProps.options = options.call({}, { ...props, data });\n  }\n\n  const fci = React.cloneElement(children, extraProps);\n\n  return <div className=\"form-component-loader\">{fci}</div>;\n};\n\nFormComponentLoader.propTypes = {};\nregisterComponent({\n  name: 'FormComponentLoader',\n  component: FormComponentLoader,\n});\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormElement.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n// this component receives a ref, so it must be a class component\nclass FormElement extends React.Component {\n    render(){\n        const { children, ...otherProps } = this.props;\n        return <form {...otherProps}>{children}</form>;\n    }\n}\nregisterComponent({\n    name:'FormElement',\n    component: FormElement\n});"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormError.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { getContext } from 'meteor/vulcan:lib';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\nimport get from 'lodash/get';\n\nconst FormError = ({ error, errorContext, getLabel }) => {\n\n  // use the error or error message as default message\n  const defaultMessage = JSON.stringify(error.message || error);\n  const id = error.id || 'app.defaultError';\n\n  // default props for all errors\n  let messageProps = {\n    id,\n    defaultMessage,\n    values: {\n      errorContext,\n      defaultMessage,\n    },\n  };\n\n  // additional properties to enhance the message\n  if (error.properties) {\n    // in case this is a nested fields, only keep last segment of path\n    const errorName = error.properties.name && error.properties.name.split('.').slice(-1)[0];\n    messageProps.values = {\n      ...messageProps.values,\n      // if the error is triggered by a field, get the relevant label\n      label: errorName && getLabel(errorName, error.properties.locale),\n      ...error.properties,\n    };\n  }\n\n  if (error.data) {\n    messageProps.values = {\n      ...messageProps.values,\n      ...error.data, // backwards compatibility\n    };\n  }\n\n  const exception = get(error, 'extensions.exception');\n  if (exception) {\n    messageProps = {\n      ...messageProps,\n      id: exception.id,\n      values: exception.data,\n    };\n  }\n  return <Components.FormattedMessage html={true} {...messageProps} />;\n};\n\nFormError.defaultProps = {\n  errorContext: '', // default context so format message does not complain\n  getLabel: name => name,\n};\n\n// TODO: pass getLabel as prop instead for consistency?\nregisterComponent(\n  'FormError',\n  FormError,\n  getContext({\n    getLabel: PropTypes.func,\n  })\n);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormErrors.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent, Components } from 'meteor/vulcan:core';\n\nconst FormErrors = ({ errors }) => (\n  <div className=\"form-errors\">\n    {!!errors.length && (\n      <Components.Alert className=\"flash-message\" variant=\"danger\">\n        <ul>\n          {errors.map((error, index) => (\n            <li key={index}>\n              <Components.FormError error={error} errorContext=\"form\" />\n            </li>\n          ))}\n        </ul>\n      </Components.Alert>\n    )}\n  </div>\n);\nregisterComponent('FormErrors', FormErrors);\n\n// /*\n\n//   Render errors\n\n//   */\n//  renderErrors = () => {\n//   return (\n//     <div className=\"form-errors\">\n//       {this.state.errors.map((error, index) => {\n//         let message;\n\n//         if (error.data && error.data.errors) {\n//           // this error is a \"multi-error\" with multiple sub-errors\n\n//           message = error.data.errors.map(error => {\n//             return {\n//               content: this.getErrorMessage(error),\n//               data: error.data,\n//             };\n//           });\n//         } else {\n//           // this is a regular error\n\n//           message = {\n//             content:\n//               error.message ||\n//               this.context.intl.formatMessage({ id: error.id, defaultMessage: error.id }, error.data),\n//           };\n//         }\n\n//         return <Components.FormFlash key={index} message={message} type=\"error\" />;\n//       })}\n//     </div>\n//   );\n// };\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormGroup.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { instantiateComponent } from 'meteor/vulcan:core';\nimport { registerComponent, mergeWithComponents } from 'meteor/vulcan:core';\nimport Users from 'meteor/vulcan:users';\n\nclass FormGroup extends PureComponent {\n\n  constructor (props) {\n    super(props);\n    this.toggle = this.toggle.bind(this);\n    this.renderHeading = this.renderHeading.bind(this);\n    this.state = {\n      collapsed: props.group.startCollapsed || false,\n    };\n  }\n\n  toggle () {\n    this.setState({\n      collapsed: !this.state.collapsed,\n    });\n  }\n\n  renderHeading (FormComponents) {\n    return (\n      <FormComponents.FormGroupHeader\n        toggle={this.toggle}\n        label={this.props.label}\n        collapsed={this.state.collapsed}\n        hidden={this.isHidden()}\n        group={this.props.group}\n      />\n    );\n  }\n\n  // if at least one of the fields in the group has an error, the group as a whole has an error\n  hasErrors = () =>\n    _.some(this.props.fields, field => {\n      return !!this.props.errors.filter(error => error.path === field.path).length;\n    });\n\n  isHidden = () => {\n    const { hidden, document } = this.props;\n    const isHidden = typeof hidden === 'function' ? hidden({ ...this.props, document }) : hidden || false;\n    return isHidden;\n  };\n\n  render() {\n    if (this.props.group.adminsOnly && !Users.isAdmin(this.props.currentUser)) {\n      return null;\n    }\n\n    const { name, fields, formComponents, label, group, document } = this.props;\n    const { collapsed } = this.state;\n\n    const FormComponents = mergeWithComponents(formComponents);\n    const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;\n\n    return (\n      <FormComponents.FormGroupLayout\n        label={label}\n        anchorName={anchorName}\n        toggle={this.toggle}\n        collapsed={collapsed}\n        hidden={this.isHidden()}\n        group={group}\n        heading={name === 'default' ? null : this.renderHeading(FormComponents)}\n        hasErrors={this.hasErrors()}\n        document={document}\n      >\n\n        {instantiateComponent(group.beforeComponent, this.props)}\n\n        {fields.map(field => (\n          <FormComponents.FormComponent\n            key={field.name}\n            disabled={this.props.disabled}\n            {...field}\n            document={document}\n            itemProperties={{ ...this.props.itemProperties, ...field.itemProperties }}\n            errors={this.props.errors}\n            throwError={this.props.throwError}\n            currentValues={this.props.currentValues}\n            updateCurrentValues={this.props.updateCurrentValues}\n            deletedValues={this.props.deletedValues}\n            addToDeletedValues={this.props.addToDeletedValues}\n            clearFieldErrors={this.props.clearFieldErrors}\n            formType={this.props.formType}\n            currentUser={this.props.currentUser}\n            prefilledProps={this.props.prefilledProps}\n            submitForm={this.props.submitForm}\n            formComponents={FormComponents}\n          />\n        ))}\n\n        {instantiateComponent(group.afterComponent, this.props)}\n      </FormComponents.FormGroupLayout>\n    );\n  }\n}\n\nFormGroup.propTypes = {\n  name: PropTypes.string,\n  label: PropTypes.string,\n  order: PropTypes.number,\n  hidden: PropTypes.func,\n  fields: PropTypes.array.isRequired,\n  group: PropTypes.object.isRequired,\n  errors: PropTypes.array.isRequired,\n  throwError: PropTypes.func.isRequired,\n  currentValues: PropTypes.object.isRequired,\n  updateCurrentValues: PropTypes.func.isRequired,\n  deletedValues: PropTypes.array.isRequired,\n  addToDeletedValues: PropTypes.func.isRequired,\n  clearFieldErrors: PropTypes.func.isRequired,\n  formType: PropTypes.string.isRequired,\n  currentUser: PropTypes.object,\n  prefilledProps: PropTypes.object,\n};\n\nexport default FormGroup;\n\nregisterComponent('FormGroup', FormGroup);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormIntl.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent, mergeWithComponents, Locales } from 'meteor/vulcan:core';\nimport omit from 'lodash/omit';\nimport { getContext } from 'meteor/vulcan:lib';\n\n// replaceable layout\nconst FormIntlLayout = ({ children }) => (\n  <div className=\"form-intl\">{children}</div>\n);\nregisterComponent({ name: 'FormIntlLayout', component: FormIntlLayout });\nconst FormIntlItemLayout = ({ locale, children }) => (\n  <div className={`form-intl-${locale.id}`}>\n    {children}\n  </div>\n);\nregisterComponent({\n  name: 'FormIntlItemLayout',\n  component: FormIntlItemLayout\n});\n\nclass FormIntl extends PureComponent {\n  /*\n\n  Note: ideally we'd try to make sure to return the right path no matter\n  the order translations are stored in, but in practice we can't guarantee it\n  so we just use the order of the Locales array.\n\n  */\n  getLocalePath = defaultIndex => {\n    return `${this.props.path}_intl.${defaultIndex}`;\n  };\n\n  render() {\n    const { name, formComponents } = this.props;\n    const FormComponents = mergeWithComponents(formComponents);\n\n    // do not pass FormIntl's own value, inputProperties, and intlInput props down\n    const properties = omit(\n      this.props,\n      'value',\n      'inputProperties',\n      'intlInput',\n      'nestedInput'\n    );\n    return (\n      <FormComponents.FormIntlLayout>\n        {Locales.map((locale, i) => (\n          <FormComponents.FormIntlItemLayout key={locale.id} locale={locale}>\n            <FormComponents.FormComponent\n              {...properties}\n              label={this.props.getLabel(name, locale.id)}\n              path={this.getLocalePath(i)}\n              locale={locale.id}\n            />\n          </FormComponents.FormIntlItemLayout>\n        ))}\n      </FormComponents.FormIntlLayout>\n    );\n  }\n}\n\nFormIntl.propTypes = {\n  name: PropTypes.string.isRequired,\n  path: PropTypes.string.isRequired,\n  formComponents: PropTypes.object\n};\n\nregisterComponent(\n  'FormIntl',\n  FormIntl,\n  getContext({\n    getLabel: PropTypes.func\n  })\n);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormLayout.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent } from 'meteor/vulcan:core';\n\nconst FormLayout = ({ FormComponents, commonProps, formProps, errorProps, repeatErrors, submitProps, children }) => (\n  <FormComponents.FormElement {...formProps}>\n    <FormComponents.FormErrors {...commonProps} {...errorProps} />\n\n    {children}\n\n    {repeatErrors && <FormComponents.FormErrors {...commonProps} {...errorProps} />}\n\n    <FormComponents.FormSubmit {...commonProps} {...submitProps} />\n  </FormComponents.FormElement>\n);\n\nexport default FormLayout;\n\nregisterComponent('FormLayout', FormLayout);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormNestedArray.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent, instantiateComponent } from 'meteor/vulcan:core';\nimport _omit from 'lodash/omit';\nimport _get from 'lodash/get';\n\n// Wraps the FormNestedItem, repeated for each object\n// Allow for example to have a label per object\nconst FormNestedArrayInnerLayout = props => {\n  const { FormComponents, label, children, addItem, beforeComponent, afterComponent } = props;\n  return (\n    <div className=\"form-nested-array-inner-layout\">\n      {instantiateComponent(beforeComponent, props)}\n      {children}\n      <FormComponents.FormNestedDivider label={label} addItem={addItem} />\n      {instantiateComponent(afterComponent, props)}\n    </div>\n  );\n};\nregisterComponent({\n  name: 'FormNestedArrayInnerLayout',\n  component: FormNestedArrayInnerLayout,\n});\n\nclass FormNestedArray extends PureComponent {\n  getCurrentValue() {\n    return this.props.value || [];\n  }\n\n  addItem = () => {\n    const { prefilledProps, path } = this.props;\n    const value = this.getCurrentValue();\n    this.props.updateCurrentValues(\n      { [`${path}.${value.length}`]: _get(prefilledProps, `${path}.$`) || {} },\n      { mode: 'merge' }\n    );\n  };\n\n  removeItem = index => {\n    this.props.updateCurrentValues({ [`${this.props.path}.${index}`]: null });\n  };\n\n  /*\n\n  Go through this.context.deletedValues and see if any value matches both the current field\n  and the given index (ex: if we want to know if the second address is deleted, we\n  look for the presence of 'addresses.1')\n  */\n  isDeleted = index => {\n    return this.props.deletedValues.includes(`${this.props.path}.${index}`);\n  };\n\n  computeVisibleIndex = values => {\n    let currentIndex = 0;\n    const visibleIndexes = values.map((subDocument, subDocumentIndx) => {\n      if (this.isDeleted(subDocumentIndx)) {\n        return 0;\n      } else {\n        currentIndex = currentIndex + 1;\n        return currentIndex;\n      }\n    });\n    return visibleIndexes;\n  };\n\n  componentDidMount() {\n    if (this.props.itemProperties.openNested) this.addItem();\n  }\n\n  render() {\n    const value = this.getCurrentValue();\n    const visibleItemIndexes = this.computeVisibleIndex(value);\n    // do not pass FormNested's own value, input and inputProperties props down\n    const properties = _omit(\n      this.props,\n      'value',\n      'input',\n      'inputProperties',\n      'nestedInput',\n      'beforeComponent',\n      'afterComponent'\n    );\n    const { errors, path, formComponents, minCount, maxCount, arrayField } = this.props;\n    const FormComponents = formComponents;\n\n    //filter out null values to calculate array length\n    let arrayLength = value.filter(singleValue => {\n      return typeof singleValue !== 'undefined' && singleValue !== null;\n    }).length;\n    properties.addItem = !maxCount || arrayLength < maxCount ? this.addItem : null;\n\n    // only keep errors specific to the nested array (and not its subfields)\n    properties.nestedArrayErrors = errors.filter(error => error.path && error.path === path);\n    properties.hasErrors = !!(properties.nestedArrayErrors && properties.nestedArrayErrors.length);\n\n    return (\n      <FormComponents.FormNestedArrayLayout {...properties}>\n        {value.map((subDocument, i) => {\n          if (this.isDeleted(i)) return null;\n          const path = `${this.props.path}.${i}`;\n          const visibleItemIndex = visibleItemIndexes[i];\n          return (\n            <FormComponents.FormNestedArrayInnerLayout\n              {...arrayField}\n              key={path}\n              FormComponents={FormComponents}\n              addItem={this.addItem}\n              itemIndex={i}\n              visibleItemIndex={visibleItemIndex}\n              path={path}>\n              <FormComponents.FormNestedItem\n                {...properties}\n                itemIndex={i}\n                visibleItemIndex={visibleItemIndex}\n                path={path}\n                removeItem={() => {\n                  this.removeItem(i);\n                }}\n                hideRemove={!!minCount && arrayLength <= minCount}\n              />\n            </FormComponents.FormNestedArrayInnerLayout>\n          );\n        })}\n      </FormComponents.FormNestedArrayLayout>\n    );\n  }\n}\n\nFormNestedArray.propTypes = {\n  currentValues: PropTypes.object,\n  path: PropTypes.string,\n  label: PropTypes.string,\n  minCount: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),\n  maxCount: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),\n  errors: PropTypes.array.isRequired,\n  deletedValues: PropTypes.array.isRequired,\n  formComponents: PropTypes.object.isRequired,\n  itemProperties: PropTypes.object,\n};\n\nFormNestedArray.defaultProps = {\n  itemProperties: {}\n};\n\nexport default FormNestedArray;\n\nregisterComponent('FormNestedArray', FormNestedArray);\n\nconst IconAdd = ({ width = 24, height = 24 }) => (\n  <svg width={width} height={height} xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 448 512\">\n    <path\n      fill=\"currentColor\"\n      d=\"M448 294.2v-76.4c0-13.3-10.7-24-24-24H286.2V56c0-13.3-10.7-24-24-24h-76.4c-13.3 0-24 10.7-24 24v137.8H24c-13.3 0-24 10.7-24 24v76.4c0 13.3 10.7 24 24 24h137.8V456c0 13.3 10.7 24 24 24h76.4c13.3 0 24-10.7 24-24V318.2H424c13.3 0 24-10.7 24-24z\"\n    />\n  </svg>\n);\n\nregisterComponent('IconAdd', IconAdd);\n\nconst IconRemove = ({ width = 24, height = 24 }) => (\n  <svg width={width} height={height} xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 448 512\">\n    <path\n      fill=\"currentColor\"\n      d=\"M424 318.2c13.3 0 24-10.7 24-24v-76.4c0-13.3-10.7-24-24-24H24c-13.3 0-24 10.7-24 24v76.4c0 13.3 10.7 24 24 24h400z\"\n    />\n  </svg>\n);\n\nregisterComponent('IconRemove', IconRemove);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormNestedArrayLayout.jsx",
    "content": "import { instantiateComponent, registerComponent } from 'meteor/vulcan:lib';\nimport PropTypes from 'prop-types';\nimport React from 'react';\n\n// Replaceable layout, default implementation\nconst FormNestedArrayLayout = props => {\n  const {\n    hasErrors,\n    nestedArrayErrors,\n    label,\n    addItem,\n    beforeComponent,\n    afterComponent,\n    formComponents,\n    children,\n  } = props;\n  const FormComponents = formComponents;\n\n  return (\n    <div className={`form-group row form-nested ${hasErrors ? 'input-error' : ''}`}>\n      {instantiateComponent(beforeComponent, props)}\n\n      <label className=\"control-label col-sm-3\">{label}</label>\n\n      <div className=\"col-sm-9\">\n        {children}\n        {addItem && (\n          <FormComponents.Button\n            className=\"form-nested-button form-nested-add\"\n            size=\"sm\"\n            variant=\"success\"\n            onClick={addItem}>\n            <FormComponents.IconAdd height={12} width={12} />\n          </FormComponents.Button>\n        )}\n        {props.hasErrors ? (\n          <FormComponents.FieldErrors key=\"form-nested-errors\" errors={nestedArrayErrors} />\n        ) : null}\n      </div>\n\n      {instantiateComponent(afterComponent, props)}\n    </div>\n  );\n};\n\nFormNestedArrayLayout.propTypes = {\n  hasErrors: PropTypes.bool.isRequired,\n  nestedArrayErrors: PropTypes.array,\n  label: PropTypes.node,\n  hideLabel: PropTypes.bool,\n  addItem: PropTypes.func,\n  beforeComponent: PropTypes.node,\n  afterComponent: PropTypes.node,\n  formComponents: PropTypes.object,\n  children: PropTypes.node,\n};\n\nregisterComponent({\n  name: 'FormNestedArrayLayout',\n  component: FormNestedArrayLayout,\n});\n\nexport default FormNestedArrayLayout;\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormNestedDivider.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent } from 'meteor/vulcan:core';\n\nconst FormNestedDivider = ({ label, addItem }) => <div/>;\n\nFormNestedDivider.propTypes = {\n  label: PropTypes.string,\n  addItem: PropTypes.func,\n};\n\nregisterComponent('FormNestedDivider', FormNestedDivider);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormNestedItem.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent, mergeWithComponents } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\n\nconst FormNestedItemLayout = ({ content, removeButton }) => (\n  <div className=\"form-nested-item\">\n    <div className=\"form-nested-item-inner\">{content}</div>\n    {removeButton && [\n      <div key=\"remove-button\" className=\"form-nested-item-remove\">\n        {removeButton}\n      </div>,\n      <div\n        key=\"remove-button-overlay\"\n        className=\"form-nested-item-deleted-overlay\"\n      />\n    ]}\n  </div>\n);\nFormNestedItemLayout.propTypes = {\n  content: PropTypes.node.isRequired,\n  removeButton: PropTypes.node\n};\nregisterComponent({\n  name: 'FormNestedItemLayout',\n  component: FormNestedItemLayout\n});\n\nconst FormNestedItem = (\n  { nestedFields, name, path, removeItem, itemIndex, formComponents, hideRemove, label, ...props },\n  { errors, intl }\n) => {\n  const FormComponents = mergeWithComponents(formComponents);\n  const isArray = typeof itemIndex !== 'undefined';\n  return (\n    <FormComponents.FormNestedItemLayout\n      content={nestedFields.map((field, i) => {\n        return (\n          <FormComponents.FormComponent\n            key={i}\n            {...props}\n            {...field}\n            path={`${path}.${field.name}`}\n            itemIndex={itemIndex}\n          />\n        );\n      })}\n      removeButton={\n        isArray && !hideRemove && [\n          <div key=\"remove-button\" className=\"form-nested-item-remove\">\n            <Components.Button\n              className=\"form-nested-button\"\n              variant=\"danger\"\n              size=\"sm\"\n              iconButton\n              tabIndex={-1}\n              onClick={() => {\n                removeItem(name);\n              }}\n              aria-label={intl.formatMessage({ id: 'forms.delete_nested_field' }, { label: label })}\n            >\n              <Components.IconRemove height={12} width={12} />\n            </Components.Button>\n          </div>,\n          <div\n            key=\"remove-button-overlay\"\n            className=\"form-nested-item-deleted-overlay\"\n          />\n        ]\n      }\n    />\n  );\n};\n\nFormNestedItem.propTypes = {\n  path: PropTypes.string.isRequired,\n  itemIndex: PropTypes.number,\n  formComponents: PropTypes.object,\n  hideRemove: PropTypes.bool\n};\n\nFormNestedItem.contextTypes = {\n  errors: PropTypes.array,\n  intl: intlShape\n};\n\nregisterComponent('FormNestedItem', FormNestedItem);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormNestedObject.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent, mergeWithComponents } from 'meteor/vulcan:core';\n\n// Replaceable layout\nconst FormNestedObjectLayout = ({ hasErrors, label, content }) => (\n  <div\n    className={`form-group row form-nested ${hasErrors ? 'input-error' : ''}`}\n  >\n    <label className=\"control-label col-sm-3\">{label}</label>\n    <div className=\"col-sm-9\">{content}</div>\n  </div>\n);\nFormNestedObjectLayout.propTypes = {\n  hasErrors: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),\n  label: PropTypes.node,\n  content: PropTypes.node\n};\nregisterComponent({\n  name: 'FormNestedObjectLayout',\n  component: FormNestedObjectLayout\n});\n\nclass FormNestedObject extends PureComponent {\n  render() {\n    const FormComponents = mergeWithComponents(this.props.formComponents);\n    //const value = this.getCurrentValue()\n    // do not pass FormNested's own value, input and inputProperties props down\n    const properties = _.omit(\n      this.props,\n      'value',\n      'input',\n      'inputProperties',\n      'nestedInput'\n    );\n    const { errors } = this.props;\n    // only keep errors specific to the nested array (and not its subfields)\n    const nestedObjectErrors = errors.filter(\n      error => error.path && error.path === this.props.path\n    );\n    const hasErrors = nestedObjectErrors && nestedObjectErrors.length;\n    return (\n      <FormComponents.FormNestedObjectLayout\n        hasErrors={hasErrors}\n        label={this.props.label}\n        content={[\n          <FormComponents.FormNestedItem\n            key=\"form-nested-item\"\n            {...properties}\n            path={`${this.props.path}`}\n          />,\n          hasErrors ? (\n            <FormComponents.FieldErrors\n              key=\"form-nested-errors\"\n              errors={nestedObjectErrors}\n            />\n          ) : null\n        ]}\n      />\n    );\n  }\n}\n\nFormNestedObject.propTypes = {\n  currentValues: PropTypes.object,\n  path: PropTypes.string,\n  label: PropTypes.string,\n  errors: PropTypes.array.isRequired,\n  formComponents: PropTypes.object\n};\n\nexport default FormNestedObject;\n\nregisterComponent('FormNestedObject', FormNestedObject);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormOptionLabel.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:core';\n\nconst FormOptionLabel = ({ option }) => {\n  const { label } = option;\n  return <span className=\"form-option-label\">{label}</span>;\n};\n\nregisterComponent('FormOptionLabel', FormOptionLabel);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormSubmit.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components } from 'meteor/vulcan:core';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport Users from 'meteor/vulcan:users';\n\nconst FormSubmit = ({\n  submitForm,\n  submitLabel,\n  cancelLabel,\n  cancelCallback,\n  revertLabel,\n  revertCallback,\n  document,\n  deleteDocument,\n  collectionName,\n  classes,\n  currentUser,\n}, {\n  isChanged,\n  clearForm,\n}) => (\n  <div className=\"form-submit\">\n    <Components.Button type=\"submit\" variant=\"primary\">\n      {submitLabel ? submitLabel : <Components.FormattedMessage id=\"forms.submit\" defaultMessage=\"Submit\" />}\n    </Components.Button>\n\n    {cancelCallback ? (\n      <a\n        className=\"form-cancel\"\n        onClick={e => {\n          e.preventDefault();\n          cancelCallback(document);\n        }}\n      >\n        {cancelLabel ? cancelLabel : <Components.FormattedMessage id=\"forms.cancel\" defaultMessage=\"Cancel\" />}\n      </a>\n    ) : null}\n  \n    {revertCallback ? (\n      <a\n        className=\"form-cancel\"\n        onClick={e => {\n          e.preventDefault();\n          clearForm();\n          revertCallback(document);\n        }}\n      >\n      {revertLabel ? revertLabel : <Components.FormattedMessage id=\"forms.revert\" defaultMessage=\"Revert\" />}\n      </a>\n    ) : null}\n  \n    { deleteDocument && Users.canDelete({\n        user: currentUser,\n        document,\n        collectionName\n      }) ? (\n      <div>\n        <hr />\n        <Components.Button variant=\"link\" onClick={deleteDocument} className={`delete-link ${collectionName}-delete-link`}>\n          <Components.Icon name=\"close\" /> <Components.FormattedMessage id=\"forms.delete\" defaultMessage=\"Delete\" />\n        </Components.Button>\n      </div>\n    ) : null}\n  </div>\n);\n\nFormSubmit.propTypes = {\n  submitLabel: PropTypes.node,\n  cancelLabel: PropTypes.node,\n  cancelCallback: PropTypes.func,\n  revertLabel: PropTypes.node,\n  revertCallback: PropTypes.func,\n  document: PropTypes.object,\n  deleteDocument: PropTypes.func,\n  collectionName: PropTypes.string,\n  classes: PropTypes.object\n};\n\nFormSubmit.contextTypes = {\n  isChanged: PropTypes.func,\n  clearForm: PropTypes.func,\n};\n\n\nregisterComponent('FormSubmit', FormSubmit);\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/FormWrapper.jsx",
    "content": "/*\n\nGenerate the appropriate fragment for the current form, then\nwrap the main Form component with the necessary HoCs while passing\nthem the fragment.\n\nThis component is itself wrapped with:\n\n- withCurrentUser\n- withApollo (used to access the Apollo client for form pre-population)\n\nAnd wraps the Form component with:\n\n- withNew\n\nOr:\n\n- withSingle\n- withUpdate\n- withDelete\n\n(When wrapping with withSingle, withUpdate, and withDelete, a special Loader\ncomponent is also added to wait for withSingle's loading prop to be false)\n\n*/\n\nimport React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { withRouter } from 'react-router';\nimport { withApollo } from '@apollo/client/react/hoc';\nimport { compose } from 'meteor/vulcan:lib';\nimport {\n  Components,\n  registerComponent,\n  withCurrentUser,\n  Utils,\n  withCreate2,\n  withUpdate2,\n  withDelete2,\n  getFragment,\n} from 'meteor/vulcan:core';\nimport gql from 'graphql-tag';\nimport { withSingle } from 'meteor/vulcan:core';\n\nimport withCollectionProps from './withCollectionProps';\nimport { callbackProps } from './propTypes';\n\nimport getFormFragments from '../modules/formFragments';\n\nclass FormWrapper extends PureComponent {\n  constructor(props) {\n    super(props);\n    // instantiate the wrapped component in constructor, not in render\n    // see https://reactjs.org/docs/higher-order-components.html#dont-use-hocs-inside-the-render-method\n    this.FormComponent = this.getComponent(props);\n  }\n  // return the current schema based on either the schema or collection prop\n  getSchema() {\n    return this.props.schema ? this.props.schema : this.props.collection.simpleSchema()._schema;\n  }\n\n  // if a document is being passed, this is an edit form\n  getFormType() {\n    return this.props.documentId || this.props.slug ? 'edit' : 'new';\n  }\n\n  // get fragment used to decide what data to load from the server to populate the form,\n  // as well as what data to ask for as return value for the mutation\n  getFragments() {\n    const { fields, addFields, typeName, collectionName } = this.props;\n\n    let queryFragment;\n    let mutationFragment;\n\n    // if queryFragment or mutationFragment props are specified, accept either fragment object or fragment string\n    if (this.props.queryFragment) {\n      queryFragment =\n        typeof this.props.queryFragment === 'string'\n          ? gql`\n              ${this.props.queryFragment}\n            `\n          : this.props.queryFragment;\n    }\n    if (this.props.mutationFragment) {\n      mutationFragment =\n        typeof this.props.mutationFragment === 'string'\n          ? gql`\n              ${this.props.mutationFragment}\n            `\n          : this.props.mutationFragment;\n    }\n\n    // same with queryFragmentName and mutationFragmentName\n    if (this.props.queryFragmentName) {\n      queryFragment = getFragment(this.props.queryFragmentName);\n    }\n    if (this.props.mutationFragmentName) {\n      mutationFragment = getFragment(this.props.mutationFragmentName);\n    }\n\n    if (!queryFragment || !mutationFragment) {\n      // autogenerated fragments\n      const autoFormFragments = getFormFragments({\n        formType: this.getFormType(),\n        collectionName,\n        typeName,\n        schema: this.getSchema(),\n        fields,\n        addFields,\n      });\n\n      queryFragment = queryFragment || autoFormFragments.queryFragment;\n      mutationFragment = mutationFragment || autoFormFragments.mutationFragment;\n    }\n\n    // get query & mutation fragments from props or else default to same as generatedFragment\n    return {\n      queryFragment,\n      mutationFragment,\n    };\n  }\n\n  getComponent() {\n    let WrappedComponent;\n\n    const prefix = `${this.props.collectionName}${Utils.capitalize(this.getFormType())}`;\n\n    const { queryFragment, mutationFragment } = this.getFragments();\n\n    // props to pass on to child component (i.e. <Form />)\n    const childProps = {\n      formType: this.getFormType(),\n      schema: this.getSchema(),\n    };\n\n    // options for withSingle HoC\n    const queryOptions = {\n      queryName: `${prefix}FormQuery`,\n      collection: this.props.collection,\n      fragment: queryFragment,\n      queryOptions: {\n        fetchPolicy: 'network-only', // we always want to load a fresh copy of the document\n        pollInterval: 0, // no polling, only load data once\n      },\n      enableCache: false,\n    };\n\n    // options for withNew, withUpdate, and withDelete HoCs\n    const mutationOptions = {\n      collection: this.props.collection,\n      fragment: mutationFragment,\n    };\n\n    // create a stateless loader component,\n    // displays the loading state if needed, and passes on loading and document/data\n    const Loader = props => {\n      const { document, loading } = props;\n      return loading ? (\n        <Components.Loading />\n      ) : (\n          <Components.Form document={document} loading={loading} {...childProps} {...props} />\n        );\n    };\n    Loader.displayName = 'withLoader(Form)';\n\n    // if this is an edit from, load the necessary data using the withSingle HoC\n    if (this.getFormType() === 'edit') {\n      WrappedComponent = compose(\n        withSingle(queryOptions),\n        withUpdate2(mutationOptions),\n        withDelete2(mutationOptions)\n      )(Loader);\n\n      return (\n        <WrappedComponent\n          selector={{\n            documentId: this.props.documentId,\n            slug: this.props.slug,\n          }}\n        />\n      );\n    } else {\n\n      WrappedComponent = compose(withCreate2(mutationOptions))(Components.Form);\n\n      return <WrappedComponent {...childProps} />;\n    }\n  }\n\n  render() {\n    const component = this.FormComponent;\n    const componentWithParentProps = React.cloneElement(component, this.props);\n    return componentWithParentProps;\n  }\n}\n\nFormWrapper.propTypes = {\n  // main options\n  collection: PropTypes.object.isRequired,\n  collectionName: PropTypes.string.isRequired,\n  typeName: PropTypes.string.isRequired,\n\n  documentId: PropTypes.string, // if a document is passed, this will be an edit form\n  schema: PropTypes.object, // usually not needed\n  queryFragment: PropTypes.object,\n  queryFragmentName: PropTypes.string,\n  mutationFragment: PropTypes.object,\n  mutationFragmentName: PropTypes.string,\n\n  // graphQL\n  // createFoo, deleteFoo, updateFoo\n  // newMutation: PropTypes.func, // the new mutation\n  // editMutation: PropTypes.func, // the edit mutation\n  // removeMutation: PropTypes.func, // the remove mutation\n\n  // form\n  prefilledProps: PropTypes.object,\n  layout: PropTypes.string,\n  fields: PropTypes.arrayOf(PropTypes.string),\n  hideFields: PropTypes.arrayOf(PropTypes.string),\n  addFields: PropTypes.arrayOf(PropTypes.string),\n  showRemove: PropTypes.bool,\n  submitLabel: PropTypes.node,\n  cancelLabel: PropTypes.node,\n  revertLabel: PropTypes.node,\n  repeatErrors: PropTypes.bool,\n  warnUnsavedChanges: PropTypes.bool,\n  formComponents: PropTypes.object,\n  disabled: PropTypes.bool,\n  itemProperties: PropTypes.object,\n  successComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),\n  contextName: PropTypes.string,\n\n  // callbacks\n  ...callbackProps,\n\n  currentUser: PropTypes.object,\n  client: PropTypes.object,\n};\n\nFormWrapper.defaultProps = {\n  layout: 'horizontal',\n};\n\nFormWrapper.contextTypes = {\n  closeCallback: PropTypes.func,\n  intl: intlShape,\n};\n\nregisterComponent({\n  name: 'SmartForm',\n  component: FormWrapper,\n  hocs: [withCurrentUser, withApollo, withRouter, withCollectionProps],\n});\n\nexport default FormWrapper;\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/propTypes.js",
    "content": "/** PropTypes for documentation purpose (not tested yet) */\nimport PropTypes from 'prop-types';\n\nexport const fieldProps = {\n  //\n  defaultValue: PropTypes.any,\n  help: PropTypes.string,\n  description: PropTypes.string,\n  // initial fields\n  name: PropTypes.string,\n  datatype: PropTypes.any, // ?\n  layout: PropTypes.any, // string?\n  input: PropTypes.any, // string, function, undefined\n  options: PropTypes.object,\n  intlInput: PropTypes.object,\n  // path relative to the main object\n  // e.g phoneNumbers.0.value\n  path: PropTypes.string,\n  // permissions\n  disabled: PropTypes.boolean,\n  // if it has an array field\n  // e.g addresses.$ : { type: .... }\n  arrayFieldSchema: PropTypes.object,\n  arrayField: PropTypes.object, //fieldProps,\n  // if it is a nested object itself\n  // eg address : { type : { ... }}\n  nestedSchema: PropTypes.object,\n  nestedInput: PropTypes.boolean, // flag\n  nestedFields: PropTypes.array //arrayOf(fieldProps)\n};\n\nexport const groupProps = {\n  name: PropTypes.string.isRequired,\n  label: PropTypes.string.isRequired,\n  order: PropTypes.number,\n  fields: PropTypes.arrayOf(PropTypes.shape(fieldProps))\n};\n\nexport const callbackProps = {\n  initCallback: PropTypes.func,\n  changeCallback: PropTypes.func,\n  submitCallback: PropTypes.func,\n  successCallback: PropTypes.func,\n  removeSuccessCallback: PropTypes.func,\n  errorCallback: PropTypes.func,\n  cancelCallback: PropTypes.func,\n  revertCallback: PropTypes.func\n};\n"
  },
  {
    "path": "packages/vulcan-forms/lib/components/withCollectionProps.js",
    "content": "import React from 'react';\nimport { extractCollectionInfo } from 'meteor/vulcan:lib';\nimport PropTypes from 'prop-types';\n\n/**\n * Handle the collection or collectionName and pass down other related\n * props (typeName, collectionName, etc.)\n */\nconst withCollectionProps = C => {\n  const CollectionPropsWrapper = ({ collection: _collection, collectionName: _collectionName, ...otherProps }) => {\n    const { collection, collectionName } = extractCollectionInfo({\n      collection: _collection,\n      collectionName: _collectionName\n    });\n    const typeName = collection.options.typeName;\n    return <C {...otherProps} collection={collection} collectionName={collectionName} typeName={typeName} />;\n  };\n  CollectionPropsWrapper.propTypes = {\n    collection: PropTypes.object,\n    collectionName: (props, propName, componentName) => {\n      if (!props.collection && !props.collectionName) {\n        return new Error(`One of props 'collection' or 'collectionName' was not specified in '${componentName}'.`);\n      }\n      if (!props.collection && typeof props['collectionName'] !== 'string') {\n        return new Error(`Prop collectionName was not of type string in '${componentName}`);\n      }\n    }\n  };\n  return CollectionPropsWrapper;\n};\nexport default withCollectionProps;\n"
  },
  {
    "path": "packages/vulcan-forms/lib/modules/components.js",
    "content": "import '../components/FieldErrors.jsx';\nimport '../components/FormElement.jsx';\nimport '../components/FormErrors.jsx';\nimport '../components/FormError.jsx';\nimport '../components/FormComponent.jsx';\nimport '../components/FormNestedArray.jsx';\nimport '../components/FormNestedArrayLayout.jsx';\nimport '../components/FormNestedDivider.jsx';\nimport '../components/FormNestedObject.jsx';\nimport '../components/FormNestedItem.jsx';\nimport '../components/FormIntl.jsx';\nimport '../components/FormGroup.jsx';\nimport '../components/FormSubmit.jsx';\nimport '../components/FormWrapper.jsx';\nimport '../components/Form.jsx';\nimport '../components/FormLayout.jsx';\nimport '../components/FormComponentLoader.jsx';\nimport '../components/FormOptionLabel.jsx';\nimport '../components/FormClear.jsx';\n"
  },
  {
    "path": "packages/vulcan-forms/lib/modules/formFragments.js",
    "content": "/**\n * Generate mutation and query fragments for a form based on the schema\n * TODO: refactor to mutualize more code with vulcan-core defaultFragment functions\n * TODO: move to lib when refactored\n */\nimport _uniq from 'lodash/uniq';\nimport _intersection from 'lodash/intersection';\nimport gql from 'graphql-tag';\nimport {\n    getCreateableFields,\n    getUpdateableFields,\n    getFragmentFieldNames,\n    //isBlackbox,\n    getFieldFragment\n} from 'meteor/vulcan:lib';\nimport {\n    Utils,\n} from 'meteor/vulcan:core';\nconst intlSuffix = '_intl';\n\n// PostsEditFormQueryFragment/PostsNewFormMutationFragment/etc.\nconst getFragmentName = (formType, collectionName, fragmentType) => \n  [collectionName, formType, 'form', fragmentType, 'fragment'].map(Utils.capitalize).join('');\n\n// get modifiable fields in the query either for update or create operations\nconst getQueryFieldNames = ({\n    schema,\n    options\n}) => {\n    const queryFields = options.formType === 'new'\n        ? getCreateableFields(schema)\n        : getUpdateableFields(schema);\n    return queryFields;\n};\n// add readable fields to mutation fields\nconst getMutationFieldNames = ({\n    readableFieldNames,\n    queryFieldNames\n}) => {\n    return _uniq(queryFieldNames.concat(readableFieldNames));\n};\n\n/*\nconst getFieldFragment = ({ schema, fieldName, options }) => {\n    let fieldFragment = fieldName;\n    const field = schema[fieldName];\n    if (!(field && field.type)) return fieldName;\n    const fieldType = field.type.singleType;\n    const fieldTypeName =\n        typeof fieldType === 'object'\n            ? 'Object'\n            : typeof fieldType === 'function'\n                ? fieldType.name\n                : fieldType;\n\n    if (fieldName.slice(-5) === intlSuffix) {\n        fieldFragment = `${fieldName}{ locale value }`;\n    } else {\n        switch (fieldTypeName) {\n            // recursive call for nested arrays and objects\n            case 'Object':\n                if (!isBlackbox(field) && fieldType._schema) {\n                    fieldFragment =\n                        getSchemaFragment({\n                            fragmentName: fieldName,\n                            schema: fieldType._schema,\n                            options,\n                        }) || null;\n                }\n                break;\n            case 'Array':\n                const arrayItemFieldName = `${fieldName}.$`;\n                const arrayItemField = schema[arrayItemFieldName];\n                // note: make sure field has an associated array item field\n                if (arrayItemField) {\n                    // child will either be native value or a an object (first case)\n                    const arrayItemFieldType = arrayItemField.type.singleType;\n                    if (!arrayItemField.blackbox && arrayItemFieldType._schema) {\n                        fieldFragment =\n                            getSchemaFragment({\n                                fragmentName: fieldName,\n                                schema: arrayItemFieldType._schema,\n                                options,\n                            }) || null;\n                    }\n                }\n                break;\n            default:\n                // handle intl or return fieldName\n                fieldFragment = fieldName;\n                break;\n        }\n    }\n    return fieldFragment;\n};\n*/\n\n// get fragment for a whole schema (root schema or nested schema of an object or an array)\nconst getSchemaFragment = ({\n    schema,\n    fragmentName,\n    options,\n    fieldNames: providedFieldNames\n}) => {\n    // differentiate mutation/query and create/update cases\n    // respect provided fieldNames if any (needed for the root schema)\n    const fieldNames = providedFieldNames || (\n        options.isMutation\n            ? getMutationFieldNames({\n                queryFieldNames: getQueryFieldNames({ schema, options }),\n                readableFieldNames: getFragmentFieldNames({ schema, options: { onlyViewable: true } })\n            })\n            : getQueryFieldNames({ schema, options })\n    );\n\n    const childFragments = fieldNames.length && fieldNames.map(fieldName => getFieldFragment({\n        schema,\n        fieldName,\n        options,\n        getObjectFragment: getSchemaFragment // allow to reuse the code from defaultFragment with another behaviour\n    }))\n        // remove empty values\n        .filter(f => !!f);\n    if (childFragments.length) {\n        return `${fragmentName} { ${childFragments.join('\\n')} }`;\n    }\n    return null;\n};\n\n/**\n * Generate query and mutation fragments for forms\n*/\nconst getFormFragments = ({\n    formType = 'new', // new || edit\n    collectionName,\n    typeName,\n    schema,\n    fields, // restrict on certain fields\n    addFields, // add additional fields (eg to display static fields)\n}) => {\n\n    // get the root schema fieldNames\n    let queryFieldNames = getQueryFieldNames({ schema, options: { formType } });\n    let mutationFieldNames = getMutationFieldNames({\n        queryFieldNames,\n        readableFieldNames: getFragmentFieldNames({ schema, options: { onlyViewable: true } })\n    });\n\n    // if \"fields\" prop is specified, restrict list of fields to it\n    if (typeof fields !== 'undefined' && fields.length > 0) {\n        // add \"_intl\" suffix to all fields in case some of them are intl fields\n        const fieldsWithIntlSuffix = fields.map(field => `${field}${intlSuffix}`);\n        const allFields = [...fields, ...fieldsWithIntlSuffix];\n        queryFieldNames = _intersection(queryFieldNames, allFields);\n        mutationFieldNames = _intersection(mutationFieldNames, allFields);\n    }\n\n    // add \"addFields\" prop contents to list of fields\n    if (addFields && addFields.length) {\n        queryFieldNames = queryFieldNames.concat(addFields);\n        mutationFieldNames = mutationFieldNames.concat(addFields);\n    }\n\n    // userId is used to check for permissions, so add it to fragments if possible\n    if (schema.userId) {\n        queryFieldNames.unshift('userId');\n        mutationFieldNames.unshift('userId');\n    }\n\n    if (schema._id) {\n        queryFieldNames.unshift('_id');\n        mutationFieldNames.unshift('_id');\n    }\n\n    // check unicity (_id can be added twice)\n    queryFieldNames = _uniq(queryFieldNames);\n    mutationFieldNames = _uniq(mutationFieldNames);\n\n\n    // generate query fragment based on the fields that can be edited. Note: always add _id, and userId if possible.\n    // TODO: support nesting\n    const queryFragmentText = getSchemaFragment({\n        schema,\n        fragmentName: `fragment ${getFragmentName(formType, collectionName, 'query')} on ${typeName}`,\n        options: { formType, isMutation: false },\n        fieldNames: queryFieldNames\n    });\n    const generatedQueryFragment = gql(queryFragmentText);\n\n    const mutationFragmentText = getSchemaFragment({\n        schema,\n        fragmentName: `fragment ${getFragmentName(formType, collectionName, 'mutation')} on ${typeName}`,\n        options: { formType, isMutation: true },\n        fieldNames: mutationFieldNames\n    });\n    // generate mutation fragment based on the fields that can be edited and/or viewed. Note: always add _id, and userId if possible.\n    // TODO: support nesting\n    const generatedMutationFragment = gql(mutationFragmentText);\n\n    // if any field specifies extra queries, add them\n    const extraQueries = _.compact(\n        getQueryFieldNames({ schema, options: { formType } }).map(fieldName => {\n            const field = schema[fieldName];\n            return field.query;\n        })\n    );\n    // get query & mutation fragments from props or else default to same as generatedFragment\n    return {\n        queryFragment: generatedQueryFragment,\n        mutationFragment: generatedMutationFragment,\n        extraQueries\n    };\n};\n\nexport default getFormFragments;\n"
  },
  {
    "path": "packages/vulcan-forms/lib/modules/index.js",
    "content": "import { registerComponent, registerSetting } from 'meteor/vulcan:core';\n\nregisterSetting('forms.warnUnsavedChanges', false, 'Warn user about unsaved changes before leaving route', true);\n\nimport './components.js';\n\nexport * from './utils';\nexport { default as FormWrapper } from '../components/FormWrapper.jsx';\n"
  },
  {
    "path": "packages/vulcan-forms/lib/modules/path_utils.js",
    "content": "import toPath from 'lodash/toPath';\nimport initial from 'lodash/initial';\nimport flow from 'lodash/fp/flow';\nimport takeRight from 'lodash/takeRight';\n\n/**\n * Splits a path in string format into an array.\n *\n * @param {String} string\n *  Path in string format\n * @return {[string|number]}\n */\nexport const splitPath = string => toPath(string);\n\n/**\n * Joins a path in array format into a string.\n *\n * @param {[string|number]} array\n *  Path in array format\n * @return {String}\n */\nexport const joinPath = array =>\n  array.reduce(\n    (string, item) =>\n      string + (\n        Number.isNaN(Number(item))\n          ? `${string === '' ? '' : '.'}${item}`\n          : `[${item}]`\n      ),\n    '',\n  );\n\n/**\n * Retrieves parent path from the given one.\n *\n * @param {String} string\n *  Path in string format\n * @return {String}\n */\nexport const getParentPath = flow(splitPath, initial, joinPath);\n\n/**\n * Removes prefix from the given paths.\n *\n * @param {String} prefix\n * @param {String[]} paths\n * @return {String[]}\n */\nexport const removePrefix = (prefix, paths) => {\n  const explodedPrefix = splitPath(prefix);\n  return paths.map(path => {\n    if (path === prefix) {\n      return path;\n    }\n    const explodedPath = splitPath(path);\n    const explodedSuffix = takeRight(\n      explodedPath,\n      explodedPath.length - explodedPrefix.length,\n    );\n    return joinPath(explodedSuffix);\n  });\n};\n\n/**\n * Filters paths that have the given prefix.\n *\n * @param {String} prefix\n * @param {String[]} paths\n * @return {String[]}\n */\nexport const filterPathsByPrefix = (prefix, paths) =>\n  paths.filter(path => (\n    path === prefix ||\n    path.startsWith(`${prefix}.`) ||\n    path.startsWith(`${prefix}[`)\n  ));\n"
  },
  {
    "path": "packages/vulcan-forms/lib/modules/schema_utils.js",
    "content": "/*\n * Schema converter/getters\n */\nimport { Utils } from 'meteor/vulcan:core';\nimport Users from 'meteor/vulcan:users';\nimport _keys from 'lodash/keys';\nimport _filter from 'lodash/filter';\n\n/* getters */\n// filter out fields with \".\" or \"$\"\nexport const getValidFields = schema => {\n  return Object.keys(schema).filter(fieldName => !fieldName.includes('$') && !fieldName.includes('.'));\n};\n\nexport const getReadableFields = schema => {\n  // OpenCRUD backwards compatibility\n  return getValidFields(schema).filter(fieldName => schema[fieldName].canRead || schema[fieldName].viewableBy);\n};\n\n/*\n\nConvert a nested SimpleSchema schema into a JSON object\nIf flatten = true, will create a flat object instead of nested tree\n\n/* permissions */\n\n/**\n * @method Mongo.Collection.getInsertableFields\n * Get an array of all fields editable by a specific user for a given collection\n * @param {Object} user – the user for which to check field permissions\n */\nexport const getInsertableFields = function(schema, user) {\n  const fields = _filter(_keys(schema), function(fieldName) {\n    var field = schema[fieldName];\n    return Users.canCreateField(user, field);\n  });\n  return fields;\n};\n\n/**\n * @method Mongo.Collection.getEditableFields\n * Get an array of all fields editable by a specific user for a given collection (and optionally document)\n * @param {Object} user – the user for which to check field permissions\n */\nexport const getEditableFields = function(schema, user, document) {\n  const fields = _.filter(_.keys(schema), function(fieldName) {\n    var field = schema[fieldName];\n    return Users.canUpdateField(user, field, document);\n  });\n  return fields;\n};\n\nexport const convertSchema = (schema, options = {}) => {\n  \n  const { flatten = false, removeArrays = true } = options;\n\n  if (schema._schema) {\n    let jsonSchema = {};\n\n    Object.keys(schema._schema).forEach(fieldName => {\n      // exclude array fields\n      if (removeArrays && fieldName.includes('$')) {\n        return;\n      }\n\n      // extract schema\n      jsonSchema[fieldName] = getFieldSchema(fieldName, schema);\n\n      // check for existence of nested field\n      // and get its schema if possible or its type otherwise\n      const subSchemaOrType = getNestedFieldSchemaOrType(fieldName, schema);\n      if (subSchemaOrType) {\n        // remember the subschema if it exists, allow to customize labels for each group of items for arrays of objects\n        jsonSchema[fieldName].arrayFieldSchema = getFieldSchema(`${fieldName}.$`, schema);\n\n        // call convertSchema recursively on the subSchema\n        const convertedSubSchema = convertSchema(subSchemaOrType, options);\n        // nested schema can be a field schema ({type, canRead, etc.}) (convertedSchema will be null)\n        // or a schema on its own with subfields (convertedSchema will return smth)\n        if (!convertedSubSchema) {\n          // subSchema is a simple field in this case (eg array of numbers)\n          jsonSchema[fieldName].isSimpleArrayField = true; //getFieldSchema(`${fieldName}.$`, schema);\n        } else {\n          // subSchema is a full schema with multiple fields (eg array of objects)\n          if (flatten) {\n            jsonSchema = { ...jsonSchema, ...convertedSubSchema };\n          } else {\n            jsonSchema[fieldName].schema = convertedSubSchema;\n          }\n        }\n      }\n    });\n    return jsonSchema;\n  } else {\n    return null;\n  }\n};\n\n/*\n\nGet a JSON object representing a field's schema\n\n*/\nexport const getFieldSchema = (fieldName, schema) => {\n  let fieldSchema = {};\n  schemaProperties.forEach(property => {\n    const propertyValue = schema.get(fieldName, property);\n    if (propertyValue) {\n      fieldSchema[property] = propertyValue;\n    }\n  });\n  return fieldSchema;\n};\n\n// type is an array due to the possibility of using SimpleSchema.oneOf\n// right now we support only fields with one type\nexport const getSchemaType = Utils.getFieldType;\n\nconst getArrayNestedSchema = (fieldName, schema) => {\n  const arrayItemSchema = schema._schema[`${fieldName}.$`];\n  const nestedSchema = arrayItemSchema && getSchemaType(arrayItemSchema);\n  return nestedSchema;\n};\n// nested object fields type is of the form \"type: new SimpleSchema({...})\"\n// so they should possess a \"_schema\" prop\nconst isNestedSchemaField = fieldSchema => {\n  const fieldType = getSchemaType(fieldSchema);\n  //console.log('fieldType', typeof fieldType, fieldType._schema)\n  return fieldType && !!fieldType._schema;\n};\nconst getObjectNestedSchema = (fieldName, schema) => {\n  const fieldSchema = schema._schema[fieldName];\n  if (!isNestedSchemaField(fieldSchema)) return null;\n  const nestedSchema = fieldSchema && getSchemaType(fieldSchema);\n  return nestedSchema;\n};\n/*\n\nGiven an array field, get its nested schema\nIf the field is not an object, this will return the subfield type instead\n*/\nexport const getNestedFieldSchemaOrType = (fieldName, schema) => {\n  const arrayItemSchema = getArrayNestedSchema(fieldName, schema);\n  if (!arrayItemSchema) {\n    // look for an object schema\n    const objectItemSchema = getObjectNestedSchema(fieldName, schema);\n    // no schema was found\n    if (!objectItemSchema) return null;\n    return objectItemSchema;\n  }\n  return arrayItemSchema;\n};\n\nexport const schemaProperties = [\n  'type',\n  'label',\n  'optional',\n  'required',\n  'min',\n  'max',\n  'exclusiveMin',\n  'exclusiveMax',\n  'minCount',\n  'maxCount',\n  'allowedValues',\n  'regEx',\n  'blackbox',\n  'trim',\n  'custom',\n  'defaultValue',\n  'autoValue',\n  'hidden', // hidden: true means the field is never shown in a form no matter what\n  'mustComplete', // mustComplete: true means the field is required to have a complete profile\n  'form', // form placeholder\n  'inputProperties', // form placeholder\n  'itemProperties',\n  'control', // SmartForm control (String or React component)\n  'input', // SmartForm control (String or React component)\n  'autoform', // legacy form placeholder; backward compatibility (not used anymore)\n  'order', // position in the form\n  'group', // form fieldset group\n  'onCreate', // field insert callback\n  'onUpdate', // field edit callback\n  'onDelete', // field remove callback\n  'onInsert', // OpenCRUD backwards compatibility\n  'onEdit', // OpenCRUD backwards compatibility\n  'onRemove', // OpenCRUD backwards compatibility\n  'canRead',\n  'canCreate',\n  'canUpdate',\n  'viewableBy', // OpenCRUD backwards compatibility\n  'insertableBy', // OpenCRUD backwards compatibility\n  'editableBy', // OpenCRUD backwards compatibility\n  'resolveAs',\n  'searchable',\n  'description',\n  'beforeComponent',\n  'afterComponent',\n  'placeholder',\n  'options',\n  'query',\n  'queryWaitsForValue',\n  'autocompleteQuery',\n  'fieldProperties',\n  'intl',\n  'intlId',\n];\n\nexport const formProperties = [\n  'optional',\n  'required',\n  'min',\n  'max',\n  'exclusiveMin',\n  'exclusiveMax',\n  'minCount',\n  'maxCount',\n  'allowedValues',\n  'regEx',\n  'blackbox',\n  'trim',\n  'custom',\n  'defaultValue',\n  'autoValue',\n  'mustComplete', // mustComplete: true means the field is required to have a complete profile\n  'form', // form placeholder\n  'inputProperties', // form placeholder\n  'itemProperties',\n  'control', // SmartForm control (String or React component)\n  'input', // SmartForm control (String or React component)\n  'order', // position in the form\n  'group', // form fieldset group\n  'description',\n  'beforeComponent',\n  'afterComponent',\n  'placeholder',\n  'options',\n  'query',\n  'queryWaitsForValue',\n  'autocompleteQuery',\n  'fieldProperties',\n];\n"
  },
  {
    "path": "packages/vulcan-forms/lib/modules/utils.js",
    "content": "import merge from 'lodash/merge';\nimport find from 'lodash/find';\nimport isPlainObject from 'lodash/isPlainObject';\nimport set from 'lodash/set';\nimport size from 'lodash/size';\n\nimport { removePrefix, filterPathsByPrefix } from './path_utils';\n\n// add support for nested properties\nexport const deepValue = function(obj, path) {\n  const pathArray = path.split('.');\n\n  for (var i = 0; i < pathArray.length; i++) {\n    obj = obj[pathArray[i]];\n  }\n\n  return obj;\n};\n\n// see http://stackoverflow.com/questions/19098797/fastest-way-to-flatten-un-flatten-nested-json-objects\nexport const flatten = function(data) {\n  var result = {};\n  function recurse(cur, prop) {\n    if (Object.prototype.toString.call(cur) !== '[object Object]') {\n      result[prop] = cur;\n    } else if (Array.isArray(cur)) {\n      for (var i = 0, l = cur.length; i < l; i++) recurse(cur[i], prop + '[' + i + ']');\n      if (l == 0) result[prop] = [];\n    } else {\n      var isEmpty = true;\n      for (var p in cur) {\n        isEmpty = false;\n        recurse(cur[p], prop ? prop + '.' + p : p);\n      }\n      if (isEmpty && prop) result[prop] = {};\n    }\n  }\n  recurse(data, '');\n  return result;\n};\n\nexport const isEmptyValue = value =>\n  typeof value === 'undefined' || value === null || value === '' || (Array.isArray(value) && value.length === 0);\n\n/**\n * Merges values. It takes into account the current, original and deleted values,\n * and the merge produces the proper type for simple objects or arrays.\n *\n * @param {Object} props\n *  Form component props. Only specific properties for this function are documented.\n * @param {*} props.currentValue\n *  Current value of the field\n * @param {*} props.documentValue\n *  Original value of the field\n * @return {*|undefined}\n *  Merged value or undefined if no merge was performed\n */\nexport const mergeValue = ({ currentValue, documentValue, deletedValues: deletedFields, path, locale, datatype }) => {\n  if (locale) {\n    // note: intl fields are of type Object but should be treated as Strings\n    return currentValue || documentValue || '';\n  }\n\n  // note: retrieve nested deleted values is performed here to avoid skipping\n  // the merge in case the current field is not in `currentValues` but a nested\n  // property has been removed directly by path\n  const deletedValues = getNestedDeletedValues(path, deletedFields);\n  const hasDeletedValues = !!size(deletedValues);\n  if ((Array.isArray(currentValue) || hasDeletedValues) && find(datatype, ['type', Array])) {\n    return merge([], documentValue, currentValue, deletedValues);\n  } else if ((isPlainObject(currentValue) || hasDeletedValues) && find(datatype, ['type', Object])) {\n    return merge({}, documentValue, currentValue, deletedValues);\n  }\n  return undefined;\n};\n\n/**\n * Converts a list of field names to an object of deleted values.\n *\n * @param {string[]|Object.<string|string>} deletedFields\n *  List of deleted field names or paths\n * @param {Object|Array=} accumulator={}\n *  Value to reduce the values to\n * @return {Object|Array}\n *  Deleted values, with the structure defined by taking the received deleted\n *  fields as paths\n * @example\n *  const deletedFields = [\n *    'field.subField',\n *    'field.subFieldArray[0]',\n *    'fieldArray[0]',\n *    'fieldArray[2].name',\n *  ];\n *  getNestedDeletedValues(deletedFields);\n *  // => { 'field': { 'subField': null, 'subFieldArray': [null] }, 'fieldArray': [null, undefined, { name: null } }\n */\nexport const getDeletedValues = (deletedFields, accumulator = {}) =>\n  deletedFields.reduce((deletedValues, path) => set(deletedValues, path, null), accumulator);\n\n/**\n * Filters the given field names by prefix, removes it from each one of them\n * and convert the list to an object of deleted values.\n *\n * @param {string=} prefix\n *  Prefix to filter and remove from deleted fields\n * @param {string[]|Object.<string|string>} deletedFields\n *  List of deleted field names or paths\n * @param {Object|Array=} accumulator={}\n *  Value to reduce the values to\n * @return {Object.<string, null>}\n *  Object keyed with the given deleted fields, valued with `null`\n * @example\n *  const deletedFields = [\n *    'field.subField',\n *    'field.subFieldArray[0]',\n *    'fieldArray[0]',\n *    'fieldArray[2].name',\n *  ];\n *  getNestedDeletedValues('field', deletedFields);\n *  // => { 'subField': null, 'subFieldArray': [null] }\n *  getNestedDeletedValues('fieldArray', deletedFields);\n *  // => { '0': null, '2': { 'name': null } }\n *  getNestedDeletedValues('fieldArray', deletedFields, []);\n *  // => [null, undefined, { 'name': null } ]\n */\nexport const getNestedDeletedValues = (prefix, deletedFields, accumulator = {}) =>\n  getDeletedValues(removePrefix(prefix, filterPathsByPrefix(prefix, deletedFields)), accumulator);\n\nexport const getFieldType = datatype => datatype[0].type;\n/**\n * Get appropriate null value for various field types\n *\n * @param {Array} datatype\n * Field's datatype property\n */\nexport const getNullValue = datatype => {\n  const fieldType = getFieldType(datatype);\n  if (fieldType === Array) {\n    return [];\n  } else if (fieldType === Boolean) {\n    return false;\n  } else if (fieldType === String) {\n    return '';\n  } else if (fieldType === Number) {\n    return '';\n  } else {\n    // normalize to null\n    return null;\n  }\n};\n"
  },
  {
    "path": "packages/vulcan-forms/lib/server/main.js",
    "content": "export * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-forms/package.js",
    "content": "Package.describe({\n  name: 'vulcan:forms',\n  summary: 'Form containers for React',\n  version: '1.16.9',\n  git: 'https://github.com/meteor-utilities/react-form-containers.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9']);\n\n  api.mainModule('lib/client/main.js', ['client']);\n  api.mainModule('lib/server/main.js', ['server']);\n});\n\nPackage.onTest(function(api) {\n  api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:test', 'vulcan:forms', 'vulcan:ui-bootstrap']);\n  api.mainModule('./test/index.js');\n});\n"
  },
  {
    "path": "packages/vulcan-forms/test/Form.test.js",
    "content": "\nimport React from 'react';\n// TODO: should be loaded from Components instead?\nimport Form from '../lib/components/Form';\nimport expect from 'expect';\n\nimport { mount, shallow } from 'enzyme';\nimport { initComponentTest } from 'meteor/vulcan:test';\n\n// we must import all the other components, so that \"registerComponent\" is called\nimport '../lib/modules/components';\n\n// TODO: init this component in Vulcan:core (currently depends on either Bootstrap or Material UI to exists)\nimport { registerComponent } from 'meteor/vulcan:core';\nregisterComponent({\n    name: 'Button',\n    component: ({ children, onClick, type, className }) => <button className={className} type={type} onClick={onClick}>{children}</button>\n});\nregisterComponent({\n    name: 'FormComponentInner',\n    component: ({ children, onChange, onBlur, value, className }) => <input className={className} value={value} onBlur={onBlur} onChange={onChange} />\n});\n\n// setup Vulcan (load components, initialize fragments)\ninitComponentTest();\n\n// fixtures\nimport SimpleSchema from 'simpl-schema';\nconst addressGroup = {\n    name: 'addresses',\n    label: 'Addresses',\n    order: 10,\n};\nconst permissions = {\n    canRead: ['guests'],\n    canUpdate: ['quests'],\n    canCreate: ['guests'],\n};\n\n// just 1 input for state testing\nconst fooSchema = {\n    foo: {\n        type: String,\n        ...permissions,\n    },\n};\n//\nconst addressSchema = {\n    street: {\n        type: String,\n        optional: true,\n        ...permissions,\n    },\n};\n// [{street, city,...}, {street, city, ...}]\nconst arrayOfObjectSchema = {\n    addresses: {\n        type: Array,\n        group: addressGroup,\n        ...permissions,\n    },\n    'addresses.$': {\n        type: new SimpleSchema(addressSchema),\n    },\n};\n// example with custom inputs for the children\n// [\"http://maps/XYZ\", \"http://maps/foobar\"]\nconst arrayOfUrlSchema = {\n    addresses: {\n        type: Array,\n        group: addressGroup,\n        ...permissions,\n    },\n    'addresses.$': {\n        type: String,\n        input: 'url',\n    },\n};\n// example with array and custom input\nconst CustomObjectInput = () => 'OBJECT INPUT';\nconst arrayOfCustomObjectSchema = {\n    addresses: {\n        type: Array,\n        group: addressGroup,\n        ...permissions,\n    },\n    'addresses.$': {\n        type: new SimpleSchema(addressSchema),\n        input: CustomObjectInput,\n    },\n};\n// example with a fully custom input for both the array and its children\nconst ArrayInput = () => 'ARRAY INPUT';\nconst arrayFullCustomSchema = {\n    addresses: {\n        type: Array,\n        group: addressGroup,\n        ...permissions,\n        input: ArrayInput,\n    },\n    'addresses.$': {\n        type: String,\n        input: 'url',\n    },\n};\n// example with a native type\n// [\"20 rue du Moulin PARIS\", \"16 rue de la poste PARIS\"]\n\n// eslint-disable-next-line no-unused-vars\nconst arrayOfStringSchema = {\n    addresses: {\n        type: Array,\n        group: addressGroup,\n        ...permissions,\n    },\n    'addresses.$': {\n        type: String,\n    },\n};\n// object (not in an array): {street, city}\nconst objectSchema = {\n    addresses: {\n        type: new SimpleSchema(addressSchema),\n        ...permissions,\n    },\n};\n// without calling SimpleSchema\n// eslint-disable-next-line no-unused-vars\nconst bareObjectSchema = {\n    addresses: {\n        type: addressSchema,\n        ...permissions,\n    },\n};\n\n// stub collection\nimport { createCollection, getDefaultResolvers, getDefaultMutations } from 'meteor/vulcan:core';\nconst createDummyCollection = (typeName, schema) =>\n    createCollection({\n        collectionName: typeName + 's',\n        typeName,\n        schema,\n        resolvers: getDefaultResolvers(typeName + 's'),\n        mutations: getDefaultMutations(typeName + 's'),\n    });\nconst Foos = createDummyCollection('Foo', fooSchema);\nconst ArrayOfObjects = createDummyCollection('ArrayOfObject', arrayOfObjectSchema);\nconst Objects = createDummyCollection('Object', objectSchema);\nconst ArrayOfUrls = createDummyCollection('ArrayOfUrl', arrayOfUrlSchema);\nconst ArrayOfCustomObjects = createDummyCollection(\n    'ArrayOfCustomObject',\n    arrayOfCustomObjectSchema\n);\nconst ArrayFullCustom = createDummyCollection('ArrayFullCustom', arrayFullCustomSchema);\n// eslint-disable-next-line no-unused-vars\nconst ArrayOfStrings = createDummyCollection('ArrayOfString', arrayOfStringSchema);\n\nconst Addresses = createCollection({\n    collectionName: 'Addresses',\n    typeName: 'Address',\n    schema: addressSchema,\n    resolvers: getDefaultResolvers('Addresses'),\n    mutations: getDefaultMutations('Addresses'),\n});\n\n// helpers\n// tests\ndescribe('vulcan-forms/Form', function () {\n    const context = {\n        intl: {\n            formatMessage: () => '',\n            formatDate: () => '',\n            formatTime: () => '',\n            formatRelative: () => '',\n            formatNumber: () => '',\n            formatPlural: () => '',\n            formatHTMLMessage: () => '',\n            now: () => '',\n        },\n    };\n    // eslint-disable-next-line no-unused-vars\n    const mountWithContext = C =>\n        mount(C, {\n            context,\n        });\n    const shallowWithContext = C =>\n        shallow(C, {\n            context,\n        });\n\n    // since some props are now handled by HOC we need to provide them manually\n    const defaultProps = {\n        collectionName: '',\n        typeName: '',\n    };\n\n    describe('Form generation', function () {\n        // getters\n        const getArrayFormGroup = wrapper => wrapper.find('FormGroup').find({ name: 'addresses' });\n        const getFields = arrayFormGroup => arrayFormGroup.prop('fields');\n        describe('basic collection - no nesting', function () {\n            it('shallow render', function () {\n                const wrapper = shallowWithContext(<Form {...defaultProps} collection={Addresses} />);\n                expect(wrapper).toBeDefined();\n            });\n        });\n        describe('nested object (not in array)', function () {\n            it('shallow render', () => {\n                const wrapper = shallowWithContext(<Form {...defaultProps} collection={Objects} />);\n                expect(wrapper).toBeDefined();\n            });\n            it('define one field', () => {\n                const wrapper = shallowWithContext(<Form {...defaultProps} collection={Objects} />);\n                const defaultGroup = wrapper.find('FormGroup').first();\n                const fields = defaultGroup.prop('fields');\n                expect(fields).toHaveLength(1); // addresses field\n            });\n\n            const getFormFields = wrapper => {\n                const defaultGroup = wrapper.find('FormGroup').first();\n                const fields = defaultGroup.prop('fields');\n                return fields;\n            };\n            const getFirstField = () => {\n                const wrapper = shallowWithContext(<Form {...defaultProps} collection={Objects} />);\n                const fields = getFormFields(wrapper);\n                return fields[0];\n            };\n            it('define the nestedSchema', () => {\n                const addressField = getFirstField();\n                expect(addressField.nestedSchema.street).toBeDefined();\n            });\n        });\n        describe('array of objects', function () {\n            it('shallow render', () => {\n                const wrapper = shallowWithContext(\n                    <Form {...defaultProps} collection={ArrayOfObjects} />\n                );\n                expect(wrapper).toBeDefined();\n            });\n            it('render a FormGroup for addresses', function () {\n                const wrapper = shallowWithContext(\n                    <Form {...defaultProps} collection={ArrayOfObjects} />\n                );\n                const formGroup = wrapper.find('FormGroup').find({ name: 'addresses' });\n                expect(formGroup).toBeDefined();\n                expect(formGroup).toHaveLength(1);\n            });\n            it('passes down the array child fields', function () {\n                const wrapper = shallowWithContext(\n                    <Form {...defaultProps} collection={ArrayOfObjects} />\n                );\n                const formGroup = getArrayFormGroup(wrapper);\n                const fields = getFields(formGroup);\n                const arrayField = fields[0];\n                expect(arrayField.nestedInput).toBe(true);\n                expect(arrayField.nestedFields).toHaveLength(Object.keys(addressSchema).length);\n            });\n            it('uses prefilled props for the whole array', () => {\n                const prefilledProps = {\n                    'addresses': [{\n                        'street': 'Rue de la paix'\n                    }]\n                };\n                const wrapper = mountWithContext(\n                    <Form {...defaultProps} collection={ArrayOfObjects} prefilledProps={prefilledProps} />\n                );\n                const input = wrapper.find('input');\n                expect(input).toHaveLength(1);\n                expect(input.prop('value')).toEqual('Rue de la paix');\n            });\n            it('passes down prefilled props to objects nested in array', () => {\n                const prefilledProps = {\n                    'addresses.$': {\n                        'street': 'Rue de la paix'\n                    }\n                };\n                const wrapper = mountWithContext(\n                    <Form {...defaultProps} collection={ArrayOfObjects} prefilledProps={prefilledProps} />\n                );\n                // press the add button\n                wrapper.find('button.form-nested-add').first().simulate('click');\n                const input = wrapper.find('input');\n                expect(input.prop('value')).toEqual('Rue de la paix');\n            });\n            it('combine prefilled prop for array and for array item', () => {\n                const prefilledProps = {\n                    'addresses': [{\n                        street: 'first'\n                    }],\n                    'addresses.$': {\n                        'street': 'second'\n                    }\n                };\n                const wrapper = mountWithContext(\n                    <Form {...defaultProps} collection={ArrayOfObjects} prefilledProps={prefilledProps} />\n                );\n                // first input matches the array default value\n                const input1 = wrapper.find('input').at(0);\n                expect(input1.prop('value')).toEqual('first');\n                // newly created input matches the child default value\n                wrapper.find('button.form-nested-add').simulate('click'); // 1st button = deletion, 2nd button = add\n                expect(wrapper.find('input')).toHaveLength(2);\n                const input2 = wrapper.find('input').at(1); // second input\n                expect(input2.prop('value')).toEqual('second');\n            });\n        });\n        describe('array with custom children inputs (e.g array of url)', function () {\n            it('shallow render', function () {\n                const wrapper = shallowWithContext(<Form {...defaultProps} collection={ArrayOfUrls} />);\n                expect(wrapper).toBeDefined();\n            });\n            it('passes down the array item custom input', () => {\n                const wrapper = shallowWithContext(<Form {...defaultProps} collection={ArrayOfUrls} />);\n                const formGroup = getArrayFormGroup(wrapper);\n                const fields = getFields(formGroup);\n                const arrayField = fields[0];\n                expect(arrayField.arrayField).toBeDefined();\n            });\n        });\n        describe('array of objects with custom children inputs', function () {\n            it('shallow render', function () {\n                const wrapper = shallowWithContext(\n                    <Form {...defaultProps} collection={ArrayOfCustomObjects} />\n                );\n                expect(wrapper).toBeDefined();\n            });\n            // TODO: does not work, schema_utils needs an update\n            it.skip('passes down the custom input', function () {\n                const wrapper = shallowWithContext(\n                    <Form {...defaultProps} collection={ArrayOfCustomObjects} />\n                );\n                const formGroup = getArrayFormGroup(wrapper);\n                const fields = getFields(formGroup);\n                const arrayField = fields[0];\n                expect(arrayField.arrayField).toBeDefined();\n            });\n        });\n        describe('array with a fully custom input (array itself and children)', function () {\n            it('shallow render', function () {\n                const wrapper = shallowWithContext(\n                    <Form {...defaultProps} collection={ArrayFullCustom} />\n                );\n                expect(wrapper).toBeDefined();\n            });\n            it('passes down the custom input', function () {\n                const wrapper = shallowWithContext(\n                    <Form {...defaultProps} collection={ArrayFullCustom} />\n                );\n                const formGroup = getArrayFormGroup(wrapper);\n                const fields = getFields(formGroup);\n                const arrayField = fields[0];\n                expect(arrayField.arrayField).toBeDefined();\n            });\n        });\n    });\n\n    describe('Form state management', function () {\n        // TODO: the change callback is triggerd but `foo` becomes null instead of \"bar\n        // so it's added to the deletedValues and not changedValues\n        it.skip('store typed value', function () {\n            const wrapper = mountWithContext(<Form {...defaultProps} collection={Foos} />);\n            //console.log(wrapper.state());\n            wrapper\n                .find('input')\n                .first()\n                .simulate('change', { target: { value: 'bar' } });\n            // eslint-disable-next-line no-console\n            console.log(\n                wrapper\n                    .find('input')\n                    .first()\n                    .html()\n            );\n            // eslint-disable-next-line no-console\n            console.log(wrapper.state());\n            expect(wrapper.state().currentValues).toEqual({ foo: 'bar' });\n        });\n        it('reset state when relevant props change', function () {\n            const wrapper = shallowWithContext(\n                <Form {...defaultProps} collectionName=\"Foos\" collection={Foos} />\n            );\n            wrapper.setState({ currentValues: { foo: 'bar' } });\n            expect(wrapper.state('currentValues')).toEqual({ foo: 'bar' });\n            wrapper.setProps({ collectionName: 'Bars' });\n            expect(wrapper.state('currentValues')).toEqual({});\n        });\n        it('does not reset state when external prop change', function () {\n            //const prefilledProps = { bar: 'foo' } // TODO\n            const changeCallback = () => 'CHANGE';\n            const wrapper = shallowWithContext(\n                <Form {...defaultProps} collection={Foos} changeCallback={changeCallback} />\n            );\n            wrapper.setState({ currentValues: { foo: 'bar' } });\n            expect(wrapper.state('currentValues')).toEqual({ foo: 'bar' });\n            const newChangeCallback = () => 'NEW';\n            wrapper.setProps({ changeCallback: newChangeCallback });\n            expect(wrapper.state('currentValues')).toEqual({ foo: 'bar' });\n        });\n    });\n});"
  },
  {
    "path": "packages/vulcan-forms/test/FormComponent.test.js",
    "content": "\n\nimport React from 'react';\n// TODO: should be loaded from Components instead?\nimport FormComponent from '../lib/components/FormComponent';\nimport expect from 'expect';\n\nimport { mount, shallow } from 'enzyme';\nimport { Components } from 'meteor/vulcan:core';\nimport { initComponentTest } from 'meteor/vulcan:test';\n\n// we must import all the other components, so that \"registerComponent\" is called\nimport '../lib/modules/components';\n\n// setup Vulcan (load components, initialize fragments)\ninitComponentTest();\n\n// fixtures\nimport SimpleSchema from 'simpl-schema';\n\n\n// helpers\n// tests\ndescribe('vulcan-forms/FormComponent', function () {\n    const shallowWithContext = C =>\n        shallow(C, {\n            context: {\n                getDocument: () => { },\n            },\n        });\n    const defaultProps = {\n        disabled: false,\n        optional: true,\n        document: {},\n        name: 'meetingPlace',\n        path: 'meetingPlace',\n        datatype: [{ type: Object }],\n        layout: 'horizontal',\n        label: 'Meeting place',\n        currentValues: {},\n        formType: 'new',\n        deletedValues: [],\n        throwError: () => { },\n        updateCurrentValues: () => { },\n        errors: [],\n        clearFieldErrors: () => { },\n    };\n    it('shallow render', function () {\n        const wrapper = shallowWithContext(<FormComponent {...defaultProps} />);\n        expect(wrapper).toBeDefined();\n    });\n    describe('array of objects', function () {\n        const props = {\n            ...defaultProps,\n            datatype: [{ type: Array }],\n            nestedSchema: {\n                street: {},\n                country: {},\n                zipCode: {},\n            },\n            nestedInput: true,\n            nestedFields: [{}, {}, {}],\n            currentValues: {},\n        };\n        it('render a FormNestedArray', function () {\n            const wrapper = shallowWithContext(<FormComponent {...props} />);\n            const formNested = wrapper.find('FormNestedArray');\n            expect(formNested).toHaveLength(1);\n        });\n    });\n    describe('nested object', function () {\n        const props = {\n            ...defaultProps,\n            datatype: [{ type: new SimpleSchema({}) }],\n            nestedSchema: {\n                street: {},\n                country: {},\n                zipCode: {},\n            },\n            nestedInput: true,\n            nestedFields: [{}, {}, {}],\n            currentValues: {},\n        };\n        it('shallow render', function () {\n            const wrapper = shallowWithContext(<FormComponent {...props} />);\n            expect(wrapper).toBeDefined();\n        });\n        it('render a FormNestedObject', function () {\n            const wrapper = shallowWithContext(<FormComponent {...props} />);\n            const formNested = wrapper.find('FormNestedObject');\n            expect(formNested).toHaveLength(1);\n        });\n    });\n    describe('array of custom inputs (e.g url)', function () {\n        it('shallow render', function () { });\n    });\n\n});\n"
  },
  {
    "path": "packages/vulcan-forms/test/FormNestedArray.test.js",
    "content": "\nimport React from 'react';\n// TODO: should be loaded from Components instead?\nimport Form from '../lib/components/Form';\nimport FormComponent from '../lib/components/FormComponent';\nimport FormNestedArray from '../lib/components/FormNestedArray';\nimport FormNestedArrayLayout from '../lib/components/FormNestedArrayLayout';\nimport expect from 'expect';\n\nimport { mount, shallow } from 'enzyme';\nimport { Components } from 'meteor/vulcan:core';\nimport { initComponentTest } from 'meteor/vulcan:test';\n\n// we must import all the other components, so that \"registerComponent\" is called\nimport '../lib/modules/components';\n\n\n\n// helpers\n// tests\ndescribe('vulcan:forms/FormNestedArray', function () {\n    const defaultProps = {\n        errors: [],\n        deletedValues: [],\n        path: 'foobar',\n        formComponents: Components,\n        //nestedFields: []\n    };\n\n    describe('Display the input n times', function () {\n        it('shallow render', function () {\n            const wrapper = shallow(<FormNestedArray {...defaultProps} currentValues={{}} />);\n            expect(wrapper).toBeDefined();\n        });\n        // TODO: broken now we use a layout...\n        it.skip('shows an add button when empty', function () {\n            const wrapper = mount(<FormNestedArray {...defaultProps} currentValues={{}} />);\n            const addButton = wrapper.find('IconAdd');\n            expect(addButton).toHaveLength(1);\n        });\n        it.skip('shows 3 items', function () {\n            const wrapper = mount(\n                <FormNestedArray {...defaultProps} currentValues={{}} value={[1, 2, 3]} />\n            );\n            const nestedItem = wrapper.find('FormNestedItem');\n            expect(nestedItem).toHaveLength(3);\n        });\n        it.skip('pass the correct path and itemIndex to each form', function () {\n            const wrapper = mount(\n                <FormNestedArray {...defaultProps} currentValues={{}} value={[1, 2]} />\n            );\n            const nestedItem = wrapper.find('FormNestedItem');\n            const item0 = nestedItem.at(0);\n            const item1 = nestedItem.at(1);\n            expect(item0.prop('itemIndex')).toEqual(0);\n            expect(item1.prop('itemIndex')).toEqual(1);\n            expect(item0.prop('path')).toEqual('foobar.0');\n            expect(item1.prop('path')).toEqual('foobar.1');\n        });\n    });\n    describe('maxCount', function () {\n        const props = {\n            ...defaultProps,\n            maxCount: 2,\n        };\n        it('should pass addItem to FormNestedArrayLayout if items < maxCount', function () {\n            const wrapper = shallow(<FormNestedArray {...props} maxCount={2} currentValues={{}} value={[1]} />);\n            const layout = wrapper.find('FormNestedArrayLayout').first();\n            const addItem = layout.props().addItem;\n            expect(typeof addItem).toBe('function');\n        });\n        it('should display add button if items < maxCount', function () {\n            const wrapper = shallow(<FormNestedArrayLayout {...defaultProps} addItem={() => { return null; }} hasError={false} />);\n            const button = wrapper.find('.form-nested-button');\n            expect(button).toHaveLength(1);\n        });\n        it('should not pass addItem to FormNestedArrayLayout if items >= maxCount', function () {\n            const wrapper = shallow(<FormNestedArray {...props} maxCount={2} currentValues={{}} value={[1, 2]} />);\n            const layout = wrapper.find('FormNestedArrayLayout').first();\n            const addItem = layout.props().addItem;\n            expect(addItem).toBeNull();\n        });\n        it('should not display add button if items >= maxCount', function () {\n            const wrapper = shallow(<FormNestedArrayLayout {...defaultProps} addItem={null} hasError={false} />);\n            const button = wrapper.find('.form-nested-button');\n            expect(button).toHaveLength(0);\n        });\n    });\n\n    describe('minCount', function () {\n        const props = {\n            ...defaultProps,\n            minCount: 2,\n        };\n        it('should display remove item button when array length > minCount', function () {\n            const wrapper = shallow(<FormNestedArray {...props} currentValues={{}} value={[1, 2, 3]} />);\n            const layout = wrapper.find('FormNestedArrayLayout').first();\n            const nestedItems = layout.find('FormNestedItem');\n            nestedItems.forEach((nestedItem, idx) => {\n                const hideRemove = nestedItem.prop('hideRemove');\n                expect({ res: hideRemove, idx }).toEqual({ res: false, idx });\n            });\n        });\n        it('should not display remove button if items <= minCount', function () {\n            const wrapper = shallow(<FormNestedArray {...props} currentValues={{}} value={[1, 2]} />);\n            const layout = wrapper.find('FormNestedArrayLayout').first();\n            const nestedItems = layout.find('FormNestedItem');\n            nestedItems.forEach((nestedItem, idx) => {\n                const hideRemove = nestedItem.prop('hideRemove');\n                expect({ res: hideRemove, idx }).toEqual({ res: true, idx });\n            });\n        });\n    });\n});\n"
  },
  {
    "path": "packages/vulcan-forms/test/FormNestedObject.test.js",
    "content": "import React from 'react';\n// TODO: should be loaded from Components instead?\nimport Form from '../lib/components/Form';\nimport FormComponent from '../lib/components/FormComponent';\nimport FormNestedArray from '../lib/components/FormNestedArray';\nimport FormNestedArrayLayout from '../lib/components/FormNestedArrayLayout';\nimport expect from 'expect';\n\nimport { mount, shallow } from 'enzyme';\nimport { Components } from 'meteor/vulcan:core';\nimport { initComponentTest } from 'meteor/vulcan:test';\n\n// we must import all the other components, so that \"registerComponent\" is called\nimport '../lib/modules/components';\n\n\n// setup Vulcan (load components, initialize fragments)\ninitComponentTest();\n\n// helpers\n// tests\ndescribe('vulcan-forms/FormNestedObject', function () {\n    const defaultProps = {\n        errors: [],\n        path: 'foobar',\n        formComponents: Components,\n    };\n    it('shallow render', function () {\n        const wrapper = shallow(<Components.FormNestedObject {...defaultProps} currentValues={{}} />);\n        expect(wrapper).toBeDefined();\n    });\n    it.skip('render a Form collectionName=\"\" for the object', function () {\n        // eslint-disable-next-line no-unused-vars\n        const wrapper = shallow(<Components.FormNestedObject {...defaultProps} currentValues={{}} />);\n        expect(false).toBe(true);\n    });\n    it('does not show any button', function () {\n        const wrapper = shallow(<Components.FormNestedObject {...defaultProps} currentValues={{}} />);\n        const button = wrapper.find('BootstrapButton');\n        expect(button).toHaveLength(0);\n    });\n    it('does not show add button', function () {\n        const wrapper = shallow(<Components.FormNestedObject {...defaultProps} currentValues={{}} />);\n        const addButton = wrapper.find('IconAdd');\n        expect(addButton).toHaveLength(0);\n    });\n    it('does not show remove button', function () {\n        const wrapper = shallow(<Components.FormNestedObject {...defaultProps} currentValues={{}} />);\n        const removeButton = wrapper.find('IconRemove');\n        expect(removeButton).toHaveLength(0);\n    });\n});\n"
  },
  {
    "path": "packages/vulcan-forms/test/formFragments.test.js",
    "content": "import expect from 'expect';\nimport { print } from 'graphql/language/printer';\n\nimport SimpleSchema from 'simpl-schema';\nimport getFormFragments from '../lib/modules/formFragments';\nconst test = it;\n\n// allow to easily test regex on a graphql string\n// all blanks and series of blanks are replaces by one single space\nconst normalizeFragment = gqlSchema => print(gqlSchema).replace(/\\s+/g, ' ').trim();\n\nconst defaultArgs = {\n    formType: 'new',\n    collectionName: 'Foos',\n    typeName: 'Foo'\n};\ndescribe('vulcan:form/formFragments', function () {\n    test('generate valid query and mutation fragment', () => {\n        const schema = new SimpleSchema({\n            field: {\n                type: String,\n                canRead: ['admins'],\n                canCreate: ['admins']\n            },\n            nonCreateableField: {\n                type: String,\n                canRead: ['admins'],\n                canUpdate: ['admins']\n            }\n        })._schema;\n        const { queryFragment, mutationFragment } = getFormFragments({\n            ...defaultArgs,\n            schema,\n        });\n        expect(queryFragment).toBeDefined();\n        expect(mutationFragment).toBeDefined();\n        expect(normalizeFragment(queryFragment)).toMatch('fragment FoosNewFormQueryFragment on Foo { field }');\n        expect(normalizeFragment(mutationFragment)).toMatch('fragment FoosNewFormMutationFragment on Foo { field nonCreateableField }');\n    });\n    test('take formType into account', function () {\n        const schema = new SimpleSchema({\n            field: {\n                type: String,\n                canRead: ['admins'],\n                canUpdate: ['admins']\n            },\n            // should not appear\n            nonUpdateableField: {\n                type: String,\n                canRead: ['admins'],\n                canCreate: ['admins']\n            }\n        })._schema;\n        const { queryFragment, mutationFragment } = getFormFragments({\n            ...defaultArgs,\n            formType: 'edit',\n            schema,\n        });\n        expect(normalizeFragment(queryFragment)).toMatch('fragment FoosEditFormQueryFragment on Foo { field }');\n        expect(normalizeFragment(mutationFragment)).toMatch('fragment FoosEditFormMutationFragment on Foo { field nonUpdateableField }');\n\n    });\n    test('create subfields for nested objects', () => {\n        const schema = new SimpleSchema({\n            nestedField: {\n\n                canCreate: ['admins'],\n                type: new SimpleSchema({\n                    firstNestedField: {\n                        canCreate: ['admins'],\n                        type: String,\n                    },\n                    secondNestedField: {\n                        canCreate: ['admins'],\n                        type: Number\n                    }\n                })\n            }\n        })._schema;\n        const { queryFragment, mutationFragment } = getFormFragments({\n            ...defaultArgs,\n            schema,\n        });\n        expect(normalizeFragment(queryFragment)).toMatch('fragment FoosNewFormQueryFragment on Foo { nestedField { firstNestedField secondNestedField } }');\n        expect(normalizeFragment(mutationFragment)).toMatch('fragment FoosNewFormMutationFragment on Foo { nestedField { firstNestedField secondNestedField } }');\n    });\n    test('create subfields for arrays of nested objects', () => {\n        const schema = new SimpleSchema({\n            arrayField: {\n                type: Array,\n                canRead: ['admins'],\n                canCreate: ['admins']\n            },\n            'arrayField.$': {\n                canCreate: ['admins'],\n                type: new SimpleSchema({\n                    firstNestedField: {\n                        canCreate: ['admins'],\n                        type: String,\n                    },\n                    secondNestedField: {\n                        canCreate: ['admins'],\n                        type: Number\n                    }\n                })\n            }\n        })._schema;\n        const { queryFragment, mutationFragment } = getFormFragments({\n            ...defaultArgs,\n            schema,\n        });\n        expect(normalizeFragment(queryFragment)).toMatch('fragment FoosNewFormQueryFragment on Foo { arrayField { firstNestedField secondNestedField } }');\n        expect(normalizeFragment(mutationFragment)).toMatch('fragment FoosNewFormMutationFragment on Foo { arrayField { firstNestedField secondNestedField } }');\n    });\n    test('add readable fields to mutation fragment', () => {\n        const schema = new SimpleSchema({\n            field: {\n                type: String,\n                canRead: ['admins'],\n                canCreate: ['admins']\n            },\n            readOnlyField: {\n                type: String,\n                canRead: ['admins']\n            }\n        })._schema;\n        const { queryFragment, mutationFragment } = getFormFragments({\n            ...defaultArgs,\n            schema,\n        });\n        expect(normalizeFragment(queryFragment)).not.toMatch('readOnlyField'); // this does not affect the queryFragment;\n        expect(normalizeFragment(mutationFragment)).toMatch('fragment FoosNewFormMutationFragment on Foo { field readOnlyField }');\n    });\n    test('ignore virtual/resolved fields', () => {\n        const schema = new SimpleSchema({\n            field: {\n                type: String,\n                canRead: ['admins'],\n                canCreate: ['admins'],\n                resolveAs: {\n                    fieldName: 'resolvedField',\n                    type: 'Whatever',\n                    addOriginalField: true,\n                    resolver: () => ({})\n                }\n            },\n            virtual: {\n                type: String,\n                canRead: ['admins'],\n                resolveAs: {\n                    type: 'Whatever',\n                    resolver: () => ({})\n                }\n            }\n        })._schema;\n        const { queryFragment, mutationFragment } = getFormFragments({\n            ...defaultArgs,\n            schema,\n        });\n        expect(normalizeFragment(queryFragment)).not.toMatch('virtual');\n        expect(normalizeFragment(mutationFragment)).toMatch('fragment FoosNewFormMutationFragment on Foo { field }');\n\n    });\n    test(\"add userId and _id when they are present in the schema\", () => {\n        const schemaWithIds = new SimpleSchema({\n            _id: {\n                type: String,\n                canRead: ['guests'],\n            },\n            userId: {\n                type: String,\n                canRead: ['guests']\n            }\n        })._schema;\n        const { queryFragment, mutationFragment } = getFormFragments({\n            ...defaultArgs,\n            schema: schemaWithIds,\n        });\n        expect(normalizeFragment(queryFragment)).toMatch(/_id/);\n        expect(normalizeFragment(mutationFragment)).toMatch(/_id/);\n        expect(normalizeFragment(queryFragment)).toMatch(/userId/);\n        expect(normalizeFragment(mutationFragment)).toMatch(/userId/);\n    })\n    test(\"do not add _id and userId if not in the schema\", () => {\n        const schemaWithoutIds = new SimpleSchema({\n            field: {\n                type: String,\n                canRead: ['guests'],\n                canCreate: ['guests']\n            },\n        })._schema;\n        const { queryFragment, mutationFragment } = getFormFragments({\n            ...defaultArgs,\n            schema: schemaWithoutIds,\n        });\n        expect(normalizeFragment(queryFragment)).not.toMatch(/_id/);\n        expect(normalizeFragment(mutationFragment)).not.toMatch(/_id/);\n        expect(normalizeFragment(queryFragment)).not.toMatch(/userId/);\n        expect(normalizeFragment(mutationFragment)).not.toMatch(/userId/);\n    })\n});\n\n"
  },
  {
    "path": "packages/vulcan-forms/test/index.js",
    "content": "import './schema_utils.test.js';\nimport './formFragments.test';\n//import './components.test.js';\nimport './Form.test.js';\nimport './FormComponent.test.js';\nimport './FormNestedObject.test.js';\nimport './FormNestedArray.test.js';\n"
  },
  {
    "path": "packages/vulcan-forms/test/package.test.js",
    "content": "import FormWrapper from 'meteor/vulcan:forms';\nimport expect from 'expect';\ndescribe('vulcan:forms', function () {\n    it.skip('initialize', function () {\n        expect(FormWrapper.name).toEqual('GraphQL');\n    });\n});"
  },
  {
    "path": "packages/vulcan-forms/test/schema_utils.test.js",
    "content": "import {\n  getNestedFieldSchemaOrType,\n} from '../lib/modules/schema_utils.js';\nimport SimpleSchema from 'simpl-schema';\nimport expect from 'expect';\n\nconst addressSchema = {\n  street: {\n    type: String\n  },\n  country: {\n    type: String\n  }\n};\nconst addressSimpleSchema = new SimpleSchema(addressSchema);\n\ndescribe('schema_utils', function () {\n  describe('getNestedFieldSchemaOrType', function () {\n    it('get nested schema of an array', function () {\n      const simpleSchema = new SimpleSchema({\n        addresses: {\n          type: Array\n        },\n        'addresses.$': {\n          // this is due to SimpleSchema objects structure\n          type: addressSimpleSchema\n        }\n      });\n      const nestedSchema = getNestedFieldSchemaOrType('addresses', simpleSchema);\n      // nestedSchema is a complex SimpleSchema object, so we can only\n      // test its type instead (might not be the simplest way though)\n      expect(Object.keys(nestedSchema._schema)).toEqual(Object.keys(addressSchema));\n    });\n    it('get nested schema of an object', function () {\n      const simpleSchema = new SimpleSchema({\n        meetingPlace: {\n          type: addressSimpleSchema\n        }\n      });\n      const nestedSchema = getNestedFieldSchemaOrType('meetingPlace', simpleSchema);\n      expect(Object.keys(nestedSchema._schema)).toEqual(Object.keys(addressSchema));\n    });\n    it('return null for other types', function () {\n      const simpleSchema = new SimpleSchema({\n        createdAt: {\n          type: Date\n        }\n      });\n      const nestedSchema = getNestedFieldSchemaOrType('createdAt', simpleSchema);\n      expect(nestedSchema).toBeNull();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-forms-tags/README.md",
    "content": "# Usage\n\n1. Add `vulcan:forms-tags` to your `.meteor/packages` file.\n2. Add `vulcan:forms-tags` to your custom package's dependencies in `package.js`. \n3. Add the [react-tag-input](https://www.npmjs.com/package/react-tag-input) NPM package with `npm install react-tag-input --save`.\n4. In your code, `import Tags from 'meteor/vulcan:forms-tags'` and then set `control: Tags` on a custom field."
  },
  {
    "path": "packages/vulcan-forms-tags/lib/components/Tags.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport ReactTagInput from 'react-tag-input';\nimport PropTypes from 'prop-types';\n\nconst ReactTags = ReactTagInput.WithContext;\n\nclass Tags extends PureComponent {\n\n  constructor(props) {\n    super(props);\n\n    this.suggestions = (props.options || []).map(\n      ({ value, label }) => ({ id: value, text: label })\n    );\n    const tags = (props.value || []).map(id => (\n      // tolerate cases when a tag is not found in suggestions (create a tag on the fly)\n      this.suggestions.find(suggestion => id === suggestion.id) || { id, text: id }\n    ));\n\n    this.state = { tags };\n  }\n\n  handleChange = reducer => value => {\n    const tags = reducer(this.state.tags, value);\n    this.setState({ tags });\n    this.props.inputProperties.onChange(this.props.name, tags.map(({ id }) => id));\n  }\n\n  render() {\n    return (\n      <div className=\"form-group row\">\n        <label className=\"control-label col-sm-3\">{this.props.label}</label>\n        <div className=\"col-sm-9\">\n          <div className=\"tags-field\">\n            <ReactTags\n              tags={this.state.tags}\n              suggestions={this.suggestions}\n              handleDelete={this.handleChange(\n                (tags, index) => [...tags.slice(0, index), ...tags.slice(index + 1)]\n              )}\n              handleAddition={this.handleChange((tags, newTag) => [...tags, newTag])}\n              minQueryLength={1}\n            />\n          </div>\n        </div>\n      </div>\n    );\n  }\n}\n\nTags.propTypes = {\n  name: PropTypes.string,\n  value: PropTypes.any,\n  label: PropTypes.string,\n  inputProperties: PropTypes.shape({\n    onChange: PropTypes.func,\n  }),\n  options: PropTypes.arrayOf(\n    PropTypes.shape({\n      value: PropTypes.any,\n      label: PropTypes.string,\n    })\n  ),\n};\n\nexport default Tags;\n"
  },
  {
    "path": "packages/vulcan-forms-tags/lib/export.js",
    "content": "import Tags from './components/Tags.jsx';\n\nexport default Tags;\n"
  },
  {
    "path": "packages/vulcan-forms-tags/package.js",
    "content": "Package.describe({\n  name: 'vulcan:forms-tags',\n  summary: 'Vulcan tag input package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9', 'vulcan:forms@=1.16.9']);\n\n  api.mainModule('lib/export.js', ['client', 'server']);\n});\n"
  },
  {
    "path": "packages/vulcan-forms-upload/README.md",
    "content": "# nova-upload\n🏖🔭 Vulcan package extending `vulcan:forms` to upload images to Cloudinary from a drop zone.\n\n\n![Screenshot](https://res.cloudinary.com/xavcz/image/upload/v1471534203/Capture_d_e%CC%81cran_2016-08-17_14.22.14_ehwv0d.png)\n\nWant to add this to your Vulcan instance? Read below:\n\n# Installation\n\n### 1. Meteor package\nI would recommend that you clone this repo in your vulcan's `/packages` folder.\n\nThen, open the `.meteor/packages` file and add at the end of the **Optional packages** section:\n`xavcz:nova-forms-upload` \n\n> **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)*\n\n```bash\n# only for Nova classic users (v0.27.5)\ncd nova-forms-upload\ngit checkout nova-classic\n```\n\n### 2. NPM dependency\nThis package depends on the awesome `react-dropzone` ([repo](https://github.com/okonet/react-dropzone)), you need to install the dependency:\n```\nnpm install react-dropzone cross-fetch\n```\n\n### 3. Cloudinary account\nCreate a [Cloudinary account](https://cloudinary.com) if you don't have one.\n\nThe upload to Cloudinary relies on **unsigned upload**:\n\n> 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.\n\nUnsigned 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.\n\nWhen 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.\n\nIt may look like this:\n\n![Screenshot-Cloudinary](https://res.cloudinary.com/xavcz/image/upload/v1471534183/Capture_d_e%CC%81cran_2016-08-18_17.07.52_tr9uoh.png)\n\n### 4. Nova Settings\nEdit your `settings.json` and add inside the `public: { ... }` block the following entries with your own credentials:\n\n```json\npublic: {\n\n\n  \"cloudinaryCloudName\": \"YOUR_APP_NAME\",\n  \"cloudinaryPresets\": {\n    \"avatar\": \"YOUR_PRESET_ID\",\n    \"posts\": \"THE_SAME_OR_ANOTHER_PRESET_ID\"\n  }\n\n\n}\n```\n\nPicture upload in Nova is now enabled! Easy-peasy, right? 👯\n\n### 5. Your custom package & custom fields\n\nMake 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.\n\nYou 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`.\n\n**⚠️ Note:** Don't forget to update your query fragments wherever needed after defining your custom fields, else they will never be available!\n\n## Image for posts\nLet's say you want to enhance your posts with a custom image. In your custom package, your new custom field could look like this:\n\n```js\n// ... your imports\nimport { getComponent, getSetting } from 'meteor/nova:lib';\nimport Posts from 'meteor/nova:posts';\n\n// extends Posts schema with a new field: 'image' 🏖\nPosts.addField({\n  fieldName: 'image',\n  fieldSchema: {\n    type: String,\n    optional: true,\n    input: getComponent('Upload'),\n    canCreate: ['members'],\n    canUpdate: ['members'],\n    canRead: ['guests'],\n    form: {\n      options: {\n        preset: getSetting('cloudinaryPresets').posts // this setting refers to the transformation you want to apply to the image\n      },\n    }\n  }\n});\n```\n\n## Avatar for users\nLet'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:\n```js\n// ... your imports\nimport { getComponent, getSetting } from 'meteor/nova:lib';\nimport Users from 'meteor/nova:users';\n\n// extends Users schema with a new field: 'avatar' 👁\nUsers.addField({\n  fieldName: 'avatar',\n  fieldSchema: {\n    type: String,\n    optional: true,\n    input: getComponent('Upload'),\n    canCreate: ['members'],\n    canUpdate: ['members'],\n    canRead: ['guests'],\n    preload: true, // ⚠️ will preload the field for the current user!\n    form: {\n      options: {\n        preset: getSetting('cloudinaryPresets').avatar // this setting refers to the transformation you want to apply to the image\n      },\n    }\n  }\n});\n```\n\nAdding 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:\n\n```js\nconst originalAvatarConstructor = Users.avatar;\n\n// extends the Users.avatar function\nUsers.avatar = {\n  ...originalAvatarConstructor,\n  getUrl(user) {\n    url = originalAvatarConstructor.getUrl(user);\n\n    return !!user && user.avatar ? user.avatar : url;\n  },\n};\n```\n\nNow, you also need to update the query fragments related to `User` when you want the custom avatar to show up :)\n\n## S3? Google Cloud?\nFeel free to contribute to add new features and flexibility to this package :)\n\nYou are welcome to come chat about it [in the Slack chatroom](http://slack.vulcanjs.org)\n\n## What about `nova:cloudinary` ?\nThis package and `nova:cloudinary` share a settings in common: `cloudinaryCloudName`. They are fully compatible.\n\nHappy hacking! 🚀\n"
  },
  {
    "path": "packages/vulcan-forms-upload/lib/Upload.jsx",
    "content": "/*\n\nThis component supports uploading and storing an array of images. \n\nNote also that an image can be stored as a simple string, or as an array of formats\n(each format being itself an object).\n\n### Deleting Images\n\nWhen clearing an image, it is addeds to `deletedValues` and set to `null` in the array,\nbut the array item itself is not deleted. The entire array is then cleaned when submitting the form.\n\n*/\nimport { Components, getSetting, registerSetting, registerComponent } from 'meteor/vulcan:lib';\nimport React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport Dropzone from 'react-dropzone';\nimport 'cross-fetch/polyfill'; // patch for browser which don't have fetch implemented\nimport set from 'lodash/set';\n\nregisterSetting('cloudinary.cloudName', null, 'Cloudinary cloud name (for image uploads)');\n\n/*\n\nDropzone styles\n\n*/\nconst baseStyle = {\n  borderWidth: 1,\n  borderStyle: 'dashed',\n  marginBottom: '10',\n  padding: '10',\n};\nconst activeStyle = {\n  borderStyle: 'solid',\n  borderColor: '#6c6',\n  backgroundColor: '#eee',\n};\nconst rejectStyle = {\n  borderStyle: 'solid',\n  borderColor: '#c66',\n  backgroundColor: '#eee',\n};\n\n/*\n\nGet a URL from an image or an array of images\n\n*/\nconst getImageUrl = imageOrImageArray => {\n  // if image is actually an array of formats, use first format\n  const image = Array.isArray(imageOrImageArray) ? imageOrImageArray[0] : imageOrImageArray;\n  // if image is an object, return secure_url; else return image itself\n  const imageUrl = typeof image === 'string' ? image : image.secure_url;\n  return imageUrl;\n};\n\n/*\n\nDisplay a single image\n\n*/\nclass Image extends PureComponent {\n  constructor() {\n    super();\n    this.clearImage = this.clearImage.bind(this);\n  }\n\n  clearImage(e) {\n    e.preventDefault();\n    this.props.clearImage(this.props.index);\n  }\n\n  render() {\n    return (\n      <div className={`upload-image ${this.props.loading ? 'upload-image-loading' : ''} ${this.props.error ? 'upload-image-error' : ''}`}>\n        <div className=\"upload-image-contents\">\n          <img style={{ width: 150 }} src={getImageUrl(this.props.image)} />\n          {this.props.loading && (\n            <div className=\"upload-loading\">\n              <Components.Loading />\n            </div>\n          )}\n        </div>\n        <Components.Button variant=\"link\" onClick={this.clearImage}>\n          <Components.Icon name=\"close\" /> Remove image\n        </Components.Button>\n      </div>\n    );\n  }\n}\n\n/*\n\nCloudinary Image Upload component\n\n*/\nclass Upload extends PureComponent {\n  constructor(props, context) {\n    super(props);\n\n    const self = this;\n\n    // add callback to clean any preview or error values\n    // (when handling multiple images)\n    function uploadKeepRealImages(data) {\n      if (Array.isArray(self.props.value)) {\n        // keep only \"real\" images\n        const images = self.getImages({\n          includePreviews: false,\n          includeDeleted: false,\n        });\n        // replace images in `data` object with real images\n        set(data, self.props.path, images);\n      }\n      return data;\n    }\n    context.addToSubmitForm(uploadKeepRealImages);\n  }\n  state = { uploading: false };\n\n  /*\n\n  Find out field type\n\n  */\n  getFieldType = () => {\n    return this.props.datatype && this.props.datatype[0].type;\n  };\n\n  /*\n\n  Check the field's type to decide if the component should handle\n  multiple image uploads or not. Default to yes.\n\n  */\n  enableMultiple = () => {\n    return this.getFieldType() !== String || this.props.maxCount !== 1;\n  };\n\n  /*\n\n  Whether to disable the dropzone. \n\n  */\n  isDisabled = () => {\n    return this.state.uploading || this.props.maxCount <= this.getImages({ includeDeleted: false }).length;\n  };\n\n  /*\n\n  When an image is uploaded\n\n  */\n  onDrop = files => {\n    const promises = [];\n    const imagesCount = this.getImages().length;\n\n    this.props.clearFieldErrors(this.props.path);\n\n    // set the component in upload mode\n    this.setState({\n      uploading: true,\n    });\n\n    // request url to cloudinary\n    const cloudinaryUrl = `https://api.cloudinary.com/v1_1/${getSetting('cloudinary.cloudName')}/upload`;\n\n    // trigger a request for each file\n    files.forEach((file, index) => {\n      // figure out update path for current image\n      const updateIndex = imagesCount + index;\n      const updatePath = this.getFieldType() === String ? this.props.path : `${this.props.path}.${updateIndex}`;\n\n      // build preview object\n      const previewObject = {\n        secure_url: file.preview,\n        loading: true,\n        preview: true,\n      };\n\n      // update current values using preview object\n      this.props.updateCurrentValues({ [updatePath]: previewObject });\n\n      // request body\n      const body = new FormData();\n      body.append('file', file);\n      body.append('upload_preset', this.props.options.preset);\n\n      // post request to cloudinary\n      promises.push(\n        fetch(cloudinaryUrl, {\n          method: 'POST',\n          body,\n        })\n          .then(res => res.json()) // json-ify the readable strem\n          .then(body => {\n            if (body.error) {\n              // eslint-disable-next-line no-console\n              console.log(body.error);\n              this.props.throwError({\n                id: 'upload.error',\n                path: this.props.path,\n                message: body.error.message,\n              });\n              const errorObject = {\n                ...previewObject,\n                loading: false,\n                error: true,\n              };\n              this.props.updateCurrentValues({ [updatePath]: errorObject });\n              return null;\n            } else {\n              // use the https:// url given by cloudinary; or eager property if using transformations\n              const imageObject = body.eager ? body.eager : body.secure_url;\n              this.props.updateCurrentValues({ [updatePath]: imageObject });\n              return imageObject;\n            }\n          })\n          .catch(error => {\n            // eslint-disable-next-line no-console\n            console.log(error);\n            this.props.throwError({\n              id: 'upload.error',\n              path: this.props.path,\n              message: error.message,\n            });\n          })\n      );\n    });\n\n    Promise.all(promises).then(values => {\n      // console.log(values);\n      // set the uploading status to false\n      this.setState({\n        uploading: false,\n      });\n    });\n  };\n\n  isDeleted = index => {\n    return this.props.deletedValues.includes(`${this.props.path}.${index}`);\n  };\n\n  /*\n\n  Remove the image at `index`\n\n  */\n  clearImage = index => {\n    this.props.updateCurrentValues({ [`${this.props.path}.${index}`]: null });\n  };\n\n  /*\n\n  Get images, with or without previews/deleted images\n\n  */\n  getImages = (args = {}) => {\n    const { includePreviews = true, includeDeleted = false } = args;\n    let images = this.props.value;\n\n    // if images is an empty string, null, etc. just return an empty array\n    if (!images) {\n      return [];\n    }\n\n    // if images is not array, make it one (for backwards compatibility)\n    if (!Array.isArray(images)) {\n      images = [images];\n    }\n    // remove previews if needed\n    images = includePreviews ? images : images.filter(image => !image.preview);\n    // remove deleted images\n    images = includeDeleted ? images : images.filter((image, index) => !this.isDeleted(index));\n    return images;\n  };\n\n  render() {\n    const { uploading } = this.state;\n    const images = this.getImages({ includeDeleted: true });\n\n    return (\n      <div className={`form-group row ${this.isDisabled() ? 'upload-disabled' : ''}`}>\n        <label className=\"control-label col-sm-3\">{this.props.label}</label>\n        <div className=\"col-sm-9\">\n          <div className=\"upload-field\">\n            <Dropzone\n              multiple={this.enableMultiple()}\n              onDrop={this.onDrop}\n              accept=\"image/*\"\n              className=\"dropzone-base\"\n              activeClassName=\"dropzone-active\"\n              rejectClassName=\"dropzone-reject\"\n              disabled={this.isDisabled()}>\n              {({ getRootProps, getInputProps, isDragActive, isDragReject }) => {\n                let styles = { ...baseStyle };\n                styles = isDragActive ? { ...styles, ...activeStyle } : styles;\n                styles = isDragReject ? { ...styles, ...rejectStyle } : styles;\n                return (\n                  <div {...getRootProps()} style={styles}>\n                    <input {...getInputProps()} />\n                    <div>\n                      <Components.FormattedMessage id=\"upload.prompt\" />\n                    </div>\n                    {uploading && (\n                      <div className=\"upload-uploading\">\n                        <span>\n                          <Components.FormattedMessage id=\"upload.uploading\" />\n                        </span>\n                      </div>\n                    )}\n                  </div>\n                );\n              }}\n            </Dropzone>\n\n            {!!images.length && (\n              <div className=\"upload-state\">\n                <div className=\"upload-images\">\n                  {images.map(\n                    (image, index) =>\n                      !this.isDeleted(index) && (\n                        <Image\n                          clearImage={this.clearImage}\n                          key={index}\n                          index={index}\n                          image={image}\n                          loading={image.loading}\n                          preview={image.preview}\n                          error={image.error}\n                        />\n                      )\n                  )}\n                </div>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    );\n  }\n}\n\nUpload.propTypes = {\n  name: PropTypes.string,\n  value: PropTypes.any,\n  label: PropTypes.string,\n};\n\nUpload.contextTypes = {\n  addToSubmitForm: PropTypes.func,\n};\n\nregisterComponent('Upload', Upload);\n\nexport default Upload;\n"
  },
  {
    "path": "packages/vulcan-forms-upload/lib/Upload.scss",
    "content": ".upload-field {\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n}\n\n.dropzone-base {\n  border: 4px dashed #ccc;\n  padding: 30px;\n  transition: \"all 0.5s\";\n  width: 250px;\n  cursor: pointer;\n  color: #ccc;\n  margin-right: 10px;\n  position: relative;\n}\n\n.upload-uploading{\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  background: rgba(255,255,255,0.8);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  span{\n    display: block;\n    font-size: 1.5rem;\n  }\n}\n\n.dropzone-active {\n  border: #4FC47F 4px solid;\n}\n\n.dropzone-reject {\n  border: #DD3A0A 4px solid;\n}\n\n.upload-images{\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n  // justify-content: space-between;\n  // align-items: center;\n}\n.upload-image{\n  margin-right: 10px;\n  a, img{\n    display: block;\n    text-align: center;\n  }\n  a{\n    font-size: 0.8rem;\n  }\n}\n\n.upload-image-contents{\n  position: relative;\n}\n\n.upload-loading{\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  background: rgba(255,255,255,0.8);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.upload-disabled{\n  .dropzone-base{\n    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\");\n  }\n}\n\n.upload-image-error{\n  .upload-image-contents{\n    position: relative;\n    &:after{\n      content: \" \";\n      display: block;\n      position: absolute;\n      height: 100%;\n      width: 100%;\n      top: 0;\n      right: 0;\n      left: 0;\n      right: 0;\n      background-color: rgba(255, 255, 255, 0.6);\n      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\");\n    }\n  }\n}"
  },
  {
    "path": "packages/vulcan-forms-upload/lib/i18n.js",
    "content": "import { addStrings } from 'meteor/vulcan:core';\n\naddStrings('en', {\n  'upload.prompt': 'Drop an image here, or click to select an image to upload.',\n  'upload.uploading': 'Uploading…'\n});"
  },
  {
    "path": "packages/vulcan-forms-upload/lib/modules.js",
    "content": "import Upload from './Upload.jsx';\n\nimport './i18n.js';\n\nexport default Upload;\n"
  },
  {
    "path": "packages/vulcan-forms-upload/package.js",
    "content": "Package.describe({\n  name: 'vulcan:forms-upload',\n  summary: 'Vulcan package extending vulcan:forms to upload images to Cloudinary from a drop zone.',\n  version: '1.16.9',\n  git: 'https://github.com/xavcz/nova-forms-upload.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9', 'vulcan:forms@=1.16.9', 'vulcan:scss@4.12.0']);\n\n  api.addFiles(['lib/Upload.scss'], 'client');\n\n  api.mainModule('lib/modules.js', ['client', 'server']);\n});\n"
  },
  {
    "path": "packages/vulcan-i18n/README.md",
    "content": "Vulcan i18n package. "
  },
  {
    "path": "packages/vulcan-i18n/lib/client/main.js",
    "content": "export * from '../modules/index.js';\n"
  },
  {
    "path": "packages/vulcan-i18n/lib/modules/context.js",
    "content": "import React from 'react';\n\nconst IntlContext = React.createContext({\n  locale: '',\n  key: '',\n  messages: [],\n});\n\nexport default IntlContext;\n"
  },
  {
    "path": "packages/vulcan-i18n/lib/modules/index.js",
    "content": "import { registerSetting } from 'meteor/vulcan:lib';\n\nregisterSetting('locale', 'en-US', 'Your app\\'s locale (“en”, “fr”, etc.)');\n\nexport { default as FormattedMessage } from './message.js';\nexport { intlShape } from './shape.js';\nexport { default as IntlProvider } from './provider.js';\nexport { default as IntlContext } from './context.js';\nexport { default as useIntl } from './useIntl.js';\n"
  },
  {
    "path": "packages/vulcan-i18n/lib/modules/message.js",
    "content": "import React, { Component } from 'react';\nimport { intlShape } from './shape';\nimport { registerComponent } from 'meteor/vulcan:lib';\n\nconst FormattedMessage = ({ id, values, defaultMessage = '', html = false, className = '' }, { intl }) => {\n  let message = intl.formatMessage({ id, defaultMessage }, values);\n  const cssClass = `i18n-message ${className}`;\n\n  // if message is empty, use [id]\n  if (message === '') {\n    message = `[${id}]`;\n  }\n\n  return html ? \n    <span data-key={id} className={cssClass} dangerouslySetInnerHTML={{__html: message}}/> :\n    <span data-key={id} className={cssClass}>{message}</span>;\n};\n\nFormattedMessage.contextTypes = {\n  intl: intlShape\n};\n\nregisterComponent('FormattedMessage', FormattedMessage);\n\nexport default FormattedMessage;"
  },
  {
    "path": "packages/vulcan-i18n/lib/modules/provider.js",
    "content": "import React, { Component } from 'react';\nimport { getString } from 'meteor/vulcan:lib';\nimport { intlShape } from './shape.js';\n\nexport default class IntlProvider extends Component {\n  formatMessage = ({ id, defaultMessage }, values = null) => {\n    const { messages, locale } = this.props;\n    return getString({ id, defaultMessage, values, messages, locale });\n  };\n\n  formatStuff = something => {\n    return something;\n  };\n\n  getChildContext() {\n    return {\n      intl: {\n        formatDate: this.formatStuff,\n        formatTime: this.formatStuff,\n        formatRelative: this.formatStuff,\n        formatNumber: this.formatStuff,\n        formatPlural: this.formatStuff,\n        formatMessage: this.formatMessage,\n        formatHTMLMessage: this.formatStuff,\n        now: this.formatStuff,\n        locale: this.props.locale,\n      },\n    };\n  }\n\n  render() {\n    return this.props.children;\n  }\n}\n\nIntlProvider.childContextTypes = {\n  intl: intlShape,\n};\n"
  },
  {
    "path": "packages/vulcan-i18n/lib/modules/shape.js",
    "content": "/*\n * Copyright 2015, Yahoo Inc.\n * Copyrights licensed under the New BSD License.\n * See the accompanying LICENSE file for terms.\n */\n\nimport PropTypes from 'prop-types';\n\nconst { bool, number, string, func, object, oneOf, shape, any } = PropTypes;\nconst localeMatcher = oneOf(['best fit', 'lookup']);\nconst narrowShortLong = oneOf(['narrow', 'short', 'long']);\nconst numeric2digit = oneOf(['numeric', '2-digit']);\nconst funcReq = func.isRequired;\n\nexport const intlConfigPropTypes = {\n  locale: string,\n  formats: object,\n  messages: object,\n  textComponent: any,\n\n  defaultLocale: string,\n  defaultFormats: object,\n};\n\nexport const intlFormatPropTypes = {\n  formatDate: funcReq,\n  formatTime: funcReq,\n  formatRelative: funcReq,\n  formatNumber: funcReq,\n  formatPlural: funcReq,\n  formatMessage: funcReq,\n  formatHTMLMessage: funcReq,\n};\n\nexport const intlShape = shape({\n  ...intlConfigPropTypes,\n  ...intlFormatPropTypes,\n  formatters: object,\n  now: funcReq,\n});\n\nexport const messageDescriptorPropTypes = {\n  id: string.isRequired,\n  description: string,\n  defaultMessage: string,\n};\n\nexport const dateTimeFormatPropTypes = {\n  localeMatcher,\n  formatMatcher: oneOf(['basic', 'best fit']),\n\n  timeZone: string,\n  hour12: bool,\n\n  weekday: narrowShortLong,\n  era: narrowShortLong,\n  year: numeric2digit,\n  month: oneOf(['numeric', '2-digit', 'narrow', 'short', 'long']),\n  day: numeric2digit,\n  hour: numeric2digit,\n  minute: numeric2digit,\n  second: numeric2digit,\n  timeZoneName: oneOf(['short', 'long']),\n};\n\nexport const numberFormatPropTypes = {\n  localeMatcher,\n\n  style: oneOf(['decimal', 'currency', 'percent']),\n  currency: string,\n  currencyDisplay: oneOf(['symbol', 'code', 'name']),\n  useGrouping: bool,\n\n  minimumIntegerDigits: number,\n  minimumFractionDigits: number,\n  maximumFractionDigits: number,\n  minimumSignificantDigits: number,\n  maximumSignificantDigits: number,\n};\n\nexport const relativeFormatPropTypes = {\n  style: oneOf(['best fit', 'numeric']),\n  units: oneOf(['second', 'minute', 'hour', 'day', 'month', 'year']),\n};\n\nexport const pluralFormatPropTypes = {\n  style: oneOf(['cardinal', 'ordinal']),\n};\n"
  },
  {
    "path": "packages/vulcan-i18n/lib/modules/useIntl.js",
    "content": "import React, { useContext } from 'react';\nimport IntlContext from './context';\n\nexport default function useIntl() {\n  const intl = useContext(IntlContext);\n  return intl;\n}\n"
  },
  {
    "path": "packages/vulcan-i18n/lib/server/graphql.js",
    "content": "import { addGraphQLQuery, addGraphQLResolvers, addGraphQLSchema, Locales, getLocale, getStrings } from 'meteor/vulcan:lib';\n\n// const localEnum = `enum LocaleID {\n//   ${Locales.map(locale => locale.id).join('/n')}\n// }`;\n\n// console.log(Locales)\n// console.log(localEnum)\n// addGraphQLSchema(localEnum);\n\nconst localeType = `type Locale {\n  id: String,\n  label: String\n  dynamic: Boolean\n  strings: JSON\n}`;\n\naddGraphQLSchema(localeType);\n\nconst locale = async (root, { localeId }, context) => {\n  const locale = getLocale(localeId);\n  const strings = getStrings(localeId);\n  const localeObject = { ...locale, strings };\n  return localeObject;\n};\n\naddGraphQLQuery('locale(localeId: String): Locale');\naddGraphQLResolvers({ Query: { locale } });\n"
  },
  {
    "path": "packages/vulcan-i18n/lib/server/main.js",
    "content": "export * from '../modules/index.js';\n\nimport './graphql.js';"
  },
  {
    "path": "packages/vulcan-i18n/package.js",
    "content": "Package.describe({\n  name: 'vulcan:i18n',\n  summary: 'i18n client polyfill',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:lib@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n\nPackage.onTest(function(api) {\n  api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:test', 'vulcan:i18n']);\n  api.mainModule('./test/index.js');\n});\n"
  },
  {
    "path": "packages/vulcan-i18n/test/index.js",
    "content": "import './provider.test.js';\n"
  },
  {
    "path": "packages/vulcan-i18n/test/provider.test.js",
    "content": "import IntlProvider from '../lib/modules/provider';\nimport React from 'react';\nimport expect from 'expect';\nimport { shallow } from 'enzyme';\nimport { addStrings } from 'meteor/vulcan:core';\nimport { initComponentTest } from 'meteor/vulcan:test';\n\ninitComponentTest();\n\n// constants for formatMessage\nconst defaultMessage = 'default';\nconst stringId = 'test_string';\nconst ENTestString = 'English test string';\nconst FRTestString = 'Phrase test en Français';\nconst valueStringId = 'valueStringId';\nconst valueStringValue = 'Vulcan';\nconst valueTestStringStatic = 'the value is ';\nconst valueTestStringDynamic = 'testValue';\nconst valueTestString = `${valueTestStringStatic}{${valueTestStringDynamic}}`;\n\n// add the strings for formatMessage\naddStrings('en', {\n  [stringId]: ENTestString,\n  [valueStringId]: valueTestString,\n});\naddStrings('fr', {\n  [stringId]: FRTestString,\n});\n\ndescribe('vulcan:i18n/IntlProvider', function() {\n  it('shallow render', function() {\n    const wrapper = shallow(<IntlProvider />);\n    expect(wrapper).toBeDefined();\n  });\n  describe('formatMessage', function() {\n    it('format a message according to locale', function() {\n      const wrapper = shallow(<IntlProvider locale=\"en\" />);\n      const ENString = wrapper.instance().formatMessage({ id: stringId });\n      expect(ENString).toEqual(ENTestString);\n      wrapper.setProps({ locale: 'fr' });\n      const FRString = wrapper.instance().formatMessage({ id: stringId });\n      expect(FRString).toEqual(FRTestString);\n    });\n    it('format a message according to a value', function() {\n      const wrapper = shallow(<IntlProvider locale=\"en\" />);\n      const dynamicString = wrapper\n        .instance()\n        .formatMessage({ id: valueStringId }, { [valueTestStringDynamic]: valueStringValue });\n      expect(dynamicString).toEqual(valueTestStringStatic + valueStringValue);\n    });\n    it('return a default message when no string is found', function() {\n      const wrapper = shallow(<IntlProvider locale=\"en\" />);\n      const ENString = wrapper.instance().formatMessage({\n        id: 'unknownStringId',\n        defaultMessage: defaultMessage,\n      });\n      expect(ENString).toEqual(defaultMessage);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-i18n-en-us/README.md",
    "content": "Vulcan i18n en_US package."
  },
  {
    "path": "packages/vulcan-i18n-en-us/lib/en_US.js",
    "content": "import { addStrings } from 'meteor/vulcan:core';\n\naddStrings('en', {\n  'accounts.error_incorrect_password': 'Incorrect password',\n  'accounts.error_email_required': 'Email required',\n  'accounts.error_email_already_exists': 'Email already exists',\n  'accounts.error_invalid_email': 'Invalid email',\n  'accounts.error_minchar': 'Your password is too short',\n  'accounts.error_username_required': 'Username required',\n  'accounts.error_accounts_': '',\n  'accounts.error_unknown': 'Unknown error',\n  'accounts.error_user_not_found': 'User not found',\n  'accounts.error_username_already_exists': 'Username already exists',\n  'accounts.enter_username_or_email': 'Enter username or email',\n  'accounts.error_internal_server_error': 'Internal server error',\n  'accounts.error_token_expired': 'Invalid password reset token',\n  'accounts.username_or_email': 'Username or email',\n  'accounts.enter_username': 'Enter username',\n  'accounts.username': 'Username',\n  'accounts.enter_email': 'Enter email',\n  'accounts.email': 'Email',\n  'accounts.enter_password': 'Enter password',\n  'accounts.password': 'Password',\n  'accounts.choose_password': 'Choose password',\n  'accounts.change_password': 'Change password',\n  'accounts.reset_your_password': 'Reset your password',\n  'accounts.set_password': 'Set password',\n  'accounts.enter_new_password': 'Enter new password',\n  'accounts.new_password': 'New password',\n  'accounts.forgot_password': 'Forgot password',\n  'accounts.sign_up': 'Sign up',\n  'accounts.sign_in': 'Sign in',\n  'accounts.sign_out': 'Sign out',\n  'accounts.cancel': 'Cancel',\n  'accounts.or_use': 'or use',\n  'accounts.info_email_sent': 'Email sent.',\n  'accounts.info_password_changed': 'Password changed.',\n  'accounts.logging_in': 'Logging in…',\n  'accounts.email_verified': 'Your email address has been verified.',\n\n  'forms.submit': 'Submit',\n  'forms.cancel': 'Cancel',\n  'forms.select_option': '-- select option --',\n  'forms.add_nested_field': 'Add a {label}',\n  'forms.delete_nested_field': 'Delete this {label}?',\n  'forms.delete': 'Delete',\n  'forms.delete_field': 'Delete the field?',\n  'forms.delete_confirm': 'Delete document?',\n  'forms.revert': 'Revert',\n  'forms.confirm_discard': 'Discard changes?',\n  'forms.day': 'Day',\n  'forms.month': 'Month',\n  'forms.year': 'Year',\n  'forms.start_adornment_url_icon': 'Web icon',\n  'forms.start_adornment_email_icon': 'Email icon',\n  'forms.start_adornment_social_icon': 'Social icon',\n  'forms.clear_field': 'Clear field value',\n\n  'users.profile': 'Profile',\n  'users.complete_profile': 'Complete your Profile',\n  'users.profile_completed': 'Profile completed.',\n  'users.edit_account': 'Edit Account',\n  'users.edit_success': 'User “{name}” edited',\n  'users.log_in': 'Log In',\n  'users.sign_up': 'Sign Up',\n  'users.sign_up_log_in': 'Sign Up/Log In',\n  'users.log_out': 'Log Out',\n  'users.bio': 'Bio',\n  'users.displayName': 'Display Name',\n  'users.email': 'Email',\n  'users.twitterUsername': 'Twitter Username',\n  'users.website': 'Website',\n  'users.groups': 'Groups',\n  'users.avatar': 'Avatar',\n  'users.notifications': 'Notifications',\n  'users.notifications_users': 'New Users Notifications',\n  'users.notifications_posts': 'New Posts Notifications',\n  'users.newsletter_subscribeToNewsletter': 'Subscribe to newsletter',\n  'users.users_admin': 'Admin',\n  'users.admin': 'Admin',\n  'users.host': 'Team member',\n  'users.isAdmin': 'Admin',\n  'users.posts': 'Posts',\n  'users.upvoted_posts': 'Upvoted Posts',\n  'users.please_log_in': 'Please log in',\n  'users.please_sign_up_log_in': 'Please sign up or log in',\n  'users.cannot_post': 'Sorry, you do not have permission to post at this time',\n  'users.cannot_comment': 'Sorry, you do not have permission to comment at this time',\n  'users.subscribe': \"Subscribe to this user's posts\",\n  'users.unsubscribe': \"Unsubscribe to this user's posts\",\n  'users.subscribed': 'You have subscribed to “{name}” posts.',\n  'users.unsubscribed': 'You have unsubscribed from “{name}” posts.',\n  'users.subscribers': 'Subscribers',\n  'users.delete': 'Delete user',\n  'users.delete_confirm': 'Delete this user?',\n  'users.email_already_taken': 'This email is already taken: {value}',\n\n  settings: 'Settings',\n  'settings.json_message': 'Note: settings already provided in your <code>settings.json</code> file will be disabled.',\n  'settings.edit': 'Edit Settings',\n  'settings.edited': 'Settings edited (please reload).',\n  'settings.title': 'Title',\n  'settings.siteUrl': 'Site URL',\n  'settings.tagline': 'Tagline',\n  'settings.description': 'Description',\n  'settings.siteImage': 'Site Image',\n  'settings.defaultEmail': 'Default Email',\n  'settings.mailUrl': 'Mail URL',\n  'settings.scoreUpdate': 'Score Update',\n  'settings.postInterval': 'Post Interval',\n  'settings.RSSLinksPointTo': 'RSS Links Point To',\n  'settings.commentInterval': 'Comment Interval',\n  'settings.maxPostsPerDay': 'Max Posts Per Day',\n  'settings.startInvitesCount': 'Start Invites Count',\n  'settings.postsPerPage': 'Posts Per Page',\n  'settings.logoUrl': 'Logo URL',\n  'settings.logoHeight': 'Logo Height',\n  'settings.logoWidth': 'Logo Width',\n  'settings.faviconUrl': 'Favicon URL',\n  'settings.twitterAccount': 'Twitter Account',\n  'settings.facebookPage': 'Facebook Page',\n  'settings.googleAnalyticsId': 'Google Analytics ID',\n  'settings.locale': 'Locale',\n  'settings.requireViewInvite': 'Require View Invite',\n  'settings.requirePostInvite': 'Require Post Invite',\n  'settings.requirePostsApproval': 'Require Posts Approval',\n  'settings.scoreUpdateInterval': 'Score Update Interval',\n\n  'app.loading': 'Loading…',\n  'app.404': \"Sorry, we couldn't find what you were looking for.\",\n  'app.empty_input': 'Single resolver cannot receive an empty input object.',\n  'app.missing_document': \"Sorry, we couldn't find the document you were looking for.\",\n  'app.powered_by': 'Built with Vulcan.js',\n  'app.or': 'Or',\n  'app.noPermission': 'Sorry, you do not have the permission to do this at this time.',\n  'app.operation_not_allowed': 'Sorry, you don\\'t have the rights to perform the operation \"{operationName}\"',\n  'app.document_not_found': 'Document not found (id: {value})',\n  'app.no_permissions_defined': 'No permissions defined for operation [{operationName}]',\n  'app.disallowed_property_detected': 'Disallowed property detected: {value}',\n  'app.something_bad_happened': 'Something bad happened...',\n  'app.embedly_not_authorized':\n    'Invalid Embedly API key provided in the settings file. To find your key, log into https://app.embed.ly -> API',\n  'app.defaultError': '{defaultMessage}',\n  'app.please_sign_up_log_in': 'Please sign up or log in',\n  'app.no_access_permissions': 'Sorry, you are not allowed to access this page.',\n\n  'cards.edit': 'Edit',\n  'datatable.new': 'New',\n  'datatable.edit': 'Edit',\n  'datatable.search': 'Search',\n  'datatable.submit': 'Submit',\n\n  admin: 'Admin',\n  notifications: 'Notifications',\n\n  'errors.expectedType': 'Expected type {dataType} for field “{label}”, received “{value}” instead.',\n  'errors.required': 'Field “{label}” is required.',\n  'errors.minString': 'Field “{label}” needs to have at least {min} characters',\n  'errors.maxString': 'Field “{label}” is limited to {max} characters.',\n  'errors.generic': 'Sorry, something went wrong: <code>{errorMessage}</code>.',\n  'errors.generic_report': 'Sorry, something went wrong:  <code>{errorMessage}</code>. <br/>An error report has been generated.',\n  'errors.minNumber': 'Field “{label}” must be higher than {min}. ',\n  'errors.maxNumber': 'Field “{label}” must be lower than {max}. ',\n  'errors.minCount': 'There needs to be at least {count} in field “{label}”.',\n  'errors.maxCount': 'Field “{label}” is only allowed {count}.',\n  'errors.regEx': 'Field “{label}”: wrong formatting',\n  'errors.badDate': 'Field “{label}” is not a date.',\n  'errors.notAllowed': 'The value for field “{label}” is not allowed.',\n  'errors.noDecimal': 'The value for field “{label}” must not be a decimal number.',\n  //TODO other simple schema errors\n  'errors.minNumberExclusive': '',\n  'errors.maxNumberExclusive': '',\n  'errors.keyNotInSchema': '',\n});\n"
  },
  {
    "path": "packages/vulcan-i18n-en-us/package.js",
    "content": "Package.describe({\n  name: 'vulcan:i18n-en-us',\n  summary: 'Vulcan i18n package (en_US)',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9']);\n\n  api.addFiles(['lib/en_US.js'], ['client', 'server']);\n});\n"
  },
  {
    "path": "packages/vulcan-i18n-es-es/README.md",
    "content": "Vulcan i18n es_ES package."
  },
  {
    "path": "packages/vulcan-i18n-es-es/lib/es_ES.js",
    "content": "import { addStrings } from 'meteor/vulcan:core';\n\naddStrings('es', {\n\n  'accounts.error_incorrect_password': 'Contraseña Incorrecta',\n  'accounts.error_email_required': 'Se requiere correo electrónico',\n  'accounts.error_email_already_exists': 'El correo electrónico ya existe',\n  'accounts.error_invalid_email': 'Correo electrónico no válido',\n  'accounts.error_minchar': 'Su contraseña es muy corta',\n  'accounts.error_username_required': 'Nombre de usuario requerido',\n  'accounts.error_accounts_': '',\n  'accounts.error_unknown': 'Error desconocido',\n  'accounts.error_user_not_found': 'Usuario no encontrado',\n  'accounts.error_username_already_exists': 'El nombre de usuario ya existe',\n  'accounts.enter_username_or_email': 'Ingresar nombre de usuario o correo electrónico',\n  'accounts.error_internal_server_error': 'Error interno del servidor',\n  'accounts.error_token_expired': 'Token de restablecimiento de contraseña inválido',\n  'accounts.username_or_email': 'Nombre de usuario o correo electrónico',\n  'accounts.enter_username': 'Ingresar nombre de usuario',\n  'accounts.username': 'Nombre de usuario',\n  'accounts.enter_email': 'Ingresar correo electrónico',\n  'accounts.email': 'Correo electrónico',\n  'accounts.enter_password': 'Ingresar contraseña',\n  'accounts.password': 'Contraseña',\n  'accounts.choose_password': 'Elegir contraseña',\n  'accounts.change_password': 'Cambiar contraseña',\n  'accounts.reset_your_password': 'Restablecer su contraseña',\n  'accounts.set_password': 'Establecer contraseña',\n  'accounts.enter_new_password': 'Introduzca una nueva contraseña',\n  'accounts.new_password': 'Nueva contraseña',\n  'accounts.forgot_password': 'Olvidé mi contraseña',\n  'accounts.sign_up': 'Registrarse',\n  'accounts.sign_in': 'Iniciar sesión',\n  'accounts.sign_out': 'Cerrar sesión',\n  'accounts.cancel': 'Cancelar',\n  'accounts.or_use': 'o usar',\n  'accounts.info_email_sent': 'Correo electrónico enviado.',\n  'accounts.info_password_changed': 'Contraseña cambiada.',\n  'accounts.logging_in': 'Iniciando sesión…',\n  'accounts.email_verified': 'Tu dirección de email ha sido verificada.',\n\n  'forms.submit': 'Enviar',\n  'forms.cancel': 'Cancelar',\n  'forms.select_option': '-- seleccionar opción --',\n  'forms.add_nested_field': 'Agregar un {label}',\n  'forms.delete_nested_field': '¿Eliminar este {label}?',\n  'forms.delete': 'Eliminar',\n  'forms.delete_field': '¿Eliminar campo?',\n  'forms.delete_confirm': '¿Eliminar documento?',\n  'forms.revert': 'Revertir',\n  'forms.confirm_discard': '¿Descartar los cambios?',\n  'forms.start_adornment_url_icon': 'Icono de internet',\n  'forms.start_adornment_email_icon': 'Icono de correo electrónico',\n  'forms.start_adornment_social_icon': '',\n\n  'users.profile': 'Perfil',\n  'users.complete_profile': 'Complete su perfil',\n  'users.profile_completed': 'Perfil completado.',\n  'users.edit_account': 'Editar cuenta',\n  'users.edit_success': 'Usuario “{name}” editado',\n  'users.log_in': 'Iniciar sesión',\n  'users.sign_up': 'Registrarse',\n  'users.sign_up_log_in': 'Registrarse/ Iniciar sesión',\n  'users.log_out': 'Cerrar sesión',\n  'users.bio': 'Bio',\n  'users.displayName': 'Nombre a Mostrar',\n  'users.email': 'Correo electrónico',\n  'users.twitterUsername': 'Nombre de usuario de Twitter',\n  'users.website': 'Sitio web',\n  'users.groups': 'Grupos',\n  'users.avatar': 'Avatar',\n  'users.notifications': 'Notificaciones',\n  'users.notifications_users': 'Notificaciones de nuevos usuarios',\n  'users.notifications_posts': 'Notificaciones de publicaciones nuevas',\n  'users.newsletter_subscribeToNewsletter': 'Suscribirse al boletín informativo',\n  'users.users_admin': 'Admin',\n  'users.admin': 'Admin',\n  'users.host': 'Miembro del equipo',\n  'users.isAdmin': 'Administrador',\n  'users.posts': 'Publicaciones',\n  'users.upvoted_posts': 'Publicaciones modificadas',\n  'users.please_log_in': 'Inicia sesión',\n  'users.please_sign_up_log_in': 'Regístrese o inicie sesión',\n  'users.cannot_post': 'Lo siento, no tienes permiso para publicar en este momento',\n  'users.cannot_comment': 'Lo siento, no tienes permiso para comentar en este momento',\n  'users.subscribe': 'Suscribirse a las publicaciones de este usuario',\n  'users.unsubscribe': 'Anular la suscripción a las publicaciones de este usuario',\n  'users.subscribed': 'Te has suscrito a “{name}” publicaciones.',\n  'users.unsubscribed': 'Ha cancelado la suscripción a publicaciones de “{name}”.',\n  'users.subscribers': 'Suscriptores',\n  'users.delete': 'Eliminar usuario',\n  'users.delete_confirm': '¿Eliminar este usuario?',\n  'users.email_already_taken': 'Este correo electrónico ya está tomado: {value}',\n\n  'settings': 'Configuración',\n  'settings.json_message': 'Nota: la configuración ya provista en su archivo <code> settings.json </ code> estará deshabilitada.',\n  'settings.edit': 'Editar configuración',\n  'settings.edited': 'Configuración editada (recargue).',\n  'settings.title': 'Título',\n  'settings.siteUrl': 'URL del sitio',\n  'settings.tagline': 'Tagline',\n  'settings.description': 'Descripción',\n  'settings.siteImage': 'Imagen del sitio',\n  'settings.defaultEmail': 'Correo electrónico predeterminado',\n  'settings.mailUrl': 'Mail URL',\n  'settings.scoreUpdate': 'Actualización de puntaje',\n  'settings.postInterval': 'Intervalo de publicación',\n  'settings.RSSLinksPointTo': 'RSS Links Point To',\n  'settings.commentInterval': 'Intervalo de comentarios',\n  'settings.maxPostsPerDay': 'Publicaciones máximas por día',\n  'settings.startInvitesCount': 'Iniciar recuentos de invitaciones',\n  'settings.postsPerPage': 'Publicaciones por página',\n  'settings.logoUrl': 'URL del Logotipo',\n  'settings.logoHeight': 'Alto del Logotipo',\n  'settings.logoWidth': 'Ancho del Logotipo',\n  'settings.faviconUrl': 'Favicon URL',\n  'settings.twitterAccount': 'Cuenta de Twitter',\n  'settings.facebookPage': 'Página de Facebook',\n  'settings.googleAnalyticsId': 'ID de Google Analytics',\n  'settings.locale': 'Locale',\n  'settings.requireViewInvite': 'Requiere Invitación para ver',\n  'settings.requirePostInvite': 'Requiere Invitación para publicar',\n  'settings.requirePostsApproval': 'Requiere aprobación de publicaciones',\n  'settings.scoreUpdateInterval': 'Intervalo de actualización de puntuación',\n\n  'app.loading': 'Cargando…',\n  'app.404': 'Disculpa, no pudimos encontrar lo que estabas buscando.',\n  'app.missing_document': 'Lo sentimos, no pudimos encontrar el documento que estaba buscando.',\n  'app.powered_by': 'Construido con VulcanJS',\n  'app.or': 'O',\n  'app.noPermission': 'Lo siento, no tiene permiso para hacer esto en este momento.',\n  'app.operation_not_allowed': 'Lo sentimos, no tiene los derechos para realizar la operación “{operationName}”',\n  'app.document_not_found': 'Documento no encontrado (id: {value})',\n  'app.disallowed_property_detected': 'Propiedad no permitida detectada: {value}',\n  'app.something_bad_happened': 'Algo malo pasó...',\n  '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',\n  'app.defaultError': '{defaultMessage}',\n  'app.please_sign_up_log_in': 'Please sign up or log in',\n  'app.no_access_permissions': 'Sorry, you are not allowed to access this page.',\n\n  'cards.edit': 'Editar',\n  'datatable.new': 'Nuevo',\n  'datatable.edit': 'Editar',\n\n  'admin': 'Administrador',\n  'notifications': 'Notificaciones',\n\n  'errors.expectedType': 'Se esperaba un campo “{label}” de tipo {dataType}, se ha recibido “{value}” en su lugar.',\n  'errors.required': 'El campo “{label}” es obligatorio.',\n  'errors.minString': 'El campo “{label}” debe tener al menos {max} caracteres.',\n  'errors.maxString': 'El campo “{label}” está limitado a {max} caracteres.',\n  'errors.generic':'Ha ocurrido un error: <code>{errorMessage}</code>',\n  'errors.generic_report':'Algo ha ido mal: <code>{errorMessage}</code>. </br>Se ha reportado el error.',\n  'errors.minNumber':'El campo “{label}” debe ser superior a {min}.',\n  'errors.maxNumber':'El campo “{label}” debe ser inferior a {max}.',\n  'errors.minCount':'El campo “{label}” debe tener al menos {count}.',\n  'errors.maxCount':'El campo “{label}” está limitado a {count}.',\n  'errors.regEx':'El campo “{label}” está mal formateado.',\n  'errors.badDate':'El campo “{label}” debe ser una fecha.',\n  'errors.notAllowed':'El valor del campo “{label}” no està permitido.',\n  'errors.noDecimal':'El campo “{label}” no puede ser un decimal.',\n\n  'errors.minNumberExclusive':'',\n  'errors.maxNumberExclusive':'',\n  'errors.keyNotInSchema':'',\n});\n"
  },
  {
    "path": "packages/vulcan-i18n-es-es/package.js",
    "content": "Package.describe({\n  name: 'vulcan:i18n-es-es',\n  summary: 'Vulcan i18n package (es_ES)',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9']);\n\n  api.addFiles(['lib/es_ES.js'], ['client', 'server']);\n});\n"
  },
  {
    "path": "packages/vulcan-i18n-fa-ir/README.md",
    "content": "Vulcan i18n fa_IR package."
  },
  {
    "path": "packages/vulcan-i18n-fa-ir/lib/fa_IR.js",
    "content": "import { addStrings } from 'meteor/vulcan:core';\n\naddStrings('fa-IR', {\n  'accounts.error_incorrect_password': 'رمزعبور نادرست است',\n  'accounts.error_email_required': 'ایمیل الزامی است',\n  'accounts.error_email_already_exists': 'ایمیل از قبل وجود دارد',\n  'accounts.error_invalid_email': 'ایمیل نامعتبر است',\n  'accounts.error_minchar': 'رمزعبور شما بیش از حد کپتاه است',\n  'accounts.error_username_required': 'نام کاربری الزامی است',\n  'accounts.error_accounts_': '',\n  'accounts.error_unknown': 'خظای ناشناخته',\n  'accounts.error_user_not_found': 'کاربر یافت نشد',\n  'accounts.error_username_already_exists': 'نام کاربری از قبل وجود دارد',\n  'accounts.enter_username_or_email': 'نام کاربری یا ایمیل را وارد نمایید',\n  'accounts.error_internal_server_error': 'خطای سرور',\n  'accounts.error_token_expired': 'توکن بازیابی رمزعبور اشتباه است',\n  'accounts.username_or_email': 'نام کاربری یا ایمیل',\n  'accounts.enter_username': 'نام کاربری را وارد نمایید',\n  'accounts.username': 'نام کاربری',\n  'accounts.enter_email': 'ایمیل را وارد نمایید',\n  'accounts.email': 'ایمیل',\n  'accounts.enter_password': 'رمزعبور را وارد نمایید',\n  'accounts.password': 'رمزعبور',\n  'accounts.choose_password': 'انتخاب رمزعبور',\n  'accounts.change_password': 'تغییر رمزعبور',\n  'accounts.reset_your_password': 'بازیابی رمزعبور',\n  'accounts.set_password': 'تنظیم رمزعبور',\n  'accounts.enter_new_password': 'رمزعبور جدید را وارد نمایید',\n  'accounts.new_password': 'رمزعبور جدید',\n  'accounts.forgot_password': 'فراموشی رمزعبور',\n  'accounts.sign_up': 'ثبت نام',\n  'accounts.sign_in': 'ورود',\n  'accounts.sign_out': 'خروج',\n  'accounts.cancel': 'انصراف',\n  'accounts.or_use': 'یا استفاده از',\n  'accounts.info_email_sent': 'ایمیل ارسال شد.',\n  'accounts.info_password_changed': 'رمزعبور تغییر یافت.',\n  'accounts.logging_in': 'درحال ورود...',\n  'accounts.email_verified': 'آدرس ایمیل شما تایید شد.',\n\n  'forms.submit': 'تایید',\n  'forms.cancel': 'انصراف',\n  'forms.select_option': '-- انتخاب گزینه --',\n  'forms.add_nested_field': '{label}أضف',\n  'forms.delete_nested_field': '{label}اضافه کردن',\n  'forms.delete': 'حذف',\n  'forms.delete_field' : 'فیلد را حذف کنید؟',\n  'forms.delete_confirm': 'سند حذف شود؟',\n  'forms.revert': 'بازگردانی',\n  'forms.confirm_discard': 'تغییرات لغو شوند؟',\n  'forms.day': 'روز',\n  'forms.month': 'ماه',\n  'forms.year': 'سال',\n  'forms.start_adornment_url_icon': 'آیکون اینترنت',\n  'forms.start_adornment_email_icon': 'نماد ایمیل',\n  'forms.start_adornment_social_icon': '',\n\n  'users.profile': 'مشخصات',\n  'users.complete_profile': 'مشخصات خود را تکمیل فرمایید.',\n  'users.profile_completed': 'مشخصات تکمیل شد.',\n  'users.edit_account': 'ویرایش حساب کاربری',\n  'users.edit_success': 'کاربر “{name}” ویرایش شد',\n  'users.log_in': 'ورود',\n  'users.sign_up': 'خروج',\n  'users.sign_up_log_in': 'ثبت نام/ورود',\n  'users.log_out': 'خروج',\n  'users.bio': 'زندگی نامه',\n  'users.displayName': 'نام نمایشی',\n  'users.email': 'ایمیل',\n  'users.twitterUsername': 'نام کاربری توییتر',\n  'users.website': 'وبسایت',\n  'users.groups': 'گروه ها',\n  'users.avatar': 'آواتار',\n  'users.notifications': 'اطلاعیه ها',\n  'users.notifications_users': 'اطلاعیه های کاربران جدید',\n  'users.notifications_posts': 'اطلاعیه های پست های جدید',\n  'users.newsletter_subscribeToNewsletter': 'عضویت در خبرنامه',\n  'users.users_admin': 'مدیر',\n  'users.admin': 'مدیر',\n  'users.host': '???',\n  'users.isAdmin': 'مدیر',\n  'users.posts': 'پست ها',\n  'users.upvoted_posts': 'پست هایی که بیشتر پسند شده اند',\n  'users.please_log_in': 'لطفا وارد شوید',\n  'users.please_sign_up_log_in': 'لطفا ثبت نام کنید یا وارد شوید.',\n  'users.cannot_post': 'متاسفانه شما اکنون دسترسی پست کردن ندارید.',\n  'users.cannot_comment': 'متاسفانه شما اکنون دسترسی ارسال نظر ندارید.',\n  'users.subscribe': 'اشتراک در پست های این کاربر',\n  'users.unsubscribe': 'لغو اشتراک از پست های این کاربر',\n  'users.subscribed': 'شما مشترک “{name}” پست ها شدید.',\n  'users.unsubscribed': 'شما اشتراکتان را از “{name}” پست ها غیرفعال کردید.',\n  'users.subscribers': 'مشترکان',\n  'users.delete': 'حذف کاربر',\n  'users.delete_confirm': 'کاربر حذف شود؟',\n  'users.email_already_taken': 'این ایمیل قبلا ثبت شده است: {value}',\n\n  settings: 'تنظیمات',\n  'settings.json_message':\n    'توجه: تنظیمات ارایه شده در <code>settings.json</code> غیرفعال خواهد شد.',\n  'settings.edit': 'ویرایش تنظیمات',\n  'settings.edited': 'تنظیمات ویرایش شد. (لطفا ریلود کنید)',\n  'settings.title': 'عنوان',\n  'settings.siteUrl': 'آدرس سایت',\n  'settings.tagline': 'تگلاین',\n  'settings.description': 'توضیحات',\n  'settings.siteImage': 'تصویر سایت',\n  'settings.defaultEmail': 'ایمیل پیشفرض',\n  'settings.mailUrl': 'آدرس ایمیل',\n  'settings.scoreUpdate': 'بروزرسانی امتیاز',\n  'settings.postInterval': 'فاصله پست کردن',\n  'settings.RSSLinksPointTo': 'RSS Links Point To',\n  'settings.commentInterval': 'فاصله نظر گذاشتن',\n  'settings.maxPostsPerDay': 'حداکثر تعداد پست در روز',\n  'settings.startInvitesCount': 'شروع شمارش دعوت ها',\n  'settings.postsPerPage': 'تعداد پست ها در صفحه',\n  'settings.logoUrl': 'آدرس لوگو',\n  'settings.logoHeight': 'ارتفاع لوگو',\n  'settings.logoWidth': 'عرض لوگو',\n  'settings.faviconUrl': 'آدرس فاویکون',\n  'settings.twitterAccount': 'حساب توییتر',\n  'settings.facebookPage': 'صفحه فیسبوک',\n  'settings.googleAnalyticsId': 'Google Analytics ID',\n  'settings.locale': 'بومی',\n  'settings.requireViewInvite': 'دعپت به مشاهده نیاز است',\n  'settings.requirePostInvite': 'دعوت به پست کردن نیاز است',\n  'settings.requirePostsApproval': 'احتیاج به تایید پست ها',\n  'settings.scoreUpdateInterval': 'فاصله بروزرسانی امتیاز ها',\n\n  'app.loading': 'درحال بارگذاری...',\n  'app.404': 'متاسفانه چیزی که دنبال آن بودید یافت نشد.',\n  'app.missing_document': 'متاسفانه سند درخواست شده یافت نشد.',\n  'app.powered_by': 'ساخته شده با Vulcan.js',\n  'app.or': 'یا',\n  'app.noPermission': 'متاسفانه شما اکنون دسترسی به انجام اینکار را ندارید.',\n  'app.operation_not_allowed':\n    'متاسفانه شما اجازه اجرای این درخواست را ندارید: \"{operationName}\"',\n  'app.document_not_found': 'سند یافت نشد (شناسه: {value})',\n  'app.disallowed_property_detected': 'Disallowed property detected: {value}',\n  'app.something_bad_happened': 'اتفاق بدی افتاد ...',\n  'app.embedly_not_authorized':\n    'Invalid Embedly API key provided in the settings file. To find your key, log into https://app.embed.ly -> API',\n  'app.defaultError': '{defaultMessage}',\n  'app.please_sign_up_log_in': 'Please sign up or log in',\n  'app.no_access_permissions': 'Sorry, you are not allowed to access this page.',\n\n  'cards.edit': 'ویرایش',\n  'datatable.new': 'جدید',\n  'datatable.edit': 'ویرایش',\n  'datatable.search': 'جستجو',\n\n  admin: 'مدیر',\n  notifications: 'اطلاعیه ها',\n\n  'errors.expectedType':\n    'Expected type {dataType} for field “{label}”, received “{value}” instead.',\n  'errors.required': 'فیلد “{label}” الزامی است.',\n  'errors.minString': 'فیلد “{label}” باید حداقل {min} کاراکتر داشته باشد',\n  'errors.maxString': 'فیلد “{label}” باید حداگثر {max} کاراکتر داشته باشد.',\n  'errors.generic': 'متاسفانه خطایی پیش آمد: <code>{errorMessage}</code>.',\n  'errors.generic_report':\n    'متاسفانه خطایی پیش آمد:  <code>{errorMessage}</code>. <br/>گزارش خطا ایجاد شد.',\n  'errors.minNumber': 'فیلد “{label}” باید بیشتر باشد از {min}. ',\n  'errors.maxNumber': 'فیلد “{label}” باید کمتر باشد از {max}. ',\n  'errors.minCount': 'باید حداقل {count} از فیلد وجود داشته باشد “{label}”.',\n  'errors.maxCount': 'فیلد “{label}” فقظ به تعداد {count} مجاز است.',\n  'errors.regEx': 'فیلد “{label}”: wrong formatting',\n  'errors.badDate': 'فیلد “{label}” تاریخ نیست.',\n  'errors.notAllowed': 'مقدار فیلد “{label}” قابل قبول نیست.',\n  'errors.noDecimal': 'مقدار فیلد “{label}” نباید اعشاری باشد.',\n  //TODO other simple schema errors\n  'errors.minNumberExclusive': '',\n  'errors.maxNumberExclusive': '',\n  'errors.keyNotInSchema': '',\n});\n"
  },
  {
    "path": "packages/vulcan-i18n-fa-ir/package.js",
    "content": "Package.describe({\n  name: 'vulcan:i18n-fa-ir',\n  summary: 'Vulcan i18n package (fa_IR)',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9']);\n\n  api.addFiles(['lib/fa_IR.js'], ['client', 'server']);\n});\n"
  },
  {
    "path": "packages/vulcan-i18n-fr-fr/README.md",
    "content": "Vulcan i18n fr_FR package.\n"
  },
  {
    "path": "packages/vulcan-i18n-fr-fr/lib/fr_FR.js",
    "content": "import { addStrings } from 'meteor/vulcan:core';\n\naddStrings('fr', {\n\n  'accounts.error_incorrect_password': 'Mot de passe invalide',\n  'accounts.error_email_required': 'Email requis',\n  'accounts.error_email_already_exists': 'Email déjà utilisé',\n  'accounts.error_invalid_email': 'Email invalide',\n  'accounts.error_minchar': 'Votre mot de passe est trop court',\n  'accounts.error_username_required': 'Nom d\\'utilisateur requis',\n  'accounts.error_accounts_': '',\n  'accounts.error_unknown': 'Erreur inconnue',\n  'accounts.error_user_not_found': 'Utilisateur inconnu',\n  'accounts.error_username_already_exists': 'Nom d\\'utilisateur déjà utilisé',\n  'accounts.enter_username_or_email': 'Nom d\\'utilisateur ou email',\n  'accounts.error_internal_server_error': 'Erreur serveur interne',\n  'accounts.error_token_expired': 'Erreur: token invalide',\n  'accounts.username_or_email': 'Nom d\\'utilisateur ou email',\n  'accounts.enter_username': 'Nom d\\'utilisateur',\n  'accounts.username': 'Nom d\\'utilisateur',\n  'accounts.enter_email': 'Email',\n  'accounts.email': 'Email',\n  'accounts.enter_password': 'Mot de passe',\n  'accounts.password': 'Mot de passe',\n  'accounts.choose_password': 'Choisir un mot de passe',\n  'accounts.change_password': 'Changer le mot de passe',\n  'accounts.reset_your_password': 'Réinitialiser le mot de passe',\n  'accounts.set_password': 'Définir le mot de passe',\n  'accounts.enter_new_password': 'Entrez un nouveau mot de passe',\n  'accounts.new_password': 'Nouveau mot de passe',\n  'accounts.forgot_password': 'Mot de passe oublié',\n  'accounts.sign_up': 'Inscription',\n  'accounts.sign_in': 'Connexion',\n  'accounts.sign_out': 'Se déconnecter',\n  'accounts.cancel': 'Annuler',\n  'accounts.or_use': 'ou utilisez',\n  'accounts.info_email_sent': 'Email envoyé.',\n  'accounts.info_password_changed': 'Mot de passe changé.',\n  'accounts.logging_in': 'Connexion en cours…',\n  'accounts.email_verified': 'Votre adresse e-mail a été vérifiée.',\n\n  'forms.submit': 'Envoyer',\n  'forms.cancel': 'Annuler',\n  'forms.select_option': '-- Choisir une option --',\n  'forms.add_nested_field': 'Ajouter un {label}',\n  'forms.delete_nested_field': 'Supprimer ce {label} ?',\n  'forms.delete': 'Supprimer',\n  'forms.delete_field': 'Supprimer le champ ?',\n  'forms.delete_confirm': 'Supprimer le document ?',\n  'forms.next': 'Suivant',\n  'forms.previous': 'Précédent',\n  'forms.revert': 'Retour',\n  'forms.confirm_discard': 'Supprimer les modifications ?',\n  'forms.start_adornment_url_icon': 'Icône de internet',\n  'forms.start_adornment_email_icon': 'Icône de courriel',\n  'forms.start_adornment_social_icon': '',\n\n  'users.profile': 'Profil',\n  'users.complete_profile': 'Complétez votre profil',\n  'users.profile_completed': 'Profil completé.',\n  'users.edit_account': 'Modifier le compte',\n  'users.edit_success': 'Utilisateur “{name}” modifié',\n  'users.log_in': 'Se connecter',\n  'users.sign_up': 'S\\'inscrire',\n  'users.sign_up_log_in': 'Inscription / Connexion',\n  'users.log_out': 'Se déconnecter',\n  'users.bio': 'Bio',\n  'users.displayName': 'Nom d\\'affichage',\n  'users.email': 'Email',\n  'users.twitterUsername': 'Pseudo Twitter',\n  'users.website': 'Website',\n  'users.groups': 'Groupes',\n  'users.avatar': 'Avatar',\n  'users.notifications': 'Notifications',\n  'users.notifications_users': 'Notifications de nouvel utilisateur',\n  'users.notifications_posts': 'Notifications de nouveau post',\n  'users.newsletter_subscribeToNewsletter': 'S\\'inscrire à la newsletter',\n  'users.users_admin': 'Admin',\n  'users.admin': 'Admin',\n  'users.host': 'Membre de l\\'équipe',\n  'users.isAdmin': 'Administrateur',\n  'users.posts': 'Posts',\n  'users.upvoted_posts': 'Posts soutenus',\n  'users.please_log_in': 'Connectez-vous',\n  'users.please_sign_up_log_in': 'Connectez-vous ou inscrivez-vous',\n  'users.cannot_post': 'Désolé, vous n\\'avez pas la permission de publier pour le moment',\n  'users.cannot_comment': 'Désolé, vous n\\'avez pas la permission de commenter pour le moment',\n  'users.subscribe': 'S\\'inscrire aux posts de cet utilisateur',\n  'users.unsubscribe': 'Se désinscrire des posts de cet utilisateur',\n  'users.subscribed': 'Vous êtes abonné aux posts de “{name}”.',\n  'users.unsubscribed': 'Vous n\\'êtes plus abonné aux posts de “{name}”.',\n  'users.subscribers': 'Abonnés',\n  'users.delete': 'Supprimer l\\'utilistateur',\n  'users.delete_confirm': 'Supprimer cet utilisateur?',\n  'users.email_already_taken': 'Email déjà pris: {value}',\n\n  'settings': 'Paramètres',\n  'settings.json_message': 'Note: les paramètres déjà renseignés dans le fichier <code>settings.json</code> seront désactivés.',\n  'settings.edit': 'Modifier les paramètres',\n  'settings.edited': 'Paramètres modifiés (recharger).',\n  'settings.title': 'Titre',\n  'settings.siteUrl': 'URL du site',\n  'settings.tagline': 'Tagline',\n  'settings.description': 'Description',\n  'settings.siteImage': 'Image du site',\n  'settings.defaultEmail': 'Email par défaut',\n  'settings.mailUrl': 'URL du mail',\n  'settings.scoreUpdate': 'Rafraichissement du score',\n  'settings.postInterval': 'Intervalle de publication',\n  'settings.RSSLinksPointTo': 'Liens RSS pointent vers',\n  'settings.commentInterval': 'Intervalle de commentaires',\n  'settings.maxPostsPerDay': 'Posts quotidiens maximum',\n  'settings.startInvitesCount': 'Démarrer le compte d\\'invitations',\n  'settings.postsPerPage': 'Posts par page',\n  'settings.logoUrl': 'URL du logo',\n  'settings.logoHeight': 'Hauteur du logo',\n  'settings.logoWidth': 'Largeur du logo',\n  'settings.faviconUrl': 'URL du favicon',\n  'settings.twitterAccount': 'Compte Twitter',\n  'settings.facebookPage': 'Page Facebook',\n  'settings.googleAnalyticsId': 'ID Google Analytics',\n  'settings.locale': 'Locale',\n  'settings.requireViewInvite': 'Nécessite une invitation pour voir',\n  'settings.requirePostInvite': 'Nécessite une invitation pour publier',\n  'settings.requirePostsApproval': 'Nécessite l\\'approbation des posts',\n  'settings.scoreUpdateInterval': 'Intervalle de mise à jour du score',\n\n  'app.loading': 'Chargement…',\n  'app.404': 'Désolé, ce contenu n\\'est pas disponible.',\n  'app.missing_document':  'Désolé, nous n\\'avons pas trouvé le document que vous cherchiez',\n  'app.powered_by': 'Construit avec Vulcan.js',\n  'app.or': 'Ou',\n  'app.noPermission': 'Désolé, vous n\\'êtes pas autorisé à faire cette action pour le moment',\n  'app.operation_not_allowed': 'Désolé, vous n\\'avez pas les droits pour faire l\\'opération \"{operationName}\"',\n  'app.document_not_found': 'Document introuvable: (id: {value})',\n  'app.disallowed_property_detected': 'Propriété refusée détectée: {value}',\n  'app.something_bad_happened': 'Quelque chose s\\'est mal passé...',\n  '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',\n  'app.defaultError': '{defaultMessage}',\n  'app.please_sign_up_log_in': 'Please sign up or log in',\n  'app.no_access_permissions': 'Sorry, you are not allowed to access this page.',\n\n  'cards.edit': 'Modifier',\n  'datatable.new': 'Nouveau',\n  'datatable.edit': 'Modifier',\n  'datatable.search': 'Rechercher',\n\n  'admin': 'Admin',\n  'notifications': 'Notifications',\n\n  'errors.expectedType': 'Un champ “{label}” de type {dataType} était attendu, “{value}” a été reçu à la place.',\n  'errors.required': 'Le champ “{label}” est requis.',\n  'errors.minString': 'Le champ \"{label}\" doit faire au moins {min} caractères.',\n  'errors.maxString': 'Le champ “{label}” est limité à {max} caractères.',\n  'errors.generic':'Désolé, une erreur est survenue: <code>{errorMessage}</code>',\n  'errors.generic_report':'Désolé, une erreur est survenue: <code>{errorMessage}</code>. </br>Un message d\\'erreur a été envoyé.',\n  'errors.minNumber':'Le champ “{label}” doit être supérieur à {min}.',\n  'errors.maxNumber':'Le champ “{label}” doit être inférieur à {max}.',\n  'errors.minCount':'Il faut au moins {count} objets dans le champ “{label}”.',\n  'errors.maxCount':'Le champ “{label}” est limité à {count} objets',\n  'errors.regEx':'Le champ “{label}” est mal formatté',\n  'errors.badDate':'Le champ “{label}” n\\'est pas une date',\n  'errors.notAllowed':'La valeur du champ \"{label}\" est interdite.',\n  'errors.noDecimal':'La valeur du champ \"{label}\" ne peut être décimale.',\n\n  'errors.minNumberExclusive':'',\n  'errors.maxNumberExclusive':'',\n  'errors.keyNotInSchema':'',\n});\n"
  },
  {
    "path": "packages/vulcan-i18n-fr-fr/package.js",
    "content": "Package.describe({\n  name: 'vulcan:i18n-fr-fr',\n  summary: 'Vulcan i18n package (fr_FR)',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9']);\n\n  api.addFiles(['lib/fr_FR.js'], ['client', 'server']);\n});\n"
  },
  {
    "path": "packages/vulcan-lib/README.md",
    "content": "Vulcan libraries package, used internally. "
  },
  {
    "path": "packages/vulcan-lib/lib/client/apollo-client/apolloClient.js",
    "content": "import { ApolloClient } from '@apollo/client';\nimport { ApolloLink } from 'apollo-link';\nimport httpLink from './links/http';\nimport meteorAccountsLink from './links/meteor';\nimport errorLink from './links/error';\n// import { createStateLink } from '../../modules/apollo-common/links/state.js';\nimport { resetReactiveState } from '../../modules/reactive-state.js';\nimport createCache from './cache';\nimport { getTerminatingLinks, getLinks } from './links/registerLinks';\n\n// these links do not change once created\nconst staticLinks = [errorLink, meteorAccountsLink];\n\nlet apolloClient;\nexport const createApolloClient = () => {\n  // links registered by packages\n  const cache = createCache();\n  const registeredLinks = getLinks();\n  const terminatingLinks = getTerminatingLinks();\n  if (terminatingLinks.length > 1) console.warn('Warning: You registered more than one terminating Apollo link.');\n\n  // const stateLink = createStateLink({ cache });\n  const newClient = new ApolloClient({\n    link: ApolloLink.from([\n      // stateLink,\n      ...registeredLinks,\n      ...staticLinks,\n      // terminating\n      ...(terminatingLinks.length ? terminatingLinks : [httpLink]),\n    ]),\n    cache,\n  });\n\n  resetReactiveState();\n  newClient.onResetStore(resetReactiveState);\n\n  // register the client\n  apolloClient = newClient;\n  return newClient;\n};\n\nexport const getApolloClient = () => {\n  if (!apolloClient) {\n    // eslint-disable-next-line no-console\n    console.warn('Warning: accessing apollo client before it is initialized.');\n  }\n  return apolloClient;\n};\n\n// This is a draft of what could be a reload of the apollo client with new Links\n// for the moment there seems to be no equivalent to Redux `replaceReducers` in apollo-client\n//@see https://github.com/apollographql/apollo-link-state/issues/306\n//export const reloadApolloClient = () => {\n//  // get the current cache\n//  const currentCache = apolloClient.cache;\n//  // get the stateLink\n//  const newApolloClient = createApolloClient({\n//    link: ApolloLink.from([getStateLink(), ...staticLinks]),\n//    cache: currentCache\n//  });\n//  // update the client\n//  apolloClient = newApolloClient;\n//  return newApolloClient;\n//};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/client/apollo-client/cache.js",
    "content": "import { InMemoryCache } from '@apollo/client';\nimport { getFragmentMatcher } from '../../modules/fragment_matcher';\n\nconst createCache = () => new InMemoryCache({ fragmentMatcher: getFragmentMatcher() })\n  //ssr\n  .restore(window.__APOLLO_STATE__);\n\nexport default createCache;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/client/apollo-client/index.js",
    "content": "export * from './apolloClient';\nexport * from './links/registerLinks';\n"
  },
  {
    "path": "packages/vulcan-lib/lib/client/apollo-client/links/error.js",
    "content": "import { onError } from '@apollo/client/link/error';\n\nconst locationsToStr = (locations=[]) => locations.map(({column, line}) => `line ${line}, col ${column}`).join(';');\nconst errorLink = onError(error => {\n  const { graphQLErrors, networkError } = error;\n  if (graphQLErrors)\n    graphQLErrors.map(({ message, locations, path }) => {\n      // eslint-disable-next-line no-console\n      console.log(`[GraphQL error]: Message: ${message}, Location: ${locationsToStr(locations)}, Path: ${path}`);\n    });\n  if (networkError) {\n    // eslint-disable-next-line no-console\n    console.log(`[${networkError.statusCode} ${networkError.response?.statusText}]: ${networkError.message}`);\n  }\n});\n\nexport default errorLink;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/client/apollo-client/links/http.js",
    "content": "import { HttpLink } from '@apollo/client';\n\nconst httpLink = new HttpLink({\n  uri: '/graphql',\n  credentials: 'same-origin',\n});\nexport default httpLink;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/client/apollo-client/links/meteor.js",
    "content": "import { MeteorAccountsLink } from 'meteor/apollo';\n\nconst meteorAccountsLink = new MeteorAccountsLink();\nexport default meteorAccountsLink;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/client/apollo-client/links/registerLinks.js",
    "content": "const terminatingLinksRegistry = [];\nconst linksRegistry = [];\n\n// register one or more links\nexport const registerLink = (link) => {\n    const links = Array.isArray(link) ? link : [link];\n    linksRegistry.unshift(...links);\n};\n\nexport const registerTerminatingLink = (link) => {\n    const links = Array.isArray(link) ? link : [link];\n    terminatingLinksRegistry.push(...links);\n};\n\n\n\nexport const getLinks = () => linksRegistry;\nexport const getTerminatingLinks = () => terminatingLinksRegistry;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/client/auth.js",
    "content": "/**\n * Manage meteor_login_token cookie\n * Necessary for authentication when the\n * Authorization header is not set\n *\n * E.g on first page loading\n */\nimport Cookies from 'universal-cookie';\n\nimport { Meteor } from 'meteor/meteor';\n\nconst cookie = new Cookies();\n\nfunction setToken(loginToken, expires) {\n  if (loginToken && expires !== -1) {\n    cookie.set('meteor_login_token', loginToken, {\n      path: '/',\n      expires,\n      sameSite: 'lax',\n      secure: document.domain !== 'localhost',\n    });\n  } else {\n    cookie.remove('meteor_login_token', {\n      path: '/',\n    });\n  }\n}\n\nfunction initToken() {\n  const loginToken = global.localStorage['Meteor.loginToken'];\n  const loginTokenExpires = new Date(global.localStorage['Meteor.loginTokenExpires']);\n\n  if (loginToken) {\n    setToken(loginToken, loginTokenExpires);\n  } else {\n    setToken(null, -1);\n  }\n}\n\nMeteor.startup(() => {\n  initToken();\n});\n\n// TODO: cleanup\n// This part of the code overrides the default localStorage function,\n// so that when Meteor.loginToken is set, it is also automatically\n// stored as a cookie (necessary for SSR to work as expected for all HTTP requests)\nconst originalSetItem = Meteor._localStorage.setItem;\nMeteor._localStorage.setItem = function setItem(key, value) {\n  if (key === 'Meteor.loginToken') {\n    Meteor.defer(initToken);\n  }\n  originalSetItem.call(Meteor._localStorage, key, value);\n};\nconst originalRemoveItem = Meteor._localStorage.removeItem;\nMeteor._localStorage.removeItem = function removeItem(key) {\n  if (key === 'Meteor.loginToken') {\n    Meteor.defer(initToken);\n  }\n  originalRemoveItem.call(Meteor._localStorage, key);\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/client/connectors.js",
    "content": "// Mock exports so resolver/mutation build doesn't fail client side\nexport const DatabaseConnectors = null;\nexport const Connectors = null;"
  },
  {
    "path": "packages/vulcan-lib/lib/client/errors.js",
    "content": "// mock apollo server errors\nexport const throwError = (error) => { if (error) throw new Error(error.id, error); };"
  },
  {
    "path": "packages/vulcan-lib/lib/client/inject_data.js",
    "content": "import { EJSON } from 'meteor/ejson';\nimport { onPageLoad } from 'meteor/server-render';\n\n// InjectData object\nexport const InjectData = {\n  // data object\n  _data: {},\n  _ready: false,\n\n  // encode object to string\n  _encode(ejson) {\n    const ejsonString = EJSON.stringify(ejson);\n    return encodeURIComponent(ejsonString);\n  },\n\n  // decode string to object\n  _decode(encodedEjson) {\n    const decodedEjsonString = decodeURIComponent(encodedEjson);\n    if (!decodedEjsonString) return null;\n    return EJSON.parse(decodedEjsonString);\n  },\n\n  _checkReady() {\n    if (!this._ready) {\n      const dom = document.querySelector('script[type=\"text/inject-data\"]');\n      const injectedDataString = dom ? dom.textContent.trim() : '';\n      this._data = InjectData._decode(injectedDataString) || {};\n      this._ready = true;\n    }\n  },\n  // sync version\n  // Must always be called inside an onPageLoad callback\n  getDataSync(key) {\n    this._checkReady();\n    return this._data[key];\n  },\n\n  // get data when DOM loaded\n  getData(key, callback) {\n    // promisified version\n    if (!callback) {\n      return new Promise((resolve, reject) => {\n        onPageLoad(() => {\n          this._checkReady();\n          resolve(this._data[key]);\n        });\n      });\n    }\n    onPageLoad(() => {\n      this._checkReady();\n      callback(this._data[key]);\n    });\n  },\n};"
  },
  {
    "path": "packages/vulcan-lib/lib/client/main.js",
    "content": "import './auth.js';\n\nexport * from '../modules/index.js';\nexport * from './inject_data.js';\n\nexport * from './apollo-client';\n\n// createCollection, resolvers and mutations mocks\n// avoid warnings when building with webpack\nexport * from './connectors';\nexport * from './mock';\nexport * from './errors';"
  },
  {
    "path": "packages/vulcan-lib/lib/client/mock.js",
    "content": "// mock mutators\nexport const createMutator = null;\nexport const updateMutator = null;\nexport const deleteMutator = null;\n\n// mock default mutations and resolvers\nexport const getDefaultResolvers = () => ({});\nexport const getDefaultMutations = () => ({});"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/admin.js",
    "content": "export let AdminColumns = [];\n\nexport const addAdminColumn = columnOrColumns => {\n  if (Array.isArray(columnOrColumns)) {\n    AdminColumns = AdminColumns.concat(columnOrColumns);\n  } else {\n    AdminColumns.push(columnOrColumns);\n  }\n};"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/apollo-common/index.js",
    "content": "export * from './links/state';\nimport './settings';\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/apollo-common/links/state.js",
    "content": "/**\n * Setup apollo-link-state\n * Apollo-link-state helps to manage a local store for caching and client-side\n * data storing\n * It replaces previous implementation using redux\n * Link state doc:\n * @see https://www.apollographql.com/docs/react/essentials/local-state.html\n * @see https://www.apollographql.com/docs/link/links/state.html\n * General presentation on Links\n * @see https://www.apollographql.com/docs/link/\n * Example\n * @see https://hackernoon.com/storing-local-state-in-react-with-apollo-link-state-738f6ca45569\n */\nimport { withClientState } from 'apollo-link-state';\n\n/**\n * Create a state link\n * TODO: Deprecated\n */\nexport const createStateLink = ({ cache, resolvers, defaults, ...otherOptions }) => {\n  const stateLink = withClientState({\n    cache,\n    defaults: defaults || getStateLinkDefaults(),\n    resolvers: resolvers || getStateLinkResolvers(),\n    ...otherOptions,\n  });\n  return stateLink;\n};\n\n// enhancement workflow\nconst registeredDefaults = {};\n/**\n * Defaults are default response to queries\n */\nexport const registerStateLinkDefault = ({ name, defaultValue, options = {} }) => {\n  registeredDefaults[name] = defaultValue;\n  return registeredDefaults;\n};\nexport const getStateLinkDefaults = () => registeredDefaults;\n\n// Mutation are equivalent to a Redux Action + Reducer\n// except it uses GraphQL to retrieve/update data in the cache\nconst registeredMutations = {};\nexport const registerStateLinkMutation = ({ name, mutation, options = {} }) => {\n  registeredMutations[name] = mutation;\n  return registeredMutations;\n};\nexport const getStateLinkMutations = () => registeredMutations;\n\nexport const getStateLinkResolvers = () => ({\n  Mutation: getStateLinkMutations(),\n});\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/apollo-common/settings.js",
    "content": "import { registerSetting } from '../settings';\n\nregisterSetting('apolloSsr.disable', false, 'Disable Server Side Rendering');\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/callbacks.js",
    "content": "import { Meteor } from 'meteor/meteor';\n\nimport { debug } from './debug.js';\nimport { Utils } from './utils';\nimport merge from 'lodash/merge';\n\n/**\n * @summary Format callback hook names\n */\nexport const formatHookName = hook => typeof hook === 'string' && hook.toLowerCase();\n\n/**\n * @summary A list of all registered callback hooks\n */\nexport const CallbackHooks = [];\n\n/**\n * @summary Callback hooks provide an easy way to add extra steps to common operations.\n * @namespace Callbacks\n */\nexport const Callbacks = {};\n\n\n/**\n * @summary Register a callback\n * @param {String} hook - The name of the hook\n * @param {Function} callback - The callback function\n */\nexport const registerCallback = function (callback) {\n  CallbackHooks.push(callback);\n};\n\n/**\n * @summary Add a callback function to a hook\n * @param {String} hook - The name of the hook\n * @param {Function} callback - The callback function\n */\nexport const addCallback = function (hook, callback) {\n\n  const formattedHook = formatHookName(hook);\n\n  if (!callback.name) {\n    // eslint-disable-next-line no-console\n    console.log(`// Warning! You are adding an unnamed callback to ${formattedHook}. Please use the function foo () {} syntax.`);\n  }\n\n  // if callback array doesn't exist yet, initialize it\n  if (typeof Callbacks[formattedHook] === 'undefined') {\n    Callbacks[formattedHook] = [];\n  }\n\n  Callbacks[formattedHook].push(callback);\n};\n\n/**\n * @summary Remove a callback from a hook\n * @param {string} hookName - The name of the hook\n * @param {string} callbackName - The name of the function to remove\n */\nexport const removeCallback = function (hookName, callbackName) {\n  const formattedHook = formatHookName(hookName);\n  Callbacks[formattedHook] = _.reject(Callbacks[formattedHook], function (callback) {\n    return callback.name === callbackName;\n  });\n};\n\n\n/**\n * @summary Remove all callbacks from a hook (mostly for testing purposes)\n * @param {string} hookName - The name of the hook\n */\nexport const removeAllCallbacks = function(hookName) {\n  const formattedHook = formatHookName(hookName);\n  Callbacks[formattedHook] = [];\n};\n\n/**\n * @summary Successively run all of a hook's callbacks on an item\n * @param {String} hook - First argument: the name of the hook, or an array\n * @param {Object} item - Second argument: the post, comment, modifier, etc. on which to run the callbacks\n * @param {Any} args - Other arguments will be passed to each successive iteration\n * @param {Array} callbacks - Optionally, pass an array of callback functions instead of passing a hook name\n * @returns {Object} Returns the item after it's been through all the callbacks for this hook\n */\nexport const runCallbacks = function () {\n\n  let hook, item, args, callbacks, formattedHook;\n  if (typeof arguments[0] === 'object' && arguments.length === 1) {\n    const singleArgument = arguments[0];\n    hook = singleArgument.name;\n    formattedHook = formatHookName(hook);\n    item = singleArgument.iterator;\n    args = singleArgument.properties;\n    // if callbacks option is passed used that, else use formatted hook name\n    callbacks = singleArgument.callbacks ? singleArgument.callbacks : Callbacks[formattedHook];\n  } else {\n    // OpenCRUD backwards compatibility\n    // the first argument is the name of the hook or an array of functions\n    hook = arguments[0];\n    formattedHook = formatHookName(hook);\n    // the second argument is the item on which to iterate\n    item = arguments[1];\n    // successive arguments are passed to each iteration\n    args = Array.prototype.slice.call(arguments).slice(2);\n    // if first argument is an array, use that as callbacks array; else use formatted hook name\n    callbacks = Array.isArray(hook) ? hook : Callbacks[formattedHook];\n  }\n\n  // flag used to detect the callback that initiated the async context\n  let asyncContext = false;\n  \n  if (typeof callbacks !== 'undefined' && callbacks.length > 0) { // if the hook exists, and contains callbacks to run\n\n    const runCallback = (accumulator, callback) => {\n      debug(`\\x1b[32m>> Running callback [${callback.name}] on hook [${formattedHook}]\\x1b[0m`);\n      const newArguments = [accumulator].concat(args);\n\n      try {\n        const result = callback.apply(this, newArguments);\n\n        // if callback is only supposed to run once, remove it\n        if (callback.runOnce) {\n          removeCallback(formattedHook, callback.name);\n        }\n\n        if (typeof result === 'undefined') {\n          // if result of current iteration is undefined, don't pass it on\n          // debug(`// Warning: Sync callback [${callback.name}] in hook [${hook}] didn't return a result!`)\n          return accumulator;\n        } else {\n          return result;\n        }\n\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.log(`\\x1b[31m// error at callback [${callback.name}] in hook [${formattedHook}]\\x1b[0m`);\n        // eslint-disable-next-line no-console\n        console.log(error);\n        if (error.break || error.data && error.data.break) {\n          throw error;\n        }\n        // pass the unchanged accumulator to the next iteration of the loop\n        return accumulator;\n      }\n    };\n\n    return callbacks.reduce(function (accumulator, callback, index) {\n      if (Utils.isPromise(accumulator)) {\n        if (!asyncContext) {\n          debug(`\\x1b[32m>> Started async context in hook [${formattedHook}] by [${callbacks[index-1] && callbacks[index-1].name}]\\x1b[0m`);\n          asyncContext = true;\n        }\n        return new Promise((resolve, reject) => {\n          accumulator\n            .then(result => {\n              try {\n                // run this callback once we have the previous value\n                resolve(runCallback(result, callback));\n              } catch (error) {\n                // error will be thrown only for breaking errors, so throw it up in the promise chain\n                reject(error);\n              }\n            })\n            .catch(reject);\n        });\n      } else {\n        return runCallback(accumulator, callback);\n      }\n    }, item);\n\n  } else { // else, just return the item unchanged\n    return item;\n  }\n};\n\n/**\n * @summary Successively run all of a hook's callbacks on an item, in async mode (only works on server)\n * @param {String} hook - First argument: the name of the hook\n * @param {Any} args - Other arguments will be passed to each successive iteration\n */\nexport const runCallbacksAsync = function() {\n  let hook, args, callbacks, formattedHook;\n  if (typeof arguments[0] === 'object' && arguments.length === 1) {\n    const singleArgument = arguments[0];\n    hook = singleArgument.name;\n    formattedHook = formatHookName(hook);\n    args = [singleArgument.properties]; // wrap in array for apply\n    callbacks = singleArgument.callbacks ? singleArgument.callbacks : Callbacks[formattedHook];\n  } else {\n    // OpenCRUD backwards compatibility\n    // the first argument is the name of the hook or an array of functions\n    hook = arguments[0];\n    formattedHook = formatHookName(hook);\n    callbacks = Array.isArray(hook) ? hook : Callbacks[formattedHook];\n    // successive arguments are passed to each iteration\n    args = Array.prototype.slice.call(arguments).slice(1);\n    // if first argument is an array, use that as callbacks array; else use formatted hook name\n    callbacks = Array.isArray(hook) ? hook : Callbacks[formattedHook];\n  }\n\n  if (typeof callbacks !== 'undefined' && !!callbacks.length) {\n    const _runCallbacksAsync = () =>\n        Promise.all(\n            callbacks.map(callback => {\n                if (!callback) {\n                  throw new Error(`Found undefined callback on hook ${hook}`);\n                }\n                debug(`\\x1b[32m>> Running async callback [${callback.name}] on hook [${hook}]\\x1b[0m`);\n                return callback.apply(this, args);\n            }),\n        );\n\n    if (Meteor.isServer) {\n      // TODO: find out if we can safely use promises on the server, too - https://github.com/VulcanJS/Vulcan/pull/2065\n      return new Promise(async (resolve, reject) => {\n          Meteor.defer(function() {\n            _runCallbacksAsync().then(resolve).catch(reject);\n          });\n      });\n    }\n    return _runCallbacksAsync();\n  }\n  return [];\n};\n\n\nexport let globalCallbacks =  {\n  create: {\n    validate: [],\n    before: [],\n    after: [],\n    async: [],\n  },\n  update: {\n    validate: [],\n    before: [],\n    after: [],\n    async: [],\n  },\n  delete: {\n    validate: [],\n    before: [],\n    after: [],\n    async: [],\n  }\n};\n\nexport const addGlobalCallbacks = callbacks => {\n  globalCallbacks = merge(globalCallbacks, callbacks);\n};"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/collections.js",
    "content": "import { Mongo } from 'meteor/mongo';\nimport SimpleSchema from 'simpl-schema';\nimport { Utils } from './utils.js';\nimport { runCallbacks, runCallbacksAsync, registerCallback, addCallback } from './callbacks.js';\nimport { getSetting, registerSetting } from './settings.js';\nimport { registerFragment } from './fragments.js';\nimport { getDefaultFragmentText } from './graphql/defaultFragment';\nimport escapeStringRegexp from 'escape-string-regexp';\nimport { validateIntlField, getIntlString, isIntlField, schemaHasIntlFields, schemaHasIntlField } from './intl';\nimport clone from 'lodash/clone';\nimport isEmpty from 'lodash/isEmpty';\nimport merge from 'lodash/merge';\nimport _omit from 'lodash/omit';\nimport mergeWith from 'lodash/mergeWith';\nimport { createSchema, isCollectionType } from './schema_utils.js';\n\nconst wrapAsync = Meteor.wrapAsync ? Meteor.wrapAsync : Meteor._wrapAsync;\n// import { debug } from './debug.js';\n\nregisterSetting('maxDocumentsPerRequest', 1000, 'Maximum documents per request');\n\n// will be set to `true` if there is one or more intl schema fields\nexport let hasIntlFields = false;\n\nexport const Collections = [];\n\nexport const getCollection = name => {\n  const collection = Collections.find(\n    ({ options: { collectionName } }) => name === collectionName || name === collectionName.toLowerCase()\n  );\n  if (!collection) {\n    throw new Error(`Could not find collection named “${name}”`);\n  }\n  return collection;\n};\n\nexport const getCollectionByTypeName = typeName => {\n  // in case typeName is for an array ('[User!]'), get rid of brackets\n  let parsedTypeName = typeName.replace('[', '').replace(']', '').replace(/!/g, '');\n  const collection = Collections.find(({ options: { typeName } }) => parsedTypeName === typeName);\n  if (!collection) {\n    throw new Error(`Could not find collection for type “${parsedTypeName}”. Registered types: ${Collections.map(({ options: { typeName } }) => typeName).join(', ')}`);\n  }\n  return collection;\n};\n\nexport const generateCollectionNameFromTypeName = typeName => Utils.pluralize(typeName);\n\nexport const getTypeNameByCollectionName = collectionName => {\n  const collection = Collections.find(({ options }) => options.collectionName === collectionName);\n  if (!collection) {\n    throw new Error(`Could not find type for collection “${collectionName}”`);\n  }\n  return collection.options.typeName;\n};\n\nexport const generateTypeNameFromCollectionName = collectionName => Utils.singularize(collectionName);\n\n/**\n * @summary replacement for Collection2's attachSchema. Pass either a schema, to\n * initialize or replace the schema, or some fields, to extend the current schema\n * @class Mongo.Collection\n */\nMongo.Collection.prototype.attachSchema = function (schemaOrFields) {\n  if (schemaOrFields instanceof SimpleSchema) {\n    this.simpleSchema = () => schemaOrFields;\n  } else {\n    this.simpleSchema().extend(schemaOrFields);\n  }\n};\n\n/**\n * @summary Add an additional field (or an array of fields) to a schema.\n * @param {Object|Object[]} fieldOrFieldArray\n */\nMongo.Collection.prototype.addField = function (fieldOrFieldArray) {\n  const collection = this;\n  const fieldSchema = {};\n\n  const fieldArray = Array.isArray(fieldOrFieldArray) ? fieldOrFieldArray : [fieldOrFieldArray];\n\n  // loop over fields and add them to schema (or extend existing fields)\n  fieldArray.forEach(function (field) {\n    fieldSchema[field.fieldName] = field.fieldSchema;\n  });\n\n  // add field schema to collection schema\n  collection.attachSchema(createSchema(merge(collection.options.schema, fieldSchema)));\n};\n\n/**\n * @summary Remove a field from a schema.\n * @param {String} fieldName\n */\nMongo.Collection.prototype.removeField = function (fieldName) {\n  var collection = this;\n  var schema = _omit(collection.simpleSchema()._schema, fieldName);\n\n  // add field schema to collection schema\n  collection.attachSchema(createSchema(schema));\n};\n\n/**\n * @summary Add a default view function.\n * @param {Function} view\n */\nMongo.Collection.prototype.addDefaultView = function (view) {\n  this.defaultView = view;\n};\n\n/**\n * @summary Add a named view function.\n * @param {String} viewName\n * @param {Function} view\n */\nMongo.Collection.prototype.addView = function (viewName, view) {\n  this.views[viewName] = view;\n};\n\n/**\n * @summary Allow mongodb aggregation\n * @param {Array} pipelines mongodb pipeline\n * @param {Object} options mongodb option object\n */\nMongo.Collection.prototype.aggregate = function (pipelines, options) {\n  var coll = this.rawCollection();\n  return wrapAsync(coll.aggregate.bind(coll))(pipelines, options);\n};\n\n// see https://github.com/dburles/meteor-collection-helpers/blob/master/collection-helpers.js\nMongo.Collection.prototype.helpers = function (helpers) {\n  var self = this;\n\n  if (self._transform && !self._helpers)\n    throw new Meteor.Error(\n      \"Can't apply helpers to '\" + self._name + \"' a transform function already exists!\"\n    );\n\n  if (!self._helpers) {\n    self._helpers = function Document(doc) {\n      return Object.assign(this, doc);\n    };\n    self._transform = function (doc) {\n      return new self._helpers(doc);\n    };\n  }\n\n  Object.keys(helpers).forEach(function (key) {\n    self._helpers.prototype[key] = helpers[key];\n  });\n};\n\nexport const extendCollection = (collection, options) => {\n  const newOptions = mergeWith({}, collection.options, options, (a, b) => {\n    if (Array.isArray(a) && Array.isArray(b)) {\n      return a.concat(b);\n    }\n    if (Array.isArray(a) && b) {\n      return a.concat([b]);\n    }\n    if (Array.isArray(b) && a) {\n      return b.concat([a]);\n    }\n  });\n  collection = createCollection(newOptions);\n  return collection;\n};\n\n/*\n\nNote: this currently isn't used because it would need to be called\nafter all collections have been initialized, otherwise we can't figure out\nif resolved field is resolving to a collection type or not\n\n*/\nexport const addAutoRelations = () => {\n  Collections.forEach(collection => {\n    const schema = collection.simpleSchema()._schema;\n    // add \"auto-relations\" to schema resolvers\n    Object.keys(schema).map(fieldName => {\n      const field = schema[fieldName];\n      // if no resolver or relation is provided, try to guess relation and add it to schema\n      if (field.resolveAs) {\n        const { resolver, relation, type } = field.resolveAs;\n        if (isCollectionType(type) && !resolver && !relation) {\n          field.resolveAs.relation = field.type === Array ? 'hasMany' : 'hasOne';\n        }\n      }\n    });\n  });\n};\n\n/*\n\nPass an existing collection to overwrite it instead of creating a new one\n\n*/\nexport const createCollection = (options) => {\n  const { typeName, collectionName = generateCollectionNameFromTypeName(typeName), dbCollectionName } = options;\n  let { schema, apiSchema, dbSchema } = options;\n\n  const existingCollectionIndex = Collections.findIndex(c => c.collectionName === collectionName);\n  const existingCollection = existingCollectionIndex >= 0 ? Collections[existingCollectionIndex] : null;\n\n  // initialize new Mongo collection or get existing collection when overwriting\n  const collection =\n    existingCollection ||\n    (collectionName === 'Users' && Meteor.users\n      ? Meteor.users\n      : new Mongo.Collection(dbCollectionName ? dbCollectionName : collectionName.toLowerCase()));\n\n  // decorate collection with options\n  collection.options = options;\n\n  // add typeName if missing\n  collection.typeName = typeName;\n  collection.options.typeName = typeName;\n  collection.options.singleResolverName = Utils.camelCaseify(typeName);\n  collection.options.multiResolverName = Utils.camelCaseify(Utils.pluralize(typeName));\n\n  // add collectionName if missing\n  collection.collectionName = collectionName;\n  collection.options.collectionName = collectionName;\n\n  // add views\n  collection.views = [];\n\n  //register individual collection callback\n  registerCollectionCallback(typeName.toLowerCase());\n\n  // if schema has at least one intl field, add intl callback just before\n  // `${collectionName}.collection` callbacks run to make sure it always runs last\n  if (schemaHasIntlFields(schema)) {\n    hasIntlFields = true; // we have at least one intl field\n    addCallback(`${typeName.toLowerCase()}.collection`, addIntlFields);\n  }\n\n  //run schema callbacks and run general callbacks last\n  schema = runCallbacks({\n    name: `${typeName.toLowerCase()}.collection`,\n    iterator: schema,\n    properties: { options },\n  });\n  schema = runCallbacks({ name: '*.collection', iterator: schema, properties: { options } });\n\n  if (schema) {\n    // attach schema to collection\n    collection.attachSchema(createSchema(schema, apiSchema, dbSchema));\n  }\n\n  runCallbacksAsync({ name: '*.collection.async', properties: { options } });\n  runCallbacksAsync({ name: `${collectionName}.collection.async`, properties: { options } });\n\n  // ------------------------------------- Default Fragment -------------------------------- //\n\n  const defaultFragment = getDefaultFragmentText(collection);\n  if (defaultFragment) registerFragment(defaultFragment);\n\n  // ------------------------------------- Parameters -------------------------------- //\n\n  // legacy\n  collection.getParameters = (terms = {}, apolloClient, context) => {\n    // console.log(terms);\n\n    const currentSchema = collection.simpleSchema()._schema;\n\n    let parameters = {\n      selector: {},\n      options: {},\n    };\n\n    if (collection.defaultView) {\n      parameters = Utils.deepExtend(true, parameters, collection.defaultView(terms, apolloClient, context));\n    }\n\n    // handle view option\n    if (terms.view && collection.views[terms.view]) {\n      const viewFn = collection.views[terms.view];\n      const view = viewFn(terms, apolloClient, context);\n      let mergedParameters = Utils.deepExtend(true, parameters, view);\n\n      if (mergedParameters.options && mergedParameters.options.sort && view.options && view.options.sort) {\n        // If both the default view and the selected view have sort options,\n        // don't merge them together; take the selected view's sort. (Otherwise\n        // they merge in the wrong order, so that the default-view's sort takes\n        // precedence over the selected view's sort.)\n        mergedParameters.options.sort = view.options.sort;\n      }\n      parameters = mergedParameters;\n    }\n\n    // iterate over posts.parameters callbacks\n    parameters = runCallbacks(`${typeName.toLowerCase()}.parameters`, parameters, clone(terms), apolloClient, context);\n    // OpenCRUD backwards compatibility\n    parameters = runCallbacks(`${collectionName.toLowerCase()}.parameters`, parameters, clone(terms), apolloClient, context);\n\n    if (Meteor.isClient) {\n      parameters = runCallbacks(`${typeName.toLowerCase()}.parameters.client`, parameters, clone(terms), apolloClient);\n      // OpenCRUD backwards compatibility\n      parameters = runCallbacks(`${collectionName.toLowerCase()}.parameters.client`, parameters, clone(terms), apolloClient);\n    }\n\n    // note: check that context exists to avoid calling this from withList during SSR\n    if (Meteor.isServer && context) {\n      parameters = runCallbacks(`${typeName.toLowerCase()}.parameters.server`, parameters, clone(terms), context);\n      // OpenCRUD backwards compatibility\n      parameters = runCallbacks(`${collectionName.toLowerCase()}.parameters.server`, parameters, clone(terms), context);\n    }\n\n    // sort using terms.orderBy (overwrite defaultView's sort)\n    if (terms.orderBy && !isEmpty(terms.orderBy)) {\n      parameters.options.sort = terms.orderBy;\n    }\n\n    // if there is no sort, default to sorting by createdAt descending\n    if (!parameters.options.sort) {\n      parameters.options.sort = { createdAt: -1 };\n    }\n\n    // extend sort to sort posts by _id to break ties, unless there's already an id sort\n    // NOTE: always do this last to avoid overriding another sort\n    //if (!(parameters.options.sort && parameters.options.sort._id)) {\n    //  parameters = Utils.deepExtend(true, parameters, { options: { sort: { _id: -1 } } });\n    //}\n\n    // remove any null fields (setting a field to null means it should be deleted)\n    Object.keys(parameters.selector).forEach(key => {\n      if (parameters.selector[key] === null) delete parameters.selector[key];\n    });\n    if (parameters.options.sort) {\n      Object.keys(parameters.options.sort).forEach(key => {\n        if (parameters.options.sort[key] === null) delete parameters.options.sort[key];\n      });\n    }\n\n    if (terms.query) {\n      const query = escapeStringRegexp(terms.query);\n      const searchableFieldNames = Object.keys(currentSchema).filter(fieldName => currentSchema[fieldName].searchable);\n      if (searchableFieldNames.length) {\n        parameters = Utils.deepExtend(true, parameters, {\n          selector: {\n            $or: searchableFieldNames.map(fieldName => ({\n              [fieldName]: { $regex: query, $options: 'i' },\n            })),\n          },\n        });\n      } else {\n        // eslint-disable-next-line no-console\n        console.warn(\n          `Warning: terms.query is set but schema ${\n            collection.options.typeName\n          } has no searchable field. Set \"searchable: true\" for at least one field to enable search.`\n        );\n      }\n    }\n\n    // limit number of items to 1000 by default\n    const maxDocuments = getSetting('maxDocumentsPerRequest', 1000);\n    const limit = terms.limit || parameters.options.limit;\n    parameters.options.limit = !limit || limit < 1 || limit > maxDocuments ? maxDocuments : limit;\n\n    // console.log(JSON.stringify(parameters, 2));\n\n    return parameters;\n  };\n\n  if (existingCollection) {\n    Collections[existingCollectionIndex] = existingCollection;\n  } else {\n    Collections.push(collection);\n  }\n\n  return collection;\n};\n\n//register collection creation hook for each collection\nfunction registerCollectionCallback(typeName) {\n  registerCallback({\n    name: `${typeName}.collection`,\n    iterator: { schema: 'the schema of the collection' },\n    properties: [\n      { schema: 'The schema of the collection' },\n      { validationErrors: 'An Object that can be used to accumulate validation errors' },\n    ],\n    runs: 'sync',\n    returns: 'schema',\n    description: 'Modifies schemas on collection creation',\n  });\n}\n\n//register colleciton creation hook\nregisterCallback({\n  name: '*.collection',\n  iterator: { schema: 'the schema of the collection' },\n  properties: [\n    { schema: 'The schema of the collection' },\n    { validationErrors: 'An object that can be used to accumulate validation errors' },\n  ],\n  runs: 'sync',\n  returns: 'schema',\n  description: 'Modifies schemas on collection creation',\n});\n\n// generate foo_intl fields\nexport function addIntlFields(schema) {\n  Object.keys(schema).forEach(fieldName => {\n    const fieldSchema = schema[fieldName];\n    if (isIntlField(fieldSchema) && !schemaHasIntlField(schema, fieldName)) {\n\n      // remove `intl` to avoid treating new _intl field as a field to internationalize\n      // eslint-disable-next-line no-unused-vars\n      const { intl, ...propertiesToCopy } = schema[fieldName];\n\n      schema[`${fieldName}_intl`] = {\n        ...propertiesToCopy, // copy properties from regular field\n        hidden: true,\n        type: Array,\n        isIntlData: true,\n      };\n\n      delete schema[`${fieldName}_intl`].intl;\n\n      schema[`${fieldName}_intl.$`] = {\n        type: getIntlString(),\n      };\n\n      // if original field is required, enable custom validation function instead of `optional` property\n      if (!schema[fieldName].optional) {\n        schema[`${fieldName}_intl`].optional = true;\n        schema[`${fieldName}_intl`].custom = validateIntlField;\n      }\n\n      // make original non-intl field optional\n      schema[fieldName].optional = true;\n    }\n  });\n  return schema;\n}\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/components.js",
    "content": "import { compose } from './compose';\nimport React from 'react';\nimport difference from 'lodash/difference';\n\nexport const Components = {}; // will be populated on startup\nexport const ComponentsTable = {}; // storage for infos about components\n\n\nexport const coreComponents = [\n  'Alert',\n  'Button',\n  'Modal',\n  'ModalTrigger',\n  'Table',\n  'FormComponentCheckbox',\n  'FormComponentCheckboxGroup',\n  'FormComponentDate',\n  'FormComponentDate2',\n  'FormComponentDateTime',\n  'FormComponentDefault',\n  'FormComponentText',\n  'FormComponentEmail',\n  'FormComponentNumber',\n  'FormComponentRadioGroup',\n  'FormComponentSelect',\n  'FormComponentSelectMultiple',\n  'FormComponentStaticText',\n  'FormComponentTextarea',\n  'FormComponentTime',\n  'FormComponentUrl',\n  'FormComponentInner',\n  'FormControl',\n  'FormElement',\n];\n\n/**\n * Register a Vulcan component with a name, a raw component than can be extended\n * and one or more optional higher order components.\n *\n * @param {String} name The name of the component to register.\n * @param {Component} rawComponent Interchangeable/extendable react component.\n * @param {...(Function|Array)} hocs The HOCs to compose with the raw component.\n *\n * Note: when a component is registered without higher order component, `hocs` will be\n * an empty array, and it's ok!\n * See https://github.com/reactjs/redux/blob/master/src/compose.js#L13-L15\n *\n * @returns Structure of a component in the list:\n *\n * ComponentsTable.Foo = {\n *    name: 'Foo',\n *    hocs: [fn1, fn2],\n *    rawComponent: React.Component,\n *    call: () => compose(...hocs)(rawComponent),\n * }\n *\n */\nexport function registerComponent(name, rawComponent, ...hocs) {\n  // support single-argument syntax\n  if (typeof arguments[0] === 'object') {\n    // note: cannot use `const` because name, components, hocs are already defined\n    // as arguments so destructuring cannot work\n    // eslint-disable-next-line no-redeclare\n    var { name, component, hocs = [] } = arguments[0];\n    rawComponent = component;\n  }\n  // store the component in the table\n  ComponentsTable[name] = {\n    name,\n    rawComponent,\n    hocs,\n  };\n}\n\n/**\n * Returns true if a component with the given name has been registered with\n * registerComponent(name, component, ...hocs).\n *\n * @param {String} name The name of the component to get.\n * @returns {Boolean}\n */\nexport const componentExists = (name) => {\n  const component = ComponentsTable[name];\n  return !!component;\n};\n\n/**\n * Get a component registered with registerComponent(name, component, ...hocs).\n *\n * @param {String} name The name of the component to get.\n * @returns {Function|React Component} A (wrapped) React component\n */\nexport const getComponent = name => {\n  const component = ComponentsTable[name];\n  if (!component) {\n    throw new Error(`Component ${name} not registered.`);\n  }\n  if (component.hocs && component.hocs.length) {\n    const hocs = component.hocs.map(hoc => {\n      if (!Array.isArray(hoc)) {\n        if (typeof hoc !== 'function') {\n          throw new Error(`In registered component ${name}, an hoc is of type ${typeof hoc}`);\n        }\n        return hoc;\n      }\n      const [actualHoc, ...args] = hoc;\n      if (typeof actualHoc !== 'function') {\n        throw new Error(`In registered component ${name}, an hoc is of type ${typeof actualHoc}`);\n      }\n      return actualHoc(...args);\n    });\n    return compose(...hocs)(component.rawComponent);\n  } else {\n    return component.rawComponent;\n  }\n};\n\n/**\n * Populate the lookup table for components to be callable\n * ℹ️ Called once on app startup\n **/\nexport const populateComponentsApp = () => {\n  const registeredComponents = Object.keys(ComponentsTable);\n\n  // loop over each component in the list\n  registeredComponents.map(name => {\n    // populate an entry in the lookup table\n    Components[name] = getComponent(name);\n\n    // uncomment for debug\n    // console.log('init component:', name);\n  });\n\n  const missingComponents = difference(coreComponents, registeredComponents);\n\n  if (missingComponents.length) {\n    // eslint-disable-next-line no-console\n    console.warn(\n      `Found the following missing core components: ${missingComponents.join(\n        ', '\n      )}. Include a UI package such as vulcan:ui-bootstrap to add them.`\n    );\n  }\n};\n\n/**\n * Get the **raw** (original) component registered with registerComponent\n * without the possible HOCs wrapping it.\n *\n * @param {String} name The name of the component to get.\n * @returns {Function|React Component} An interchangeable/extendable React component\n */\nexport const getRawComponent = name => {\n  return ComponentsTable[name].rawComponent;\n};\n\n/**\n * Replace a Vulcan component with the same name with a new component or\n * an extension of the raw component and one or more optional higher order components.\n * This function keeps track of the previous HOCs and wrap the new HOCs around previous ones\n *\n * @param {String} name The name of the component to register.\n * @param {React Component} newComponent Interchangeable/extendable component.\n * @param {...Function} newHocs The HOCs to compose with the raw component.\n * @returns {Function|React Component} A component callable with Components[name]\n *\n * Note: when a component is registered without higher order component, `hocs` will be\n * an empty array, and it's ok!\n * See https://github.com/reactjs/redux/blob/master/src/compose.js#L13-L15\n */\nexport function replaceComponent(name, newComponent, ...newHocs) {\n  // support single argument syntax\n  if (typeof arguments[0] === 'object') {\n    // eslint-disable-next-line no-redeclare\n    var { name, component, hocs = [] } = arguments[0];\n    newComponent = component;\n    newHocs = hocs;\n  }\n\n  const previousComponent = ComponentsTable[name];\n  const previousHocs = (previousComponent && previousComponent.hocs) || [];\n\n  if (!previousComponent) {\n    // eslint-disable-next-line no-console\n    console.warn(\n      `Trying to replace non-registered component ${name}. The component is ` +\n        'being registered. If you were trying to replace a component defined by ' +\n        \"another package, make sure that you haven't misspelled the name. Check \" +\n        'also if the original component is still being registered or that it ' +\n        \"hasn't been renamed.\"\n    );\n  }\n\n  return registerComponent(name, newComponent, ...newHocs, ...previousHocs);\n}\n\nexport const copyHoCs = (sourceComponent, targetComponent) => {\n  return compose(...sourceComponent.hocs)(targetComponent);\n};\n\n/**\n * Returns an instance of the given component name of function\n * @param {string|function} component  A component, the name of a component, or a react element\n * @param {Object} [props]  Optional properties to pass to the component\n */\n//eslint-disable-next-line react/display-name\nexport const instantiateComponent = (component, props) => {\n  if (!component) {\n    return null;\n  } else if (typeof component === 'string') {\n    const Component = Components[component];\n    return <Component {...props} />;\n  } else if (React.isValidElement(component)) {\n    return React.cloneElement(component, props);\n  } else if (typeof component === 'function' &&\n    component.prototype &&\n    component.prototype.isReactComponent\n  ) {\n    const Component = component;\n    return <Component {...props} />;\n  } else if (typeof component === 'function') {\n    return component(props);\n  } else if (typeof component === 'object' && component.$$typeof && component.render) {\n    const Component = component;\n    return <Component {...props} />;\n  } else {\n    return component;\n  }\n};\n\n/**\n * Creates a component that will render the registered component with the given name.\n *\n * This function  may be useful when in need for some registered component, but in contexts\n * where they have not yet been initialized, for example at compile time execution. In other\n * words, when using `Components.ComponentName` is not allowed (because it has not yet been\n * populated, hence would be `undefined`), then `delayedComponent('ComponentName')` can be\n * used instead.\n *\n * @example Create a container for a registered component\n *  // SomeContainer.js\n *  import { compose } from 'meteor/vulcan:lib';\n *  import { delayedComponent } from 'meteor/vulcan:core';\n *\n *  export default compose(\n *    // ...some hocs with container logic\n *  )(delayedComponent('ComponentName')); // cannot use Components.ComponentName in this context!\n *\n * @example {@link dynamicLoader}\n * @param {String} name Component name\n * @return {Function}\n *  Functional component that will render the given registered component\n */\nexport const delayedComponent = name => {\n  return props => {\n    const Component = Components[name] || null;\n    return Component && <Component {...props} />;\n  };\n};\n\n// Example with Proxy (might be unstable/hard to reason about)\n//const mergeWithComponents = (myComponents = {}) => {\n//  const handler = {\n//    get: function(target, name) {\n//      return name in target ? target[name] : Components[name];\n//    }\n//  };\n//  const proxy = new Proxy(myComponents, handler);\n//  return proxy;\n//};\nexport const mergeWithComponents = myComponents =>\n  myComponents ? { ...Components, ...myComponents } : Components;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/compose.js",
    "content": "import { createFactory } from 'react';\n\nexport const compose = (...funcs) => funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg);\n\nexport const setStatic = (key, value) => BaseComponent => {\n  /* eslint-disable no-param-reassign */\n  BaseComponent[key] = value;\n  /* eslint-enable no-param-reassign */\n  return BaseComponent;\n};\n\nexport const getDisplayName = Component => {\n  if (typeof Component === 'string') {\n    return Component;\n  }\n\n  if (!Component) {\n    return undefined;\n  }\n\n  return Component.displayName || Component.name || 'Component';\n};\n\nexport const wrapDisplayName = (BaseComponent, hocName) => `${hocName}(${getDisplayName(BaseComponent)})`;\n\nexport const setDisplayName = displayName => setStatic('displayName', displayName);\n\nexport const getContext = contextTypes => BaseComponent => {\n  const factory = createFactory(BaseComponent);\n  const GetContext = (ownerProps, context) =>\n    factory({\n      ...ownerProps,\n      ...context,\n    });\n\n  GetContext.contextTypes = contextTypes;\n\n  if (process.env.NODE_ENV !== 'production') {\n    return setDisplayName(wrapDisplayName(BaseComponent, 'getContext'))(GetContext);\n  }\n  return GetContext;\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/config.js",
    "content": "import SimpleSchema from 'simpl-schema';\n\n/**\n * @summary Kick off the namespace for Vulcan.\n * @namespace Vulcan\n */\n\n// eslint-disable-next-line no-undef\nVulcan = {};\n\n// eslint-disable-next-line no-undef\nVulcan.VERSION = '1.16.9';\n\n// ------------------------------------- Schemas -------------------------------- //\n\nexport const additionalFieldKeys = [\n  'hidden', // hidden: true means the field is never shown in a form no matter what\n  'mustComplete', // mustComplete: true means the field is required to have a complete profile\n  'form', // extra form properties\n  'inputProperties', // extra form properties\n  'itemProperties', // extra properties for the form row\n  'input', // SmartForm control (String or React component)\n  'control', // SmartForm control (String or React component) (legacy)\n  'order', // position in the form\n  'group', // form fieldset group\n  'arrayItem', // properties for array items\n\n  'onCreate', // field insert callback\n  'onInsert', // field insert callback (OpenCRUD backwards compatibility)\n\n  'onUpdate', // field edit callback\n  'onEdit', // field edit callback (OpenCRUD backwards compatibility)\n\n  'onDelete', // field remove callback\n  'onRemove', // field remove callback (OpenCRUD backwards compatibility)\n\n  'canRead', // who can view the field\n  'viewableBy', // who can view the field (OpenCRUD backwards compatibility)\n\n  'canCreate', // who can insert the field\n  'insertableBy', // who can insert the field (OpenCRUD backwards compatibility)\n\n  'canUpdate', // who can edit the field\n  'editableBy', // who can edit the field (OpenCRUD backwards compatibility)\n\n  'typeName', // the type to resolve the field with\n  'resolveAs', // field-level resolver\n  'searchable', // whether a field is searchable\n  'description', // description/help\n  'beforeComponent', // before form component\n  'afterComponent', // after form component\n  'placeholder', // form field placeholder value\n  'options', // form options\n  'query', // field-specific data loading query\n  'dynamicQuery', // field-specific data loading query\n  'staticQuery', // field-specific data loading query\n  'queryWaitsForValue', // whether the data loading query should wait for a field to have a value to run\n  'autocompleteQuery', // query used to populate autocomplete\n  'selectable', // field can be used as part of a selector when querying for data\n  'unique', // field can be used as part of a selectorUnique when querying for data\n  'orderable', // field can be used to order results when querying for data (backwards-compatibility)\n  'sortable', // field can be used to order results when querying for data\n\n  'apiOnly', // field should not be inserted in database\n  'relation', // define a relation to another model\n\n  'intl', // set to `true` to make a field international\n  'isIntlData', // marker for the actual schema fields that hold intl strings\n  'intlId', // set an explicit i18n key for a field\n];\n\nSimpleSchema.extendOptions(additionalFieldKeys);\n\n// eslint-disable-next-line no-undef\nexport default Vulcan;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/debug.js",
    "content": "import { getSetting } from './settings.js';\n\nexport const debug = function () {\n  if (getSetting('debug', false)) {\n    // eslint-disable-next-line no-console\n    console.log.apply(null, arguments);\n  }\n};\n\nexport const debugGroup = function () {\n  if (getSetting('debug', false)) {\n    // eslint-disable-next-line no-console\n    console.groupCollapsed.apply(null, arguments);\n  }\n};\nexport const debugGroupEnd = function () {\n  if (getSetting('debug', false)) {\n    // eslint-disable-next-line no-console\n    console.groupEnd.apply(null, arguments);\n  }\n};\n\n// Show a deprecation message, with a version so we keep track of deprecated features\nexport const deprecate = (nextVulcanVersion, message) => {\n  if (process.env.NODE_ENV === 'development') {\n    console.warn(`DEPRECATED (${nextVulcanVersion}):`, message);\n  }\n};"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/deep.js",
    "content": "/* eslint-disable */\n// see https://gist.github.com/furf/3208381\n\n_.mixin({\n\n  // Get/set the value of a nested property\n  deep: function (obj, key, value) {\n\n    var keys = key.replace(/\\[([\"']?)([^\\1]+?)\\1?\\]/g, '.$2').replace(/^\\./, '').split('.'),\n        root,\n        i = 0,\n        n = keys.length;\n\n    // Set deep value\n    if (arguments.length > 2) {\n\n      root = obj;\n      n--;\n\n      while (i < n) {\n        key = keys[i++];\n        obj = obj[key] = _.isObject(obj[key]) ? obj[key] : {};\n      }\n\n      obj[keys[i]] = value;\n\n      value = root;\n\n    // Get deep value\n    } else {\n      while ((obj = obj[keys[i++]]) !== null && i < n) {};\n      value = i < n ? void 0 : obj;\n    }\n\n    return value;\n  }\n\n});\n\n// Usage:\n//\n// var obj = {\n//   a: {\n//     b: {\n//       c: {\n//         d: ['e', 'f', 'g']\n//       }\n//     }\n//   }\n// };\n//\n// Get deep value\n// _.deep(obj, 'a.b.c.d[2]'); // 'g'\n//\n// Set deep value\n// _.deep(obj, 'a.b.c.d[2]', 'george');\n//\n// _.deep(obj, 'a.b.c.d[2]'); // 'george'\n\n\n_.mixin({\n  pluckDeep: function (obj, key) {\n    return _.map(obj, function (value) { return _.deep(value, key); });\n  }\n});\n\n\n_.mixin({\n\n // Return a copy of an object containing all but the blacklisted properties.\n  unpick: function (obj) {\n    obj = obj || {};\n    return _.pick(obj, _.difference(_.keys(obj), _.flatten(Array.prototype.slice.call(arguments, 1))));\n  }\n\n});\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/deep_extend.js",
    "content": "import { Utils } from './utils.js';\n\n// see: http://stackoverflow.com/questions/9399365/deep-extend-like-jquerys-for-nodejs\nUtils.deepExtend = function () {\n  var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},\n      i = 1,\n      length = arguments.length,\n      deep = false,\n      toString = Object.prototype.toString,\n      hasOwn = Object.prototype.hasOwnProperty,\n      class2type = {\n        '[object Boolean]': 'boolean',\n        '[object Number]': 'number',\n        '[object String]': 'string',\n        '[object Function]': 'function',\n        '[object Array]': 'array',\n        '[object Date]': 'date',\n        '[object RegExp]': 'regexp',\n        '[object Object]': 'object'\n      },\n      jQuery = {\n        isFunction: function (obj) {\n          return jQuery.type(obj) === 'function';\n        },\n        isArray: Array.isArray ||\n        function (obj) {\n          return jQuery.type(obj) === 'array';\n        },\n        isWindow: function (obj) {\n          return obj !== null && obj === obj.window;\n        },\n        isNumeric: function (obj) {\n          return !isNaN(parseFloat(obj)) && isFinite(obj);\n        },\n        type: function (obj) {\n          return obj === null ? String(obj) : class2type[toString.call(obj)] || 'object';\n        },\n        isPlainObject: function (obj) {\n          if (!obj || jQuery.type(obj) !== 'object' || obj.nodeType) {\n            return false;\n          }\n          try {\n            if (obj.constructor && !hasOwn.call(obj, 'constructor') && !hasOwn.call(obj.constructor.prototype, 'isPrototypeOf')) {\n              return false;\n            }\n          } catch (e) {\n            return false;\n          }\n          var key;\n          return key === undefined || hasOwn.call(obj, key);\n        }\n      };\n    if (typeof target === 'boolean') {\n      deep = target;\n      target = arguments[1] || {};\n      i = 2;\n    }\n    if (typeof target !== 'object' && !jQuery.isFunction(target)) {\n      target = {};\n    }\n    if (length === i) {\n      target = this;\n      --i;\n    }\n    for (i; i < length; i++) {\n      if ((options = arguments[i]) !== null) {\n        for (name in options) {\n          src = target[name];\n          copy = options[name];\n          if (target === copy) {\n            continue;\n          }\n          if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {\n            if (copyIsArray) {\n              copyIsArray = false;\n              clone = src && jQuery.isArray(src) ? src : [];\n            } else {\n              clone = src && jQuery.isPlainObject(src) ? src : {};\n            }\n            // WARNING: RECURSION\n            target[name] = Utils.deepExtend(deep, clone, copy);\n          } else if (copy !== undefined) {\n            target[name] = copy;\n          }\n        }\n      }\n    }\n    return target;\n  };\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/dynamic_loader.js",
    "content": "import React from 'react';\nimport loadable from 'react-loadable';\nimport isFunction from 'lodash/isFunction';\nimport { delayedComponent } from './components';\n\n/**\n * @callback dynamicLoader~importComponent\n * @return {Promise<React.Component>}\n */\n\n/**\n * Returns a component that will perform the given dynamic import and render\n * `Components.DynamicLoading` in the meantime.\n *\n * @example Register a component with a dynamic import\n *  registerComponent('MyComponent', dynamicLoader(() => import('./path/to/MyComponent')));\n *\n * @example Pass a dynamic component to a route\n *  import { addRoute, dynamicLoader, getDynamicComponent } from 'meteor/vulcan:core';\n *\n *  addRoute({\n *    name: 'home',\n *    path: '/',\n *    component: dynamicLoader(() => import('./path/to/HomeComponent')),\n *  });\n *\n * @param {dynamicLoader~importComponent|Promise<React.Component>} importComponent\n *  Function where the dynamic import is performed\n * @return {React.Component}\n *  Component that will load the dynamic import on mount\n */\nexport const dynamicLoader = importComponent =>\n  loadable({\n    loader: isFunction(importComponent) ? importComponent : () => importComponent, // backwards compatibility,\n    // use delayedComponent, as this function can be used when Components is not populated yet\n    loading: delayedComponent('DynamicLoading'),\n  });\n\n/**\n * Renders a dynamic component with the given props.\n *\n * @param {dynamicLoader~importComponent|Promise<React.Component>} importComponent\n * @param {Object} props\n */\nexport const renderDynamicComponent = (importComponent, props = {}) =>\n  React.createElement(dynamicLoader(importComponent), props);\n\nexport const getDynamicComponent = componentImport => {\n  // eslint-disable-next-line no-console\n  console.warn(\n    'getDynamicComponent is deprecated, use renderDynamicComponent instead.',\n    'If you want to retrieve the component instead that of just rendering it,',\n    'use dynamicLoader. See this issue to know how to do it: https://github.com/VulcanJS/Vulcan/issues/1997'\n  );\n  return renderDynamicComponent(componentImport);\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/errors.js",
    "content": "import get from 'lodash/get';\n\n/*\n\nGet whatever word is contained between the first two double quotes\n\n*/\nconst getFirstWord = input => {\n  const parts = /\"([^\"]*)\"/.exec(input);\n  if (parts === null) {\n    return null;\n  }\n  return parts[1];\n};\n\n/* \n\nParse a GraphQL error message\n\nTODO: check if still useful?\n\nSample message: \n\n\"GraphQL error: Variable \"$data\" got invalid value {\"meetingDate\":\"2018-08-07T06:05:51.704Z\"}.\nIn field \"name\": Expected \"String!\", found null.\nIn field \"stage\": Expected \"String!\", found null.\nIn field \"addresses\": Expected \"[JSON]!\", found null.\"\n\n*/\n\nexport const parseErrorMessage = message => {\n\n  if (!message) {\n    return null;\n  }\n\n  // note: optionally add .slice(1) at the end to get rid of the first error, which is not that helpful\n  let fieldErrors = message.split('\\n');\n\n  fieldErrors = fieldErrors.map(error => {\n    // field name is whatever is between the first to double quotes\n    const fieldName = getFirstWord(error);\n    if (error.includes('found null')) {\n      // missing field errors\n      return {\n        id: 'errors.required',\n        path: fieldName,\n        properties: {\n          name: fieldName,\n        },\n      };\n    } else {\n      // other generic GraphQL errors\n      return {\n        message: error,\n      };\n    }\n  });\n  return fieldErrors;\n};\n\n/*\n\nErrors can have the following properties stored on their `data` property:\n  - id: used as an internationalization key, for example `errors.required`\n  - path: for field-specific errors inside forms, the path of the field with the issue\n  - properties: additional data. Will be passed to vulcan-i18n as values\n  - message: if id cannot be used as i81n key, message will be used\n  \n*/\nexport const getErrors = error => {\n\n  const graphQLErrors = error.graphQLErrors;\n\n  // error thrown using new ApolloError\n  const apolloErrors = get(graphQLErrors, '0.extensions.data.errors');\n\n  // regular server error (with schema stitching)\n  const regularErrors = get(graphQLErrors, '0.extensions.exception.errors');\n\n  return apolloErrors || regularErrors || graphQLErrors || [];\n\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/findbyids.js",
    "content": "import { Connectors } from '../server/connectors.js';\n\n/**\n * @summary Find by ids, for DataLoader, inspired by https://github.com/tmeasday/mongo-find-by-ids/blob/master/index.js\n */\nconst findByIds = async function(collection, ids, context) {\n  \n  // get documents\n  const documents = await Connectors.find(collection, { _id: { $in: ids } });\n\n  // order documents in the same order as the ids passed as argument\n  const orderedDocuments = ids.map(id => _.findWhere(documents, {_id: id}));\n\n  return orderedDocuments;\n};\n\nexport default findByIds;"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/fragment_matcher.js",
    "content": "import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';\n\nexport const FragmentMatcher = [];\n\nexport const addToFragmentMatcher = fragmentMatcher => {\n  FragmentMatcher.push(fragmentMatcher);\n};\n\nexport const getFragmentMatcher = () => {\n  const fm = {\n    introspectionQueryResultData: {\n      __schema: {\n        types: FragmentMatcher,\n      },\n    }\n  };\n  return new IntrospectionFragmentMatcher(fm);\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/fragments.js",
    "content": "import gql from 'graphql-tag';\nimport { getDefaultFragmentText } from './graphql/defaultFragment';\nimport uniq from 'lodash/uniq';\nimport flattenDeep from 'lodash/flattenDeep';\nimport stringSimilarity from 'string-similarity';\n\nexport const Fragments = {};\nexport const FragmentsExtensions = {}; // will be used on startup\n\nexport const throwUnregisteredFragmentError = fragmentName => {\n  const similarFragments = stringSimilarity.findBestMatch(fragmentName, Object.keys(Fragments));\n  throw new Error(`A registered fragment named \"${fragmentName}\" cannot be found, did you mean \"${similarFragments.bestMatch.target}\"?`);\n};\n\n/**\n * @param {*} collectionOrName A collection name, or a whole collection\n */\nexport const getDefaultFragmentName = (collectionOrName) => {\n  const collectionName = typeof collectionOrName === 'string' ? collectionOrName : collectionOrName.options.collectionName;\n  return `${collectionName}DefaultFragment`;\n};\n\n/*\n\nGet a fragment's name from its text\n\n*/\nexport const extractFragmentName = fragmentText => fragmentText.match(/fragment (.*) on/)[1];\n\n/*\n\nGet a query resolver's name from its text\n\n*/\nexport const extractResolverName = resolverText => resolverText.trim().substr(0, resolverText.trim().indexOf('{'));\n\n\n/*\n\nRegister a fragment, including its text, the text of its subfragments, and the fragment object\n\n*/\nexport const registerFragment = fragmentTextSource => {\n  // remove comments\n  const fragmentText = fragmentTextSource.replace(/\\#.*\\n/g, '\\n');\n\n  // extract name from fragment text\n  const fragmentName = extractFragmentName(fragmentText);\n\n  // extract subFragments from text\n  const matchedSubFragments = fragmentText.match(/\\.{3}([_A-Za-z][_0-9A-Za-z]*)/g) || [];\n  const subFragments = _.unique(matchedSubFragments.map(f => f.replace('...', '')));\n\n  // register fragment\n  Fragments[fragmentName] = {\n    fragmentText\n  };\n\n  // also add subfragments if there are any\n  if (subFragments && subFragments.length) {\n    Fragments[fragmentName].subFragments = subFragments;\n  }\n\n};\n\n/*\n\nCreate gql fragment object from text and subfragments\n\n*/\nexport const getFragmentObject = (fragmentText, subFragments) => {\n  // pad the literals array with line returns for each subFragments\n  const literals = subFragments ? [fragmentText, ...subFragments.map(x => '\\n')] : [fragmentText];\n\n  // the gql function expects an array of literals as first argument, and then sub-fragments as other arguments\n  const gqlArguments = subFragments ? [literals, ...subFragments.map(subFragmentName => {\n    // return subfragment's gql fragment\n    if (!Fragments[subFragmentName] || !Fragments[subFragmentName].fragmentObject) {\n      throw new Error(`Subfragment “${subFragmentName}” of fragment “${extractFragmentName(fragmentText)}” has not been initialized yet.`);\n    }\n    return Fragments[subFragmentName].fragmentObject;\n  })] : [literals];\n\n  return gql.apply(null, gqlArguments);\n};\n\nexport const getDefaultFragment = collection => {\n  const fragmentText = getDefaultFragmentText(collection);\n  return fragmentText ? gql`${fragmentText}` : null;\n};\n/*\n \nQueue a fragment to be extended with additional properties.\n \nNote: can be used even before the fragment has been registered. \n \n*/\nexport const extendFragment = (fragmentName, newProperties) => {\n  FragmentsExtensions[fragmentName] = FragmentsExtensions[fragmentName] ? [...FragmentsExtensions[fragmentName], newProperties] : [newProperties];\n};\n\n/*\n \nPerform fragment extension (called from initializeFragments()\n \nNote: will call registerFragment again each time, resulting in multiple fragments\nwith the same name (but duplicate fragments warning is disabled).\n \n*/\nexport const extendFragmentWithProperties = (fragmentName, newProperties) => {\n  const fragment = Fragments[fragmentName];\n  const fragmentEndPosition = fragment.fragmentText.lastIndexOf('}');\n  const newFragmentText = [\n    fragment.fragmentText.slice(0, fragmentEndPosition),\n    newProperties,\n    fragment.fragmentText.slice(fragmentEndPosition)\n  ].join('');\n  registerFragment(newFragmentText);\n};\n\n/*\n \nRemove a property from a fragment\n \nNote: can only be called *after* a fragment is registered\n \n*/\nexport const removeFromFragment = (fragmentName, propertyName) => {\n  const fragment = Fragments[fragmentName];\n  const newFragmentText = fragment.fragmentText.replace(propertyName, '');\n  registerFragment(newFragmentText);\n};\n\n/*\n \nGet fragment name from fragment object\n \n*/\nexport const getFragmentName = fragment => fragment && fragment.definitions[0] && fragment.definitions[0].name.value;\n\n/*\n \nGet actual gql fragment\n \n*/\nexport const getFragment = fragmentName => {\n  if (!Fragments[fragmentName]) {\n    throwUnregisteredFragmentError(fragmentName);\n  }\n  if (!Fragments[fragmentName].fragmentObject) {\n    initializeFragments([fragmentName]);\n  }\n  // return fragment object created by gql\n  return Fragments[fragmentName].fragmentObject;\n};\n\n/*\n \nGet gql fragment text\n \n*/\nexport const getFragmentText = fragmentName => {\n  if (!Fragments[fragmentName]) {\n    throwUnregisteredFragmentError(fragmentName);\n  }\n  // return fragment object created by gql\n  return Fragments[fragmentName].fragmentText;\n};\n\n/*\n \nGet names of non initialized fragments.\n \n*/\nexport const getNonInitializedFragmentNames = () =>\n  _.keys(Fragments).filter(name => !Fragments[name].fragmentObject);\n\n/*\n \nPerform all fragment extensions (called from routing)\n \n*/\nexport const initializeFragments = (fragments = getNonInitializedFragmentNames()) => {\n\n  const errorFragmentKeys = [];\n\n  // extend fragment texts (if extended fragment exists)\n  _.forEach(FragmentsExtensions, (extensions, fragmentName) => {\n    if (Fragments[fragmentName]) {\n      extensions.forEach(newProperties => {\n        extendFragmentWithProperties(fragmentName, newProperties);\n      });\n    }\n  });\n\n  // create fragment objects\n\n  // initialize fragments *with no subfragments* first to avoid unresolved dependencies\n  const keysWithoutSubFragments = _.filter(fragments, fragmentName => !Fragments[fragmentName].subFragments);\n  _.forEach(keysWithoutSubFragments, fragmentName => {\n    const fragment = Fragments[fragmentName];\n    fragment.fragmentObject = getFragmentObject(fragment.fragmentText, fragment.subFragments);\n  });\n\n  // next, initialize fragments that *have* subfragments\n  const keysWithSubFragments = _.filter(_.keys(Fragments), fragmentName => !!Fragments[fragmentName].subFragments);\n  _.forEach(keysWithSubFragments, fragmentName => {\n    const fragment = Fragments[fragmentName];\n    try {\n      fragment.fragmentObject = getFragmentObject(fragment.fragmentText, fragment.subFragments);\n    } catch (error) {\n      // if fragment initialization triggers an error, store fragment and try again later\n      // common error causes include cross-dependencies\n      errorFragmentKeys.push(fragmentName);\n    }\n  });\n\n  // finally, try initializing any fragment that triggered an error again\n  _.forEach(errorFragmentKeys, fragmentName => {\n    const fragment = Fragments[fragmentName];\n    fragment.fragmentObject = getFragmentObject(fragment.fragmentText, fragment.subFragments);\n  });\n\n};\n\n/*\n\nTake a text query, and expand any subfragments inside it\n\n*/\nexport const expandQueryFragments = query => {\n  let expandedQuery = query;\n  // get all fragment names\n  const fragmentNames = extractSubFragmentsFlat(query);\n  // append each fragment text to the end of the query\n  fragmentNames.forEach(fragmentName => {\n    expandedQuery = expandedQuery + '\\n' + Fragments[fragmentName].fragmentText;\n  });\n  return expandedQuery;\n};\n\n/*\n\nRecursively extract all nested fragment dependency names into nested arrays\n\nWorks on any string (query or fragment)\n\nNote: only extracts *sub*fragments (e.g. not the current fragment itself)\n\n*/\nexport const extractSubFragments = (text) => {\n  // extract subFragments from text\n  const matchedSubFragments = text.match(/\\.{3}([_A-Za-z][_0-9A-Za-z]*)/g) || [];\n  if (matchedSubFragments.length > 0) {\n    // return an array of arrays\n    return matchedSubFragments.map(s => {\n      const subFragmentName = s.replace('...', '');\n      if (!Fragments[subFragmentName]) {\n        throwUnregisteredFragmentError(subFragmentName);\n      }\n      const subFragmentText = Fragments[subFragmentName].fragmentText;\n      // Return the name of the matched subfragment, then call function recursively\n      return [subFragmentName, ...extractSubFragments(subFragmentText)];\n    });\n  } else {\n    return [];\n  }\n};\n\n/*\n\nFlatten nested fragments array and only keep unique fragment names\n\n*/\nexport const extractSubFragmentsFlat = text => uniq(flattenDeep(extractSubFragments(text)));\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/graphql/defaultFragment.js",
    "content": "/**\n * Generates the default fragment for a collection\n * = a fragment containing all fields\n */\nimport { getFragmentFieldNames } from '../schema_utils';\nimport { isBlackbox } from '../simpleSchema_utils';\n\nconst intlSuffix = '_intl';\n\n// get fragment for a whole object (root schema or nested schema of an object or an array)\nconst getObjectFragment = ({\n    schema,\n    fragmentName,\n    options\n}) => {\n    const fieldNames = getFragmentFieldNames({ schema, options });\n    const childFragments = fieldNames.length && fieldNames.map(fieldName => getFieldFragment({\n        schema,\n        fieldName,\n        options,\n        getObjectFragment: getObjectFragment\n    }))\n        // remove empty values\n        .filter(f => !!f);\n    if (childFragments.length) {\n        return `${fragmentName} { ${childFragments.join('\\n')} }`;\n    }\n    return null;\n};\n\n// get fragment for a specific field (either the field name or a nested fragment)\nexport const getFieldFragment = ({\n    schema,\n    fieldName,\n    options,\n    getObjectFragment = getObjectFragment // a callback to call on nested schema\n}) => {\n    // intl\n    if (fieldName.slice(-5) === intlSuffix) {\n        return `${fieldName}{ locale value }`;\n    }\n    if (fieldName === '_id') return fieldName;\n    const field = schema[fieldName];\n\n    const fieldType = field.type.singleType;\n    const fieldTypeName =\n        typeof fieldType === 'object' ? 'Object' : typeof fieldType === 'function' ? fieldType.name : fieldType;\n\n    switch (fieldTypeName) {\n        case 'Object':\n            if (!isBlackbox(field) && fieldType._schema) {\n                return getObjectFragment({\n                    fragmentName: fieldName,\n                    schema: fieldType._schema,\n                    options\n                }) || null;\n            }\n            return fieldName;\n        case 'Array':\n            const arrayItemFieldName = `${fieldName}.$`;\n            const arrayItemField = schema[arrayItemFieldName];\n            // note: make sure field has an associated array item field\n            if (arrayItemField) {\n                // child will either be native value or a an object (first case)\n                const arrayItemFieldType = arrayItemField.type.singleType;\n                if (!isBlackbox(field) && arrayItemFieldType._schema) {\n                    return getObjectFragment({\n                        fragmentName: fieldName,\n                        schema: arrayItemFieldType._schema,\n                        options\n                    }) || null;\n                }\n            }\n            return fieldName;\n        default:\n            return fieldName; // fragment = fieldName\n    }\n};\n\n/*\n\nCreate default \"dumb\" gql fragment object for a given collection\n\n*/\nexport const getDefaultFragmentText = (collection, options = { onlyViewable: true }) => {\n    const schema = collection.simpleSchema()._schema;\n    return getObjectFragment({\n        schema,\n        fragmentName: `fragment ${collection.options.collectionName}DefaultFragment on ${collection.typeName}`,\n        options\n    }) || null;\n};\n\nexport default getDefaultFragmentText;"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/graphql/index.js",
    "content": "export * from './defaultFragment';\nexport * from './utils';\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/graphql/utils.js",
    "content": "import { Utils } from '../utils';\nimport { isBlackbox, unarrayfyFieldName, getFieldType, getFieldTypeName } from '../simpleSchema_utils';\n\nexport const getGraphQLType = ({ fieldSchema, schema, fieldName, typeName, isInput = false, isParentBlackbox = false }) => {\n  const field = fieldSchema || schema[fieldName];\n\n  if (field.typeName) return field.typeName; // respect typeName provided by user\n\n  const fieldType = getFieldType(field);\n  const fieldTypeName = getFieldTypeName(fieldType);\n\n  // NOTE: we DON't USE isInputField! we don't want to match \"field.intl\", only \"field.intlData\"\n  /**\n   * Expected GraphQL Schema:\n   * \n   *   # The room name\n  * name(locale: String): String @intl\n  * # The room name\n  * name_intl(locale: String): [IntlValue] @intl\n  * \n  * JS schema:\n  * \n  * name: {\n  *   type: String,\n  *   optional: false,\n  *   canRead: ['guests'],\n  *   canCreate: ['admins'],\n  *   intl: true,\n  * },\n   */\n  if (field.isIntlData) {\n    return isInput ? '[IntlValueInput]' : '[IntlValue]';\n  }\n\n  switch (fieldTypeName) {\n    case 'String':\n      /*\n      Getting Enums from allowed values is counter productive because enums syntax is limited\n      @see https://github.com/VulcanJS/Vulcan/issues/2332\n      if (hasAllowedValues(field) && isValidEnum(getAllowedValues(field))) {\n        return getEnumType(typeName, fieldName);\n      }*/\n      return 'String';\n\n    case 'Boolean':\n      return 'Boolean';\n\n    case 'Number':\n      return 'Float';\n\n    case 'SimpleSchema.Integer':\n      return 'Int';\n\n    // for arrays, look for type of associated schema field or default to [String]\n    case 'Array':\n      const arrayItemFieldName = `${fieldName}.$`;\n      // note: make sure field has an associated array\n      if (schema[arrayItemFieldName]) {\n        // try to get array type from associated array\n        const arrayItemType = getGraphQLType({\n          schema,\n          fieldName: arrayItemFieldName,\n          typeName,\n          isInput,\n          isParentBlackbox: isParentBlackbox || isBlackbox(field) // blackbox field may not be nested items\n        });\n        return arrayItemType ? `[${arrayItemType}]` : null;\n      }\n      return null;\n\n    case 'Object':\n      // 4 cases: \n      // - it's the child of a blackboxed array  => will be blackbox JSON\n      // - a nested Schema, \n      // - a referenced schema, or an actual JSON\n      if (isParentBlackbox) return 'JSON';\n      if (!isBlackbox(field) && fieldType._schema) {\n        return getNestedGraphQLType(typeName, fieldName, isInput);\n      }\n\n      // referenced Schema\n      if (/*field.type.definitions[0].blackbox && */field.typeName && field.typeName !== 'JSON') {\n        return isInput ? field.typeName + 'Input' : field.typeName;\n      }\n      // blackbox JSON object\n      return 'JSON';\n    case 'Date':\n      return 'Date';\n\n    default:\n      return null;\n  }\n};\n\n\n// get GraphQL type for a nested object (<MainTypeName><FieldName> e.g PostAuthor, EventAdress, etc.)\nexport const getNestedGraphQLType = (typeName, fieldName, isInput) =>\n  `${typeName}${Utils.capitalize(unarrayfyFieldName(fieldName))}${isInput ? 'Input' : ''}`;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/graphql_templates/filtering.js",
    "content": "import { convertToGraphQL } from './types.js';\nimport { Utils } from '../utils.js';\n\n// field types that support filtering\nconst supportedFieldTypes = ['String', 'Int', 'Float', 'Boolean', 'Date'];\nconst getContentType = type =>\n  type\n    .replace('[', '')\n    .replace(']', '')\n    .replace('!', '');\nconst isSupportedFieldType = type => supportedFieldTypes.includes(type);\n\n/* ------------------------------------- Selector Types ------------------------------------- */\n\n/*\n\nThe selector type is used to query for one or more documents\n\ntype MovieSelectorInput {\n  AND: [MovieSelectorInput]\n  OR: [MovieSelectorInput]\n  ...\n}\n\n// TODO: not currently used\n\n*/\nexport const selectorInputType = typeName => `${typeName}SelectorInput`;\nexport const selectorInputTemplate = ({ typeName, fields }) =>\n  `input ${selectorInputType(typeName)} {\n  _and: [${selectorInputType(typeName)}]\n  _or: [${selectorInputType(typeName)}]\n${convertToGraphQL(fields, '  ')}\n}`;\n\n/*\n\nThe unique selector type is used to query for exactly one document\n\ntype MovieSelectorUniqueInput {\n  _id: String\n  slug: String\n}\n\n*/\nexport const selectorUniqueInputType = typeName => `${typeName}SelectorUniqueInput`;\nexport const selectorUniqueInputTemplate = ({ typeName, fields }) =>\n  `input ${selectorUniqueInputType(typeName)} {\n  _id: String\n  documentId: String # OpenCRUD backwards compatibility\n  slug: String\n${convertToGraphQL(fields, '  ')}\n}`;\n\nconst formatFilterName = s => Utils.capitalize(s.replace('_', ''));\n\n/*\n\nSee https://docs.hasura.io/1.0/graphql/manual/queries/query-filters.html#\n \nNote: if a filter doesn't take arguments just use a boolean (e.g. `_onlyPublic: true`)\ninstead of defining a custom type. \n\n*/\nexport const filterInputType = typeName => `${typeName}FilterInput`;\nexport const fieldFilterInputTemplate = ({ typeName, fields, customFilters = [], customSorts = [] }) =>\n  `input ${filterInputType(typeName)} {\n  _and: [${filterInputType(typeName)}]\n  _not: ${filterInputType(typeName)}\n  _or: [${filterInputType(typeName)}]\n${customFilters.map(filter => `  ${filter.name}: ${filter.arguments ? customFilterType(typeName, filter) : 'Boolean'}`)}\n${customSorts.map(sort => `  ${sort.name}: ${customSortType(typeName, sort)}`)}\n${fields\n  .map(field => {\n    const { name, type } = field;\n    const contentType = getContentType(type);\n    if (isSupportedFieldType(contentType)) {\n      const isArrayField = type[0] === '[';\n      return `  ${name}: ${contentType}_${isArrayField ? 'Array_' : ''}Selector`;\n    } else {\n      return '';\n    }\n  })\n  .join('\\n')}\n}`;\n\nexport const sortInputType = typeName => `${typeName}SortInput`;\nexport const fieldSortInputTemplate = ({ typeName, fields }) =>\n  `input ${sortInputType(typeName)} {\n${fields.map(({ name }) => `  ${name}: SortOptions`).join('\\n')}\n}`;\n\nexport const customFilterType = (typeName, filter) => `${typeName}${formatFilterName(filter.name)}FilterInput`;\nexport const customFilterTemplate = ({ typeName, filter }) =>\n  `input ${customFilterType(typeName, filter)}{\n  ${filter.arguments}\n}`;\n\n// TODO: not currently used\nexport const customSortType = (typeName, filter) => `${typeName}${formatFilterName(filter.name)}SortInput`;\nexport const customSortTemplate = ({ typeName, sort }) =>\n  `input ${customSortType(typeName, sort)}{\n  ${sort.arguments}\n}`;\n\n// export const customFilterTemplate = ({ typeName, customFilters }) =>\n//   `enum ${typeName}CustomFilter{\n// ${Object.keys(customFilters).map(name => `  ${name}`).join('\\n')}\n// }`;\n\n// export const customSortTemplate = ({ typeName, customFilters }) =>\n//   `enum ${typeName}CustomSort{\n// ${Object.keys(customFilters).map(name => `  ${name}`).join('\\n')}\n// }`;\n\n/*\nexport const orderByInputTemplate = ({ typeName, fields }) =>\n  `enum ${typeName}SortInput {\n  ${Array.isArray(fields) && fields.length ? fields.join('\\n  ') : 'foobar'}\n}`;\n*/\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/graphql_templates/index.js",
    "content": "export * from './types.js';\nexport * from './queries.js';\nexport * from './mutations.js';\nexport * from './filtering.js';\nexport * from './other.js';\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/graphql_templates/mutations.js",
    "content": "import { convertToGraphQL } from './types.js';\nimport { filterInputType, selectorUniqueInputType } from './filtering.js';\n\n// eslint-disable-next-line\nconst deprecated = `# Deprecated (use 'input' field instead).`;\n\nconst mutationReturnProperty = 'data';\n\n/* ------------------------------------- Mutation Types ------------------------------------- */\n\n/*\n\nMutation for creating a new document\n\ncreateMovie(input: CreateMovieInput) : MovieOutput\n\n*/\nexport const createMutationType = typeName => `create${typeName}`;\nexport const createMutationTemplate = ({ typeName }) =>\n  `${createMutationType(typeName)}(\n  input: ${createInputType(typeName, false)},\n  ${deprecated}\n  data: ${createDataInputType(typeName, false)}\n) : ${mutationOutputType(typeName)}`;\n\n/*\n\nMutation for updating an existing document\n\nupdateMovie(input: UpdateMovieInput) : MovieOutput\n\n*/\nexport const updateMutationType = typeName => `update${typeName}`;\nexport const updateMutationTemplate = ({ typeName }) =>\n  `${updateMutationType(typeName)}(\n  input: ${updateInputType(typeName, false)},\n  ${deprecated}\n  selector: ${selectorUniqueInputType(typeName)},\n  ${deprecated}\n  data: ${updateDataInputType(typeName)}\n) : ${mutationOutputType(typeName)}`;\n\n/*\n\nMutation for updating an existing document; or creating it if it doesn't exist yet\n\nupsertMovie(input: UpsertMovieInput) : MovieOutput\n\n*/\nexport const upsertMutationType = typeName => `upsert${typeName}`;\nexport const upsertMutationTemplate = ({ typeName }) =>\n  `${upsertMutationType(typeName)}(\n  input: ${upsertInputType(typeName, false)},\n  ${deprecated}\n  selector: ${selectorUniqueInputType(typeName)},\n  ${deprecated}\n  data: ${updateDataInputType(typeName, false)}\n) : ${mutationOutputType(typeName)}`;\n\n/*\n\nMutation for deleting an existing document\n\ndeleteMovie(input: DeleteMovieInput) : MovieOutput\n\n*/\nexport const deleteMutationType = typeName => `delete${typeName}`;\nexport const deleteMutationTemplate = ({ typeName }) =>\n  `${deleteMutationType(typeName)}(\n  input: ${deleteInputType(typeName, false)},\n  ${deprecated}\n  selector: ${selectorUniqueInputType(typeName)}\n) : ${mutationOutputType(typeName)}`;\n\n/* ------------------------------------- Mutation Input Types ------------------------------------- */\n\n/*\n\nType for create mutation input argument\n\ntype CreateMovieInput {\n  data: CreateMovieDataInput!\n}\n\n*/\nexport const createInputType = typeName => `Create${typeName}Input`;\nexport const createInputTemplate = ({ typeName }) =>\n  `input ${createInputType(typeName)} {\n  data: ${createDataInputType(typeName, true)}\n  # An identifier to name the mutation's execution context\n  contextName: String\n}`;\n\n/*\n\nType for update mutation input argument\n\ntype UpdateMovieInput {\n  selector: MovieSelectorUniqueInput!\n  data: UpdateMovieDataInput!\n}\n\nNote: selector is for backwards-compatibility\n\n*/\nexport const updateInputType = typeName => `Update${typeName}Input`;\nexport const updateInputTemplate = ({ typeName }) =>\n  `input ${updateInputType(typeName)}{\n  filter: ${filterInputType(typeName)}\n  id: String\n  data: ${updateDataInputType(typeName, true)}\n  # An identifier to name the mutation's execution context\n  contextName: String\n}`;\n\n/*\n\nType for upsert mutation input argument\n\nNote: upsertInputTemplate uses same data type as updateInputTemplate\n\ntype UpsertMovieInput {\n  selector: MovieSelectorUniqueInput!\n  data: UpdateMovieDataInput!\n}\n\nNote: selector is for backwards-compatibility\n\n*/\nexport const upsertInputType = typeName => `Upsert${typeName}Input`;\nexport const upsertInputTemplate = ({ typeName }) =>\n  `input ${upsertInputType(typeName)}{\n  filter: ${filterInputType(typeName)}\n  id: String\n  data: ${updateDataInputType(typeName, true)}\n  # An identifier to name the mutation's execution context\n  contextName: String\n}`;\n\n/*\n\nType for delete mutation input argument\n\ntype DeleteMovieInput {\n  selector: MovieSelectorUniqueInput!\n}\n\nNote: selector is for backwards-compatibility\n\n*/\nexport const deleteInputType = typeName => `Delete${typeName}Input`;\nexport const deleteInputTemplate = ({ typeName }) =>\n  `input ${deleteInputType(typeName)}{\n  filter: ${filterInputType(typeName)}\n  id: String\n}`;\n\n/*\n\nType for the create mutation input argument's data property\n\ntype CreateMovieDataInput {\n  title: String\n  description: String\n}\n\n*/\nexport const createDataInputType = (typeName, nonNull = false) => `Create${typeName}DataInput${nonNull ? '!' : ''}`;\nexport const createDataInputTemplate = ({ typeName, fields }) =>\n  `input ${createDataInputType(typeName)} {\n${convertToGraphQL(fields, '  ')}\n}`;\n\n/*\n\nType for the update & upsert mutations input argument's data property\n\ntype UpdateMovieDataInput {\n  title: String\n  description: String\n}\n\n*/\nexport const updateDataInputType = (typeName, nonNull = false) => `Update${typeName}DataInput${nonNull ? '!' : ''}`;\nexport const updateDataInputTemplate = ({ typeName, fields }) =>\n  `input ${updateDataInputType(typeName)} {\n${convertToGraphQL(fields, '  ')}\n}`;\n\n/* ------------------------------------- Mutation Output Type ------------------------------------- */\n\n/*\n\nType for the return value of all mutations\n\ntype MovieOutput {\n  data: Movie\n}\n\n*/\nexport const mutationOutputType = typeName => `${typeName}MutationOutput`;\nexport const mutationOutputTemplate = ({ typeName }) =>\n  `type ${mutationOutputType(typeName)}{\n  ${mutationReturnProperty}: ${typeName}\n}`;\n\n/* ------------------------------------- Mutation Queries ------------------------------------- */\n\n/*\n\nCreate mutation query used on the client\n\nmutation createMovie($data: CreateMovieDataInput!) {\n  createMovie(data: $data) {\n    data {\n      _id\n      name\n      __typename\n    }\n    __typename\n  }\n}\n\n*/\nexport const createClientTemplate = ({ typeName, fragmentName }) =>\n  `mutation ${createMutationType(typeName)}($input: ${createInputType(typeName)}, $data: ${createDataInputType(typeName)}) {\n  ${createMutationType(typeName)}(input: $input, data: $data) {\n    ${mutationReturnProperty} {\n      ...${fragmentName}\n    }\n  }\n}`;\n\n/*\n\nUpdate mutation query used on the client\n\nmutation updateMovie($selector: MovieSelectorUniqueInput!, $data: UpdateMovieDataInput!) {\n  updateMovie(selector: $selector, data: $data) {\n    data {\n      _id\n      name\n      __typename\n    }\n    __typename\n  }\n}\n\n*/\nexport const updateClientTemplate = ({ typeName, fragmentName }) =>\n  `mutation ${updateMutationType(typeName)}($input: ${updateInputType(typeName)}, $selector: ${selectorUniqueInputType(\n    typeName\n  )}, $data: ${updateDataInputType(typeName, false)}) {\n  ${updateMutationType(typeName)}(input: $input, selector: $selector, data: $data) {\n    ${mutationReturnProperty} {\n      ...${fragmentName}\n    }\n  }\n}`;\n\n/*\n\nUpsert mutation query used on the client\n\nmutation upsertMovie($selector: MovieSelectorUniqueInput!, $data: UpdateMovieDataInput!) {\n  upsertMovie(selector: $selector, data: $data) {\n    data {\n      _id\n      name\n      __typename\n    }\n    __typename\n  }\n}\n\n*/\nexport const upsertClientTemplate = ({ typeName, fragmentName }) =>\n  `mutation ${upsertMutationType(typeName)}($input: ${upsertInputType(typeName)}, $selector: ${selectorUniqueInputType(\n    typeName\n  )}, $data: ${updateDataInputType(typeName, false)}) {\n  ${upsertMutationType(typeName)}(input: $input, selector: $selector, data: $data) {\n    ${mutationReturnProperty} {\n      ...${fragmentName}\n    }\n  }\n}`;\n\n/*\n\nDelete mutation query used on the client\n\nmutation deleteMovie($selector: MovieSelectorUniqueInput!) {\n  deleteMovie(selector: $selector) {\n    data {\n      _id\n      name\n      __typename\n    }\n    __typename\n  }\n}\n\n*/\nexport const deleteClientTemplate = ({ typeName, fragmentName }) =>\n  `mutation ${deleteMutationType(typeName)}($input: ${deleteInputType(typeName)}, $selector: ${selectorUniqueInputType(typeName)}) {\n  ${deleteMutationType(typeName)}(input: $input, selector: $selector) {\n    ${mutationReturnProperty} {\n      ...${fragmentName}\n    }\n  }\n}`;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/graphql_templates/other.js",
    "content": "import { capitalize } from '../utils';\n\n/*\n\nField-specific data loading query template for a dynamic array of item IDs\n\n(example: `categoriesIds` where $value is ['foo123', 'bar456'])\n\n*/\nexport const fieldDynamicQueryTemplate = ({ queryResolverName, autocompletePropertyName, valuePropertyName = '_id', fragmentName }) =>\n  `query FormComponentDynamic${capitalize(queryResolverName)}Query($value: [String!]) {\n    ${queryResolverName}(input: { \n      filter: {  ${valuePropertyName}: { _in: $value } },\n      sort: { ${autocompletePropertyName}: asc }\n    }){\n      results{\n        ${valuePropertyName}\n        ${autocompletePropertyName}\n        ${fragmentName && `...${fragmentName}` || ''}\n      }\n    }\n  }\n`;\n\n/*\n\nField-specific data loading query template for *all* items in a collection\n\n*/\nexport const fieldStaticQueryTemplate = ({ queryResolverName, autocompletePropertyName, valuePropertyName = '_id', fragmentName }) =>\n  `query FormComponentStatic${capitalize(queryResolverName)}Query {\n  ${queryResolverName}(input: { \n    sort: { ${autocompletePropertyName}: asc }\n  }){\n    results{\n      ${valuePropertyName}\n      ${autocompletePropertyName}\n      ${fragmentName && `...${fragmentName}` || ''}\n    }\n  }\n}\n`;\n\n\n/*\n\nQuery template for loading a list of autocomplete suggestions\n\n*/\nexport const autocompleteQueryTemplate = ({ queryResolverName, autocompletePropertyName, valuePropertyName = '_id', fragmentName }) => `\n  query Autocomplete${capitalize(queryResolverName)}Query($queryString: String) {\n    ${queryResolverName}(\n      input: {\n        filter: {\n          ${autocompletePropertyName}: { _like: $queryString }\n        },\n        limit: 20\n      }\n    ){\n      results{\n        ${valuePropertyName}\n        ${autocompletePropertyName}\n        ${fragmentName && `...${fragmentName}` || ''}\n      }\n    }\n  }\n`;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/graphql_templates/queries.js",
    "content": "import { Utils } from '../utils.js';\nimport { selectorUniqueInputType, filterInputType, sortInputType } from './filtering.js';\n\n// eslint-disable-next-line\nconst deprecated1 = `# Deprecated (use 'filter/id' fields instead).`;\n// eslint-disable-next-line\nconst deprecated2 = `# Deprecated (use 'filter/id' fields instead).`;\n\nconst singleReturnProperty = 'result';\nconst multiReturnProperty = 'results';\n\n/* ------------------------------------- Query Types ------------------------------------- */\n\n/*\n\nA query for a single document\n\nmovie(input: SingleMovieInput) : SingleMovieOutput\n\n*/\nexport const singleQueryType = typeName => Utils.camelCaseify(typeName);\nexport const singleQueryTemplate = ({ typeName }) =>\n  `${singleQueryType(typeName)}(input: ${singleInputType(typeName, true)}): ${singleOutputType(typeName)}`;\n\n/*\n\nA query for multiple documents\n\nmovies(input: MultiMovieInput) : MultiMovieOutput\n\n*/\nexport const multiQueryType = typeName => Utils.camelCaseify(Utils.pluralize(typeName));\nexport const multiQueryTemplate = ({ typeName }) =>\n  `${multiQueryType(typeName)}(input: ${multiInputType(typeName, false)}): ${multiOutputType(typeName)}`;\n\n/* ------------------------------------- Query Input Types ------------------------------------- */\n\n/*\n\nThe argument type when querying for a single document\n\ntype SingleMovieInput {\n  filter: MovieFilterInput\n  sort: MovieSortInput\n  search: String\n  enableCache: Boolean\n}\n\n*/\nexport const singleInputType = (typeName, nonNull = false) => `Single${typeName}Input${nonNull ? '!' : ''}`;\nexport const singleInputTemplate = ({ typeName }) =>\n  `input ${singleInputType(typeName)} {\n  # filtering\n  filter: ${filterInputType(typeName)}\n  sort: ${sortInputType(typeName)}\n  search: String\n  id: String\n\n  # backwards-compatibility\n  ${deprecated1}\n  selector: ${selectorUniqueInputType(typeName)}\n\n  # options (backwards-compatibility)\n  # Whether to enable caching for this query\n  enableCache: Boolean\n  # Return null instead of throwing MissingDocumentError\n  allowNull: Boolean\n  # An identifier to name the query's execution context\n  contextName: String\n}`;\n\n/*\n\nThe argument type when querying for multiple documents\n\ntype MultiMovieInput {\n  terms: JSON\n  offset: Int\n  limit: Int\n  enableCache: Boolean\n}\n\n*/\nexport const multiInputType = (typeName, nonNull = false) => `Multi${typeName}Input${nonNull ? '!' : ''}`;\nexport const multiInputTemplate = ({ typeName }) =>\n  `input ${multiInputType(typeName)} {\n\n  # filtering\n  filter: ${filterInputType(typeName)}\n  sort: ${sortInputType(typeName)}\n  search: String\n  offset: Int\n  limit: Int\n\n  # backwards-compatibility\n  # A JSON object that contains the query terms used to fetch data\n  ${deprecated2}\n  terms: JSON\n\n  # options (backwards-compatibility)\n  # Whether to enable caching for this query\n  enableCache: Boolean\n  # Whether to calculate totalCount for this query\n  enableTotal: Boolean\n  # An identifier to name the query's execution context\n  contextName: String\n\n}`;\n\n/* ------------------------------------- Query Output Types ------------------------------------- */\n\n/*\n\nThe type for the return value when querying for a single document\n\ntype SingleMovieOuput{\n  result: Movie\n}\n\n*/\nexport const singleOutputType = typeName => `Single${typeName}Output`;\nexport const singleOutputTemplate = ({ typeName }) =>\n  `type ${singleOutputType(typeName)}{\n  ${singleReturnProperty}: ${typeName}\n}`;\n\n/*\n\nThe type for the return value when querying for multiple documents\n\ntype MultiMovieOuput{\n  results: [Movie]\n  totalCount: Int\n}\n\n*/\nexport const multiOutputType = typeName => ` Multi${typeName}Output`;\nexport const multiOutputTemplate = ({ typeName }) =>\n  `type ${multiOutputType(typeName)}{\n  ${multiReturnProperty}: [${typeName}]\n  totalCount: Int\n}`;\n\n/* ------------------------------------- Query Queries ------------------------------------- */\n\n/*\n\nSingle query used on the client\n\nquery singleMovieQuery($input: SingleMovieInput) {\n  movie(input: $input) {\n    result {\n      _id\n      name\n      __typename\n    }\n    __typename\n  }\n}\n\n*/\n// TODO: with hooks, extraQueries becomes less necessary?\nexport const singleClientTemplate = ({ typeName, fragmentName, extraQueries }) =>\n  `query ${singleQueryType(typeName)}($input: ${singleInputType(typeName, true)}) {\n  ${singleQueryType(typeName)}(input: $input) {\n    ${singleReturnProperty} {\n      ...${fragmentName}\n    }\n    __typename\n  }\n  ${extraQueries ? extraQueries : ''}\n}`;\n\n/*\n\nMulti query used on the client\n\nmutation multiMovieQuery($input: MultiMovieInput) {\n  movies(input: $input) {\n    results {\n      _id\n      name\n      __typename\n    }\n    totalCount\n    __typename\n  }\n}\n\n*/\nexport const multiClientTemplate = ({ typeName, fragmentName, extraQueries }) =>\n  `query ${multiQueryType(typeName)}($input: ${multiInputType(typeName, false)}) {\n  ${multiQueryType(typeName)}(input: $input) {\n    ${multiReturnProperty} {\n      ...${fragmentName}\n    }\n    totalCount\n    __typename\n  }\n  ${extraQueries ? extraQueries : ''}\n}`;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/graphql_templates/types.js",
    "content": "export const convertToGraphQL = (fields, indentation) => {\n  return fields.length > 0 ? fields.map(f => fieldTemplate(f, indentation)).join('\\n') : '';\n};\n\nexport const arrayToGraphQL = fields => fields.map(f => `${f.name}: ${f.type}`).join(', ');\n\n/*\n\nFor backwards-compatibility reasons, args can either be a string or an array of objects\n\n*/\nexport const getArguments = args => {\n  if (Array.isArray(args) && args.length > 0) {\n    return `(${arrayToGraphQL(args)})`;\n  } else if (typeof args === 'string') {\n    return `(${args})`;\n  } else {\n    return '';\n  }\n};\n\n/* ------------------------------------- Generic Field Template ------------------------------------- */\n\n// export const fieldTemplate = ({ name, type, args, directive, description, required }, indentation = '') =>\n// `${description ?  `${indentation}# ${description}\\n` : ''}${indentation}${name}${getArguments(args)}: ${type}${required ? '!' : ''} ${directive ? directive : ''}`;\n\n// version that does not make any fields required\nexport const fieldTemplate = ({ name, type, args, directive, description, required }, indentation = '') =>\n  `${description ? `${indentation}# ${description}\\n` : ''}${indentation}${name}${getArguments(args)}: ${type} ${\n    directive ? directive : ''\n  }`;\n\n/* ------------------------------------- Main Type ------------------------------------- */\n\n/*\n\nThe main type\n\ntype Movie{\n  _id: String\n  title: String\n  description: String\n  createdAt: Date\n}\n\n*/\nexport const mainTypeTemplate = ({ typeName, description, interfaces, fields }) =>\n  `${description ? `# ${description}` : ''}\ntype ${typeName} ${interfaces.length ? `implements ${interfaces.join(' & ')} ` : ''}{\n${convertToGraphQL(fields, '  ')}\n}\n`;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/handleOptions.js",
    "content": "/** Helpers to get values depending on name\n * E.g. retrieving a collection and its name when only one value is provided\n *\n */\n\nimport { getCollection } from './collections';\nimport { getFragment, getFragmentName } from './fragments';\n/**\n * Extract collectionName from collection\n * or collection from collectionName\n * @param {*} param0\n */\nexport const extractCollectionInfo = ({ collectionName, collection }) => {\n  if (!(collectionName || collection)) throw new Error('Please specify either collection or collectionName');\n  const _collectionName = collectionName || collection.options.collectionName;\n  const _collection = collection || getCollection(collectionName);\n  return { collection: _collection, collectionName: _collectionName };\n};\n/**\n * Extract fragmentName from fragment\n * or fragment from fragmentName\n */\nexport const extractFragmentInfo = ({ fragment, fragmentName }, collectionName) => {\n  if (!(fragment || fragmentName || collectionName))\n    throw new Error('Please specify either fragment or fragmentName, or pass a collectionName');\n  if (fragment) {\n    return {\n      fragment,\n      fragmentName: fragmentName || getFragmentName(fragment)\n    };\n  } else {\n    const _fragmentName = fragmentName || `${collectionName}DefaultFragment`;\n    return {\n      fragment: getFragment(_fragmentName),\n      fragmentName: _fragmentName\n    };\n  }\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/headtags.js",
    "content": "export const Head = {\n  meta: [],\n  link: [],\n  script: [],\n  components: [],\n};\n\nexport const removeFromHeadTags = (type, name)=>{\n  Head[type] = Head[type].filter((tag)=>{\n    return (!tag.name || tag.name && tag.name !== name);\n  });\n\n  return Head;\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/icons.js",
    "content": "// TODO: get rid of this?\n\n/*\n\nUtilities for displaying icons.\n\n*/\n\nimport { Utils } from './utils.js';\n\n// ------------------------------ Dynamic Icons ------------------------------ //\n\n/**\n * @summary Take an icon name (such as \"open\") and return the HTML code to display the icon\n * @param {string} iconName - the name of the icon\n * @param {string} [iconClass] - an optional class to assign to the icon\n */\nUtils.getIcon = function (iconName, iconClass) {\n  var icons = Utils.icons;\n  var iconCode = !!icons[iconName] ? icons[iconName] : iconName;\n  iconClass = (typeof iconClass === 'string') ? ' '+iconClass : '';\n  return '<i class=\"icon fa fa-fw fa-' + iconCode + ' icon-' + iconName + iconClass+ '\" aria-hidden=\"true\"></i>';\n};\n\n/**\n * @summary A directory of icon keys and icon codes\n */\nUtils.icons = {\n  expand: 'angle-right',\n  collapse: 'angle-down',\n  next: 'angle-right',\n  close: 'times',\n  upvote: 'chevron-up',\n  voted: 'check',\n  downvote: 'chevron-down',\n  facebook: 'facebook-square',\n  twitter: 'twitter',\n  googleplus: 'google-plus',\n  linkedin: 'linkedin-square',\n  comment: 'comment-o',\n  share: 'share-square-o',\n  more: 'ellipsis-h',\n  menu: 'bars',\n  subscribe: 'envelope-o',\n  delete: 'trash-o',\n  edit: 'pencil',\n  popularity: 'fire',\n  time: 'clock-o',\n  best: 'star',\n  search: 'search',\n  approve: 'check-circle-o',\n  reject: 'times-circle-o',\n  views: 'eye',\n  clicks: 'mouse-pointer', \n  score: 'line-chart',\n  reply: 'reply',\n  spinner: 'spinner',\n  new: 'plus',\n  user: 'user',\n  like: 'heart',\n  image: 'picture-o',\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/index.js",
    "content": "// import './utils.js';\n// import './callbacks.js';\n// import './settings.js';\n// import './collections.js';\nimport './deep.js';\nimport './deep_extend.js';\n// import './intl_polyfill.js';\n// import './graphql.js';\nimport './icons.js';\n\nexport * from './config';\nexport * from './graphql/';\nexport * from './graphql_templates/index.js';\nexport * from './components.js';\nexport * from './collections.js';\nexport * from './callbacks.js';\nexport * from './routes.js';\nexport * from './utils.js';\nexport * from './settings.js';\nexport * from './headtags.js';\nexport * from './fragments.js';\nexport * from './apollo-common';\nexport * from './dynamic_loader.js';\nexport * from './admin.js';\nexport * from './fragment_matcher.js';\nexport * from './debug.js';\nexport * from './startup.js';\nexport * from './errors.js';\nexport * from './intl.js';\nexport * from './validation.js';\nexport * from './handleOptions.js';\nexport * from './ui_utils.js';\nexport * from './schema_utils.js';\nexport * from './simpleSchema_utils.js';\n// export * from './resolvers.js';\nexport * from './random_id.js';\nexport * from './mongoParams';\nexport * from './reactive-state.js';\nexport * from './compose.js';\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/intl.js",
    "content": "import React from 'react';\nimport SimpleSchema from 'simpl-schema';\nimport { getSetting } from './settings';\nimport { debug, Utils } from 'meteor/vulcan:lib';\n\nexport const defaultLocale = getSetting('locale', 'en-US');\n\nexport const Strings = {};\n\nexport const Domains = {};\n\nexport const addStrings = (localeId, strings) => {\n  if (typeof Strings[localeId] === 'undefined') {\n    Strings[localeId] = {};\n  }\n  Strings[localeId] = {\n    ...Strings[localeId],\n    ...strings,\n  };\n};\n\nexport const getString = ({ id, values, defaultMessage, messages, locale }) => {\n  let message = '';\n\n  if (messages && messages[id]) {\n    // first, look in messages object passed through arguments\n    // note: if defined, messages should also contain Strings[locale]\n    message = messages[id];\n  } else if (Strings[locale] && Strings[locale][id]) {\n    // then look in bundled Strings object\n    message = Strings[locale][id];\n  } else if (Strings[defaultLocale] && Strings[defaultLocale][id]) {\n    // debug(`\\x1b[32m>> INTL: No string found for id \"${id}\" in locale \"${locale}\", using defaultLocale \"${defaultLocale}\".\\x1b[0m`);\n    message = Strings[defaultLocale] && Strings[defaultLocale][id];\n  } else if (defaultMessage) {\n    // debug(`\\x1b[32m>> INTL: No string found for id \"${id}\" in locale \"${locale}\", using default message \"${defaultMessage}\".\\x1b[0m`);\n    message = defaultMessage;\n  }\n\n  if (values && typeof values === 'object' && typeof message === 'string') {\n    message = pluralizeString(message, values);\n    message = substituteStringValues(message, values);\n  }\n\n  return message;\n};\n\nexport const getStrings = localeId => {\n  return Strings[localeId];\n};\n\n/**\n * Pluralize a string using [ICU Message syntax used by react-intl](https://formatjs.io/docs/core-concepts/icu-syntax/#plural-format).\n * Note: `few` and `many` categories are not supported.\n *\n * @param {string} message\n * @param {object} values\n * @return {string}\n */\nexport const pluralizeString = (message, values) => {\n  const results = message.match(/{[^,]+, plural, .+?}}/g);\n  if (!results || !values) {\n    return message;\n  }\n\n  let pluralizedMessage = message;\n\n  for (let result of results) {\n    const parts = result.replace(/^{|}$/g, '').split(', ');\n    const key = parts[0];\n    const value = values[key];\n    const matches = parts[2].replace(/}$/, '').split('} ');\n    let translation;\n    for (const match of matches) {\n      const category = match.split(' {')[0];\n      if (\n        (category === 'zero' && value === 0) ||\n        (category === 'one' && value === 1) ||\n        (category === 'two' && value === 2) ||\n        (category.startsWith('=') && parseInt(category.replace(/^=/, '')) === value) ||\n        category === 'other'\n      ) {\n        const phrase = match.split(' {')[1];\n        translation = phrase.replace('#', value);\n        break;\n      }\n    }\n    pluralizedMessage = pluralizedMessage.replace(result, translation);\n  }\n\n  return pluralizedMessage;\n};\n\n/**\n * Substitute values in a message using [react-intl Simple Argument syntax](https://formatjs.io/docs/core-concepts/icu-syntax/#simple-argument)\n *\n * @param {string} message\n * @param {object} values Object with keys that may contain string, number, and React Node values\n * @return {string|React.ReactNodeArray} If `values` only contains string and/or number values, a string is returned,\n * otherwise an array of React Nodes is returned; both types of results can be used in the same way in a .jsx file\n */\nexport const substituteStringValues = (message, values) => {\n  let messageArray = [message];\n\n  Object.keys(values).forEach(key => {\n    const value = values[key];\n    messageArray = messageArray.reduce((accumulator, message) => {\n      if (typeof message !== 'string') {\n        // if this message array element is not a string, pass it on without substituting values\n        accumulator.push(message);\n      } else if (typeof value === 'string' || typeof value === 'number') {\n        // if this value is a string or a number, substitute it\n        accumulator.push(message.replaceAll(`{${key}}`, value));\n      } else {\n        // if this value is a node, break this message array element into three parts:\n        // 1) the text before the pattern; 2) the React Node; 3) the text after the pattern\n        const parts = message.split(new RegExp(`{${key}}`, 'g'));\n        parts.forEach((part, index, array) => {\n          accumulator.push(part);\n          if (index < array.length - 1) {\n            accumulator.push(value);\n          }\n        });\n      }\n      return accumulator;\n    }, []);\n  });\n\n  if (messageArray.length === 1) {\n    // if there is only one array element, it's just a simple string\n    messageArray = messageArray[0];\n  } else {\n    // filter out empty array elements\n    messageArray = messageArray.reduce((accumulator, message, index) => {\n      if (typeof message === 'string' && message.length) {\n        // pass on non-empty string elements\n        accumulator.push(message);\n      } else if (!!message) {\n        // pass on node elements augmented with a `key` prop (required for node arrays)\n        accumulator.push(React.cloneElement(message, { key: index }));\n      }\n      return accumulator;\n    }, []);\n  }\n\n  return messageArray;\n};\n\nexport const registerDomain = (locale, domain) => {\n  Domains[domain] = locale;\n};\n\nexport const Locales = [];\n\nexport const registerLocale = locale => {\n  Locales.push(locale);\n};\n\n// TODO: add support for dynamically loaded locales here\nexport const getLocale = (localeId) => {\n  const locales = Locales;\n  return locales.find(locale => locale.id === localeId);\n};\n\n/*\n\nHelper to detect current browser locale\n\n*/\nexport const detectLocale = () => {\n  let lang;\n\n  if (typeof navigator === 'undefined') {\n    return null;\n  }\n\n  if (navigator.languages && navigator.languages.length) {\n    // latest versions of Chrome and Firefox set this correctly\n    lang = navigator.languages[0];\n  } else if (navigator.userLanguage) {\n    // IE only\n    lang = navigator.userLanguage;\n  } else {\n    // latest versions of Chrome, Firefox, and Safari set this correctly\n    lang = navigator.language;\n  }\n\n  return lang;\n};\n\n/*\n\nFigure out the correct locale to use based on the current user, cookies,\nand browser settings\n\n*/\nexport const initLocale = ({ currentUser = {}, cookies = {}, locale }) => {\n  let userLocaleId = '';\n  let localeMethod = '';\n  const detectedLocale = detectLocale();\n\n  if (locale) {\n    // 1. locale is passed from AppGenerator through SSR process\n    userLocaleId = locale;\n    localeMethod = 'SSR';\n  } else if (cookies.locale) {\n    // 2. look for a cookie\n    userLocaleId = cookies.locale;\n    localeMethod = 'cookie';\n  } else if (currentUser && currentUser.locale) {\n    // 3. if user is logged in, check for their preferred locale\n    userLocaleId = currentUser.locale;\n    localeMethod = 'user';\n  } else if (detectedLocale) {\n    // 4. else, check for browser settings\n    userLocaleId = detectedLocale;\n    localeMethod = 'browser';\n  }\n\n  /*\n\n  NOTE: locale fallback doesn't work anymore because we can now load locales dynamically\n  and Strings[userLocale] will then be empty\n\n  */\n  // if user locale is available, use it; else compare first two chars\n  // of user locale with first two chars of available locales\n  // const availableLocales = Object.keys(Strings);\n  // const availableLocale = Strings[userLocale] ? userLocale : availableLocales.find(locale => locale.slice(0, 2) === userLocale.slice(0, 2));\n\n  const validLocale = getValidLocale(userLocaleId);\n\n  // 4. if user-defined locale is available, use it; else default to setting or `en-US`\n  if (validLocale) {\n    return { id: validLocale.id, originalId: userLocaleId, method: localeMethod };\n  } else {\n    return { id: getSetting('locale', 'en-US'), originalId: userLocaleId, method: 'setting' };\n  }\n};\n\n/*\n\nFind best matching locale\n\nen-US -> en-US\nen-us -> en-US\nen-gb -> en-US\netc.\n\n*/\nexport const truncateKey = key => key.split('-')[0];\n\nexport const getValidLocale = localeId => {\n  const validLocale = Locales.find(locale => {\n    const { id } = locale;\n    return id.toLowerCase() === localeId.toLowerCase() || truncateKey(id) === truncateKey(localeId);\n  });\n  return validLocale;\n};\n\n/*\n\nLook for type name in a few different places\nNote: look into simplifying this\n\n*/\nexport const isIntlField = fieldSchema => !!fieldSchema.intl;\n\n/*\n\nLook for type name in a few different places\nNote: look into simplifying this\n\n*/\nexport const isIntlDataField = fieldSchema => !!fieldSchema.isIntlData;\n\n/*\n\nCheck if a schema already has a corresponding intl field\n\n*/\nexport const schemaHasIntlField = (schema, fieldName) => !!schema[`${fieldName}_intl`];\n\n/*\n\nGenerate custom IntlString SimpleSchema type\n\n*/\nexport const getIntlString = () => {\n  const schema = {\n    locale: {\n      type: String,\n      optional: true,\n    },\n    value: {\n      type: String,\n      optional: true,\n    },\n  };\n\n  const IntlString = new SimpleSchema(schema);\n  IntlString.name = 'IntlString';\n  return IntlString;\n};\n\n/*\n\nCheck if a schema has at least one intl field\n\n*/\nexport const schemaHasIntlFields = schema => Object.keys(schema).some(fieldName => isIntlField(schema[fieldName]));\n\n/*\n\nCustom validation function to check for required locales\n\nSee https://github.com/aldeed/simple-schema-js#custom-field-validation\n\n*/\nexport const validateIntlField = function() {\n  let errors = [];\n\n  // go through locales to check which one are required\n  const requiredLocales = Locales.filter(locale => locale.required);\n\n  requiredLocales.forEach((locale, index) => {\n    const strings = this.value;\n    const hasString = strings && Array.isArray(strings) && strings.some(s => s && s.locale === locale.id && s.value);\n    if (!hasString) {\n      const originalFieldName = this.key.replace('_intl', '');\n      errors.push({\n        id: 'errors.required',\n        path: `${this.key}.${index}`,\n        properties: { name: originalFieldName, locale: locale.id },\n      });\n    }\n  });\n\n  if (errors.length > 0) {\n    // hack to work around the fact that custom validation function can only return a single string\n    return `intlError|${JSON.stringify(errors)}`;\n  }\n};\n\n/*\n\nGet an array of intl keys to try for a field\n\n*/\nexport const getIntlKeys = ({ fieldName, collectionName, schema }) => {\n  const fieldSchema = (schema && schema[fieldName]) || {};\n\n  const { intlId } = fieldSchema;\n\n  const intlKeys = [];\n  if (intlId) {\n    intlKeys.push(intlId);\n  }\n  if (collectionName) {\n    intlKeys.push(`${collectionName.toLowerCase()}.${fieldName}`);\n  }\n  intlKeys.push(`global.${fieldName}`);\n  intlKeys.push(fieldName);\n\n  return intlKeys;\n};\n\n/**\n * getIntlLabel - Get a label for a field, for a given collection, in the current language.\n * The evaluation is as follows :\n * i18n(intlId) >\n * i18n(collectionName.fieldName) >\n * i18n(global.fieldName) >\n * i18n(fieldName)\n *\n * @param  {object} params\n * @param  {object} params.intl               An intlShape object obtained from the react context for example\n * @param  {string} params.fieldName          The name of the field to evaluate (required)\n * @param  {string} params.collectionName     The name of the collection the field belongs to\n * @param  {object} params.schema             The schema of the collection\n * @param  {object} values                    The values to pass to format the i18n string\n * @return {string}                           The translated label\n */\nexport const getIntlLabel = ({ intl, fieldName, collectionName, schema, isDescription }, values) => {\n  if (!fieldName) {\n    throw new Error('fieldName option passed to formatLabel cannot be empty or undefined');\n  }\n\n  // if this is a description, just add .description at the end of the intl key\n  const suffix = isDescription ? '.description' : '';\n\n  const intlKeys = getIntlKeys({ fieldName, collectionName, schema });\n\n  let intlLabel;\n\n  for (const intlKey of intlKeys) {\n    const intlString = intl.formatMessage({ id: intlKey + suffix }, values);\n\n    if (intlString !== '') {\n      intlLabel = intlString;\n      break;\n    }\n  }\n  return intlLabel;\n};\n\n/*\n\nGet intl label or fallback\n\n*/\nexport const formatLabel = (options, values) => {\n  const { fieldName, schema } = options;\n  const fieldSchema = (schema && schema[fieldName]) || {};\n  const { label: schemaLabel } = fieldSchema;\n  return getIntlLabel(options, values) || schemaLabel || Utils.camelToSpaces(fieldName);\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/intl_polyfill.js",
    "content": "/*\n\nintl polyfill. See https://github.com/andyearnshaw/Intl.js/\n\n*/\n\nimport { getSetting } from './settings.js';\n\nimport areIntlLocalesSupported from 'intl-locales-supported'\n\nvar localesMyAppSupports = [\n  getSetting('locale', 'en-US')\n];\n\nif (global.Intl) {\n  // Determine if the built-in `Intl` has the locale data we need.\n  if (!areIntlLocalesSupported(localesMyAppSupports)) {\n    // `Intl` exists, but it doesn't have the data we need, so load the\n    // polyfill and replace the constructors with need with the polyfill's.\n    var IntlPolyfill = require('intl');\n    Intl.NumberFormat   = IntlPolyfill.NumberFormat;\n    Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;\n  }\n} else {\n  // No `Intl`, so use and load the polyfill.\n  global.Intl = require('intl');\n}"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/mongoParams.js",
    "content": "/**\n * Converts selector and options to Mongo parameters (selector, fields)\n */\nimport mapValues from 'lodash/mapValues';\nimport uniq from 'lodash/uniq';\nimport isEmpty from 'lodash/isEmpty';\nimport escapeStringRegexp from 'escape-string-regexp';\nimport merge from 'lodash/merge';\nimport { Utils } from './utils';\n\nimport { getSetting } from './settings.js';\n// convert GraphQL selector into Mongo-compatible selector\n// TODO: add support for more than just documentId/_id and slug, potentially making conversion unnecessary\n// see https://github.com/VulcanJS/Vulcan/issues/2000\nexport const convertSelector = selector => {\n  return selector;\n};\nexport const convertUniqueSelector = selector => {\n  if (selector.documentId) {\n    selector._id = selector.documentId;\n    delete selector.documentId;\n  }\n  return selector;\n};\n\n// see https://stackoverflow.com/a/3561711\nexport const escapeRegex = s => s.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&');\n\n/*\n\nFiltering\n\nNote: we use $elemMatch syntax for consistency so that we can be sure that every mongo operator function\nreturns an object.\n\n*/\nconst conversionTable = {\n  _eq: '$eq',\n  _gt: '$gt',\n  _gte: '$gte',\n  _in: '$in',\n  _lt: '$lt',\n  _lte: '$lte',\n  _neq: '$ne',\n  _nin: '$nin',\n  _is_null: value => ({ $exists: !value }),\n  _is: value => ({ $elemMatch: { $eq: value } }),\n  _contains: value => ({ $elemMatch: { $eq: value } }),\n  _contains_all: '$all',\n  asc: 1,\n  desc: -1,\n  _like: value => ({\n    $regex: escapeRegex(value),\n    $options: 'i',\n  }),\n};\n\n// get all fields mentioned in an expression like [ { foo: { _gt: 2 } }, { bar: { _eq : 3 } } ]\nconst getFieldNames = expressionArray => {\n  return expressionArray.map(exp => {\n    const [fieldName] = Object.keys(exp);\n    return fieldName;\n  });\n};\n\nexport const filterFunction = async (collection, input = {}, context) => {\n  // eslint-disable-next-line no-unused-vars\n  const { filter, limit, sort, search, filterArguments, offset, id } = input;\n  let selector = {};\n  let options = {\n    sort: {},\n  };\n  let filteredFields = [];\n\n  const schema = collection.simpleSchema()._schema;\n\n  /*\n\n    Convert GraphQL expression into MongoDB expression, for example\n\n    { fieldName: { operator: value } }\n\n    { title: { _in: [\"foo\", \"bar\"] } }\n\n    to:\n\n    { title: { $in: [\"foo\", \"bar\"] } }\n\n    or (intl fields):\n\n    { title_intl.value: { $in: [\"foo\", \"bar\"] } }\n\n    */\n  const convertExpression = fieldExpression => {\n    const [fieldName] = Object.keys(fieldExpression);\n    const operators = Object.keys(fieldExpression[fieldName]);\n    const mongoExpression = {};\n    operators.forEach(operator => {\n      const value = fieldExpression[fieldName][operator];\n      if (Utils.isEmptyOrUndefined(value)) {\n        throw new Error(`Detected empty filter value for field “${fieldName}” with operator “${operator}”`);\n      }\n      const mongoOperator = conversionTable[operator];\n      if (!mongoOperator) {\n        throw new Error(`Operator ${operator} is not valid. Possible operators are: ${Object.keys(conversionTable)}`);\n      }\n      const mongoObject = typeof mongoOperator === 'function' ? mongoOperator(value) : { [mongoOperator]: value };\n      merge(mongoExpression, mongoObject);\n    });\n    const isIntl = schema[fieldName].intl;\n    const mongoFieldName = isIntl ? `${fieldName}_intl.value` : fieldName;\n    return { [mongoFieldName]: mongoExpression };\n  };\n\n  // id\n  if (id) {\n    selector = { _id: id };\n  }\n\n  // filter\n  if (!isEmpty(filter)) {\n    Object.keys(filter).forEach(fieldName => {\n      switch (fieldName) {\n        case '_and':\n          filteredFields = filteredFields.concat(getFieldNames(filter._and));\n          selector['$and'] = filter._and.map(convertExpression);\n          break;\n\n        case '_or':\n          filteredFields = filteredFields.concat(getFieldNames(filter._or));\n          selector['$or'] = filter._or.map(convertExpression);\n          break;\n\n        case '_not':\n          filteredFields = filteredFields.concat(getFieldNames(filter._not));\n          selector['$not'] = filter._not.map(convertExpression);\n          break;\n\n        case 'search':\n          break;\n\n        default:\n          const customFilters = collection.options.customFilters;\n          const customFilter = customFilters && customFilters.find(f => f.name === fieldName);\n          if (customFilter) {\n            // field is not actually a field, but a custom filter\n            const filterArguments = filter[customFilter.name];\n            // TODO: make this work with await\n            const filterObject = customFilter.filter({\n              input,\n              context,\n              filterArguments,\n            });\n            selector = merge({}, selector, filterObject.selector);\n            options = merge({}, options, filterObject.options);\n          } else {\n            // regular field\n            filteredFields.push(fieldName);\n            selector = { ...selector, ...convertExpression({ [fieldName]: filter[fieldName] }) };\n          }\n          break;\n      }\n    });\n  }\n\n  // sort\n  if (!isEmpty(sort)) {\n    options.sort = merge(\n      {},\n      options.sort,\n      mapValues(sort, order => {\n        const mongoOrder = conversionTable[order];\n        if (!order) {\n          throw new Error(`Operator ${order} is not valid. Possible operators: asc, desc`);\n        }\n        return mongoOrder;\n      })\n    );\n  } else {\n    options.sort = { createdAt: -1 }; // reliable default order\n  }\n\n  // search\n  if (!isEmpty(search)) {\n    const searchQuery = escapeStringRegexp(search);\n    const searchableFieldNames = Object.keys(schema).filter(\n      // do not include intl fields here\n      fieldName => !fieldName.includes('_intl') && schema[fieldName].searchable\n    );\n    if (searchableFieldNames.length) {\n      selector = {\n        ...selector,\n        $or: searchableFieldNames.map(fieldName => {\n          const isIntl = schema[fieldName].intl;\n          return {\n            [isIntl ? `${fieldName}_intl.value` : fieldName]: {\n              $regex: searchQuery,\n              $options: 'i',\n            },\n          };\n        }),\n      };\n    } else {\n      // eslint-disable-next-line no-console\n      console.warn(\n        `Warning: search argument is set but schema ${\n          collection.options.collectionName\n        } has no searchable field. Set \"searchable: true\" for at least one field to enable search.`\n      );\n    }\n  }\n\n  // limit\n  const maxLimit = getSetting('maxDocumentsPerRequest', 1000);\n  options.limit = limit ? Math.min(limit, maxLimit) : maxLimit;\n\n  // offest\n  if (offset) {\n    options.skip = offset;\n  }\n\n  // console.log('// collection');\n  // console.log(collection.options.collectionName);\n  // console.log('// input');\n  // console.log(JSON.stringify(input, 2));\n  // console.log('// selector');\n  // console.log(JSON.stringify(selector, 2));\n  // console.log('// options');\n  // console.log(JSON.stringify(options, 2));\n  // console.log('// filterFields');\n  // console.log(uniq(filteredFields));\n\n  return {\n    selector,\n    options,\n    filteredFields: uniq(filteredFields),\n  };\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/mongo_redux.js",
    "content": "// TODO: get rid of this?\n\nimport Mingo from 'mingo';\n\nMongo.Collection.prototype.findInStore = function (store, selector = {}, options = {}) {\n  const typeName = this.options && this.options.typeName;\n  const docs = _.where(store.getState().apollo.data, {__typename: typeName});\n  \n  const mingoQuery = new Mingo.Query(selector);\n\n  const cursor = mingoQuery.find(docs);\n  const sortedDocs = cursor.sort(options.sort).all();\n\n  // console.log('// findRedux')\n  // console.log(\"typeName: \", typeName)\n  // console.log(\"selector: \", selector)\n  // console.log(\"options: \", options)\n  // console.log(\"all docs: \", docs)\n  // console.log(\"selected docs: \", cursor.all())\n  // console.log(\"sorted docs: \", cursor.sort(options.sort).all())\n\n  return {fetch: () => sortedDocs};\n};\n\nMongo.Collection.prototype.findOneInStore = function (store, _idOrObject) {\n  const docs = typeof _idOrObject === 'string' ? this.findInStore(store, {_id: _idOrObject}).fetch() : this.findInStore(store, _idOrObject).fetch();\n  return docs.length === 0 ? undefined: docs[0];\n};"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/random_id.js",
    "content": "export const Random = {};\nimport range from 'lodash/range';\nimport sample from 'lodash/sample';\n\nRandom.id = function(length = 17) {\n  const chars = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz';\n  return range(length)\n    .map(() => sample(chars))\n    .join('');\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/reactive-state.js",
    "content": "/**\n * Simple state management based on Apollo Client reactive variables.\n * @see {@link https://www.apollographql.com/docs/react/local-state/reactive-variables/}\n * Use it to store session data that survives re-renders and router transitions, unlike component state.\n * Register multiple scalar or object states with optional SimpleSchemas for cleaning and validation.\n *\n * @module reactive-state\n */\n\n\nimport {createSchema} from './schema_utils';\nimport {makeVar} from '@apollo/client';\n// eslint-disable-next-line no-unused-vars\nimport SimpleSchema from 'simpl-schema';\nimport _forOwn from 'lodash/forOwn';\n\n\nconst reactiveStates = {};\n\n\n/**\n * An object for storing global state based on Apollo Client reactive variables\n * @typedef {function} ReactiveState\n * @property {string} stateKey - The name/id/key of the state\n * @property {SimpleSchema} [schema] - Optional schema\n * @property {*} [defaultValue] - Optional default value\n * @property {function} reactiveVar - The reactive variable\n */\n\n\n/**\n * Create a new reactive state\n * @param {string} stateKey The name/id/key for the new reactive state\n * @param {Object|SimpleSchema} [schema] Optional schema definition object that will be converted to `SimpleSchema`\n *   using `createSchema()`\n * @param {*} [defaultValue] Optional default value; alternatively you can define `defaultValue`s in the schema\n * @param {boolean} [skipDuplicate] If you try to create a reactive state with a key that's already used, an exception\n *   will be thrown; use this option to prevent the exception and use the existing state without changing it\n * @returns {ReactiveState} Returns the newly created state object\n * @throws Will throw an error if there is already a reactive state with the given key - unless `skipDuplicates` is `true`\n */\nexport const createReactiveState = ({stateKey, schema, defaultValue, skipDuplicate}) => {\n  if (reactiveStates[stateKey]) {\n    if (skipDuplicate) return reactiveStates[stateKey];\n    throw new Error(`There is already a reactive state named ${stateKey}`);\n  }\n\n  if (schema) {\n    schema = createSchema(schema);\n    defaultValue = cleanReactiveStateValue(defaultValue || {}, schema);\n  }\n\n  const reactiveVar = makeVar(defaultValue);\n\n  const reactiveState = function (updates) {\n    let value = reactiveVar();\n    if (arguments.length > 0) {\n      if (typeof updates === 'function') {\n        value = updates(value);\n      } else if (typeof value === 'object' && typeof updates === 'object') {\n        value = Object.assign({}, value, updates);\n      } else if (value === null) {\n        value = defaultValue;\n      } else {\n        value = updates;\n      }\n      value = cleanReactiveStateValue(value, schema);\n      value = reactiveVar(value);\n    }\n    return value;\n  };\n  reactiveState.stateKey = stateKey;\n  reactiveState.schema = schema;\n  reactiveState.defaultValue = defaultValue;\n  reactiveState.reactiveVar = reactiveVar;\n\n  reactiveStates[stateKey] = reactiveState;\n\n  return reactiveState;\n};\n\n\n/**\n * Return a reactive state previously created\n * @param {string} stateKey The key of the desired reactive state\n * @returns {ReactiveState}\n * @throws Will throw an error if there is no reactive state with the given key\n */\nexport const getReactiveState = (stateKey) => {\n  const stateObject = reactiveStates[stateKey];\n  if (!stateObject) {\n    throw new Error(`There is no reactive state with stateKey ${stateKey}`);\n  }\n\n  return stateObject;\n};\n\n\n/**\n * Given a value to be stored in state, this functions clones, cleans and validates it\n * @param {Object} value The value object\n * @param {SimpleSchema} [schema] Optional schema for validation\n * @returns {Object} The cleaned value\n */\nexport const cleanReactiveStateValue = (value, schema) => {\n  if (typeof value === 'object') {\n    value = {...value};\n    if (schema) {\n      value = schema.clean(value);\n      schema.validate(value);\n    }\n  }\n\n  return value;\n};\n\n\n/**\n * Resets the value of all reactive states to their defaults\n */\nexport const resetReactiveState = () => {\n  _forOwn(reactiveStates, function (stateObject, stateKey) {\n    stateObject(null);\n  });\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/routes.js",
    "content": "import { Components, getComponent } from './components';\n\nexport const Routes = {}; // will be populated on startup \nexport const RoutesTable = {}; // storage for infos about routes themselves\n\n/*\n A route is defined in the list like:\n RoutesTable.foobar = {\n name: 'foobar',\n path: '/xyz',\n component: getComponent('FooBar')\n componentName: 'FooBar' // optional\n }\n\n if there there is value for parentRouteName it will look for the route and add the new route as a child of it\n */\nexport const addRoute = (routeOrRouteArray, options = {}) => {\n  const { parentRouteName, defaultLayoutComponent } = options;\n\n  // be sure to have an array of routes to manipulate\n  const addedRoutes = Array.isArray(routeOrRouteArray) ? routeOrRouteArray : [routeOrRouteArray];\n\n  // if there is a value for parentRouteName you are adding this route as new child\n  if (parentRouteName) {\n    addAsChildRoute(parentRouteName, addedRoutes, options);\n  } else {\n    // modify the routes table with the new routes\n    addedRoutes.map(({ name, path, ...properties }) => {\n      // check if there is already a route registered to this path\n      const routeWithSamePath = _.findWhere(RoutesTable, { path });\n\n      if (routeWithSamePath) {\n        // delete the route registered with same path\n        delete RoutesTable[routeWithSamePath.name];\n      }\n\n      const routeObject = {\n        name,\n        path,\n        ...properties,\n      };\n\n      if (defaultLayoutComponent && !routeObject.layoutComponent) {\n        routeObject.layoutComponent = defaultLayoutComponent;\n      }\n\n      // register the new route\n      RoutesTable[name] = routeObject;\n    });\n  }\n};\n\nexport const extendRoute = (routeName, routeProps) => {\n\n  const route = _.findWhere(RoutesTable, { name: routeName });\n\n  if (route) {\n    RoutesTable[route.name] = {\n      ...route,\n      ...routeProps\n    };\n  }\n};\n\n\n/**\n A route is defined in the list like: (same as above)\n RoutesTable.foobar = {\n name: 'foobar',\n path: '/xyz',\n component: getComponent('FooBar')\n componentName: 'FooBar' // optional\n }\n\n NOTE: This is implemented on single level deep ONLY for now\n **/\n\n\nexport const addAsChildRoute = (parentRouteName, addedRoutes) => {\n\n  // if the parentRouteName does not exist, error\n  if (!RoutesTable[parentRouteName]) {\n    throw new Error(`Route ${parentRouteName} doesn't exist`);\n  }\n\n  // modify the routes table with the new routes\n  addedRoutes.map(({ name, path, ...properties }) => {\n\n    // get the current child routes for this Route\n    const childRoutes = RoutesTable[parentRouteName]['childRoutes'] || [];\n\n    // check if there is already a route registered to this path\n    const [routeWithSamePath] = _.filter(childRoutes, route => route.path === path);\n\n    if (routeWithSamePath) {\n      // delete the route registered with same path\n      delete childRoutes[routeWithSamePath.name];\n    }\n\n    // append to the child routes the new route\n    childRoutes.push({\n      name,\n      path,\n      ...properties\n    });\n\n    // register the new child route (overwriting the current which is fine)\n    RoutesTable[parentRouteName]['childRoutes'] = childRoutes;\n\n  });\n};\n\n\nexport const getRoute = name => {\n  const routeDef = RoutesTable[name];\n\n  // components should be loaded by now (populateComponentsApp function), we can grab the component in the lookup table and assign it to the route\n  if (!routeDef.component && routeDef.componentName) {\n    routeDef.component = getComponent(routeDef.componentName);\n  }\n\n  return routeDef;\n};\n\nexport const getChildRoute = (name, index) => {\n  const routeDef = RoutesTable[name]['childRoutes'][index];\n\n  // components should be loaded by now (populateComponentsApp function), we can grab the component in the lookup table and assign it to the route\n  if (!routeDef.component && routeDef.componentName) {\n    routeDef.component = getComponent(routeDef.componentName);\n  }\n\n  return routeDef;\n};\n\n/**\n * Populate the lookup table for routes to be callable\n * ℹ️ Called once on app startup\n **/\nexport const populateRoutesApp = () => {\n  // loop over each component in the list\n  Object.keys(RoutesTable).map(name => {\n    // loop over child routes if available\n    if (typeof RoutesTable[name]['childRoutes'] !== typeof undefined) {\n      RoutesTable[name]['childRoutes'].map((item, index) => {\n        RoutesTable[name]['childRoutes'][index] = getChildRoute(name, index);\n      });\n    }\n\n    // populate an entry in the lookup table\n    Routes[name] = getRoute(name);\n\n    // uncomment for debug\n    // console.log('init route:', name);\n  });\n};\n\n// Should be used only in tests\nexport const emptyRoutes = () => {\n  Object.keys(Routes).map((key) => {\n    delete Routes[key];\n  });\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/routes.ts",
    "content": "import { Components, getComponent } from './components';\n\nexport type Route = {\n  name: string;\n  path: string;\n  componentName?: string,\n  layoutName?: string,\n}\n\nexport const Routes = new Map(); // will be populated on startup \nexport const RoutesTable = new Map(); // storage for infos about routes themselves\n\n/*\n A route is defined in the list like:\n RoutesTable.foobar = {\n name: 'foobar',\n path: '/xyz',\n component: getComponent('FooBar')\n componentName: 'FooBar' // optional\n }\n\n if there there is value for parentRouteName it will look for the route and add the new route as a child of it\n */\nexport const addRoute = (routeOrRouteArray: Route|Array<Route>, parentRouteName?: string) => {\n\n  // be sure to have an array of routes to manipulate\n  const addedRoutes = Array.isArray(routeOrRouteArray) ? routeOrRouteArray : [routeOrRouteArray];\n\n  // if there is a value for parentRouteName you are adding this route as new child\n  if (parentRouteName) {\n\n    addAsChildRoute(parentRouteName, addedRoutes);\n\n  } else {\n\n    // modify the routes table with the new routes\n    addedRoutes.forEach(({ name, path, ...properties }) => {\n\n      // check if there is already a route registered to this path\n      const routeWithSamePath = Object.values(RoutesTable).find(route => route.path === path);\n\n      if (routeWithSamePath) {\n        // delete the route registered with same path\n        delete RoutesTable[routeWithSamePath.name];\n      }\n\n      // register the new route\n      RoutesTable[name] = {\n        name,\n        path,\n        ...properties\n      };\n\n    });\n  }\n};\n\nexport const extendRoute = (routeName, routeProps) => {\n\n  const route = Object.values(RoutesTable).find(route => route.name === routeName);\n\n  if (route) {\n    RoutesTable[route.name] = {\n      ...route,\n      ...routeProps\n    };\n  }\n};\n\n\n/**\n A route is defined in the list like: (same as above)\n RoutesTable.foobar = {\n name: 'foobar',\n path: '/xyz',\n component: getComponent('FooBar')\n componentName: 'FooBar' // optional\n }\n\n NOTE: This is implemented on single level deep ONLY for now\n **/\n\n\nexport const addAsChildRoute = (parentRouteName, addedRoutes) => {\n\n  // if the parentRouteName does not exist, error\n  if (!RoutesTable[parentRouteName]) {\n    throw new Error(`Route ${parentRouteName} doesn't exist`);\n  }\n\n  // modify the routes table with the new routes\n  addedRoutes.map(({ name, path, ...properties }) => {\n\n    // get the current child routes for this Route\n    const childRoutes = RoutesTable[parentRouteName]['childRoutes'] || [];\n\n    // check if there is already a route registered to this path\n    const routeWithSamePath = childRoutes.find(route => route.path === path);\n\n    if (routeWithSamePath) {\n      // delete the route registered with same path\n      delete childRoutes[routeWithSamePath.name];\n    }\n\n    // append to the child routes the new route\n    childRoutes.push({\n      name,\n      path,\n      ...properties\n    });\n\n    // register the new child route (overwriting the current which is fine)\n    RoutesTable[parentRouteName]['childRoutes'] = childRoutes;\n\n  });\n};\n\n\nexport const getRoute = name => {\n  const routeDef = RoutesTable[name];\n\n  // components should be loaded by now (populateComponentsApp function), we can grab the component in the lookup table and assign it to the route\n  if (!routeDef.component && routeDef.componentName) {\n    routeDef.component = getComponent(routeDef.componentName);\n  }\n\n  return routeDef;\n};\n\nexport const getChildRoute = (name, index) => {\n  const routeDef = RoutesTable[name]['childRoutes'][index];\n\n  // components should be loaded by now (populateComponentsApp function), we can grab the component in the lookup table and assign it to the route\n  if (!routeDef.component && routeDef.componentName) {\n    routeDef.component = getComponent(routeDef.componentName);\n  }\n\n  return routeDef;\n};\n\n/**\n * Populate the lookup table for routes to be callable\n * ℹ️ Called once on app startup\n **/\nexport const populateRoutesApp = () => {\n  // loop over each component in the list\n  Object.keys(RoutesTable).map(name => {\n    // loop over child routes if available\n    if (typeof RoutesTable[name]['childRoutes'] !== typeof undefined) {\n      RoutesTable[name]['childRoutes'].map((item, index) => {\n        RoutesTable[name]['childRoutes'][index] = getChildRoute(name, index);\n      });\n    }\n\n    // populate an entry in the lookup table\n    Routes[name] = getRoute(name);\n\n    // uncomment for debug\n    // console.log('init route:', name);\n  });\n};\n\n// Should be used only in tests\nexport const emptyRoutes = () => {\n  Object.keys(Routes).map((key) => {\n    delete Routes[key];\n  });\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/schema_utils.js",
    "content": "import _reject from 'lodash/reject';\nimport _keys from 'lodash/keys';\nimport { Collections } from './collections.js';\nimport { getNestedSchema, getArrayChild, isBlackbox } from 'meteor/vulcan:lib/lib/modules/simpleSchema_utils';\nimport _isArray from 'lodash/isArray';\nimport _get from 'lodash/get';\nimport _isEmpty from 'lodash/isEmpty';\nimport _omit from 'lodash/omit';\nimport SimpleSchema from 'simpl-schema';\nimport moment from 'moment-timezone';\nimport { getSetting } from './settings';\n\nexport const formattedDateResolver = fieldName => {\n  return (document = {}, args = {}, context = {}) => {\n    const { format } = args;\n    const { timezone = getSetting('timezone') } = context;\n    if (!document[fieldName]) return;\n    let m = moment(document[fieldName]);\n    if (timezone) {\n      m = m.tz(timezone);\n    }\n    return format === 'ago' ? m.fromNow() : m.format(format);\n  };\n};\n\n// extract array items recursively\n// first level: foo.$; second level: foo.$.$; etc.\nexport const extractArrayItems = (schema, fieldName, arrayItem, level = 1) => {\n  const delimiter = '.$';\n  const key = fieldName + delimiter.repeat(level);\n  schema[key] = arrayItem;\n  if (arrayItem.arrayItem) {\n    extractArrayItems(schema, fieldName, arrayItem.arrayItem, ++level);\n  }\n};\n\nexport const createSchema = (schema, apiSchema = {}, dbSchema = {}) => {\n  let modifiedSchema = { ...schema };\n\n  Object.keys(modifiedSchema).forEach(fieldName => {\n    const field = schema[fieldName];\n    const { arrayItem, type, canRead } = field;\n\n    if (field.resolveAs) {\n      // backwards compatibility: copy resolveAs.type to resolveAs.typeName\n      if (!field.resolveAs.typeName) {\n        field.resolveAs.typeName = field.resolveAs.type;\n      }\n    }\n\n    if (field.relation) {\n      // for now, \"translate\" new relation field syntax into resolveAs\n      const { typeName, fieldName, kind } = field.relation;\n      field.resolveAs = {\n        typeName,\n        fieldName,\n        relation: kind,\n      };\n    }\n\n    // find any field with an `arrayItem` property defined and add corresponding\n    // `foo.$` array item field to schema\n    if (arrayItem) {\n      extractArrayItems(modifiedSchema, fieldName, arrayItem);\n    }\n    // if this is a date field, and field is readable, and fieldFormatted doesn't already exist in the schema\n    // or as a resolveAs field, then add fieldFormatted to apiSchema\n    const formattedFieldName = `${fieldName}Formatted`;\n\n    if (type === Date && canRead && !schema[formattedFieldName] && !(_get(field, 'resolveAs.fieldName', '') === formattedFieldName)) {\n      apiSchema[formattedFieldName] = {\n        typeName: 'String',\n        canRead,\n        arguments: 'format: String = \"YYYY/MM/DD\"',\n        resolver: formattedDateResolver(fieldName),\n      };\n    }\n  });\n\n  // if apiSchema contains fields, copy them over to main schema\n  if (!_isEmpty(apiSchema)) {\n    Object.keys(apiSchema).forEach(fieldName => {\n      const field = apiSchema[fieldName];\n      const { canRead = ['guests'], description, ...resolveAs } = field;\n      modifiedSchema[fieldName] = {\n        type: Object,\n        optional: true,\n        apiOnly: true,\n        canRead,\n        description,\n        resolveAs,\n      };\n    });\n  }\n\n  // for added security, remove any API-related permission checks from db fields\n  const filteredDbSchema = {};\n  const blacklistedFields = ['canRead', 'canCreate', 'canUpdate'];\n  Object.keys(dbSchema).forEach(dbFieldName => {\n    filteredDbSchema[dbFieldName] = _omit(dbSchema[dbFieldName], blacklistedFields);\n  });\n  // add dbSchema *after* doing the apiSchema stuff so we are sure\n  // its fields are not exposed through the GraphQL API\n  modifiedSchema = { ...modifiedSchema, ...filteredDbSchema };\n\n  return new SimpleSchema(modifiedSchema);\n};\n\n/* getters */\n// filter out fields with \".\" or \"$\"\nexport const getValidFields = schema => {\n  return Object.keys(schema).filter(fieldName => !fieldName.includes('$') && !fieldName.includes('.'));\n};\n\n// NOTE: this include fields that should'n't go into the default fragment (pure virtual fields and resolved fields)\n// use getFragmentFieldNames for fragments\nexport const getReadableFields = schema => {\n  // OpenCRUD backwards compatibility\n  return getValidFields(schema).filter(fieldName => schema[fieldName].canRead || schema[fieldName].viewableBy);\n};\n\nexport const getCreateableFields = schema => {\n  // OpenCRUD backwards compatibility\n  return getValidFields(schema).filter(fieldName => schema[fieldName].canCreate || schema[fieldName].insertableBy);\n};\n\nexport const getUpdateableFields = schema => {\n  // OpenCRUD backwards compatibility\n  return getValidFields(schema).filter(fieldName => schema[fieldName].canUpdate || schema[fieldName].editableBy);\n};\n\n/*\n\nTest if a schema non-nested  field should be added to the GraphQL schema or not.\nRule: we always add it except if:\n\n1. addOriginalField: false is specified in one or more resolveAs fields\n2. A resolveAs field has the same name as the main field (we don't want two fields with same name)\n3. A resolveAs field doesn't have a name (in which case it will take the name of the main field)\n\n*/\nexport const shouldAddOriginalField = (fieldName, field) => {\n  if (!field.resolveAs) return true;\n\n  const resolveAsArray = Array.isArray(field.resolveAs) ? field.resolveAs : [field.resolveAs];\n\n  const removeOriginalField = resolveAsArray.some(\n    resolveAs => resolveAs.addOriginalField === false || resolveAs.fieldName === fieldName || typeof resolveAs.fieldName === 'undefined'\n  );\n  return !removeOriginalField;\n};\n// list fields that can be included in the default fragment for a schema\nexport const getFragmentFieldNames = ({ schema, options }) =>\n  _reject(_keys(schema), fieldName => {\n    /*\n   \n    Exclude a field from the default fragment if\n    1. it has a resolver and original field should not be added\n    2. it has $ in its name\n    3. it's not viewable (if onlyViewable option is true)\n    4. it is not a reference type (typeName is defined for the field or an array child)\n    */\n    const field = schema[fieldName];\n\n    // OpenCRUD backwards compatibility\n    return (\n      (field.resolveAs && !shouldAddOriginalField(fieldName, field)) ||\n      fieldName.includes('$') ||\n      fieldName.includes('.') ||\n      (options.onlyViewable && !(field.canRead || field.viewableBy)) ||\n      field.typeName ||\n      (schema[`${fieldName}.$`] && schema[`${fieldName}.$`].typeName)\n    );\n  });\n\n/*\n\nCheck if a type corresponds to a collection or else \nis just a regular or custom scalar type.\n\n*/\nexport const isCollectionType = typeName =>\n  Collections.some(c => c.options.typeName === typeName || `[${c.options.typeName}]` === typeName);\n\n/**\n * Iterate over a document fields and run a callback with side effect\n * Works recursively for nested fields and arrays of objects (but excluding blackboxed objects, native JSON, and arrays of native values)\n * @param {*} document Current document\n * @param {*} schema Document schema\n * @param {*} callback Called on each field with the corresponding field schema, including fields of nested objects and arrays of nested object\n * @param {*} currentPath Global path of the document (to track recursive calls)\n * @param {*} isNested Differentiate nested fields\n */\nexport const forEachDocumentField = (document, schema, callback, currentPath = '') => {\n  if (!document) return;\n\n  Object.keys(document).forEach(fieldName => {\n    const fieldSchema = schema[fieldName];\n    callback({ fieldName, fieldSchema, currentPath, document, schema, isNested: !!currentPath });\n    // Check if we need a recursive call\n    if (!fieldSchema) return; // field has no corresponding schema, we are done\n    const value = document[fieldName];\n    if (!value) return;\n    // if value is an array, validate permissions for all children\n    if (_isArray(value)) {\n      const arrayChildField = getArrayChild(fieldName, schema);\n      if (arrayChildField) {\n        const arrayFieldSchema = getNestedSchema(arrayChildField);\n        // apply only if the field is an array of objects\n        if (arrayFieldSchema) {\n          value.forEach((item, idx) => {\n            forEachDocumentField(item, arrayFieldSchema, callback, `${currentPath}${fieldName}[${idx}].`);\n          });\n        }\n      }\n      // if value is an object, run recursively\n    } else if (typeof value === 'object' && !isBlackbox(fieldSchema)) {\n      const nestedFieldSchema = getNestedSchema(fieldSchema);\n      if (nestedFieldSchema) {\n        forEachDocumentField(value, nestedFieldSchema, callback, `${currentPath}${fieldName}.`);\n      }\n    }\n  });\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/settings.js",
    "content": "import Vulcan from './config.js';\nimport flatten from 'flat';\n\nconst getNestedProperty = function (obj, desc) {\n  var arr = desc.split('.');\n  while(arr.length && (obj = obj[arr.shift()]));\n  return obj;\n};\n\nexport const Settings = {};\n\nexport const getAllSettings = () => {\n\n  const settingsObject = {};\n\n  let rootSettings = _.clone(Meteor.settings);\n  delete rootSettings.public;\n  delete rootSettings.private;\n\n  // root settings & private settings are both private\n  rootSettings = flatten(rootSettings, {safe: true});\n  const privateSettings = flatten(Meteor.settings.private || {}, {safe: true});\n\n  // public settings\n  const publicSettings = flatten(Meteor.settings.public || {}, {safe: true});\n\n  // registered default values\n  const registeredSettings = Settings;\n\n  const allSettingKeys = _.union(_.keys(rootSettings), _.keys(publicSettings), _.keys(privateSettings), _.keys(registeredSettings));\n\n  allSettingKeys.sort().forEach(key => {\n\n    settingsObject[key] = {};\n\n    if (typeof rootSettings[key] !== 'undefined') {\n      settingsObject[key].value = rootSettings[key];\n    } else if (typeof privateSettings[key] !== 'undefined') {\n      settingsObject[key].value = privateSettings[key];\n    } else if (typeof publicSettings[key] !== 'undefined') {\n      settingsObject[key].value = publicSettings[key];\n    }\n    \n    if (typeof publicSettings[key] !== 'undefined'){\n      settingsObject[key].isPublic = true;\n    }\n\n    if (registeredSettings[key]) {\n      if (registeredSettings[key].defaultValue !== null || registeredSettings[key].defaultValue !== undefined) settingsObject[key].defaultValue = registeredSettings[key].defaultValue;\n      if (registeredSettings[key].description) settingsObject[key].description = registeredSettings[key].description;\n    }\n\n  });\n\n  return _.map(settingsObject, (setting, key) => ({name: key, ...setting}));\n};\n\n\nVulcan.showSettings = () => {\n  return getAllSettings();\n};\n\nexport const registerSetting = (settingName, defaultValue, description, isPublic) => {\n  Settings[settingName] = { defaultValue, description, isPublic };\n};\n\nexport const getSetting = (settingName, settingDefault) => {\n\n  let setting;\n\n  // if a default value has been registered using registerSetting, use it\n  if (typeof settingDefault === 'undefined' && Settings[settingName])\n    settingDefault = Settings[settingName].defaultValue;\n\n  if (Meteor.isServer) {\n    // look in public, private, and root\n    const rootSetting = getNestedProperty(Meteor.settings, settingName);\n    const privateSetting = Meteor.settings.private && getNestedProperty(Meteor.settings.private, settingName);\n    const publicSetting = Meteor.settings.public && getNestedProperty(Meteor.settings.public, settingName);\n    \n    // if setting is an object, \"collect\" properties from all three places\n    if (typeof rootSetting === 'object' || typeof privateSetting === 'object' || typeof publicSetting === 'object') {\n      setting = {\n        ...settingDefault,\n        ...rootSetting,\n        ...privateSetting,\n        ...publicSetting,\n      };\n    } else {\n      if (typeof rootSetting !== 'undefined') {\n        setting = rootSetting;\n      } else if (typeof privateSetting !== 'undefined') {\n        setting = privateSetting;\n      } else if (typeof publicSetting !== 'undefined') {\n        setting = publicSetting;\n      } else {\n        setting = settingDefault;\n      }\n    }\n\n  } else {\n    // look only in public\n    const publicSetting = Meteor.settings.public && getNestedProperty(Meteor.settings.public, settingName);\n    setting = typeof publicSetting !== 'undefined' ? publicSetting : settingDefault;\n  }\n\n  // Settings[settingName] = {...Settings[settingName], settingValue: setting};\n\n  return setting;\n\n};\n\nregisterSetting('debug', false, 'Enable debug mode (more verbose logging)');\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/simpleSchema_utils.js",
    "content": "/**\n * Helpers specific to Simple Schema\n * See \"schema_utils\" for more generic methods\n*/\n\n// remove \".$\" at the end of array child fieldName\nexport const unarrayfyFieldName = (fieldName) => {\n    return fieldName ? fieldName.split('.')[0] : fieldName;\n};\n\n// allowed values of a field if present\nexport const getAllowedValues = (field) => field.type.definitions[0].allowedValues;\nexport const hasAllowedValues = field => {\n    const allowedValues = getAllowedValues(field);\n    if (allowedValues && !allowedValues.length) {\n        console.warn(`Field ${field} as empty allowed values`);\n        return false;\n    }\n    return !!allowedValues;\n};\n\n\nexport const isArrayChildField = fieldName => fieldName.indexOf('$') !== -1;\nexport const isBlackbox = (field) => !!field.type.definitions[0].blackbox;\n//export const isBlackbox = (fieldName, schema) => {\n//    const field = schema[fieldName];\n//    // for array field, check parent recursively to find a blackbox\n//    if (isArrayChildField(fieldName)) {\n//        const parentField = schema[fieldName.slice(0, -2)];\n//        return isBlackbox(parentField);\n//    }\n//    return field.type.definitions[0].blackbox;\n//};\n\nexport const getFieldType = field => field.type.singleType || field.type[0].type;\nexport const getFieldTypeName = fieldType =>\n    typeof fieldType === 'object'\n        ? 'Object'\n        : typeof fieldType === 'function'\n            ? fieldType.name\n            : fieldType;\n\nexport const getArrayChild = (fieldName, schema) => schema[`${fieldName}.$`];\n\nexport const getNestedSchema = field => field.type.singleType._schema;\n\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/startup.js",
    "content": "import { runCallbacks } from './callbacks';\n\nMeteor.startup(() => {\n  runCallbacks('app.startup');\n});"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/ui_utils.js",
    "content": "import pick from 'lodash/pick';\n\n/**\n * Extract input props for the FormComponentInner\n * @param {*} props All component props\n * @returns Initial props + props specific to the HTML input in an inputProperties object\n */\nexport const getHtmlInputProps = props => {\n  const { name, path, options, label, onChange, onBlur, value, disabled } = props;\n\n  // these properties are whitelisted so that they can be safely passed to the actual form input\n  // and avoid https://facebook.github.io/react/warnings/unknown-prop.html warnings\n  const inputProperties = {\n    ...props.inputProperties,\n    name,\n    path,\n    options,\n    label,\n    onChange,\n    onBlur,\n    value,\n    disabled,\n  };\n\n  return {\n    ...props,\n    inputProperties,\n  };\n};\n\n/**\n * Extract input props for the FormComponentInner\n * @param {*} props All component props\n * @returns Initial props + props specific to the HTML input in an inputProperties object\n */\nexport const whitelistInputProps = props => {\n  const whitelist = ['name', 'path', 'options', 'label', 'onChange', 'onBlur', 'value', 'disabled', 'placeholder'];\n  return pick(props, whitelist);\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/utils.js",
    "content": "/*\n\nUtilities\n\n*/\n\nimport marked from 'marked';\nimport urlObject from 'url';\nimport moment from 'moment';\nimport getSlug from 'speakingurl';\nimport { getSetting, registerSetting } from './settings.js';\nimport { Routes } from './routes.js';\nimport { getCollection } from './collections.js';\nimport set from 'lodash/set';\nimport get from 'lodash/get';\nimport isFunction from 'lodash/isFunction';\nimport pluralize from 'pluralize';\nimport { getFieldType } from './simpleSchema_utils';\nimport { forEachDocumentField } from './schema_utils';\nimport isEmpty from 'lodash/isEmpty';\n\nregisterSetting('debug', false, 'Enable debug mode (more verbose logging)');\n\n/**\n * @summary The global namespace for Vulcan utils.\n * @namespace Telescope.utils\n */\nexport const Utils = {};\n\n/**\n * @summary Convert a camelCase string to dash-separated string\n * @param {String} str\n */\nUtils.camelToDash = function (str) {\n  return str\n    .replace(/\\W+/g, '-')\n    .replace(/([a-z\\d])([A-Z])/g, '$1-$2')\n    .toLowerCase();\n};\n\n/**\n * @summary Convert a camelCase string to a space-separated capitalized string\n * See http://stackoverflow.com/questions/4149276/javascript-camelcase-to-regular-form\n * @param {String} str\n */\nUtils.camelToSpaces = function (str) {\n  return str.replace(/([A-Z])/g, ' $1').replace(/^./, function (str) {\n    return str.toUpperCase();\n  });\n};\n\n/**\n * @summary Convert a string to title case ('foo bar baz' to 'Foo Bar Baz')\n * See https://stackoverflow.com/questions/4878756/how-to-capitalize-first-letter-of-each-word-like-a-2-word-city\n * @param {String} str\n */\nUtils.toTitleCase = str =>\n  str &&\n  str\n    .toLowerCase()\n    .split(' ')\n    .map(s => s.charAt(0).toUpperCase() + s.substring(1))\n    .join(' ');\n\n/**\n * @summary Convert an underscore-separated string to dash-separated string\n * @param {String} str\n */\nUtils.underscoreToDash = function (str) {\n  return str.replace('_', '-');\n};\n\n/**\n * @summary Convert a dash separated string to camelCase.\n * @param {String} str\n */\nUtils.dashToCamel = function (str) {\n  return str.replace(/(\\-[a-z])/g, function ($1) {\n    return $1.toUpperCase().replace('-', '');\n  });\n};\n\n/**\n * @summary Convert a string to camelCase and remove spaces.\n * @param {String} str\n */\nUtils.camelCaseify = function (str) {\n  str = this.dashToCamel(str.replace(' ', '-'));\n  str = str.slice(0, 1).toLowerCase() + str.slice(1);\n  return str;\n};\n\n/**\n * @summary Trim a sentence to a specified amount of words and append an ellipsis.\n * @param {String} s - Sentence to trim.\n * @param {Number} numWords - Number of words to trim sentence to.\n */\nUtils.trimWords = function (s, numWords) {\n  if (!s) return s;\n\n  var expString = s.split(/\\s+/, numWords);\n  if (expString.length >= numWords) return expString.join(' ') + '…';\n  return s;\n};\n\n/**\n * @summary Trim a block of HTML code to get a clean text excerpt\n * @param {String} html - HTML to trim.\n */\nUtils.trimHTML = function (html, numWords) {\n  var text = Utils.stripHTML(html);\n  return Utils.trimWords(text, numWords);\n};\n\n/**\n * @summary Capitalize a string.\n * @param {String} str\n */\nexport const capitalize = function (str) {\n  return str && str.charAt(0).toUpperCase() + str.slice(1);\n};\n\nUtils.capitalize = capitalize;\n\nUtils.t = function (message) {\n  var d = new Date();\n  console.log(\n    '### ' + message + ' rendered at ' + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds()\n  ); // eslint-disable-line\n};\n\nUtils.nl2br = function (str) {\n  var breakTag = '<br />';\n  return (str + '').replace(/([^>\\r\\n]?)(\\r\\n|\\n\\r|\\r|\\n)/g, '$1' + breakTag + '$2');\n};\n\nUtils.scrollPageTo = function (selector) {\n  $('body').scrollTop($(selector).offset().top);\n};\n\nUtils.scrollIntoView = function (selector) {\n  if (!document) return;\n\n  const element = document.querySelector(selector);\n  if (element) {\n    element.scrollIntoView();\n  }\n};\n\nUtils.getDateRange = function (pageNumber) {\n  var now = moment(new Date());\n  var dayToDisplay = now.subtract(pageNumber - 1, 'days');\n  var range = {};\n  range.start = dayToDisplay.startOf('day').valueOf();\n  range.end = dayToDisplay.endOf('day').valueOf();\n  // console.log(\"after: \", dayToDisplay.startOf('day').format(\"dddd, MMMM Do YYYY, h:mm:ss a\"));\n  // console.log(\"before: \", dayToDisplay.endOf('day').format(\"dddd, MMMM Do YYYY, h:mm:ss a\"));\n  return range;\n};\n\n//////////////////////////\n// URL Helper Functions //\n//////////////////////////\n\n/**\n * @summary Returns the user defined site URL or Meteor.absoluteUrl. Add trailing '/' if missing\n */\nUtils.getSiteUrl = function (addSlash = true) {\n  let url = getSetting('siteUrl', Meteor.absoluteUrl());\n  if (url.slice(-1) !== '/' && addSlash) {\n    url += '/';\n  }\n  return url;\n};\n\n/**\n * @summary Returns the user defined site URL or Meteor.absoluteUrl. Remove trailing '/' if it exists\n */\nUtils.getRootUrl = function () {\n  let url = getSetting('siteUrl', Meteor.absoluteUrl());\n  if (url.slice(-1) === '/') {\n    url = url.slice(0, -1);\n  }\n  return url;\n};\n\n/**\n * @summary The global namespace for Vulcan utils.\n * @param {String} url - the URL to redirect\n */\nUtils.getOutgoingUrl = function (url) {\n  return Utils.getSiteUrl() + 'out?url=' + encodeURIComponent(url);\n};\n\nUtils.slugify = function (s) {\n  let slug = getSlug(s, {\n    truncate: 60,\n  });\n\n  // can't have posts with an \"edit\" slug\n  if (slug === 'edit') {\n    slug = 'edit-1';\n  }\n\n  return slug;\n};\n\n/**\n * @summary Given a collection and a slug, returns the same or modified slug that's unique within the collection;\n * It's modified by appending a dash and an integer; eg: my-slug  =>  my-slug-1\n * @param {Object} collection\n * @param {string} slug\n * @param {string} [documentId] If you are generating a slug for an existing document, pass it's _id to\n * avoid the slug changing\n * @returns {string} The slug passed in the 2nd param, but may be\n */\nUtils.getUnusedSlug = function (collection, slug, documentId) {\n  // test if slug is already in use\n  for (let index = 0; index <= Number.MAX_SAFE_INTEGER; index++) {\n    const suffix = index ? '-' + index : '';\n    const documentWithSlug = collection.findOne({ slug: slug + suffix });\n    if (!documentWithSlug || (documentId && documentWithSlug._id === documentId)) {\n      return slug + suffix;\n    }\n  }\n};\n\n// Different version, less calls to the db but it cannot be used until we figure out how to use async for onCreate functions\n// Utils.getUnusedSlug = async function (collection, slug) {\n//   let suffix = '';\n//   let index = 0;\n//\n//   const slugRegex = new RegExp('^' + slug + '-[0-9]+$');\n//   // get all the slugs matching slug or slug-123 in that collection\n//   const results = await collection.find( { slug: { $in: [slug, slugRegex] } }, { fields: { slug: 1, _id: 0 } });\n//   const usedSlugs = results.map(item => item.slug);\n//   // increment the index at the end of the slug until we find an unused one\n//   while (usedSlugs.indexOf(slug + suffix) !== -1) {\n//     index++;\n//     suffix = '-' + index;\n//   }\n//   return slug + suffix;\n// };\n\n/**\n * @summary This is the same as Utils.getUnusedSlug(), but takes the name of the collection instead\n * @param {string} collectionName\n * @param {string} slug\n * @param {string} [documentId]\n * @returns {string}\n */\nUtils.getUnusedSlugByCollectionName = function (collectionName, slug, documentId) {\n  return Utils.getUnusedSlug(getCollection(collectionName), slug, documentId);\n};\n\nUtils.getShortUrl = function (post) {\n  return post.shortUrl || post.url;\n};\n\nUtils.getDomain = function (url) {\n  try {\n    return urlObject.parse(url).hostname.replace('www.', '');\n  } catch (error) {\n    return null;\n  }\n};\n\n// add http: if missing\nUtils.addHttp = function (url) {\n  try {\n    if (url.substring(0, 5) !== 'http:' && url.substring(0, 6) !== 'https:') {\n      url = 'http:' + url;\n    }\n    return url;\n  } catch (error) {\n    return null;\n  }\n};\n\n/////////////////////////////\n// String Helper Functions //\n/////////////////////////////\n\nUtils.cleanUp = function (s) {\n  return this.stripHTML(s);\n};\n\nUtils.sanitize = function (s) {\n  return s;\n};\n\nUtils.stripHTML = function (s) {\n  return s && s.replace(/<(?:.|\\n)*?>/gm, '');\n};\n\nUtils.stripMarkdown = function (s) {\n  var htmlBody = marked(s);\n  return Utils.stripHTML(htmlBody);\n};\n\n// http://stackoverflow.com/questions/2631001/javascript-test-for-existence-of-nested-object-key\nUtils.checkNested = function (obj /*, level1, level2, ... levelN*/) {\n  var args = Array.prototype.slice.call(arguments);\n  obj = args.shift();\n\n  for (var i = 0; i < args.length; i++) {\n    if (!obj.hasOwnProperty(args[i])) {\n      return false;\n    }\n    obj = obj[args[i]];\n  }\n  return true;\n};\n\nUtils.log = function (s) {\n  if (getSetting('debug', false) || process.env.NODE_ENV === 'development') {\n    console.log(s); // eslint-disable-line\n  }\n};\n\n// see http://stackoverflow.com/questions/8051975/access-object-child-properties-using-a-dot-notation-string\nUtils.getNestedProperty = function (obj, desc) {\n  var arr = desc.split('.');\n  while (arr.length && (obj = obj[arr.shift()]));\n  return obj;\n};\n\n// see http://stackoverflow.com/a/14058408/649299\n_.mixin({\n  compactObject: function (object) {\n    var clone = _.clone(object);\n    _.each(clone, function (value, key) {\n      /*\n\n        Remove a value if:\n        1. it's not a boolean\n        2. it's not a number\n        3. it's undefined\n        4. it's an empty string\n        5. it's null\n        6. it's an empty array\n\n      */\n      if (typeof value === 'boolean' || typeof value === 'number') {\n        return;\n      }\n\n      if (\n        value === undefined ||\n        value === null ||\n        value === '' ||\n        (Array.isArray(value) && value.length === 0)\n      ) {\n        delete clone[key];\n      }\n    });\n    return clone;\n  },\n});\n\nUtils.getFieldLabel = (fieldName, collection) => {\n  const label = collection.simpleSchema()._schema[fieldName].label;\n  const nameWithSpaces = Utils.camelToSpaces(fieldName);\n  return label || nameWithSpaces;\n};\n\nUtils.getLogoUrl = () => {\n  const logoUrl = getSetting('logoUrl');\n  if (logoUrl) {\n    const prefix = Utils.getSiteUrl().slice(0, -1);\n    // the logo may be hosted on another website\n    return logoUrl.indexOf('://') > -1 ? logoUrl : prefix + logoUrl;\n  }\n};\n\nUtils.findIndex = (array, predicate) => {\n  let index = -1;\n  let continueLoop = true;\n  array.forEach((item, currentIndex) => {\n    if (continueLoop && predicate(item)) {\n      index = currentIndex;\n      continueLoop = false;\n    }\n  });\n  return index;\n};\n\n// adapted from http://stackoverflow.com/a/22072374/649299\nUtils.unflatten = function (array, options, parent, level = 0, tree) {\n  const {\n    idProperty = '_id',\n    parentIdProperty = 'parentId',\n    childrenProperty = 'childrenResults',\n  } = options;\n\n  level++;\n\n  tree = typeof tree !== 'undefined' ? tree : [];\n\n  let children = [];\n\n  if (typeof parent === 'undefined') {\n    // if there is no parent, we're at the root level\n    // so we return all root nodes (i.e. nodes with no parent)\n    children = _.filter(array, node => !get(node, parentIdProperty));\n  } else {\n    // if there *is* a parent, we return all its child nodes\n    // (i.e. nodes whose parentId is equal to the parent's id.)\n    children = _.filter(array, node => get(node, parentIdProperty) === get(parent, idProperty));\n  }\n\n  // if we found children, we keep on iterating\n  if (!!children.length) {\n    if (typeof parent === 'undefined') {\n      // if we're at the root, then the tree consist of all root nodes\n      tree = children;\n    } else {\n      // else, we add the children to the parent as the \"childrenResults\" property\n      set(parent, childrenProperty, children);\n    }\n\n    // we call the function on each child\n    children.forEach(child => {\n      child.level = level;\n      Utils.unflatten(array, options, child, level);\n    });\n  }\n\n  return tree;\n};\n\n// remove the telescope object from a schema and duplicate it at the root\nUtils.stripTelescopeNamespace = schema => {\n  // grab the users schema keys\n  const schemaKeys = Object.keys(schema);\n\n  // remove any field beginning by telescope: .telescope, .telescope.upvotedPosts.$, ...\n  const filteredSchemaKeys = schemaKeys.filter(key => key.slice(0, 9) !== 'telescope');\n\n  // replace the previous schema by an object based on this filteredSchemaKeys\n  return filteredSchemaKeys.reduce((sch, key) => ({ ...sch, [key]: schema[key] }), {});\n};\n\n/**\n * Get the display name of a React component\n * @param {React Component} WrappedComponent\n */\nUtils.getComponentDisplayName = WrappedComponent => {\n  return WrappedComponent.displayName || WrappedComponent.name || 'Component';\n};\n\n/**\n * Take a collection and a list of documents, and convert all their date fields to date objects\n * This is necessary because Apollo doesn't support custom scalars, and stores dates as strings\n * @param {Object} collection\n * @param {Array} list\n */\nUtils.convertDates = (collection, listOrDocument) => {\n  // if undefined, just return\n  if (!listOrDocument) return listOrDocument;\n  const isArray = listOrDocument && Array.isArray(listOrDocument);\n  if (isArray && !listOrDocument.length) return listOrDocument;\n  const list = isArray ? listOrDocument : [listOrDocument];\n  const schema = collection.simpleSchema()._schema;\n  //Nested version\n  const convertedList = list.map((document) => {\n    forEachDocumentField(document, schema, ({ fieldName, fieldSchema, currentPath }) => {\n      if (fieldSchema && getFieldType(fieldSchema) === Date) {\n        const valuePath = `${currentPath}${fieldName}`;\n        const value = get(document, valuePath);\n        set(document, valuePath, new Date(value));\n      }\n    });\n    return document;\n  });\n  return isArray ? convertedList : convertedList[0];\n};\n\nUtils.encodeIntlError = error => (typeof error !== 'object' ? error : JSON.stringify(error));\n\nUtils.decodeIntlError = (error, options = { stripped: false }) => {\n  try {\n    // do we get the error as a string or as an error object?\n    let strippedError = typeof error === 'string' ? error : error.message;\n\n    // if the error hasn't been cleaned before (ex: it's not an error from a form)\n    if (!options.stripped) {\n      // strip the \"GraphQL Error: message [error_code]\" given by Apollo if present\n      const graphqlPrefixIsPresent = strippedError.match(/GraphQL error: (.*)/);\n      if (graphqlPrefixIsPresent) {\n        strippedError = graphqlPrefixIsPresent[1];\n      }\n\n      // strip the error code if present\n      const errorCodeIsPresent = strippedError.match(/(.*)\\[(.*)\\]/);\n      if (errorCodeIsPresent) {\n        strippedError = errorCodeIsPresent[1];\n      }\n    }\n\n    // the error is an object internationalizable\n    const parsedError = JSON.parse(strippedError);\n\n    // check if the error has at least an 'id' expected by react-intl\n    if (!parsedError.id) {\n      console.error('[Undecodable error]', error); // eslint-disable-line\n      return { id: 'app.something_bad_happened', value: '[undecodable error]' };\n    }\n\n    // return the parsed error\n    return parsedError;\n  } catch (__) {\n    // the error is not internationalizable\n    return error;\n  }\n};\n\nUtils.findWhere = (array, criteria) =>\n  array.find(item => Object.keys(criteria).every(key => item[key] === criteria[key]));\n\nUtils.defineName = (o, name) => {\n  Object.defineProperty(o, 'name', { value: name });\n  return o;\n};\n\nUtils.getRoutePath = routeName => {\n  return Routes[routeName] && Routes[routeName].path;\n};\n\nString.prototype.replaceAll = function (search, replacement) {\n  var target = this;\n  return target.replace(new RegExp(search, 'g'), replacement);\n};\n\nUtils.isPromise = value => isFunction(get(value, 'then'));\n\n/**\n * Pluralize helper with clash name prevention (adds an S)\n */\nUtils.pluralize = (text, ...args) => {\n  const res = pluralize(text, ...args);\n  // avoid edge case like \"people\" where plural is identical to singular, leading to name clash\n  // in resolvers\n  if (res === text) {\n    return res + 's';\n  }\n  return res;\n};\n\n\n\nUtils.singularize = pluralize.singular;\n\nUtils.removeProperty = (obj, propertyName) => {\n  for (const prop in obj) {\n    if (prop === propertyName) {\n      delete obj[prop];\n    } else if (typeof obj[prop] === 'object') {\n      Utils.removeProperty(obj[prop], propertyName);\n    }\n  }\n};\n\n/**\n * Convert an array of field options into an allowedValues array\n * @param {Array} schemaFieldOptionsArray\n */\nUtils.getSchemaFieldAllowedValues = schemaFieldOptionsArray => {\n  if (!Array.isArray(schemaFieldOptionsArray)) {\n    throw new Error('Utils.getAllowedValues: Expected Array');\n  }\n  return schemaFieldOptionsArray.map(schemaFieldOption => schemaFieldOption.value);\n};\n\n/**\n * type is an array due to the possibility of using SimpleSchema.oneOf\n * right now we support only fields with one type\n * @param {Object} field\n */\nUtils.getFieldType = field => get(field, 'type.definitions.0.type');\n\n/**\n * Convert an array of field names into a Mongo fields specifier\n * @param {Array} fieldsArray\n */\nUtils.arrayToFields = fieldsArray => {\n  return _.object(\n    fieldsArray,\n    _.map(fieldsArray, function () {\n      return true;\n    })\n  );\n};\n\nUtils.isEmptyOrUndefined = value =>\n  typeof value === 'undefined' ||\n  value === null ||\n  //value === '' ||\n  (\n    typeof value === 'object' &&\n    isEmpty(value) &&\n    !(value instanceof Date) &&\n    !(value instanceof RegExp)\n  );\n"
  },
  {
    "path": "packages/vulcan-lib/lib/modules/validation.js",
    "content": "import pickBy from 'lodash/pickBy';\nimport mapValues from 'lodash/mapValues';\nimport { forEachDocumentField } from './schema_utils';\n\nexport const dataToModifier = data => ({\n  $set: pickBy(data, f => f !== null),\n  $unset: mapValues(pickBy(data, f => f === null), () => true),\n});\n\nexport const modifierToData = modifier => ({\n  ...modifier.$set,\n  ...mapValues(modifier.$unset, () => null),\n});\n\n\n\n/**\n * Validate a document permission recursively\n * @param {*} fullDocument (must not be partial since permission logic may rely on full document)\n * @param {*} documentToValidate document to validate\n * @param {*} schema Simple schema\n * @param {*} context Current user and Users collectionœ\n * @param {*} mode create or update\n * @param {*} currentPath current path for recursive calls (nested, nested[0].foo, ...)\n */\nconst validateDocumentPermissions = (fullDocument, documentToValidate, schema, context, mode = 'create', currentPath = '') => {\n  let validationErrors = [];\n  const { Users, currentUser } = context;\n  forEachDocumentField(documentToValidate, schema,\n    ({ fieldName, fieldSchema, currentPath, isNested }) => {\n      if (isNested && (!fieldSchema || (mode === 'create' ? !fieldSchema.canCreate : !fieldSchema.canUpdate))) return; // ignore nested without permission\n      if (!fieldSchema\n        || (mode === 'create' ? !Users.canCreateField(currentUser, fieldSchema) : !Users.canUpdateField(currentUser, fieldSchema, fullDocument))\n      ) {\n        validationErrors.push({\n          id: 'errors.disallowed_property_detected',\n          properties: {\n            name: `${currentPath}${fieldName}`\n          },\n        });\n      }\n    });\n  return validationErrors;\n};\n/*\n\n  If document is not trusted, run validation steps:\n\n  1. Check that the current user has permission to edit each field\n  2. Run SimpleSchema validation step\n\n*/\nexport const validateDocument = (document, collection, context, validationContextName = 'defaultContext') => {\n  const schema = collection.simpleSchema()._schema;\n\n  let validationErrors = [];\n\n  // validate creation permissions (and other Vulcan-specific constraints)\n  validationErrors = validationErrors.concat(\n    validateDocumentPermissions(document, document, schema, context, 'create')\n  );\n\n  // run simple schema validation (will check the actual types, required fields, etc....)\n  const validationContext = collection.simpleSchema().namedContext(validationContextName);\n  validationContext.validate(document);\n\n  if (!validationContext.isValid()) {\n    const errors = validationContext.validationErrors();\n    errors.forEach(error => {\n      // eslint-disable-next-line no-console\n      // console.log(error);\n      if (error.type.includes('intlError')) {\n        const intlError = JSON.parse(error.type.replace('intlError|', ''));\n        validationErrors = validationErrors.concat(intlError);\n      } else {\n        validationErrors.push({\n          id: `errors.${error.type}`,\n          path: error.name,\n          properties: {\n            collectionName: collection.options.collectionName,\n            typeName: collection.options.typeName,\n            ...error,\n          },\n        });\n      }\n    });\n  }\n\n  return validationErrors;\n};\n\n/*\n\n  If document is not trusted, run validation steps:\n\n  1. Check that the current user has permission to insert each field\n  2. Run SimpleSchema validation step\n\n*/\nexport const validateModifier = (modifier, data, document, collection, context, validationContextName = 'defaultContext') => {\n  const schema = collection.simpleSchema()._schema;\n  const set = modifier.$set;\n  const unset = modifier.$unset;\n\n  let validationErrors = [];\n\n  // 1. check that the current user has permission to edit each field\n  validationErrors = validationErrors.concat(\n    validateDocumentPermissions(document, data, schema, context, 'update')\n  );\n\n  // 2. run SS validation\n  const validationContext = collection.simpleSchema().namedContext(validationContextName);\n  validationContext.validate({ $set: set, $unset: unset }, { modifier: true, extendedCustomContext: { documentId: document._id } });\n\n  if (!validationContext.isValid()) {\n    const errors = validationContext.validationErrors();\n    errors.forEach(error => {\n      // eslint-disable-next-line no-console\n      // console.log(error);\n      if (error.type.includes('intlError')) {\n        validationErrors = validationErrors.concat(JSON.parse(error.type.replace('intlError|', '')));\n      } else {\n        validationErrors.push({\n          id: `errors.${error.type}`,\n          path: error.name,\n          properties: {\n            collectionName: collection.options.collectionName,\n            typeName: collection.options.typeName,\n            ...error,\n          },\n        });\n      }\n    });\n  }\n\n  return validationErrors;\n};\n\nexport const validateData = (data, document, collection, context) => {\n  return validateModifier(dataToModifier(data), data, document, collection, context);\n};\n\n/*\n\nThe following versions were written to be more SimpleSchema-agnostic, but\nare not currently used\n\n*/\n\n/*\n\n  If document is not trusted, run validation steps:\n\n  1. Check that the current user has permission to edit each field\n  2. Check field lengths\n  3. Check field types\n  4. Check for missing fields\n  5. Run SimpleSchema validation step (for now)\n\n*/\nexport const validateDocumentNotUsed = (document, collection, context) => {\n  const { Users, currentUser } = context;\n  const schema = collection.simpleSchema()._schema;\n\n  let validationErrors = [];\n\n  // Check validity of inserted document\n  _.forEach(document, (value, fieldName) => {\n    const fieldSchema = schema[fieldName];\n\n    // 1. check that the current user has permission to insert each field\n    if (!fieldSchema || !Users.canCreateField(currentUser, fieldSchema)) {\n      validationErrors.push({\n        id: 'app.disallowed_property_detected',\n        fieldName,\n      });\n    }\n\n    // 2. check field lengths\n    if (fieldSchema.limit && value.length > fieldSchema.limit) {\n      validationErrors.push({\n        id: 'app.field_is_too_long',\n        data: { fieldName, limit: fieldSchema.limit },\n      });\n    }\n\n    // 3. check that fields have the proper type\n    // TODO\n  });\n\n  // 4. check that required fields have a value\n  _.keys(schema).forEach(fieldName => {\n    const fieldSchema = schema[fieldName];\n\n    if ((fieldSchema.required || !fieldSchema.optional) && typeof document[fieldName] === 'undefined') {\n      validationErrors.push({\n        id: 'app.required_field_missing',\n        data: { fieldName },\n      });\n    }\n  });\n\n  // 5. still run SS validation for now for backwards compatibility\n  try {\n    collection.simpleSchema().validate(document);\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.log(error);\n    validationErrors.push({\n      id: 'app.schema_validation_error',\n      data: { message: error.message },\n    });\n  }\n\n  return validationErrors;\n};\n\n/*\n\n  If document is not trusted, run validation steps:\n\n  1. Check that the current user has permission to insert each field\n  2. Check field lengths\n  3. Check field types\n  4. Check for missing fields\n  5. Run SimpleSchema validation step (for now)\n\n*/\nexport const validateModifierNotUsed = (modifier, document, collection, context) => {\n  const { Users, currentUser } = context;\n  const schema = collection.simpleSchema()._schema;\n  const set = modifier.$set;\n  const unset = modifier.$unset;\n\n  let validationErrors = [];\n\n  // 1. check that the current user has permission to edit each field\n  const modifiedProperties = _.keys(set).concat(_.keys(unset));\n  modifiedProperties.forEach(function (fieldName) {\n    var field = schema[fieldName];\n    if (!field || !Users.canUpdateField(currentUser, field, document)) {\n      validationErrors.push({\n        id: 'app.disallowed_property_detected',\n        data: { name: fieldName },\n      });\n    }\n  });\n\n  // Check validity of set modifier\n  _.forEach(set, (value, fieldName) => {\n    const fieldSchema = schema[fieldName];\n\n    // 2. check field lengths\n    if (fieldSchema.limit && value.length > fieldSchema.limit) {\n      validationErrors.push({\n        id: 'app.field_is_too_long',\n        data: { name: fieldName, limit: fieldSchema.limit },\n      });\n    }\n\n    // 3. check that fields have the proper type\n    // TODO\n  });\n\n  // 4. check that required fields have a value\n  // when editing, we only want to require fields that are actually part of the form\n  // so we make sure required keys are present in the $unset object\n  _.keys(schema).forEach(fieldName => {\n    const fieldSchema = schema[fieldName];\n\n    if (unset[fieldName] && (fieldSchema.required || !fieldSchema.optional) && typeof set[fieldName] === 'undefined') {\n      validationErrors.push({\n        id: 'app.required_field_missing',\n        data: { name: fieldName },\n      });\n    }\n  });\n\n  // 5. still run SS validation for now for backwards compatibility\n  const validationContext = collection.simpleSchema().newContext();\n  validationContext.validate({ $set: set, $unset: unset }, { modifier: true });\n\n  if (!validationContext.isValid()) {\n    const errors = validationContext.validationErrors();\n    errors.forEach(error => {\n      // eslint-disable-next-line no-console\n      // console.log(error);\n      validationErrors.push({\n        id: 'app.schema_validation_error',\n        data: error,\n      });\n    });\n  }\n\n  return validationErrors;\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/accounts_helpers.js",
    "content": "import crypto from 'crypto';\n\nexport const _hashLoginToken = (loginToken) => {\n  var hash = crypto.createHash('sha256');\n  hash.update(loginToken);\n  return hash.digest('base64');\n};\n\nexport const _tokenExpiration = (when) => {\n  // We pass when through the Date constructor for backwards compatibility;\n  // `when` used to be a number.\n  return new Date((new Date(when)).getTime() + _getTokenLifetimeMs());\n};\n\n// A large number of expiration days (approximately 100 years worth) that is\n// used when creating unexpiring tokens.\nconst LOGIN_UNEXPIRING_TOKEN_DAYS = 365 * 100;\n\n// how long (in days) until a login token expires\nconst DEFAULT_LOGIN_EXPIRATION_DAYS = 90;\n\nexport const _getTokenLifetimeMs = () => {\n  // When loginExpirationInDays is set to null, we'll use a really high\n  // number of days (LOGIN_UNEXPIRABLE_TOKEN_DAYS) to simulate an\n  // unexpiring token.\n  const loginExpirationInDays = LOGIN_UNEXPIRING_TOKEN_DAYS;\n  return (loginExpirationInDays|| DEFAULT_LOGIN_EXPIRATION_DAYS) * 24 * 60 * 60 * 1000;\n};"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-server/apollo_server.js",
    "content": "/**\n * @see https://www.apollographql.com/docs/apollo-server/whats-new.html\n * @see https://www.apollographql.com/docs/apollo-server/migration-two-dot.html\n */\n\n// Meteor WebApp use a Connect server, so we need to\n// use apollo-server-express integration\n// We also add Express to WebApp in order to use any kind of middlewares\nimport express from 'express';\nimport { ApolloServer } from 'apollo-server-express';\n\nimport { Meteor } from 'meteor/meteor';\n\nimport { WebApp } from 'meteor/webapp';\nimport _get from 'lodash/get';\nimport { bodyParserGraphQL } from 'body-parser-graphql';\n\n// import cookiesMiddleware from 'universal-cookie-express';\n// import Cookies from 'universal-cookie';\nimport voyagerMiddleware from 'graphql-voyager/middleware/express';\nimport getVoyagerConfig from './voyager';\nimport { graphiqlMiddleware, getGraphiqlConfig } from './graphiql';\nimport getPlaygroundConfig from './playground';\n\nimport initGraphQL from './initGraphQL';\nimport './settings';\nimport { engineConfig } from './engine';\nimport { initContext, computeContextFromReq } from './context.js';\n\nimport { GraphQLSchema } from '../graphql/index.js';\n\nimport { enableSSR } from '../apollo-ssr';\n\nimport universalCookiesMiddleware from 'universal-cookie-express';\n\nimport { getApolloApplyMiddlewareOptions, getApolloServerOptions } from './settings';\n\nimport { getSetting } from '../../modules/settings.js';\nimport { formatError } from 'apollo-errors';\nimport { runCallbacks } from '../../modules/callbacks';\n\nexport const setupGraphQLMiddlewares = async (apolloServer, config, apolloApplyMiddlewareOptions) => {\n  // IMPORTANT: order matters !\n  // 1 - Add request parsing middleware\n  // 2 - Add apollo specific middlewares\n  // 3 - CLOSE CONNEXION (otherwise the endpoint hungs)\n  // 4 - ONLY THEN you can start adding other middlewares (graphql voyager etc.)\n\n  // WebApp.connectHandlers is a connect server\n  // you can add middlware as usual when using Express/Connect\n  // Use the Express app instead of just Node connect (allow better middleware chaining)\n  const app = express();\n  // parse cookies and assign req.universalCookies object\n  app.use(universalCookiesMiddleware());\n  // parse request (order matters)\n\n  app.use(\n    config.path,\n    // won't handle graphql\n    //bodyParser.json({ limit: getSetting('apolloServer.jsonParserOptions.limit') })\n    bodyParserGraphQL({ limit: getSetting('apolloServer.jsonParserOptions.limit') })\n  );\n\n  //WebApp.connectHandlers.use(config.path, bodyParser.text({ type: 'application/graphql' }));\n  WebApp.connectHandlers.use(app);\n\n  // enhance webapp\n  runCallbacks({\n    name: 'graphql.middlewares.setup',\n    iterator: WebApp,\n    properties: {},\n  });\n\n  await apolloServer.start();\n\n  // Provide the Meteor WebApp Connect server instance to Apollo\n  // Apollo will use it instead of its own HTTP server when handling requests\n\n  //   For the list of already set middlewares (cookies, compression...), see:\n  //  @see https://github.com/meteor/meteor/blob/master/packages/webapp/webapp_server.js\n  apolloServer.applyMiddleware({\n    ...apolloApplyMiddlewareOptions,\n  });\n\n  // setup the end point otherwise the request hangs\n  // TODO: undestand why this is necessary\n  // @see\n  WebApp.connectHandlers.use(config.path, (req, res) => {\n    if (req.method === 'GET') {\n      res.end();\n    }\n  });\n};\n\nexport const setupToolsMiddlewares = config => {\n  // Voyager is a GraphQL schema visual explorer\n  // available on /voyager as a default\n  WebApp.connectHandlers.use(config.voyagerPath, voyagerMiddleware(getVoyagerConfig(config)));\n  // Setup GraphiQL\n  WebApp.connectHandlers.use(config.graphiqlPath, graphiqlMiddleware(getGraphiqlConfig(config)));\n};\n\n/**\n *  setup CORS\n *  @see https://expressjs.com/en/resources/middleware/cors.html\n *  @see https://www.apollographql.com/docs/apollo-server/api/apollo-server/#apolloserver\n *  In Apollo, default cors is defined in packages/apollo-server/src/index.ts, it's too permissive so we use \"false\" in production\n */\nconst getCorsOptions = () => {\n  // enable all cors\n  const enableAllcors = _get(Meteor.settings, 'apolloServer.corsEnableAll', false);\n  if (enableAllcors) return true; // will allow all distant queries DANGEROUS\n  // enable only a whitelist or nothing\n  const corsWhitelist = _get(Meteor.settings, 'apolloServer.corsWhitelist', []);\n  const corsOptions =\n    corsWhitelist && corsWhitelist.length\n      ? {\n          origin: function(origin, callback) {\n            if (!origin) {\n              callback(null, true); // same origin\n            } else if (corsWhitelist.indexOf(origin) !== -1) {\n              callback(null, true);\n            } else {\n              callback(new Error(`Origin ${origin} not allowed by CORS`));\n            }\n          },\n          credentials: true,\n        }\n      : process.env.NODE_ENV === 'development'; // default behaviour is activating all in dev, deactivating all in production\n  return corsOptions;\n};\n\n/**\n * Options: Apollo server usual options\n * Config: a config specific to Vulcan\n */\nexport const createApolloServer = ({\n  apolloServerOptions = {}, // apollo options\n  config, // Vulcan options\n}) => {\n  // given options contains the schema\n  const apolloServer = new ApolloServer({\n    // graphql playground (replacement to graphiql), available on the app path\n    playground: getPlaygroundConfig(config),\n    // context optionbject or a function of the current request (+ maybe some other params)\n    debug: Meteor.isDevelopment,\n    cache: 'bounded',\n    ...apolloServerOptions,\n  });\n\n  // default function does nothing\n  if (config.configServer) {\n    config.configServer(apolloServer);\n  }\n\n  return apolloServer;\n};\n\nexport const onStart = () => {\n  // Vulcan specific options\n  const config = {\n    path: '/graphql',\n    maxAccountsCacheSizeInMB: 1,\n    configServer: apolloServer => {},\n    voyagerPath: '/graphql-voyager',\n    graphiqlPath: '/graphiql',\n    // customConfigFromReq\n  };\n  const corsOptions = getCorsOptions();\n  const apolloApplyMiddlewareOptions = {\n    // @see https://github.com/meteor/meteor/blob/master/packages/webapp/webapp_server.js\n    // @see https://www.apollographql.com/docs/apollo-server/api/apollo-server.html#Parameters-2\n    bodyParser: false, // added manually later\n    path: config.path,\n    app: WebApp.connectHandlers,\n    cors: corsOptions,\n    ...getApolloApplyMiddlewareOptions(),\n  };\n  // init context\n  const initialContext = initContext();\n  // this replace the previous syntax graphqlExpress(async req => { ... })\n  // this function takes the context, which contains the current request,\n  // and setup the options accordingly ({req}) => { ...; return options }\n  const context = computeContextFromReq(initialContext);\n\n  // define executableSchema\n  initGraphQL();\n\n  // create server\n  const apolloServer = createApolloServer({\n    config,\n    apolloServerOptions: {\n      engine: engineConfig,\n      schema: GraphQLSchema.executableSchema,\n      formatError,\n      tracing: getSetting('apolloTracing', Meteor.isDevelopment),\n      cacheControl: {\n        defaultMaxAge: 1000,\n      },\n      context: ({ req }) => context(req),\n      ...getApolloServerOptions(),\n    },\n  });\n  // NOTE: order matters here\n  // /graphql middlewares (request parsing)\n  setupGraphQLMiddlewares(apolloServer, config, apolloApplyMiddlewareOptions);\n  //// other middlewares (dev tools etc.)\n  if (Meteor.isDevelopment) {\n    setupToolsMiddlewares(config);\n  }\n  // ssr\n  const disableSSR = getSetting('apolloSsr.disable', false);\n  if (!disableSSR) {\n    enableSSR({ computeContext: context });\n  }\n  return apolloServer;\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-server/context.js",
    "content": "/**\n * Context prop of the ApolloServer config\n *\n * It sets up the server options based on the current request\n * Replacement to the syntax graphqlExpress(async req => {... })\n * Current pattern:\n * @see https://www.apollographql.com/docs/apollo-server/migration-two-dot.html#request-headers\n * @see https://github.com/apollographql/apollo-server/issues/1066\n * Previous implementation:\n * @see https://github.com/apollographql/apollo-server/issues/420\n */\n\n//import deepmerge from 'deepmerge';\nimport DataLoader from 'dataloader';\nimport { Collections } from '../../modules/collections.js';\nimport { runCallbacks } from '../../modules/callbacks.js';\nimport findByIds from '../../modules/findbyids.js';\nimport { GraphQLSchema } from '../graphql/index.js';\nimport _merge from 'lodash/merge';\nimport { getHeaderLocale } from '../intl.js';\nimport { getLocale } from '../../modules/intl.js';\nimport { getSetting } from '../../modules/settings.js';\nimport { WebApp } from 'meteor/webapp';\nimport { Accounts } from 'meteor/accounts-base';\nimport { Meteor } from 'meteor/meteor';\nimport { check } from 'meteor/check';\n\n\n\n/**\n * Called once on server creation\n * @param {*} currentContext\n */\nexport const initContext = currentContext => {\n  let context;\n\n  if (currentContext) {\n    context = { ...currentContext };\n  } else {\n    context = {};\n  }\n\n  // add all collections to context\n  Collections.forEach(c => (context[c.collectionName] = c));\n\n  // merge with custom context\n  // TODO: deepmerge created an infinite loop here\n  context = _merge({}, context, GraphQLSchema.context);\n  return context;\n};\n\nimport Cookies from 'universal-cookie';\n\n// initial request will get the login token from a cookie, subsequent requests from\n// the header\nexport const getAuthToken = req => {\n  return req.headers.authorization || new Cookies(req.cookies).get('meteor_login_token');\n};\n\nconst getUser = async loginToken => {\n  if (loginToken) {\n    check(loginToken, String)\n\n    const hashedToken = Accounts._hashLoginToken(loginToken)\n\n    const user = await Meteor.users.rawCollection().findOne({\n      'services.resume.loginTokens.hashedToken': hashedToken\n    })\n\n    if (user) {\n      // find the right login token corresponding, the current user may have\n      // several sessions logged on different browsers / computers\n      const tokenInformation = user.services.resume.loginTokens.find(\n        tokenInfo => tokenInfo.hashedToken === hashedToken\n      )\n\n      const expiresAt = Accounts._tokenExpiration(tokenInformation.when)\n\n      const isExpired = expiresAt < new Date()\n\n      if (!isExpired) {\n        return user\n      }\n    }\n  }\n}\n\n// @see https://www.apollographql.com/docs/react/recipes/meteor#Server\nexport const setupAuthToken = async (context, req) => {\n  const authToken = getAuthToken(req);\n  const user = await getUser(authToken);\n  if (user) {\n    context.userId = user._id;\n    context.currentUser = user;\n    // Not useful\n    //context.authToken = authToken;\n    // identify user to any server-side analytics providers\n    runCallbacks('events.identify', user);\n  } else {\n    context.userId = undefined;\n    context.currentUser = undefined;\n  }\n};\n\n// @see https://github.com/facebook/dataloader#caching-per-request\nconst generateDataLoaders = context => {\n  // go over context and add Dataloader to each collection\n  Collections.forEach(collection => {\n    context[collection.options.collectionName].loader = new DataLoader(ids => findByIds(collection, ids, context), {\n      cache: true,\n    });\n  });\n  return context;\n};\n\n// Returns a function called on every request to compute context\nexport const computeContextFromReq = (currentContext, customContextFromReq) => {\n  // givenOptions can be either a function of the request or an object\n  const getBaseContext = req => (customContextFromReq ? { ...currentContext, ...customContextFromReq(req) } : { ...currentContext });\n\n  // create options given the current request\n  const handleReq = async req => {\n    const { headers } = req;\n    let context;\n\n    // eslint-disable-next-line no-unused-vars\n    let user = null;\n\n    context = getBaseContext(req);\n\n    generateDataLoaders(context);\n\n    // note: custom default resolver doesn't currently work\n    // see https://github.com/apollographql/apollo-server/issues/716\n    // @options.fieldResolver = (source, args, context, info) => {\n    //   return source[info.fieldName];\n    // }\n\n    await setupAuthToken(context, req);\n\n    //add the headers to the context\n    context.headers = headers;\n    // pass the whole req for advanced usage, like fetching IP from connection\n    context.req = req;\n\n    // if apiKey is present, assign \"fake\" currentUser with admin rights\n    if (headers.apikey && headers.apikey === getSetting('vulcan.apiKey')) {\n      context.currentUser = { isAdmin: true, isApiUser: true };\n    }\n\n    context.locale = getHeaderLocale(headers, context.currentUser && context.currentUser.locale);\n    const locale = getLocale(context.locale);\n\n    // see https://forums.meteor.com/t/can-i-edit-html-tag-in-meteor/5867/7\n    WebApp.addHtmlAttributeHook(function() {\n      let htmlAttributes = {\n        lang: context.locale\n      };\n      if (locale?.rtl === true) {\n        htmlAttributes.class = 'rtl';\n      } else {\n        htmlAttributes.class = 'ltr';\n      }\n      return htmlAttributes;\n    });\n\n    context = await runCallbacks({ name: 'graphql.context', iterator: context });\n\n    return context;\n  };\n\n  return handleReq;\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-server/engine.js",
    "content": "import { getSetting } from '../../modules/settings.js';\n// @see https://www.apollographql.com/docs/apollo-server/api/apollo-server.html#EngineReportingOptions\n\nlet engineConfigObject = getSetting('apolloEngine');\n\nif (!engineConfigObject || !engineConfigObject.apiKey) {\n  engineConfigObject = {\n    apiKey: process.env.ENGINE_API_KEY,\n    schemaTag: process.env.ENGINE_SCHEMA_TAG\n  };\n}\n\nexport const engineConfig = engineConfigObject && engineConfigObject.apiKey ? engineConfigObject : undefined;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-server/graphiql.js",
    "content": "export const getGraphiqlConfig = currentConfig => ({\n  endpointURL: currentConfig.path,\n  passHeader: \"'Authorization': localStorage['Meteor.loginToken']\", // eslint-disable-line quotes\n});\n\n// LEGACY SUPPORT FOR GRAPHIQL\n// Code is taken from apollo 1.4 code and\n// @see https://github.com/eritikass/express-graphiql-middleware\n// This is the only way to get graphiql to work\n\nimport url from 'url';\n\n// @seehttps://github.com/apollographql/apollo-server/blob/v1.4.0/packages/apollo-server-module-graphiql/src/resolveGraphiQLString.ts\n\n// renderGraphiQL\n/*\n * Mostly taken straight from express-graphql, so see their licence\n * (https://github.com/graphql/express-graphql/blob/master/LICENSE)\n */\n/*\n * Arguments:\n *\n * - endpointURL: the relative or absolute URL for the endpoint which GraphiQL will make queries to\n * - (optional) query: the GraphQL query to pre-fill in the GraphiQL UI\n * - (optional) variables: a JS object of variables to pre-fill in the GraphiQL UI\n * - (optional) operationName: the operationName to pre-fill in the GraphiQL UI\n * - (optional) result: the result of the query to pre-fill in the GraphiQL UI\n * - (optional) passHeader: a string that will be added to the header object.\n * For example \"'Authorization': localStorage['Meteor.loginToken']\" for meteor\n * - (optional) editorTheme: a CodeMirror theme to be applied to the GraphiQL UI\n * - (optional) websocketConnectionParams: an object to pass to the web socket server\n */\n// Current latest version of GraphiQL.\nconst GRAPHIQL_VERSION = '0.11.11';\nconst SUBSCRIPTIONS_TRANSPORT_VERSION = '0.9.9';\n\n// Ensures string values are safe to be used within a <script> tag.\n// TODO: I don't think that's the right escape function\nfunction safeSerialize(data) {\n  return data ? JSON.stringify(data).replace(/\\//g, '\\\\/') : null;\n}\n\nexport function renderGraphiQL(data) {\n  const endpointURL = data.endpointURL;\n  const endpointWs = endpointURL.startsWith('ws://') || endpointURL.startsWith('wss://');\n  const subscriptionsEndpoint = data.subscriptionsEndpoint;\n  const usingHttp = !endpointWs;\n  const usingWs = endpointWs || !!subscriptionsEndpoint;\n  const endpointURLWs = usingWs && (endpointWs ? endpointURL : subscriptionsEndpoint);\n\n  const queryString = data.query;\n  const variablesString = data.variables ? JSON.stringify(data.variables, null, 2) : null;\n  const resultString = null;\n  const operationName = data.operationName;\n  const passHeader = data.passHeader ? data.passHeader : '';\n  const editorTheme = data.editorTheme;\n  const usingEditorTheme = !!editorTheme;\n  const websocketConnectionParams = data.websocketConnectionParams || null;\n  const rewriteURL = !!data.rewriteURL;\n\n  /* eslint-disable max-len */\n  return `\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\" />\n  <title>GraphiQL</title>\n  <meta name=\"robots\" content=\"noindex\" />\n  <style>\n    html, body {\n      height: 100%;\n      margin: 0;\n      overflow: hidden;\n      width: 100%;\n    }\n  </style>\n  <link href=\"//unpkg.com/graphiql@${GRAPHIQL_VERSION}/graphiql.css\" rel=\"stylesheet\" />\n  <script src=\"//unpkg.com/react@15.6.1/dist/react.min.js\"></script>\n  <script src=\"//unpkg.com/react-dom@15.6.1/dist/react-dom.min.js\"></script>\n  <script src=\"//unpkg.com/graphiql@${GRAPHIQL_VERSION}/graphiql.min.js\"></script>\n  ${\n    usingEditorTheme\n      ? `<link href=\"//cdn.jsdelivr.net/npm/codemirror@5/theme/${editorTheme}.min.css\" rel=\"stylesheet\" />`\n      : ''\n  }\n  ${usingHttp ? '<script src=\"//cdn.jsdelivr.net/fetch/2.0.1/fetch.min.js\"></script>' : ''}\n  ${\n    usingWs\n      ? `<script src=\"//unpkg.com/subscriptions-transport-ws@${SUBSCRIPTIONS_TRANSPORT_VERSION}/browser/client.js\"></script>`\n      : ''\n  }\n  ${\n    usingWs && usingHttp\n      ? '<script src=\"//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js\"></script>'\n      : ''\n  }\n</head>\n<body>\n  <script>\n    // Collect the URL parameters\n    var parameters = {};\n    window.location.search.substr(1).split('&').forEach(function (entry) {\n      var eq = entry.indexOf('=');\n      if (eq >= 0) {\n        parameters[decodeURIComponent(entry.slice(0, eq))] =\n          decodeURIComponent(entry.slice(eq + 1));\n      }\n    });\n    // Produce a Location query string from a parameter object.\n    function locationQuery(params, location) {\n      return (location ? location: '') + '?' + Object.keys(params).map(function (key) {\n        return encodeURIComponent(key) + '=' +\n          encodeURIComponent(params[key]);\n      }).join('&');\n    }\n    // Derive a fetch URL from the current URL, sans the GraphQL parameters.\n    var graphqlParamNames = {\n      query: true,\n      variables: true,\n      operationName: true\n    };\n    var otherParams = {};\n    for (var k in parameters) {\n      if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {\n        otherParams[k] = parameters[k];\n      }\n    }\n    ${\n      usingWs\n        ? `\n    var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient('${endpointURLWs}', {\n      reconnect: true${\n        websocketConnectionParams\n          ? `,\n      connectionParams: ${JSON.stringify(websocketConnectionParams)}`\n          : ''\n      }\n    });\n    var graphQLWSFetcher = subscriptionsClient.request.bind(subscriptionsClient);\n    `\n        : ''\n    }\n    ${\n      usingHttp\n        ? `\n      // We don't use safe-serialize for location, because it's not client input.\n      var fetchURL = locationQuery(otherParams, '${endpointURL}');\n      // Defines a GraphQL fetcher using the fetch API.\n      function graphQLHttpFetcher(graphQLParams) {\n          return fetch(fetchURL, {\n            method: 'post',\n            headers: {\n              'Accept': 'application/json',\n              'Content-Type': 'application/json',\n              ${passHeader}\n            },\n            body: JSON.stringify(graphQLParams),\n            credentials: 'same-origin',\n          }).then(function (response) {\n            return response.text();\n          }).then(function (responseBody) {\n            try {\n              return JSON.parse(responseBody);\n            } catch (error) {\n              return responseBody;\n            }\n          });\n      }\n    `\n        : ''\n    }\n    ${\n      usingWs && usingHttp\n        ? `\n      var fetcher =\n        window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLHttpFetcher);\n    `\n        : `\n      var fetcher = ${usingWs ? 'graphQLWSFetcher' : 'graphQLHttpFetcher'};\n    `\n    }\n    // When the query and variables string is edited, update the URL bar so\n    // that it can be easily shared.\n    function onEditQuery(newQuery) {\n      parameters.query = newQuery;\n      ${rewriteURL ? 'updateURL();' : ''}\n    }\n    function onEditVariables(newVariables) {\n      parameters.variables = newVariables;\n      ${rewriteURL ? 'updateURL();' : ''}\n    }\n    function onEditOperationName(newOperationName) {\n      parameters.operationName = newOperationName;\n      ${rewriteURL ? 'updateURL();' : ''}\n    }\n    function updateURL() {\n      var cleanParams = Object.keys(parameters).filter(function(v) {\n        return parameters[v];\n      }).reduce(function(old, v) {\n        old[v] = parameters[v];\n        return old;\n      }, {});\n      history.replaceState(null, null, locationQuery(cleanParams) + window.location.hash);\n    }\n    // Render <GraphiQL /> into the body.\n    ReactDOM.render(\n      React.createElement(GraphiQL, {\n        fetcher: fetcher,\n        onEditQuery: onEditQuery,\n        onEditVariables: onEditVariables,\n        onEditOperationName: onEditOperationName,\n        query: ${safeSerialize(queryString)},\n        response: ${safeSerialize(resultString)},\n        variables: ${safeSerialize(variablesString)},\n        operationName: ${safeSerialize(operationName)},\n        editorTheme: ${safeSerialize(editorTheme)},\n        websocketConnectionParams: ${safeSerialize(websocketConnectionParams)},\n      }),\n      document.body\n    );\n  </script>\n</body>\n</html>`;\n}\n\n/////////////////////////////\n// resolveGraphiqlString\nfunction isOptionsFunction(arg) {\n  return typeof arg === 'function';\n}\n\nasync function resolveGraphiQLOptions(options, ...args) {\n  if (isOptionsFunction(options)) {\n    try {\n      return await options(...args);\n    } catch (e) {\n      throw new Error(`Invalid options provided for GraphiQL: ${e.message}`);\n    }\n  } else {\n    return options;\n  }\n}\n\nfunction createGraphiQLParams(query) {\n  const queryObject = query || {};\n  return {\n    query: queryObject.query || '',\n    variables: queryObject.variables,\n    operationName: queryObject.operationName || '',\n  };\n}\n\nfunction createGraphiQLData(params, options) {\n  return {\n    endpointURL: options.endpointURL,\n    subscriptionsEndpoint: options.subscriptionsEndpoint,\n    query: params.query || options.query,\n    variables: (params.variables && JSON.parse(params.variables)) || options.variables,\n    operationName: params.operationName || options.operationName,\n    passHeader: options.passHeader,\n    editorTheme: options.editorTheme,\n    websocketConnectionParams: options.websocketConnectionParams,\n    rewriteURL: options.rewriteURL,\n  };\n}\n\nasync function resolveGraphiQLString(query, options, ...args) {\n  const graphiqlParams = createGraphiQLParams(query);\n  const graphiqlOptions = await resolveGraphiQLOptions(options, ...args);\n  const graphiqlData = createGraphiQLData(graphiqlParams, graphiqlOptions);\n  return renderGraphiQL(graphiqlData);\n}\n\n//////////////////\n// https://github.com/eritikass/express-graphiql-middleware\n\n/* This middleware returns the html for the GraphiQL interactive query UI\n *\n * GraphiQLData arguments\n *\n * - endpointURL: the relative or absolute URL for the endpoint which GraphiQL will make queries to\n * - (optional) query: the GraphQL query to pre-fill in the GraphiQL UI\n * - (optional) variables: a JS object of variables to pre-fill in the GraphiQL UI\n * - (optional) operationName: the operationName to pre-fill in the GraphiQL UI\n * - (optional) result: the result of the query to pre-fill in the GraphiQL UI\n */\n\nexport const graphiqlMiddleware = options => {\n  const graphiqlHandler = (req, res, next) => {\n    const query = req.url && url.parse(req.url, true).query;\n    resolveGraphiQLString(query, options, req).then(\n      graphiqlString => {\n        res.setHeader('Content-Type', 'text/html');\n        res.write(graphiqlString);\n        res.end();\n      },\n      error => next(error)\n    );\n  };\n  return graphiqlHandler;\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-server/index.js",
    "content": "export * from './apollo_server';\nexport * from './settings';\nexport * from './context.js';\n\nexport { default as initGraphQL } from './initGraphQL';\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-server/initGraphQL.js",
    "content": "/**\n * Init the graphQL schema\n */\n\nimport { makeExecutableSchema } from '@graphql-tools/schema';\nimport { mergeSchemas } from '@graphql-tools/merge';\n\nimport { GraphQLSchema, generateTypeDefs } from '../graphql/index.js';\nimport { runCallbacks } from '../../modules/callbacks.js';\n\n\nconst initGraphQL = () => {\n  runCallbacks('graphql.init.before');\n  const typeDefs = generateTypeDefs(GraphQLSchema);\n\n  const executableSchema = makeExecutableSchema({\n    typeDefs,\n    resolvers: GraphQLSchema.resolvers,\n  });\n  // only call mergeSchemas if we actually have stitchedSchemas\n  let mergedSchema = GraphQLSchema.stitchedSchemas.length > 0 ? mergeSchemas({ schemas: [executableSchema, ...GraphQLSchema.stitchedSchemas] }) : executableSchema;\n\n  // execute each directive transformer successively\n  for (const directiveTransformer of GraphQLSchema.directiveTransformers) {\n    mergedSchema = directiveTransformer(mergedSchema);\n  }\n\n  GraphQLSchema.finalSchema = typeDefs;\n  GraphQLSchema.executableSchema = mergedSchema;\n  return executableSchema;\n};\n\nexport default initGraphQL;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-server/playground.js",
    "content": "/** GraphQL Playground setup, through Apollo \"gui\" option */\n\nexport const getPlaygroundConfig = currentConfig => {\n  // NOTE: this is redundant, Apollo won't show the GUI if NODE_ENV=\"production\"\n  if (!Meteor.isDevelopment) return undefined;\n  return {\n    endpoint: currentConfig.path,\n    // allow override\n    //FIXME: this global option does not exist yet...\n    // @see https://github.com/prisma/graphql-playground/issues/510\n    //headers: { [\"Authorization\"]: 'localStorage[\\'Meteor.loginToken\\']' },\n    // to set up headers, we are forced to create a tab\n    tabs: [\n      {\n        endpoint: currentConfig.path,\n        query: '{ currentUser { _id }}',\n        // TODO: does not work, we should use a cookie instead?\n        // @see https://github.com/prisma/graphql-playground/issues/849\n        // headers: {['Authorization']: \"localStorage['Meteor.loginToken']\"},\n      },\n    ],\n    settings: {\n      'editor.reuseHeaders': true,\n      // pass cookies?\n      'request.credentials': 'same-origin',\n    },\n    ...(currentConfig.gui || {}),\n  };\n};\nexport default getPlaygroundConfig;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-server/settings.js",
    "content": "import _merge from 'lodash/merge';\nimport { registerSetting } from '../../modules/settings';\n\nregisterSetting('apolloServer.corsWhitelist', [], \"Array of domains allowed for CORS e.g ['https://my-frontend.com', 'https://my-admin-dashboard']\", false);\n\n\n//import { registerSetting } from '../../modules/settings.js';\n// TODO: is this still necessary?\n//registerSetting('apolloEngine.logLevel', 'INFO', 'Log level (one of INFO, DEBUG, WARN, ERROR');\n//registerSetting(\n//  'apolloTracing',\n//  Meteor.isDevelopment,\n//  'Tracing by Apollo. Default is true on development and false on prod',\n//  true\n//);\n// registerSetting('apolloServer.jsonParserOptions.limit', undefined, 'bodyParser jsonParser limit');\n\n// NOTE: some option can be functions, so they cannot be\n// defined as Meteor settings, which are pure JSON (no function)\n\n// @see https://www.apollographql.com/docs/apollo-server/api/apollo-server.html#constructor-options-lt-ApolloServer-gt\nlet apolloServerOptions = {};\nexport const registerApolloServerOptions = options => {\n  apolloServerOptions = _merge(apolloServerOptions, options);\n};\nexport const getApolloServerOptions = () => apolloServerOptions;\n\n// @see https://www.apollographql.com/docs/apollo-server/api/apollo-server.html#Parameters-2\nlet apolloApplyMiddlewareOptions = {};\nexport const registerApolloApplyMiddlewareOptions = options => {\n  apolloApplyMiddlewareOptions = _merge(apolloApplyMiddlewareOptions, options);\n};\nexport const getApolloApplyMiddlewareOptions = () => apolloApplyMiddlewareOptions;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-server/startup.js",
    "content": "const { onStart } = require('./apollo_server');\n// createApolloServer when server startup\nMeteor.startup(onStart);\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-server/voyager.js",
    "content": "export const getVoyagerConfig = currentConfig => ({\n  endpointUrl: currentConfig.path,\n});\nexport default getVoyagerConfig;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-ssr/apolloClient.js",
    "content": "/*\n * This client is used to prefetch data server side\n * (necessary for SSR)\n *\n * /!\\ It must be recreated on every request\n */\n\nimport { ApolloClient, InMemoryCache } from '@apollo/client';\n\nimport { SchemaLink } from '@apollo/client/link/schema';\nimport { GraphQLSchema } from '../graphql/index.js';\n\n// import { createStateLink } from '../../modules/apollo-common/links/state.js';\nimport { ApolloLink } from 'apollo-link';\n\nimport { getFragmentMatcher } from '../../modules/fragment_matcher';\n\n// @see https://www.apollographql.com/docs/react/features/server-side-rendering.html#local-queries\n// import { createHttpLink } from 'apollo-link-http';\n// import fetch from 'node-fetch'\n\nexport const createClient = async ({ req, computeContext }) => {\n  // init\n  // stateLink will init the client internal state\n  const cache = new InMemoryCache({ fragmentMatcher: getFragmentMatcher() });\n  // const stateLink = createStateLink({ cache });\n  // schemaLink will fetch data directly based on the executable schema\n  const schema = GraphQLSchema.getExecutableSchema();\n  // this is the resolver context\n  const context = await computeContext(req);\n  const schemaLink = new SchemaLink({ schema, context });\n  const client = new ApolloClient({\n    ssrMode: true,\n    link: ApolloLink.from([/*stateLink,*/ schemaLink]),\n    // @see https://www.apollographql.com/docs/react/features/server-side-rendering.html#local-queries\n    // Remember that this is the interface the SSR server will use to connect to the\n    // API server, so we need to ensure it isn't firewalled, etc\n    //link: createHttpLink({\n    //    uri: 'http://localhost:3000',\n    //    credentials: 'same-origin',\n    //    headers: {\n    //        // NOTE: this is a Connect req, not an Express req,\n    //        // so req.header is not defined\n    //        // cookie: req.header('Cookie'),\n    //        cookie: req.headers['cookie'],\n    //    },\n    //    // need to explicitely pass fetch server side\n    //    fetch\n    //}),\n    cache,\n  });\n  return client;\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-ssr/components/ApolloState.jsx",
    "content": "/**\n * Component that serialize the Apollo client state\n * \n * The client can then deserialize it and avoid unecessary requests\n */\nimport React from 'react';\n\nconst ApolloState = ({ initialState }) => (\n  <script\n    dangerouslySetInnerHTML={{\n      __html: `window.__APOLLO_STATE__ = ${JSON.stringify(initialState).replace(\n        /</g,\n        '\\\\u003c'\n      )};`\n    }}\n  />\n);\nexport default ApolloState;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-ssr/components/AppGenerator.jsx",
    "content": "/**\n * The App + relevant wrappers\n */\nimport React from 'react';\nimport { ApolloProvider } from '@apollo/client';\nimport { StaticRouter } from 'react-router';\n\nimport { Components } from 'meteor/vulcan:lib';\n\nimport { CookiesProvider } from 'react-cookie';\n\nimport Cookies from 'universal-cookie';\nimport { getHeaderLocale } from '../../intl.js';\n\n// The client-side App will instead use <BrowserRouter>\n// see client-side vulcan:core/lib/client/start.jsx implementation\n// we do the same server side\nconst AppGenerator = ({ req, apolloClient, context }) => {\n  // TODO: universalCookies should be defined here, but it isn't\n  // @see https://github.com/meteor/meteor-feature-requests/issues/174#issuecomment-441047495\n  const cookies = new Cookies(req.cookies); // req.universalCookies;\n  const App = (\n    <ApolloProvider client={apolloClient}>\n      <StaticRouter location={req.url} context={context}>\n        <CookiesProvider cookies={cookies}>\n          <Components.App locale={getHeaderLocale(req.headers)}/>\n        </CookiesProvider>\n      </StaticRouter>\n    </ApolloProvider>\n  );\n  return App;\n};\nexport default AppGenerator;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-ssr/components/Head.jsx",
    "content": "import React from 'react';\nimport { Helmet } from 'react-helmet';\n\nconst Head = () => {\n  // Helmet.rewind() is deprecated in favour of renderStatic() for better readability\n  //@see https://github.com/nfl/react-helmet/releases/tag/5.0.0\n  const helmet = Helmet.renderStatic();\n  return (\n    <React.Fragment>\n      {helmet.title.toComponent()}\n      {helmet.meta.toComponent()}\n      {helmet.link.toComponent()}\n    </React.Fragment>\n  );\n};\nexport default Head;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-ssr/enableSSR.js",
    "content": "/**\n * Actually enable SSR\n */\n\nimport { runCallbacks, populateComponentsApp, populateRoutesApp, initializeFragments } from 'meteor/vulcan:lib';\n// onPageLoad is mostly equivalent to an Express middleware\n// excepts it is tailored to handle Meteor server side rendering\nimport { onPageLoad } from 'meteor/server-render';\n\nimport makePageRenderer from './renderPage';\n\nconst enableSSR = ({ computeContext }) => {\n  Meteor.startup(() => {\n    runCallbacks('populate.before');\n    // init the application components and routes, including components & routes from 3rd-party packages\n    initializeFragments();\n    populateComponentsApp();\n    populateRoutesApp();\n    // render the page\n    onPageLoad(makePageRenderer({ computeContext }));\n  });\n};\n\nexport default enableSSR;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-ssr/index.js",
    "content": "export { default as enableSSR } from './enableSSR';\nexport * from './inject_data';\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-ssr/injectDefaultData.js",
    "content": "import { InjectData } from './inject_data';\nimport moment from 'moment';\n\n/**\n * Data included in every requests\n */\n\nconst injectDefaultData = (req, { responseHeaders }) => {\n    /* legacy: InjectData was part of fast-render and use to take the http res as first param \n    now we simultate the _headers props to allow checking CORS\n    */\n    const res = { _headers: responseHeaders };\n    const data =\n        [\n            ['utcOffset', moment().utcOffset()],\n            ['url', req.url]\n        ];\n    data.forEach(([key, value]) => {\n        // TODO: InjectData is not very efficient\n        InjectData.pushData(\n            res,\n            key,\n            value\n        );\n    });\n    return res;\n};\nexport default injectDefaultData;"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-ssr/inject_data.js",
    "content": "import { EJSON } from 'meteor/ejson';\n//import { webAppConnectHandlersUse } from './meteor_patch.js';\n\n// InjectData object\nexport const InjectData = {\n\n  // data object initialized with offset\n  _data: {},\n\n  // encode object to string\n  _encode(ejson) {\n    try {\n      const ejsonString = EJSON.stringify(ejson);\n      return encodeURIComponent(ejsonString);\n    } catch (error) {\n      return null;\n    }\n  },\n\n  // decode string to object\n  _decode(encodedEjson) {\n    const decodedEjsonString = decodeURIComponent(encodedEjson);\n    if (!decodedEjsonString) return null;\n    return EJSON.parse(decodedEjsonString);\n  },\n\n  // push data to res._injectPayload and generate res._injectHtml\n  pushData(res, key, value) {\n\n    this._data[key] = value;\n\n    if (!res._injectPayload) {\n      res._injectPayload = {};\n    }\n\n    res._injectPayload[key] = value;\n\n    // if cors headers included if may cause some security holes\n    // so we simply turn off injecting if we detect an cors header\n    // read more: http://goo.gl/eGwb4e\n    if (res._headers && res._headers['access-control-allow-origin']) {\n      const warnMessage =\n        'warn: injecting data turned off due to CORS headers. ' +\n        'read more: http://goo.gl/eGwb4e';\n      console.warn(warnMessage); // eslint-disable-line no-console\n      return;\n    }\n\n    // inject data\n    const data = this._encode(res._injectPayload);\n    res._injectHtml = `<script type=\"text/inject-data\">${data}</script>`;\n  },\n\n  // get data from res._injectPayload\n  getData(res, key) {\n    if (res._injectPayload) {\n      // same as _.clone(res._injectPayload[key]);\n      const data = res._injectPayload[key];\n      try {\n        const clonedData = EJSON.parse(EJSON.stringify(data));\n        return clonedData;\n      } catch (error) {\n        return null;\n      }\n    }\n    return null;\n  },\n};\n\n// **injectDataMiddleware, Notes that it must after router connect handler**\n/*\nNow used directly during render\nwebAppConnectHandlersUse(function injectDataMiddleware(req, res, next) {\n  if (res._injectHtml) {\n    req.dynamicHead = req.dynamicHead || '';\n    req.dynamicHead += res._injectHtml;\n  }\n  next();\n}, { order: 900 });\n\n*/"
  },
  {
    "path": "packages/vulcan-lib/lib/server/apollo-ssr/renderPage.js",
    "content": "/**\n * Render the page server side\n * @see https://github.com/szomolanyi/MeteorApolloStarter/blob/master/imports/startup/server/ssr.js\n * @see https://github.com/apollographql/GitHunt-React/blob/master/src/server.js\n * @see https://www.apollographql.com/docs/react/features/server-side-rendering.html#renderToStringWithData\n */\nimport React from 'react';\nimport ReactDOM from 'react-dom/server';\nimport { getDataFromTree } from '@apollo/client/react/ssr';\n\nimport { runCallbacks } from '../../modules/callbacks';\nimport { createClient } from './apolloClient';\n\nimport Head from './components/Head';\nimport ApolloState from './components/ApolloState';\nimport AppGenerator from './components/AppGenerator';\nimport injectDefaultData from './injectDefaultData';\n\nconst makePageRenderer = ({ computeContext }) => {\n  // onPageLoad callback\n  const renderPage = async sink => {\n    const req = sink.request;\n    // according to the Apollo doc, client needs to be recreated on every request\n    // this avoids caching server side\n    const client = await createClient({ req, computeContext });\n\n    // Used by callbacks to handle side effects\n    // E.g storing the stylesheet generated by styled-components\n    const context = {};\n\n    // TODO: req object does not seem to have been processed by the Express\n    // middlewares at this point\n    // @see https://github.com/meteor/meteor-feature-requests/issues/174#issuecomment-441047495\n\n    const App = <AppGenerator req={req} apolloClient={client} context={context} />;\n\n    let htmlContent = '';\n    try {\n      // run user registered callbacks that wraps the React app\n      // The wrappers must NOT have any side effect during React tree traversal\n      // otherwise SSR may fail\n      const WrappedApp = runCallbacks({\n        name: 'router.server.wrapper',\n        iterator: App,\n        properties: { req, context, apolloClient: client },\n      });\n\n\n      // run wrappers that must only me applied during the data collection step\n      // eg Material UI theming WITHOUT style generation\n      // The wrappers must NOT have any side effect during React tree traversal\n      // otherwise SSR may fail\n      const DataWrappedApp = runCallbacks({\n        name: 'router.server.dataWrapper',\n        iterator: WrappedApp,\n        properties: { req, context, apolloClient: client },\n      });\n      // fill apollo store\n      // NOTE: we CAN'T use renderToStringWithData on the wrapped app (so with Material UI, styled components etc.), \n      //because react-apollo may trigger style generation while walking the React tree to find query\n      await getDataFromTree(DataWrappedApp);\n\n      // run callback related to rendering\n      // eg Material UI theming WITH style generation\n      // those wrapper can tolerate side effects during React tree traversal (eg className/styles generation)\n      const StyledWrappedApp = runCallbacks({\n        name: 'router.server.renderWrapper',\n        iterator: WrappedApp,\n        properties: { req, context, apolloClient: client },\n      });\n      // equivalent to calling getDataFromTree and then renderToStringWithData\n      htmlContent = await ReactDOM.renderToString(StyledWrappedApp);\n    } catch (err) {\n      // eslint-disable-next-line no-console\n      console.error(`Error while server-rendering. date: ${new Date().toString()} url: ${req.url}`); // eslint-disable-line no-console\n      // eslint-disable-next-line no-console\n      console.error(err);\n      // show error in client in dev\n      if (Meteor.isDevelopment) {\n        htmlContent = `Error while server-rendering: ${err.message}`;\n      }\n    }\n\n    // TODO: there should be a cleaner way to set this wrapper\n    // id must always match the client side start.jsx file\n    const wrappedHtmlContent = `<div id=\"react-app\">${htmlContent}</div>`;\n    sink.appendToBody(wrappedHtmlContent);\n    // TODO: this sounds cleaner but where do we add the <div id=\"react-app\"> ?\n    //sink.renderIntoElementById('react-app', content)\n\n    // add headers using helmet\n    const head = ReactDOM.renderToString(<Head />);\n    sink.appendToHead(head);\n\n    // add complementary data to the HTML (previously done by inject_data)\n    const dataToInject = injectDefaultData(req, { responseHeaders: sink.responseHeaders });\n    if (dataToInject._injectHtml) {\n      sink.appendToHead(dataToInject._injectHtml);\n    }\n\n\n    // add Apollo state, the client will then parse the string\n    const initialState = client.extract();\n    const serializedApolloState = ReactDOM.renderToString(\n      <ApolloState initialState={initialState} />\n    );\n    sink.appendToBody(serializedApolloState);\n\n    // post render callback\n    runCallbacks({\n      name: 'router.server.postRender',\n      iterator: sink,\n      properties: { context },\n    });\n  };\n  return renderPage;\n};\n\nexport default makePageRenderer;"
  },
  {
    "path": "packages/vulcan-lib/lib/server/caching.js",
    "content": "import { Mongo } from 'meteor/mongo';\nimport { graphql } from 'graphql';\nimport { GraphQLSchema } from './graphql/index.js';\nimport NodeCache from 'node-cache';\n\nexport const nodeCache = new NodeCache();\n\nexport const CachedResults = new Mongo.Collection('cached_results');\n\n/*\n\nCache a server-side query based on the query text and variables.\n\nMongoDB version (not currently used internally)\n\n*/\nexport const useMongoQueryCache = async ({ key, query, variables, context }) => {\n  const executableSchema = GraphQLSchema.getExecutableSchema();\n  const cachedItem = (await key) ? CachedResults.findOne({ key }) : CachedResults.findOne({ query, variables });\n  if (cachedItem) {\n    return cachedItem.result;\n  } else {\n    const result = await graphql(executableSchema, query, {}, context, variables);\n    await CachedResults.insert({ createdAt: new Date(), query, variables, result, key });\n    return result;\n  }\n};\n\nexport const invalidateMongoCache = async () => {\n  CachedResults.remove({});\n};\n\n/*\n\nIn-memory version\n\n*/\nexport const useQueryCache = async ({ key, query, variables, context }) => {\n  const executableSchema = GraphQLSchema.getExecutableSchema();\n  const cachedItem = nodeCache.get(key);\n  if (cachedItem) {\n    return cachedItem.result;\n  } else {\n    const result = await graphql(executableSchema, query, {}, context, variables);\n    nodeCache.set(key, { createdAt: new Date(), query, variables, result });\n    return result;\n  }\n};\n\nexport const invalidateCache = () => {\n  nodeCache.flushAll();\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/connectors/mongo.js",
    "content": "import { DatabaseConnectors } from '../connectors.js';\nimport merge from 'lodash/merge';\nimport isEmpty from 'lodash/isEmpty';\n\nimport { convertSelector, convertUniqueSelector, filterFunction } from '../../modules/mongoParams';\nimport { runCallbacks } from '../../modules/index.js';\n\n/*\n\nConnectors\n\n*/\nDatabaseConnectors.mongo = {\n  get: async (collection, selector = {}, options = {}) => {\n    return await collection.findOne(convertUniqueSelector(selector), options);\n  },\n  find: async (collection, selector = {}, options = {}) => {\n    return await collection.find(convertSelector(selector), options).fetch();\n  },\n  count: async (collection, selector = {}, options = {}) => {\n    return await collection.find(convertSelector(selector), options).count();\n  },\n  create: async (collection, document, options = {}) => {\n    return await collection.insert(document);\n  },\n  update: async (collection, selector, modifier, options = {}) => {\n    return await collection.update(convertUniqueSelector(selector), modifier, options);\n  },\n  delete: async (collection, selector, options = {}) => {\n    return await collection.remove(convertUniqueSelector(selector));\n  },\n  filter: async (collection, input, context) => {\n    /*\n\n    When a collection is created, a defaultInput option can be passed\n    in order to specify default `filter`, `limit`, `sort`, etc. \n    values that should always apply. \n\n    */\n    const defaultInputObject = await filterFunction(collection, collection.options.defaultInput, context);\n    const currentInputObject = await filterFunction(collection, input, context);\n    if (defaultInputObject.options.sort && currentInputObject.options.sort) {\n      // for sort only, delete default sort instead of merging to avoid issue with\n      // default sort coming first in list of sort specifiers\n      delete defaultInputObject.options.sort;\n    }\n    const mergedInputObject = {\n      selector: isEmpty(currentInputObject.selector) ? defaultInputObject.selector : currentInputObject.selector,\n      options: isEmpty(currentInputObject.options) ? defaultInputObject.options : currentInputObject.options,\n      filteredFields: currentInputObject.filteredFields || [],\n    };\n\n    const { typeName } = collection.options;\n    const finalInputObject = await runCallbacks({\n      name: `${typeName.toLowerCase()}.connector.filter`,\n      iterator: mergedInputObject,\n      properties: { collection, context },\n    });\n    return finalInputObject;\n  },\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/connectors.js",
    "content": "import { getSetting } from '../modules/settings';\nimport { addCallback } from '../modules/callbacks';\n\nconst database = getSetting('database', 'mongo');\n\nexport const DatabaseConnectors = {};\n\nexport let Connectors = {};\n\nfunction initializeConnectors () {\n  Connectors = DatabaseConnectors[database];\n}\n\naddCallback('app.startup', initializeConnectors);"
  },
  {
    "path": "packages/vulcan-lib/lib/server/debug.js",
    "content": "import { GraphQLSchema } from './graphql/graphql.js';\nimport { generateTypeDefs } from './graphql/typedefs.js';\nimport Vulcan from '../modules/config.js';\nimport fs, { promises as fsAsync } from 'fs';\n\nimport { Collections } from '../modules/collections.js';\nimport { extractCollectionInfo, extractFragmentInfo } from '../modules/handleOptions';\n\nimport { multiClientTemplate, singleClientTemplate } from '../modules/graphql_templates';\nimport { Fragments } from '../modules/fragments';\nimport { Utils } from '../modules/utils';\n\nimport get from 'lodash/get';\n\n/*\n\n  Can be called from the Meteor shell (type `meteor shell` in your app repo)\n\n  */\nexport const getGraphQLSchema = fileName => {\n  let schema;\n  if (!GraphQLSchema.finalSchema) {\n    schema = generateTypeDefs(GraphQLSchema)[0];\n    // eslint-disable-next-line no-console\n    console.log('Warning: trying to access final GraphQL schema before it has been created by the server.');\n  } else {\n    schema = GraphQLSchema.getSchema();\n  }\n\n  const name = fileName ? fileName : 'schema.graphql';\n  logToFile(name, schema, { mode: 'overwrite' });\n\n  return schema;\n};\n\nVulcan.getGraphQLSchema = getGraphQLSchema;\n\nconst logsDirectory = '.logs';\n\nexport const logToFile = async (fileName, object, options = {}) => {\n  const { mode = 'append', timestamp = false } = options;\n  // the server path is of type \"/Users/foo/bar/appName/.meteor/local/build/programs/server\"\n  // we remove the last five segments to get the app directory\n  // eslint-disable-next-line no-undef\n  const path = __meteor_bootstrap__.serverDir\n    .split('/')\n    .slice(1, -5)\n    .join('/');\n  const logsDirPath = `/${path}/${logsDirectory}`;\n  if (!fs.existsSync(logsDirPath)) {\n    fs.mkdirSync(logsDirPath, { recursive: true });\n  }\n  const fullPath = `${logsDirPath}/${fileName}`;\n  const contents = typeof object === 'string' ? object : JSON.stringify(object, null, 2);\n  const now = new Date();\n  const text = timestamp ? now.toString() + '\\n---\\n' + contents : contents;\n  if (mode === 'append') {\n    const stream = fs.createWriteStream(fullPath, { flags: 'a' });\n    stream.write(text + '\\n');\n    stream.end();\n  } else {\n    fs.readFile(fullPath, (error, data) => {\n      let shouldWrite = false;\n      if (error && error.code === 'ENOENT') {\n        // the file just does not exist, ok to write\n        shouldWrite = true;\n      } else if (error) {\n        // maybe EACCESS or something wrong with the disk\n        throw error;\n      } else {\n        const fileContent = data.toString();\n        if (fileContent !== text) {\n          shouldWrite = true;\n        }\n      }\n\n      if (shouldWrite) {\n        fs.writeFile(fullPath, text, error => {\n          // throws an error, you could also catch it here\n          if (error) throw error;\n          \n          // eslint-disable-next-line no-console\n          console.log(`New graphql schema saved to ${fullPath}`);\n        });\n      }\n    })\n  }\n};\n\nVulcan.logToFile = logToFile;\n\n/*\n  This function is aimed at enabling generation of typescript definitions\n  for the default queries provided by Vulcan.\n\n  Tools like apollo codegen:generate generate typescript definitions when\n  provided with a schema and queries/fragments.\n  */\nexport const generateGraphQLQueries = fileName => {\n  let fd;\n\n  const name = fileName ? fileName : 'queries.graphql';\n\n  try {\n    // eslint-disable-next-line no-undef\n    const path = __meteor_bootstrap__.serverDir\n      .split('/')\n      .slice(1, -5)\n      .join('/');\n    const fullPath = `/${path}/${name}`;\n    fd = fsAsync.openSync(fullPath, 'w');\n\n    Object.keys(Fragments).forEach(fragment => fsAsync.appendFileSync(fd, Fragments[fragment].fragmentText + '\\n'));\n\n    fsAsync.appendFileSync(fd, '\\n');\n\n    Collections.forEach(collection => {\n      const { collectionName } = extractCollectionInfo({ collection });\n      const { fragmentName } = extractFragmentInfo({}, collectionName);\n\n      const typeName = collection.options.typeName;\n\n      if (get(GraphQLSchema.resolvers, `Query.${Utils.camelCaseify(typeName)}`)) {\n        const singleQueryString = singleClientTemplate({\n          typeName,\n          fragmentName,\n        });\n        fsAsync.appendFileSync(fd, singleQueryString + '\\n');\n      }\n\n      if (get(GraphQLSchema.resolvers, `Query.${Utils.camelCaseify(Utils.pluralize(typeName))}`)) {\n        const multiQueryString = multiClientTemplate({\n          typeName,\n          fragmentName,\n        });\n        fsAsync.appendFileSync(fd, multiQueryString + '\\n');\n      }\n    });\n  } catch (err) {\n    console.log(err);\n  } finally {\n    if (fd !== undefined) fsAsync.closeSync(fd);\n  }\n};\n\nVulcan.generateGraphQLQueries = generateGraphQLQueries;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/default_mutations.js",
    "content": "/*\n\nDefault mutations\n\n*/\n\n// import { registerCallback } from '../modules/callbacks.js';\nimport { createMutator, updateMutator, deleteMutator } from './mutators.js';\nimport { Utils } from '../modules/utils.js';\nimport { Connectors } from './connectors.js';\nimport { generateTypeNameFromCollectionName, getCollection, getCollectionByTypeName } from '../modules/collections.js';\nimport isEmpty from 'lodash/isEmpty';\nimport get from 'lodash/get';\n\nconst defaultOptions = { create: true, update: true, upsert: true, delete: true };\n\nconst getCreateMutationName = typeName => `create${typeName}`;\nconst getUpdateMutationName = typeName => `update${typeName}`;\nconst getDeleteMutationName = typeName => `delete${typeName}`;\nconst getUpsertMutationName = typeName => `upsert${typeName}`;\n//const getMultiQueryName = (typeName) => `multi${typeName}Query`;\n\nexport function getDefaultMutations(options) {\n  let typeName, collectionName, mutationOptions;\n\n  if (typeof arguments[0] === 'object') {\n    // new single-argument API\n    typeName = arguments[0].typeName;\n    // collectionName = arguments[0].collectionName || getCollectionByTypeName(typeName).options.collectionName;\n    mutationOptions = { ...defaultOptions, ...arguments[0].options };\n  } else {\n    // OpenCRUD backwards compatibility\n    collectionName = arguments[0];\n    typeName = generateTypeNameFromCollectionName(collectionName);\n    mutationOptions = { ...defaultOptions, ...arguments[1] };\n  }\n\n  // register callbacks for documentation purposes\n  // registerCollectionCallbacks(typeName, mutationOptions);\n\n  const mutations = {};\n\n  if (mutationOptions.create) {\n    // mutation for inserting a new document\n\n    const mutationName = getCreateMutationName(typeName);\n\n    const createMutation = {\n      description: `Mutation for creating new ${typeName} documents`,\n      name: mutationName,\n\n      // check function called on a user to see if they can perform the operation\n      check(user, document, context) {\n\n        collectionName = collectionName || getCollectionByTypeName(typeName).options.collectionName;\n\n        const { Users } = context;\n\n        // new API\n        const permissionCheck = get(getCollection(collectionName), 'options.permissions.canCreate');\n        if (permissionCheck) {\n          return Users.permissionCheck({\n            check: permissionCheck,\n            user,\n            document,\n            context,\n            collection: context[collectionName],\n            operationName: 'create',\n          });\n        }\n\n        // OpenCRUD backwards compatibility\n        const check = mutationOptions.createCheck || mutationOptions.newCheck;\n        if (check) {\n          return check(user, document);\n        }\n\n        // check if they can perform \"foo.new\" operation (e.g. \"movie.new\")\n        // OpenCRUD backwards compatibility\n        return Users.canDo(user, [\n          `${typeName.toLowerCase()}.create`,\n          `${collectionName.toLowerCase()}.new`,\n        ]);\n      },\n\n      async mutation(root, args, context) {\n\n        const { input = {}, data: backwardsCompatibilityData } = args;\n\n        const data = input.data || backwardsCompatibilityData;\n        \n        if (isEmpty(data)) {\n          throw new Error(`create${typeName} received empty data object`);\n        }\n\n        collectionName = collectionName || getCollectionByTypeName(typeName).options.collectionName;\n\n        const collection = context[collectionName];\n\n        // check if current user can pass check function; else throw error\n        Utils.performCheck(\n          this.check,\n          context.currentUser,\n          data,\n          context,\n          '',\n          `${typeName}.create`,\n          collectionName\n        );\n\n        // pass document to boilerplate newMutator function\n        return await createMutator({\n          collection,\n          data,\n          currentUser: context.currentUser,\n          validate: true,\n          context,\n          contextName: input.contextName,\n        });\n      },\n    };\n    mutations.create = createMutation;\n    // OpenCRUD backwards compatibility\n    mutations.new = createMutation;\n  }\n\n  if (mutationOptions.update) {\n    // mutation for editing a specific document\n\n    const mutationName = getUpdateMutationName(typeName);\n\n    const updateMutation = {\n      description: `Mutation for updating a ${typeName} document`,\n      name: mutationName,\n\n      // check function called on a user and document to see if they can perform the operation\n      check(user, document, context) {\n\n        collectionName = collectionName || getCollectionByTypeName(typeName).options.collectionName;\n\n        const { Users } = context;\n\n        // new API\n        const permissionCheck = get(getCollection(collectionName), 'options.permissions.canUpdate');\n        if (permissionCheck) {\n          return Users.permissionCheck({\n            check: permissionCheck,\n            user,\n            document,\n            context,\n            collection: context[collectionName],\n            operationName: 'update',\n          });\n        }\n\n        // OpenCRUD backwards compatibility\n        const check = mutationOptions.updateCheck || mutationOptions.editCheck;\n        if (check) {\n          return check(user, document);\n        }\n\n        if (!user || !document) return false;\n        // check if user owns the document being edited.\n        // if they do, check if they can perform \"foo.edit.own\" action\n        // if they don't, check if they can perform \"foo.edit.all\" action\n        // OpenCRUD backwards compatibility\n        return (Users.owns(user, document)\n          && Users.canDo(user, [\n              `${typeName.toLowerCase()}.update.own`,\n              `${collectionName.toLowerCase()}.edit.own`,\n            ]))\n          || Users.canDo(user, [\n              `${typeName.toLowerCase()}.update.all`,\n              `${collectionName.toLowerCase()}.edit.all`,\n            ]);\n      },\n\n      async mutation(root, args, context) {\n\n        const { input = {}, selector: oldSelector, data: backwardsCompatibilityData } = args;\n        const { filter, id } = input;\n        const data = input.data || backwardsCompatibilityData;\n\n        collectionName = collectionName || getCollectionByTypeName(typeName).options.collectionName;\n\n        const collection = context[collectionName];\n\n        // handle both `filter` and `selector` for backwards-compatibility\n        let selector;\n        if (id) {\n          selector = { _id: id };\n        } else if (!isEmpty(filter)) {\n          const filterParameters = await Connectors.filter(collection, { filter }, context);\n          selector = filterParameters.selector;\n        } else {\n          if (!isEmpty(oldSelector)) {\n            selector = oldSelector;\n          } else {\n            throw new Error('Selector cannot be empty');\n          }\n        }\n\n        // get entire unmodified document from database\n        const document = await Connectors.get(collection, selector);\n\n        if (!document) {\n          throw new Error(\n            `Could not find document to update for selector: ${JSON.stringify(selector)}`\n          );\n        }\n\n        // check if user can perform operation; if not throw error\n        Utils.performCheck(\n          this.check,\n          context.currentUser,\n          document,\n          context,\n          document._id,\n          `${typeName}.update`,\n          collectionName\n        );\n\n        // call editMutator boilerplate function\n        return await updateMutator({\n          collection,\n          selector,\n          data,\n          currentUser: context.currentUser,\n          validate: true,\n          context,\n          document,\n          contextName: input.contextName,\n        });\n      },\n    };\n\n    mutations.update = updateMutation;\n    // OpenCRUD backwards compatibility\n    mutations.edit = updateMutation;\n  }\n  if (mutationOptions.upsert) {\n\n    // mutation for upserting a specific document\n    const mutationName = getUpsertMutationName(typeName);\n    mutations.upsert = {\n      description: `Mutation for upserting a ${typeName} document`,\n      name: mutationName,\n\n      async mutation(root, { filter, selector, data }, context) {\n\n        collectionName = collectionName || getCollectionByTypeName(typeName).options.collectionName;\n\n        const collection = context[collectionName];\n\n        // check if documeet exists already\n        const existingDocument = await Connectors.get(collection, selector, {\n          fields: { _id: 1 },\n        });\n\n        if (existingDocument) {\n          return await collection.options.mutations.update.mutation(\n            root,\n            { filter, selector, data },\n            context\n          );\n        } else {\n          return await collection.options.mutations.create.mutation(root, { data }, context);\n        }\n      },\n    };\n  }\n\n  if (mutationOptions.delete) {\n\n    // mutation for removing a specific document (same checks as edit mutation)\n\n    const mutationName = getDeleteMutationName(typeName);\n\n    const deleteMutation = {\n      description: `Mutation for deleting a ${typeName} document`,\n      name: mutationName,\n\n      check(user, document, context) {\n\n        collectionName = collectionName || getCollectionByTypeName(typeName).options.collectionName;\n\n        const { Users } = context;\n\n        // new API\n        const permissionCheck = get(getCollection(collectionName), 'options.permissions.canDelete');\n        if (permissionCheck) {\n          return Users.permissionCheck({\n            check: permissionCheck,\n            user,\n            document,\n            context,\n            collection: context[collectionName],\n            operationName: 'delete',\n          });\n        }\n\n        // OpenCRUD backwards compatibility\n        const check = mutationOptions.deleteCheck || mutationOptions.removeCheck;\n        if (check) {\n          return check(user, document);\n        }\n\n        if (!user || !document) return false;\n        // OpenCRUD backwards compatibility\n        return (Users.owns(user, document)\n          && Users.canDo(user, [\n              `${typeName.toLowerCase()}.delete.own`,\n              `${collectionName.toLowerCase()}.remove.own`,\n            ]))\n          || Users.canDo(user, [\n              `${typeName.toLowerCase()}.delete.all`,\n              `${collectionName.toLowerCase()}.remove.all`,\n            ]);\n      },\n\n      async mutation(root, args, context) {\n\n        const { input = {}, selector: oldSelector } = args;\n        const { filter, id } = input;\n\n        collectionName = collectionName || getCollectionByTypeName(typeName).options.collectionName;\n\n        const collection = context[collectionName];\n\n        // handle both `filter` and `selector` for backwards-compatibility\n        let selector;\n        if (id) {\n          selector = { _id: id };\n        } else if (!isEmpty(filter)) {\n          const filterParameters = await Connectors.filter(collection, { filter }, context);\n          selector = filterParameters.selector;\n        } else {\n          if (!isEmpty(oldSelector)) {\n            selector = oldSelector;\n          } else {\n            throw new Error('Selector cannot be empty');\n          }\n        }\n\n        const document = await Connectors.get(collection, selector);\n\n        if (!document) {\n          throw new Error(\n            `Could not find document to delete for selector: ${JSON.stringify(selector)}`\n          );\n        }\n\n        Utils.performCheck(\n          this.check,\n          context.currentUser,\n          document,\n          context,\n          document._id,\n          `${typeName}.delete`,\n          collectionName\n        );\n\n        return await deleteMutator({\n          collection,\n          selector: { _id: document._id },\n          currentUser: context.currentUser,\n          validate: true,\n          context,\n          document,\n        });\n      },\n    };\n\n    mutations.delete = deleteMutation;\n    // OpenCRUD backwards compatibility\n    mutations.remove = deleteMutation;\n  }\n\n  return mutations;\n}\n\n// const registerCollectionCallbacks = (typeName, options) => {\n//   typeName = typeName.toLowerCase();\n\n//   if (options.create) {\n//     registerCallback({\n//       name: `${typeName}.create.validate`,\n//       iterator: { validationErrors: 'An array that can be used to accumulate validation errors' },\n//       properties: [\n//         { document: 'The document being inserted' },\n//         { currentUser: 'The current user' },\n//         { collection: 'The collection the document belongs to' },\n//         { context: 'The context of the mutation' },\n//       ],\n//       runs: 'sync',\n//       returns: 'document',\n//       description:\n//         'Validate a document before insertion (can be skipped when inserting directly on server).',\n//     });\n//     registerCallback({\n//       name: `${typeName}.create.before`,\n//       iterator: { document: 'The document being inserted' },\n//       properties: [{ currentUser: 'The current user' }],\n//       runs: 'sync',\n//       returns: 'document',\n//       description: \"Perform operations on a new document before it's inserted in the database.\",\n//     });\n//     registerCallback({\n//       name: `${typeName}.create.after`,\n//       iterator: { document: 'The document being inserted' },\n//       properties: [{ currentUser: 'The current user' }],\n//       runs: 'sync',\n//       returns: 'document',\n//       description:\n//         \"Perform operations on a new document after it's inserted in the database but *before* the mutation returns it.\",\n//     });\n//     registerCallback({\n//       name: `${typeName}.create.async`,\n//       iterator: { document: 'The document being inserted' },\n//       properties: [\n//         { currentUser: 'The current user' },\n//         { collection: 'The collection the document belongs to' },\n//       ],\n//       runs: 'async',\n//       returns: null,\n//       description:\n//         \"Perform operations on a new document after it's inserted in the database asynchronously.\",\n//     });\n//   }\n//   if (options.update) {\n//     registerCallback({\n//       name: `${typeName}.update.validate`,\n//       iterator: { validationErrors: 'An object that can be used to accumulate validation errors' },\n//       properties: [\n//         { document: 'The document being edited' },\n//         { data: 'The client data' },\n//         { currentUser: 'The current user' },\n//         { collection: 'The collection the document belongs to' },\n//         { context: 'The context of the mutation' },\n//       ],\n//       runs: 'sync',\n//       returns: 'modifier',\n//       description:\n//         'Validate a document before update (can be skipped when updating directly on server).',\n//     });\n//     registerCallback({\n//       name: `${typeName}.update.before`,\n//       iterator: { data: 'The client data' },\n//       properties: [{ document: 'The document being edited' }, { currentUser: 'The current user' }],\n//       runs: 'sync',\n//       returns: 'modifier',\n//       description: \"Perform operations on a document before it's updated in the database.\",\n//     });\n//     registerCallback({\n//       name: `${typeName}.update.after`,\n//       iterator: { newDocument: 'The document after the update' },\n//       properties: [{ document: 'The document being edited' }, { currentUser: 'The current user' }],\n//       runs: 'sync',\n//       returns: 'document',\n//       description:\n//         \"Perform operations on a document after it's updated in the database but *before* the mutation returns it.\",\n//     });\n//     registerCallback({\n//       name: `${typeName}.update.async`,\n//       iterator: { newDocument: 'The document after the edit' },\n//       properties: [\n//         { document: 'The document before the edit' },\n//         { currentUser: 'The current user' },\n//         { collection: 'The collection the document belongs to' },\n//       ],\n//       runs: 'async',\n//       returns: null,\n//       description:\n//         \"Perform operations on a document after it's updated in the database asynchronously.\",\n//     });\n//   }\n//   if (options.delete) {\n//     registerCallback({\n//       name: `${typeName}.delete.validate`,\n//       iterator: { validationErrors: 'An object that can be used to accumulate validation errors' },\n//       properties: [\n//         { currentUser: 'The current user' },\n//         { document: 'The document being removed' },\n//         { collection: 'The collection the document belongs to' },\n//         { context: 'The context of this mutation' },\n//       ],\n//       runs: 'sync',\n//       returns: 'document',\n//       description:\n//         'Validate a document before removal (can be skipped when removing directly on server).',\n//     });\n//     registerCallback({\n//       name: `${typeName}.delete.before`,\n//       iterator: { document: 'The document being removed' },\n//       properties: [{ currentUser: 'The current user' }],\n//       runs: 'sync',\n//       returns: null,\n//       description: \"Perform operations on a document before it's removed from the database.\",\n//     });\n//     registerCallback({\n//       name: `${typeName}.delete.async`,\n//       properties: [\n//         { document: 'The document being removed' },\n//         { currentUser: 'The current user' },\n//         { collection: 'The collection the document belongs to' },\n//       ],\n//       runs: 'async',\n//       returns: null,\n//       description:\n//         \"Perform operations on a document after it's removed from the database asynchronously.\",\n//     });\n//   }\n// };\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/default_mutations2.js",
    "content": "/*\n\nDefault mutations\n\n*/\n\nimport { createMutator, updateMutator, deleteMutator } from './mutators.js';\nimport { Connectors } from './connectors.js';\nimport { getCollectionByTypeName } from '../modules/collections.js';\nimport get from 'lodash/get';\nimport { throwError } from './errors.js';\n\nconst defaultOptions = { create: true, update: true, upsert: true, delete: true };\n\nconst getCreateMutationName = typeName => `create${typeName}`;\nconst getUpdateMutationName = typeName => `update${typeName}`;\nconst getDeleteMutationName = typeName => `delete${typeName}`;\nconst getUpsertMutationName = typeName => `upsert${typeName}`;\n\nconst operationChecks = {\n  create: 'canCreate',\n  update: 'canUpdate',\n  delete: 'canDelete',\n};\n\n/*\n\nPerform security check before calling mutators\n\n*/\nexport const performMutationCheck = options => {\n  const { user, document, collection, context, typeName, operationName } = options;\n  const { Users } = context;\n  const documentId = document._id;\n  const permissionsCheck = get(collection, `options.permissions.${operationChecks[operationName]}`);\n  let allowOperation = false;\n  const fullOperationName = `${typeName}:${operationName}`;\n  const data = { documentId, operationName: fullOperationName };\n\n  // 1. if no permission has been defined, throw error\n  if (!permissionsCheck) {\n    throwError({ id: 'app.no_permissions_defined', data });\n  }\n  // 2. if no document is passed, throw error\n  if (!document) {\n    throwError({ id: 'app.document_not_found', data });\n  }\n\n  if (typeof permissionsCheck === 'function') {\n    allowOperation = permissionsCheck(options);\n  } else if (Array.isArray(permissionsCheck)) {\n    allowOperation = Users.isMemberOf(user, permissionsCheck, document);\n  }\n\n  // 3. if permission check is defined but fails, disallow operation\n  if (!allowOperation) {\n    throwError({ id: 'app.operation_not_allowed', data });\n  }\n};\n\n/*\n\nDefault Mutations\n\n*/\nexport function getNewDefaultMutations({ typeName, collectionName, options }) {\n  collectionName = collectionName || getCollectionByTypeName(typeName);\n  const mutationOptions = { ...defaultOptions, ...options };\n\n  const mutations = {};\n\n  if (mutationOptions.create) {\n    mutations.create = {\n      description: `Mutation for creating new ${typeName} documents`,\n      name: getCreateMutationName(typeName),\n      async mutation(root, { data }, context) {\n        const collection = context[collectionName];\n        const { currentUser } = context;\n\n        performMutationCheck({\n          user: currentUser,\n          document: data,\n          collection,\n          context,\n          typeName,\n          operationName: 'create',\n        });\n\n        return await createMutator({\n          collection,\n          data,\n          currentUser: context.currentUser,\n          validate: true,\n          context,\n        });\n      },\n    };\n  }\n\n  // get a single document based on the mutation params\n  const getMutationDocument = async ({ input, _id, collection }) => {\n    let document;\n    let selector;\n    if (_id) { // _id bypass input\n      document = await collection.loader.load(_id);\n    } else {\n      const filterParameters = await Connectors.filter(collection, input, context);\n      selector = filterParameters.selector;\n      // get entire unmodified document from database\n      document = await Connectors.get(collection, selector);\n    }\n    return { selector, document };\n  };\n\n  if (mutationOptions.update) {\n    mutations.update = {\n      description: `Mutation for updating a ${typeName} document`,\n      name: getUpdateMutationName(typeName),\n      async mutation(root, { input, _id: argsId, selector: oldSelector, data }, context) {\n        const { currentUser } = context;\n        const collection = context[collectionName];\n        const _id = argsId || (data && typeof data === 'object' && data._id); // use provided id or documentId if available\n\n        const { document, selector } = await getMutationDocument({ input, _id, collection });\n\n        performMutationCheck({\n          user: currentUser,\n          document,\n          collection,\n          context,\n          operationName: 'update',\n        });\n\n        // call editMutator boilerplate function\n        return await updateMutator({\n          collection,\n          selector,\n          data,\n          currentUser: context.currentUser,\n          validate: true,\n          context,\n          document,\n        });\n      },\n    };\n  }\n\n  if (mutationOptions.upsert) {\n    mutations.upsert = {\n      description: `Mutation for upserting a ${typeName} document`,\n      name: getUpsertMutationName(typeName),\n      async mutation(root, { input, _id: argsId, data }, context) {\n        const collection = context[collectionName];\n        const _id = argsId || (data && typeof data === 'object' && data._id); // use provided id or documentId if available\n\n        // check if document exists already\n        const { document: existingDocument, selector } = await getMutationDocument({ input, _id, collection });\n\n        if (existingDocument) {\n          return await collection.options.mutations.update.mutation(\n            root,\n            { input, _id, selector, data },\n            context\n          );\n        } else {\n          return await collection.options.mutations.create.mutation(root, { data }, context);\n        }\n      },\n    };\n  }\n\n  if (mutationOptions.delete) {\n    mutations.delete = {\n      description: `Mutation for deleting a ${typeName} document`,\n      name: getDeleteMutationName(typeName),\n      async mutation(root, { input, _id }, context) {\n        const { currentUser } = context;\n        const collection = context[collectionName];\n\n        const { document, /*selector*/ } = await getMutationDocument({ input, _id, collection });\n\n        performMutationCheck({\n          user: currentUser,\n          document,\n          collection,\n          context,\n          operationName: 'delete',\n        });\n\n        return await deleteMutator({\n          collection,\n          selector: { _id: document._id },\n          currentUser: context.currentUser,\n          validate: true,\n          context,\n          document,\n        });\n      },\n    };\n  }\n\n  return mutations;\n}\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/default_resolvers.js",
    "content": "/*\n\nDefault list, single, and total resolvers\n\n*/\n\nimport { Utils } from '../modules/utils.js';\nimport { debug, debugGroup, debugGroupEnd } from '../modules/debug.js';\nimport { Connectors } from './connectors.js';\nimport { generateTypeNameFromCollectionName, getTypeNameByCollectionName, getCollectionByTypeName } from '../modules/collections.js';\nimport { throwError } from './errors.js';\nimport isEmpty from 'lodash/isEmpty';\nimport get from 'lodash/get';\n\nconst defaultOptions = {\n  cacheMaxAge: 300,\n};\n\n// note: for some reason changing resolverOptions to \"options\" throws error\nexport function getDefaultResolvers(options) {\n  let typeName, collectionName, resolverOptions;\n  if (typeof arguments[0] === 'object') {\n    // new single-argument API\n    typeName = arguments[0].typeName;\n    // collectionName = arguments[0].collectionName || getCollectionByTypeName(typeName).options.collectionName;\n    resolverOptions = { ...defaultOptions, ...arguments[0].options };\n  } else {\n    // OpenCRUD backwards compatibility\n    collectionName = arguments[0];\n    typeName = generateTypeNameFromCollectionName(collectionName);\n    resolverOptions = { ...defaultOptions, ...arguments[1] };\n  }\n\n  return {\n    // resolver for returning a list of documents based on a set of query terms\n\n    multi: {\n      description: `A list of ${typeName} documents matching a set of query terms`,\n\n      async resolver(root, { input = {} }, context, { cacheControl }) {\n        const { terms = {}, enableCache = false, enableTotal = true } = input;\n        // get currentUser and Users collection from context\n        const { currentUser, Users } = context;\n\n        collectionName = getCollectionByTypeName(typeName).options.collectionName;\n\n        debug('');\n        debugGroup(`--------------- start \\x1b[35m${typeName} Multi Resolver\\x1b[0m ---------------`);\n        debug(`Options: ${JSON.stringify(resolverOptions)}`);\n        debug(`Input: ${JSON.stringify(input)}`);\n\n        if (cacheControl && enableCache ) {\n          const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;\n          cacheControl.setCacheHint({ maxAge });\n        }\n\n        // get collection based on collectionName argument\n        const collection = context[collectionName];\n\n        // get selector and options from terms and perform Mongo query\n\n        let { selector = {}, options = {}, filteredFields = [] } = isEmpty(terms)\n          ? await Connectors.filter(collection, input, context)\n          : await collection.getParameters(terms, {}, context);\n\n        // make sure all filtered fields are allowed\n        Users.checkFields(currentUser, collection, filteredFields);\n\n        if (!isEmpty(terms)) {\n          options.skip = terms.offset;\n        }\n\n        // debug({ selector, options });\n\n        const docs = await Connectors.find(collection, selector, options);\n\n        // default to allowing access to all documents\n        let viewableDocs = docs;\n\n        // new API (Oct 2019)\n        const canRead = get(collection, 'options.permissions.canRead');\n        if (canRead) {\n          if (typeof canRead === 'function') {\n            // if canRead is a function, use it to filter list of documents\n            viewableDocs = docs.filter(document => canRead({ user: currentUser, document, context, operationName: 'multi' }));\n          } else if (Array.isArray(canRead)) {\n            if (canRead.includes('owners')) {\n              // if canReady array includes the owners group, test each document\n              // to see if it's owned by the current user\n              viewableDocs = docs.filter(doc => Users.isMemberOf(currentUser, canRead, doc));\n            } else {\n              // else, we don't need a per-document check and just allow or disallow\n              // access to all documents at once\n              viewableDocs = Users.isMemberOf(currentUser, canRead) ? viewableDocs : [];\n            }\n          }\n        } else if (collection.checkAccess) {\n          // old API\n          // if collection has a checkAccess function defined, remove any documents that doesn't pass the check\n          viewableDocs = docs.filter(doc => collection.checkAccess(currentUser, doc));\n        }\n\n        // check again that the fields used for filtering were all valid, this time based on documents\n        // this second check is necessary for document based permissions like canRead:[\"owners\", customFunctionThatNeedDoc]\n        if (filteredFields.length) {\n          viewableDocs = viewableDocs.filter(document => Users.canFilterDocument(currentUser, collection, filteredFields, document));\n        }\n\n        // take the remaining documents and remove any fields that shouldn't be accessible\n        const restrictedDocs = Users.restrictViewableFields(currentUser, collection, viewableDocs);\n\n        // prime the cache\n        restrictedDocs.forEach(doc => collection.loader.prime(doc._id, doc));\n\n        debug(`\\x1b[33m=> ${restrictedDocs.length} documents returned\\x1b[0m`);\n        debugGroupEnd();\n        debug(`--------------- end \\x1b[35m${typeName} Multi Resolver\\x1b[0m ---------------`);\n        debug('');\n\n        const data = { results: restrictedDocs };\n\n        if (enableTotal) {\n          // get total count of documents matching the selector\n          data.totalCount = await Connectors.count(collection, selector);\n        } else {\n          data.totalCount = null;\n        }\n\n        // return results\n        return data;\n      },\n    },\n\n    // resolver for returning a single document queried based on id or slug\n\n    single: {\n      description: `A single ${typeName} document fetched by ID or slug`,\n\n      async resolver(root, { input = {} }, context, { cacheControl }) {\n        const { selector: oldSelector = {}, enableCache = false, allowNull = false } = input;\n\n        collectionName = getCollectionByTypeName(typeName).options.collectionName;\n\n        let doc;\n\n        debug('');\n        debugGroup(`--------------- start \\x1b[35m${typeName} Single Resolver\\x1b[0m ---------------`);\n        debug(`Options: ${JSON.stringify(resolverOptions)}`);\n        debug(`Input: ${JSON.stringify(input)}`);\n\n        if (cacheControl && enableCache) {\n          const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;\n          cacheControl.setCacheHint({ maxAge });\n        }\n\n        const { currentUser, Users } = context;\n        const collection = context[collectionName];\n\n        // use Dataloader if doc is selected by documentId/_id\n        const documentId = oldSelector.documentId || oldSelector._id || input.id;\n        const slug = oldSelector.slug;\n\n        if (documentId) {\n          doc = await collection.loader.load(documentId);\n        } else if (slug) {\n          // make an exception for slug\n          doc = await Connectors.get(collection, { slug });\n        } else {\n\n          if (isEmpty(input)) {\n            throwError({\n              id: 'app.empty_input'\n            });\n          }\n\n          let { selector, options, filteredFields } = await Connectors.filter(collection, input, context);\n\n          // make sure all filtered fields are allowed\n          Users.checkFields(currentUser, collection, filteredFields);\n\n          doc = await Connectors.get(collection, selector, options);\n\n          // check again that the fields used for filtering were all valid, this time based on retrieved document\n          // this second check is necessary for document based permissions like canRead:[\"owners\", customFunctionThatNeedDoc]\n          Users.checkFields(currentUser, collection, filteredFields, doc);\n        }\n\n        if (!doc) {\n          if (allowNull) {\n            return { result: null };\n          } else {\n            throwError({\n              id: 'app.missing_document',\n              data: { input, collectionName },\n            });\n          }\n        }\n\n        // if collection has a checkAccess function defined, use it to perform a check on the current document\n        // (will throw an error if check doesn't pass)\n        let canReadFunction;\n\n        // new API (Oct 2019)\n        const canRead = get(collection, 'options.permissions.canRead');\n        if (canRead) {\n          if (typeof canRead === 'function') {\n            // if canRead is a function, use it to check current document\n            canReadFunction = (user, document, context) => canRead({ user, document, context, operationName: 'single' });\n          } else if (Array.isArray(canRead)) {\n            // else if it's an array of groups, check if current user belongs to them\n            // for the current document\n            canReadFunction = (currentUser, doc) => Users.isMemberOf(currentUser, canRead, doc);\n          }\n        } else if (collection.checkAccess) {\n          // old API\n          canReadFunction = collection.checkAccess;\n        } else {\n          // default to allowing access to all documents\n          canReadFunction = () => true;\n        }\n\n        Utils.performCheck(canReadFunction, currentUser, doc, collection, documentId, `${typeName}.read.single`, collectionName);\n\n        const restrictedDoc = Users.restrictViewableFields(currentUser, collection, doc);\n\n        debugGroupEnd();\n        debug(`--------------- end \\x1b[35m${typeName} Single Resolver\\x1b[0m ---------------`);\n        debug('');\n\n        // filter out disallowed properties and return resulting document\n        return { result: restrictedDoc };\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/default_resolvers2.js",
    "content": "/*\n\nDefault list, single, and total resolvers\n\n*/\n\nimport { debug, debugGroup, debugGroupEnd } from '../modules/debug.js';\nimport { Connectors } from './connectors.js';\nimport { getCollectionByTypeName } from '../modules/collections.js';\nimport { throwError } from './errors.js';\nimport get from 'lodash/get';\n\nconst defaultOptions = {\n  cacheMaxAge: 300,\n};\n\n// note: for some reason changing resolverOptions to \"options\" throws error\nexport function getNewDefaultResolvers({ typeName, collectionName, options }) {\n  collectionName = collectionName || getCollectionByTypeName(typeName);\n  const resolverOptions = { ...defaultOptions, ...options };\n\n  return {\n    // resolver for returning a list of documents based on a set of query terms\n\n    multi: {\n      description: `A list of ${typeName} documents matching a set of query terms`,\n\n      async resolver(root, { input = {} }, context, { cacheControl }) {\n        const { terms = {}, enableCache = false, enableTotal = true } = input;\n        const operationName = `${typeName}.read.multi`;\n\n        debug('');\n        debugGroup(`--------------- start \\x1b[35m${typeName} Multi Resolver\\x1b[0m ---------------`);\n        debug(`Options: ${JSON.stringify(resolverOptions)}`);\n        debug(`Terms: ${JSON.stringify(terms)}`);\n\n        if (cacheControl && enableCache) {\n          const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;\n          cacheControl.setCacheHint({ maxAge });\n        }\n\n        // get currentUser and Users collection from context\n        const { currentUser, Users } = context;\n\n        // get collection based on collectionName argument\n        const collection = context[collectionName];\n\n        // get selector and options from terms and perform Mongo query\n\n        let { selector, options } = await Connectors.filter(collection, input, context);\n        const filteredFields = Object.keys(selector);\n\n        // make sure all filtered fields are allowed, before fetching the document\n        // (ignore ambiguous field that will need the document to be checked)\n        Users.checkFields(currentUser, collection, filteredFields);\n\n        options.skip = terms.offset;\n\n        debug({ selector, options });\n\n        const docs = await Connectors.find(collection, selector, options);\n        // in restrictViewableFields, null value will return {} instead of [] (because it works both for array and single doc)\n        let viewableDocs = [];\n\n        // check again if all fields used for filtering were actually allowed, this time based on actually retrieved documents\n\n        // new API (Oct 2019)\n        const canRead = get(collection, 'options.permissions.canRead');\n        if (canRead) {\n          if (typeof canRead === 'function') {\n            // if canRead is a function, use it to filter list of documents\n            viewableDocs = docs.filter(doc => canRead({ user: currentUser, document: doc, collection, context, operationName }));\n          } else if (Array.isArray(canRead)) {\n            if (canRead.includes('owners')) {\n              // if canReady array includes the owners group, test each document\n              // to see if it's owned by the current user\n              viewableDocs = docs.filter(doc => Users.isMemberOf(currentUser, canRead, doc));\n            } else {\n              // else, we don't need a per-document check and just allow or disallow\n              // access to all documents at once\n              viewableDocs = Users.isMemberOf(currentUser, canRead) ? docs : [];\n            }\n          }\n        }\n\n        // check again that the fields used for filtering were all valid, this time based on documents\n        // this second check is necessary for document based permissions like canRead:[\"owners\", customFunctionThatNeedDoc]\n        if (filteredFields.length) {\n          viewableDocs = viewableDocs.filter(document => Users.canFilterDocument(currentUser, collection, filteredFields, document));\n        }\n\n        // take the remaining documents and remove any fields that shouldn't be accessible\n        const restrictedDocs = Users.restrictViewableFields(currentUser, collection, viewableDocs);\n\n        // prime the cache\n        restrictedDocs.forEach(doc => collection.loader.prime(doc._id, doc));\n\n        debug(`\\x1b[33m=> ${restrictedDocs.length} documents returned\\x1b[0m`);\n        debugGroupEnd();\n        debug(`--------------- end \\x1b[35m${typeName} Multi Resolver\\x1b[0m ---------------`);\n        debug('');\n\n        const data = { results: restrictedDocs };\n\n        if (enableTotal) {\n          // get total count of documents matching the selector\n          data.totalCount = await Connectors.count(collection, selector);\n        } else {\n          data.totalCount = null;\n        }\n\n        // return results\n        return data;\n      },\n    },\n\n    // resolver for returning a single document queried based on id or slug\n\n    single: {\n      description: `A single ${typeName} document fetched by ID or slug`,\n\n      async resolver(root, { input = {}, _id }, context, { cacheControl }) {\n        const { selector: oldSelector = {}, enableCache = false, allowNull = false } = input;\n        const operationName = `${typeName}.read.single`;\n        //const { _id } = input; // _id is passed from the root\n        let doc;\n\n        debug('');\n        debugGroup(`--------------- start \\x1b[35m${typeName} Single Resolver\\x1b[0m ---------------`);\n        debug(`Options: ${JSON.stringify(resolverOptions)}`);\n        debug(`Selector: ${JSON.stringify(oldSelector)}`);\n\n        if (cacheControl && enableCache) {\n          const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;\n          cacheControl.setCacheHint({ maxAge });\n        }\n\n        const { currentUser, Users } = context;\n        const collection = context[collectionName];\n\n        // use Dataloader if doc is selected by _id\n        if (_id) {\n          doc = await collection.loader.load(_id);\n        } else {\n          let { selector, options, filteredFields } = await Connectors.filter(collection, input, context);\n          // make sure all filtered fields are actually readable, for basic roles\n          Users.checkFields(currentUser, collection, filteredFields);\n          doc = await Connectors.get(collection, selector, options);\n\n          // check again that the fields used for filtering were all valid, this time based on retrieved document\n          // this second check is necessary for document based permissions like canRead:[\"owners\", customFunctionThatNeedDoc]\n          if (filteredFields.length) {\n            doc = Users.canFilterDocument(currentUser, collection, filteredFields, doc) ? doc : null;\n          }\n        }\n\n        if (!doc) {\n          if (allowNull) {\n            return { result: null };\n          } else {\n            throwError({\n              id: 'app.missing_document',\n              data: { documentId: _id, input, collectionName },\n            });\n          }\n        }\n\n        // new API (Oct 2019)\n        let canReadFunction;\n        const canRead = get(collection, 'options.permissions.canRead');\n        if (canRead) {\n          if (typeof canRead === 'function') {\n            // if canRead is a function, use it to check current document\n            canReadFunction = canRead;\n          } else if (Array.isArray(canRead)) {\n            // else if it's an array of groups, check if current user belongs to them\n            // for the current document\n            canReadFunction = ({ user, document }) => Users.isMemberOf(user, canRead, document);\n          }\n        } else {\n          // default to allowing access to all documents\n          canReadFunction = () => true;\n        }\n\n        if (!canReadFunction({ user: currentUser, document, collection, context, operationName })) {\n          throwError({\n            id: 'app.operation_not_allowed',\n            data: { documentId: document._id, operationName },\n          });\n        }\n\n        const restrictedDoc = Users.restrictViewableFields(currentUser, collection, doc);\n\n        debugGroupEnd();\n        debug(`--------------- end \\x1b[35m${typeName} Single Resolver\\x1b[0m ---------------`);\n        debug('');\n\n        // filter out disallowed properties and return resulting document\n        return { result: restrictedDoc };\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/errors.js",
    "content": "import { UserInputError } from 'apollo-server';\n\n/*\n\nAn error should have: \n\n- id: will be used as i18n key (note: available as `name` on the client)\n- message: optionally, a plain-text message\n- data: data/values to give more context to the error\n\n*/\nexport const throwError = error => {\n  const { id, data } = error;\n  if (data) {\n    // console.log(`// throwError: ${id}`);\n    // console.log(JSON.stringify(data, '', 2));\n  }\n  throw new UserInputError(id, error);\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/graphql/collection.js",
    "content": "/**\n * Generates the GraphQL schema and\n * the resolvers and mutations for a Vulcan collectio\n */\nimport { getDefaultResolvers } from '../default_resolvers';\nimport { getDefaultMutations } from '../default_mutations';\nimport { getSchemaFields } from './schemaFields';\nimport {\n  selectorInputTemplate,\n  mainTypeTemplate,\n  createInputTemplate,\n  createDataInputTemplate,\n  updateInputTemplate,\n  updateDataInputTemplate,\n  selectorUniqueInputTemplate,\n  deleteInputTemplate,\n  upsertInputTemplate,\n  singleInputTemplate,\n  multiInputTemplate,\n  multiOutputTemplate,\n  singleOutputTemplate,\n  mutationOutputTemplate,\n  singleQueryTemplate,\n  multiQueryTemplate,\n  createMutationTemplate,\n  updateMutationTemplate,\n  upsertMutationTemplate,\n  deleteMutationTemplate,\n  enumTypeTemplate,\n  fieldFilterInputTemplate,\n  fieldSortInputTemplate,\n  customFilterTemplate,\n  // customSortTemplate, // not currently used\n  //nestedInputTemplate,\n} from '../../modules/graphql_templates';\n\nimport { Utils } from '../utils.js';\n\nimport _isEmpty from 'lodash/isEmpty';\nimport _initial from 'lodash/initial';\n\n/**\n * Extract relevant collection information and set default values\n * @param {*} collection\n */\nconst getCollectionInfos = collection => {\n  const collectionName = collection.options.collectionName;\n  const typeName = collection.typeName ? collection.typeName : Utils.camelToSpaces(_initial(collectionName).join('')); // default to posts -> Post\n  const schema = collection.simpleSchema()._schema;\n  const description = collection.options.description ? collection.options.description : `Type for ${collectionName}`;\n  return {\n    ...collection.options,\n    collectionName,\n    typeName,\n    schema,\n    description,\n  };\n};\n\nconst createResolvers = ({ resolvers: providedResolvers, typeName }) => {\n  const queryResolvers = {};\n  const queriesToAdd = [];\n  const resolversToAdd = [];\n  if (providedResolvers === null) {\n    // user explicitely disabled default resolvers\n    return { queriesToAdd, resolversToAdd };\n  }\n  // if resolvers are empty, use defaults\n  const resolvers = _isEmpty(providedResolvers) ? getDefaultResolvers({ typeName }) : providedResolvers;\n  // single\n  if (resolvers.single) {\n    queriesToAdd.push([singleQueryTemplate({ typeName }), resolvers.single.description]);\n    //addGraphQLQuery(singleQueryTemplate({ typeName }), resolvers.single.description);\n    queryResolvers[Utils.camelCaseify(typeName)] = resolvers.single.resolver.bind(resolvers.single);\n  }\n  // multi\n  if (resolvers.multi) {\n    queriesToAdd.push([multiQueryTemplate({ typeName }), resolvers.multi.description]);\n    //addGraphQLQuery(multiQueryTemplate({ typeName }), resolvers.multi.description);\n    queryResolvers[Utils.camelCaseify(Utils.pluralize(typeName))] = resolvers.multi.resolver.bind(resolvers.multi);\n  }\n  //addGraphQLResolvers({ Query: { ...queryResolvers } });\n  resolversToAdd.push({ Query: { ...queryResolvers } });\n  return {\n    queriesToAdd,\n    resolversToAdd,\n  };\n};\nconst createMutations = ({ mutations: providedMutations = {}, typeName, collectionName, fields }) => {\n  const mutationResolvers = {};\n  const mutationsToAdd = [];\n  const mutationsResolversToAdd = [];\n  if (providedMutations === null) {\n    // user explicitely disabled mutations\n    return { mutationsResolversToAdd, mutationsToAdd };\n  }\n  // extend defaults with provided mutations\n  const mutations = { ...getDefaultMutations({ typeName }), ...providedMutations };\n\n  const { create, update } = fields;\n\n  // create\n  if (mutations.create) {\n    // e.g. \"createMovie(input: CreateMovieInput) : Movie\"\n    if (create.length === 0) {\n      // eslint-disable-next-line no-console\n      console.log(\n        `// Warning: you defined a \"create\" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the \"create\" mutation or define a \"canCreate\" property on a field to disable this warning`\n      );\n    } else {\n      //addGraphQLMutation(createMutationTemplate({ typeName }), mutations.create.description);\n      mutationsToAdd.push([createMutationTemplate({ typeName }), mutations.create.description]);\n      mutationResolvers[`create${typeName}`] = mutations.create.mutation.bind(mutations.create);\n    }\n  }\n  // update\n  if (mutations.update) {\n    // e.g. \"updateMovie(input: UpdateMovieInput) : Movie\"\n    if (update.length === 0) {\n      // eslint-disable-next-line no-console\n      console.log(\n        `// Warning: you defined an \"update\" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the \"update\" mutation or define a \"canUpdate\" property on a field to disable this warning`\n      );\n    } else {\n      mutationsToAdd.push([updateMutationTemplate({ typeName }), mutations.update.description]);\n      //addGraphQLMutation(updateMutationTemplate({ typeName }), mutations.update.description);\n      mutationResolvers[`update${typeName}`] = mutations.update.mutation.bind(mutations.update);\n    }\n  }\n  // upsert\n  if (mutations.upsert) {\n    // e.g. \"upsertMovie(input: UpsertMovieInput) : Movie\"\n    if (update.length === 0) {\n      // eslint-disable-next-line no-console\n      console.log(\n        `// Warning: you defined an \"upsert\" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the \"upsert\" mutation or define a \"canUpdate\" property on a field to disable this warning`\n      );\n    } else {\n      mutationsToAdd.push([upsertMutationTemplate({ typeName }), mutations.upsert.description]);\n      //addGraphQLMutation(upsertMutationTemplate({ typeName }), mutations.upsert.description);\n      mutationResolvers[`upsert${typeName}`] = mutations.upsert.mutation.bind(mutations.upsert);\n    }\n  }\n  // delete\n  if (mutations.delete) {\n    // e.g. \"deleteMovie(input: DeleteMovieInput) : Movie\"\n    //addGraphQLMutation(deleteMutationTemplate({ typeName }), mutations.delete.description);\n    mutationsToAdd.push([deleteMutationTemplate({ typeName }), mutations.delete.description]);\n    mutationResolvers[`delete${typeName}`] = mutations.delete.mutation.bind(mutations.delete);\n  }\n  //addGraphQLResolvers({ Mutation: { ...mutationResolvers } });\n  mutationsResolversToAdd.push({ Mutation: { ...mutationResolvers } });\n  return { mutationsResolversToAdd, mutationsToAdd };\n};\n\n// generate types, input and enums\nconst generateSchemaFragments = ({ collection, typeName, description, interfaces = [], fields, isNested = false }) => {\n  const schemaFragments = [];\n  const {\n    mainType,\n    create,\n    update,\n    selector,\n    selectorUnique,\n    //orderBy,\n    readable,\n    filterable,\n    enums,\n  } = fields;\n\n  if (!mainType || mainType.length === 0) {\n    throw new Error(`GraphQL type ${typeName} has no fields. Please add readable fields or remove the type.`);\n  }\n\n  schemaFragments.push(mainTypeTemplate({ typeName, description, interfaces, fields: mainType }));\n\n  if (enums) {\n    for (const { allowedValues, typeName: enumTypeName } of enums) {\n      schemaFragments.push(enumTypeTemplate({ typeName: enumTypeName, allowedValues }));\n    }\n  }\n  if (isNested) {\n    // TODO: this is wrong because the mainType includes resolveAs fields\n    // + this input type does not seem to be actually used?\n    // schemaFragments.push(nestedInputTemplate({ typeName, fields: mainType }));\n\n    //schemaFragments.push(deleteInputTemplate({ typeName }));\n    //schemaFragments.push(singleInputTemplate({ typeName }));\n    //schemaFragments.push(multiInputTemplate({ typeName }));\n    //schemaFragments.push(singleOutputTemplate({ typeName }));\n    //schemaFragments.push(multiOutputTemplate({ typeName }));\n    //schemaFragments.push(mutationOutputTemplate({ typeName }));\n\n    if (create.length) {\n      schemaFragments.push(createInputTemplate({ typeName }));\n      schemaFragments.push(createDataInputTemplate({ typeName, fields: create }));\n    }\n\n    if (update.length) {\n      schemaFragments.push(updateInputTemplate({ typeName }));\n      schemaFragments.push(upsertInputTemplate({ typeName }));\n      schemaFragments.push(updateDataInputTemplate({ typeName, fields: update }));\n    }\n    if (filterable.length) {\n      schemaFragments.push(fieldFilterInputTemplate({ typeName, fields: filterable }));\n      schemaFragments.push(fieldSortInputTemplate({ typeName, fields: filterable }));\n    }\n\n    //   schemaFragments.push(selectorInputTemplate({ typeName, fields: selector }));\n\n    //    schemaFragments.push(selectorUniqueInputTemplate({ typeName, fields: selectorUnique }));\n\n    //    schemaFragments.push(orderByInputTemplate({ typeName, fields: orderBy }));\n    return schemaFragments; // return now\n  }\n\n  if (readable.length) {\n    schemaFragments.push(singleInputTemplate({ typeName }));\n    schemaFragments.push(multiInputTemplate({ typeName }));\n    schemaFragments.push(singleOutputTemplate({ typeName }));\n    schemaFragments.push(multiOutputTemplate({ typeName }));\n  }\n\n  if (create.length || update.length) {\n    schemaFragments.push(mutationOutputTemplate({ typeName }));\n  }\n  if (create.length) {\n    schemaFragments.push(createInputTemplate({ typeName }));\n    schemaFragments.push(createDataInputTemplate({ typeName, fields: create }));\n  }\n\n  if (update.length) {\n    schemaFragments.push(updateInputTemplate({ typeName }));\n    schemaFragments.push(upsertInputTemplate({ typeName }));\n    schemaFragments.push(updateDataInputTemplate({ typeName, fields: update }));\n    schemaFragments.push(deleteInputTemplate({ typeName }));\n  }\n\n  if (filterable.length) {\n    const customFilters = collection.options.customFilters;\n    schemaFragments.push(fieldFilterInputTemplate({ typeName, fields: filterable, customFilters }));\n    if (customFilters) {\n      customFilters.forEach(filter => {\n        // if filter has no argument we don't need to create a custom type for it\n        if (filter.arguments) {\n          schemaFragments.push(customFilterTemplate({ typeName, filter }));\n        }\n      });\n    }\n    const customSorts = collection.options.customSorts;\n    schemaFragments.push(fieldSortInputTemplate({ typeName, fields: filterable, customSorts }));\n    // TODO: not currently working\n    // if (customSorts) {\n    //   customSorts.forEach(sort => {\n    //     schemaFragments.push(customSortTemplate({ typeName, sort }));\n    //   });\n    // }\n  }\n\n  schemaFragments.push(selectorInputTemplate({ typeName, fields: selector }));\n\n  schemaFragments.push(selectorUniqueInputTemplate({ typeName, fields: selectorUnique }));\n\n  return schemaFragments;\n};\nconst collectionToGraphQL = collection => {\n  let graphQLSchema = '';\n  const schemaFragments = [];\n\n  const { collectionName, typeName, schema, description, interfaces = [], resolvers, mutations } = getCollectionInfos(collection);\n\n  const { nestedFieldsList, fields, resolvers: schemaResolvers } = getSchemaFields(schema, typeName);\n\n  const { mainType } = fields;\n\n  if (mainType.length) {\n    schemaFragments.push(\n      ...generateSchemaFragments({\n        collection,\n        typeName,\n        description,\n        interfaces,\n        fields,\n        isNested: false,\n      })\n    );\n    /* NESTED */\n    // TODO: factorize to use the same function as for non nested fields\n    // the schema may produce a list of additional graphQL types for nested arrays/objects\n    if (nestedFieldsList) {\n      for (const nestedFields of nestedFieldsList) {\n        schemaFragments.push(\n          ...generateSchemaFragments({\n            typeName: nestedFields.typeName,\n            fields: nestedFields.fields,\n            isNested: true,\n          })\n        );\n      }\n    }\n\n    const { queriesToAdd, resolversToAdd } = createResolvers({ resolvers, typeName });\n    const { mutationsToAdd, mutationsResolversToAdd } = createMutations({\n      mutations,\n      typeName,\n      collectionName,\n      fields,\n    });\n\n    graphQLSchema = schemaFragments.join('\\n\\n') + '\\n\\n\\n';\n\n    return {\n      graphQLSchema,\n      queriesToAdd,\n      schemaResolvers,\n      resolversToAdd,\n      mutationsToAdd,\n      mutationsResolversToAdd,\n    };\n  } else {\n    // eslint-disable-next-line no-console\n    console.log(\n      `// Warning: collection ${collectionName} doesn't have any GraphQL-enabled fields, so no corresponding type can be generated. Pass generateGraphQLSchema = false to createCollection() to disable this warning`\n    );\n  }\n\n  return { graphQLSchema };\n};\n\nexport default collectionToGraphQL;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/graphql/graphql.js",
    "content": "// TODO: this should not be loaded on the client?\n/*\n\nUtilities to generate the app's GraphQL schema\nand register schema parts based on the application collections\n\n*/\n\nimport deepmerge from 'deepmerge';\nimport GraphQLJSON from 'graphql-type-json';\nimport GraphQLDate from 'graphql-date';\n//import Vulcan from '../config.js'; // used for global export\nimport { disableFragmentWarnings } from 'graphql-tag';\n\nimport collectionToGraphQL from './collection';\nimport { generateResolversFromSchema } from './resolvers';\nimport {\n  mainTypeTemplate,\n  createDataInputTemplate,\n  updateDataInputTemplate,\n  selectorUniqueInputTemplate,\n  deleteInputTemplate,\n  upsertInputTemplate,\n  singleInputTemplate,\n  multiInputTemplate,\n  multiOutputTemplate,\n  singleOutputTemplate,\n  mutationOutputTemplate,\n  singleQueryTemplate,\n  multiQueryTemplate,\n  createMutationTemplate,\n  updateMutationTemplate,\n  upsertMutationTemplate,\n  deleteMutationTemplate,\n  fieldFilterInputTemplate,\n  fieldSortInputTemplate,\n} from '../../modules/graphql_templates/index.js';\nimport getSchemaFields from './schemaFields';\n\ndisableFragmentWarnings();\n\n\nimport { getDefaultResolvers } from '../../server/default_resolvers.js';\nimport { getDefaultMutations } from '../../server/default_mutations.js';\nimport isEmpty from 'lodash/isEmpty';\nimport { Collections } from '../../modules/collections.js';\n\n\nconst defaultResolvers = {\n  JSON: GraphQLJSON,\n  Date: GraphQLDate,\n};\n\n/**\n * Extract relevant collection information and set default values\n * @param {*} collection\n */\nconst getCollectionInfos = collection => {\n  const collectionName = collection.options.collectionName;\n  const typeName = collection.typeName;\n  const schema = collection.simpleSchema();\n  const description = collection.options.description\n    ? collection.options.description\n    : `Type for ${collectionName}`;\n  return {\n    ...collection.options,\n    collectionName,\n    typeName,\n    schema,\n    description,\n  };\n};\n\nexport const GraphQLSchema = {\n  // reinit the schema (testing purposes only)\n  init() {\n    this.collections = [];\n    this.schemas = [];\n    this.queries = [];\n    this.mutations = [];\n    this.resolvers = defaultResolvers;\n    this.context = {};\n    this.directiveTransformers = [];\n  },\n\n  // used for schema stitching\n  stitchedSchemas: [],\n  addStitchedSchema(schema) {\n    this.stitchedSchemas.push(schema);\n  },\n\n  // collections used to auto-generate schemas\n  collections: [],\n  addCollection(collection) {\n    this.collections.push(collection);\n  },\n  // generate GraphQL schemas for all registered collections\n  getCollectionsSchemas() {\n    const collections = Collections.filter(c => c.options.generateGraphQLSchema !== false);\n\n    const collectionsSchemas = collections\n      .map(collection => {\n        return this.generateSchema(collection);\n      })\n      .join('');\n    return collectionsSchemas;\n  },\n\n  // additional schemas\n  schemas: [],\n  addSchema(schema) {\n    this.schemas.push(schema);\n  },\n  // get extra schemas defined manually\n  getAdditionalSchemas() {\n    const additionalSchemas = this.schemas.join('\\n');\n    return additionalSchemas;\n  },\n\n  // queries\n  queries: [],\n  addQuery(query, description) {\n    this.queries.push({ query, description });\n  },\n\n  // mutations\n  mutations: [],\n  addMutation(mutation, description) {\n    this.mutations.push({ mutation, description });\n  },\n\n  // add resolvers\n  resolvers: defaultResolvers,\n  addResolvers(resolvers) {\n    this.resolvers = deepmerge(this.resolvers, resolvers);\n  },\n  removeResolver(typeName, resolverName) {\n    delete this.resolvers[typeName][resolverName];\n  },\n\n  // add objects to context\n  context: {},\n  addToContext(object) {\n    this.context = deepmerge(this.context, object);\n  },\n\n  directiveTransformers: [],\n  addDirectiveTransformer(directiveTransformer) {\n    this.directiveTransformers = [...this.directiveTransformers, directiveTransformer];\n  },\n\n  addTypeAndResolvers({ typeName, schema, description = '', interfaces = [] }) {\n    if (!typeName) {\n      throw Error('Error: trying to add type without typeName');\n    }\n\n    const { fields, resolvers: schemaResolvers = [] } = getSchemaFields(schema._schema, typeName);\n    const mainType = fields.mainType;\n\n    if (!mainType || mainType.length === 0) {\n      // do not create GraphQL types\n      return;\n    }\n\n    // generate a graphql type def from the simpleSchema\n    const mainGraphQLSchema = mainTypeTemplate({\n      typeName,\n      fields: mainType,\n      description,\n      interfaces,\n    });\n\n    // add the type and its resolver\n    this.addSchema(mainGraphQLSchema);\n\n    // createTypeDataInput\n    if ((fields.create || []).length) {\n      this.addSchema(createDataInputTemplate({ typeName, fields: fields.create }));\n    }\n    // updateTypeDataInput\n    if ((fields.update || []).length) {\n      this.addSchema(updateDataInputTemplate({ typeName, fields: fields.update }));\n    }\n    const resolvers = generateResolversFromSchema(schema);\n    // only add resolvers if there is at least one\n    if (typeof resolvers === 'object' && Object.keys(resolvers).length >= 1) {\n      this.addResolvers({ [typeName]: resolvers });\n    }\n    schemaResolvers.forEach(addGraphQLResolvers);\n  },\n\n  /**\n   * getType - pass this into the schema to make a nested object type,\n   * referencing another type. This type sould be declared through\n   * createCollection or addTypeAndResolvers\n   *\n   * @param {*} typeName\n   * @returns\n   */\n  getType(typeName) {\n    return {\n      type: Object,\n      blackbox: true,\n      typeName: typeName,\n    };\n  },\n\n  /*\n  // generate a GraphQL schema corresponding to a given collection\n  generateSchema(collection) {\n    let graphQLSchema = '';\n\n    const schemaFragments = [];\n\n    const collectionName = collection.options.collectionName;\n    let { interfaces = [], resolvers, mutations } = collection.options;\n\n    const description = collection.options.description\n      ? collection.options.description\n      : `Type for ${collectionName}`;\n\n    const { mainType, create, update, selector, selectorUnique, readable } = fields;\n\n    if (mainType.length) {\n      schemaFragments.push(\n        mainTypeTemplate({ typeName, description, interfaces, fields: mainType })\n      );\n      schemaFragments.push(deleteInputTemplate({ typeName }));\n      schemaFragments.push(singleInputTemplate({ typeName }));\n      schemaFragments.push(multiInputTemplate({ typeName }));\n      schemaFragments.push(singleOutputTemplate({ typeName }));\n      schemaFragments.push(multiOutputTemplate({ typeName }));\n      schemaFragments.push(mutationOutputTemplate({ typeName }));\n\n      if (create.length) {\n        schemaFragments.push(createInputTemplate({ typeName }));\n        schemaFragments.push(createDataInputTemplate({ typeName, fields: create }));\n      }\n\n      if (update.length) {\n        schemaFragments.push(updateInputTemplate({ typeName }));\n        schemaFragments.push(upsertInputTemplate({ typeName }));\n        schemaFragments.push(updateDataInputTemplate({ typeName, fields: update }));\n      }\n\n      schemaFragments.push(selectorInputTemplate({ typeName, fields: selector }));\n\n      if (readable.length) {\n        schemaFragments.push(fieldFilterInputTemplate({ typeName, fields: readable }));\n        schemaFragments.push(fieldSortInputTemplate({ typeName, fields: readable }));\n      }\n\n      schemaFragments.push(selectorUniqueInputTemplate({ typeName, fields: selectorUnique }));\n\n      if (resolvers !== null) {\n        // if resolvers are empty, use defaults\n        resolvers = isEmpty(resolvers) ? getDefaultResolvers({ typeName }) : resolvers;\n\n        const queryResolvers = {};\n\n        // single\n        if (resolvers.single) {\n          addGraphQLQuery(singleQueryTemplate({ typeName }), resolvers.single.description);\n          queryResolvers[Utils.camelCaseify(typeName)] = resolvers.single.resolver.bind(\n            resolvers.single\n          );\n        }\n\n        // multi\n        if (resolvers.multi) {\n          addGraphQLQuery(multiQueryTemplate({ typeName }), resolvers.multi.description);\n          queryResolvers[\n            Utils.camelCaseify(Utils.pluralize(typeName))\n          ] = resolvers.multi.resolver.bind(resolvers.multi);\n        }\n        addGraphQLResolvers({ Query: { ...queryResolvers } });\n      }\n\n      if (mutations !== null) {\n        // if mutations are undefined, use defaults\n        mutations = isEmpty(mutations) ? getDefaultMutations({ typeName }) : mutations;\n\n        const mutationResolvers = {};\n        // create\n        if (mutations.create) {\n          // e.g. \"createMovie(input: CreateMovieInput) : Movie\"\n          if (create.length === 0) {\n            // eslint-disable-next-line no-console\n            console.log(\n              `// Warning: you defined a \"create\" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the \"create\" mutation or define a \"canCreate\" property on a field to disable this warning`\n            );\n          } else {\n            addGraphQLMutation(createMutationTemplate({ typeName }), mutations.create.description);\n            mutationResolvers[`create${typeName}`] = mutations.create.mutation.bind(\n              mutations.create\n            );\n          }\n        }\n        // update\n        if (mutations.update) {\n          // e.g. \"updateMovie(input: UpdateMovieInput) : Movie\"\n          if (update.length === 0) {\n            // eslint-disable-next-line no-console\n            console.log(\n              `// Warning: you defined an \"update\" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the \"update\" mutation or define a \"canUpdate\" property on a field to disable this warning`\n            );\n          } else {\n            addGraphQLMutation(updateMutationTemplate({ typeName }), mutations.update.description);\n            mutationResolvers[`update${typeName}`] = mutations.update.mutation.bind(\n              mutations.update\n            );\n          }\n        }\n        // upsert\n        if (mutations.upsert) {\n          // e.g. \"upsertMovie(input: UpsertMovieInput) : Movie\"\n          if (update.length === 0) {\n            // eslint-disable-next-line no-console\n            console.log(\n              `// Warning: you defined an \"upsert\" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the \"upsert\" mutation or define a \"canUpdate\" property on a field to disable this warning`\n            );\n          } else {\n            addGraphQLMutation(upsertMutationTemplate({ typeName }), mutations.upsert.description);\n            mutationResolvers[`upsert${typeName}`] = mutations.upsert.mutation.bind(\n              mutations.upsert\n            );\n          }\n        }\n        // delete\n        if (mutations.delete) {\n          // e.g. \"deleteMovie(input: DeleteMovieInput) : Movie\"\n          addGraphQLMutation(deleteMutationTemplate({ typeName }), mutations.delete.description);\n          mutationResolvers[`delete${typeName}`] = mutations.delete.mutation.bind(mutations.delete);\n        }\n        addGraphQLResolvers({ Mutation: { ...mutationResolvers } });\n      }\n    }\n    graphQLSchema = schemaFragments.join('\\n\\n') + '\\n\\n\\n';\n  }*/\n  // generate a GraphQL schema corresponding to a given collection\n  generateSchema(collection) {\n    const {\n      collectionName,\n      typeName,\n      schema,\n      description,\n      interfaces = [],\n      resolvers,\n      mutations,\n    } = getCollectionInfos(collection);\n\n    // const { nestedFieldsList, fields, resolvers: schemaResolvers = [] } = getSchemaFields(schema._schema, typeName);\n\n    addTypeAndResolvers({ typeName, schema, description, interfaces });\n\n    const {\n      graphQLSchema,\n      resolversToAdd = [],\n      queriesToAdd = [],\n      mutationsToAdd = [],\n      mutationsResolversToAdd = [],\n    } = collectionToGraphQL(collection);\n\n    // register the generated resolvers\n    // schemaResolvers.forEach(addGraphQLResolvers);\n    queriesToAdd.forEach(([query, description]) => {\n      addGraphQLQuery(query, description);\n    });\n    resolversToAdd.forEach(addGraphQLResolvers);\n    mutationsToAdd.forEach(([mutation, description]) => {\n      addGraphQLMutation(mutation, description);\n    });\n    mutationsResolversToAdd.forEach(addGraphQLResolvers);\n    return graphQLSchema;\n  },\n  // getters\n  getSchema() {\n    if (!(this.finalSchema && this.finalSchema.length)) {\n      throw new Error('Warning: trying to access schema before it has been created by the server.');\n    }\n    return this.finalSchema[0];\n  },\n  getExecutableSchema() {\n    if (!this.executableSchema) {\n      throw new Error(\n        'Warning: trying to access executable schema before it has been created by the server.'\n      );\n    }\n    return this.executableSchema;\n  },\n};\n\n\n// Vulcan.getGraphQLSchema = () => {\n//   if (!GraphQLSchema.finalSchema) {\n//     throw new Error(\n//       'Warning: trying to access graphQL schema before it has been created by the server.'\n//     );\n//   }\n//   const schema = GraphQLSchema.finalSchema[0];\n//   // eslint-disable-next-line no-console\n//   console.log(schema);\n//   return schema;\n// };\n\nexport const addGraphQLCollection = GraphQLSchema.addCollection.bind(GraphQLSchema);\nexport const addGraphQLSchema = GraphQLSchema.addSchema.bind(GraphQLSchema);\nexport const addGraphQLQuery = GraphQLSchema.addQuery.bind(GraphQLSchema);\nexport const addGraphQLMutation = GraphQLSchema.addMutation.bind(GraphQLSchema);\nexport const addGraphQLResolvers = GraphQLSchema.addResolvers.bind(GraphQLSchema);\nexport const removeGraphQLResolver = GraphQLSchema.removeResolver.bind(GraphQLSchema);\nexport const addToGraphQLContext = GraphQLSchema.addToContext.bind(GraphQLSchema);\nexport const addGraphQLDirectiveTransformer = GraphQLSchema.addDirectiveTransformer.bind(GraphQLSchema);\nexport const addStitchedSchema = GraphQLSchema.addStitchedSchema.bind(GraphQLSchema);\nexport const addTypeAndResolvers = GraphQLSchema.addTypeAndResolvers.bind(GraphQLSchema);\nexport const getType = GraphQLSchema.getType.bind(GraphQLSchema);\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/graphql/index.js",
    "content": "export * from './graphql';\nexport * from './typedefs';"
  },
  {
    "path": "packages/vulcan-lib/lib/server/graphql/relations.js",
    "content": "/*\n\nDefault Relation Resolvers\n\n*/\nimport { getCollectionByTypeName } from '../../modules/collections.js';\n\nexport const hasOne = async ({ document, fieldName, context, typeName }) => {\n  // if document doesn't have a \"foreign key\" field, return null\n  if (!document[fieldName]) return null;\n  // get related collection\n  const relatedCollection = getCollectionByTypeName(typeName);\n  // get related document\n  const relatedDocument = await relatedCollection.loader.load(document[fieldName]);\n  // filter related document to restrict viewable fields\n  return context.Users.restrictViewableFields(\n    context.currentUser,\n    relatedCollection,\n    relatedDocument\n  );\n};\n\nexport const hasMany = async ({ document, fieldName, context, typeName }) => {\n  // if document doesn't have a \"foreign key\" field, return null\n  if (!document[fieldName]) return null;\n  // get related collection\n  const relatedCollection = getCollectionByTypeName(typeName);\n  // get related documents\n  const relatedDocuments = await relatedCollection.loader.loadMany(document[fieldName]);\n  // filter related document to restrict viewable fields\n  return context.Users.restrictViewableFields(\n    context.currentUser,\n    relatedCollection,\n    relatedDocuments\n  );\n};"
  },
  {
    "path": "packages/vulcan-lib/lib/server/graphql/resolvers.js",
    "content": "import SimpleSchema from 'simpl-schema';\n\nexport /**\n * Generate field resolvers for the type defined in the SimpleSchema.\n *\n *\n * @param {SimpleSchema} schema\n * @returns an object mapping the field names to a GraphQL resolver function\n */\n  const generateResolversFromSchema = schema => {\n    if (!(schema instanceof SimpleSchema)) {\n      throw Error('must pass a SimpleSchema to generate Resolvers');\n    }\n    const { _schema, _firstLevelSchemaKeys } = schema;\n    const resolvers = {};\n\n    _firstLevelSchemaKeys.forEach(key => {\n      const field = _schema[key];\n      // only add resolvers for the fields that can be read\n      if (field && (field.canRead || field.viewableBy)) {\n        const resolver = (root, args, context) => {\n          const result = root[key];\n          if (typeof result === 'undefined') return null;\n          const { currentUser, Users } = context;\n          if (Users.canReadField(currentUser, field, root)) {\n            return result;\n          } else {\n            return null;\n          }\n        };\n        resolvers[key] = resolver;\n      }\n    });\n    return resolvers;\n  };\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/graphql/schemaFields.js",
    "content": "\n/*\n/**\n * Generate graphQL for Vulcan schema fields\nimport { isIntlField } from '../../modules/intl.js';\nimport relations from './relations.js';\n\n// get GraphQL type for a given schema and field name\nconst getGraphQLType = (schema, fieldName, isInput = false) => {\n  const field = schema[fieldName];\n  const type = field.type.singleType;\n  const typeName =\n    typeof type === 'object' ? 'Object' : typeof type === 'function' ? type.name : type;\n\n  if (field.isIntlData) {\n    return isInput ? '[IntlValueInput]' : '[IntlValue]';\n  }\n\n  switch (typeName) {\n    case 'String':\n      return 'String';\n\n    case 'Boolean':\n      return 'Boolean';\n\n    case 'Number':\n      return 'Float';\n\n    case 'SimpleSchema.Integer':\n      return 'Int';\n\n    // for arrays, look for type of associated schema field or default to [String]\n    case 'Array':\n      const arrayItemFieldName = `${fieldName}.$`;\n      // note: make sure field has an associated array\n      if (schema[arrayItemFieldName]) {\n        // try to get array type from associated array\n        const arrayItemType = getGraphQLType(schema, arrayItemFieldName);\n        return arrayItemType ? `[${arrayItemType}]` : null;\n      }\n      return null;\n\n    case 'Object':\n      return 'JSON';\n\n    case 'Date':\n      return 'Date';\n\n    default:\n      return null;\n  }\n};\n\n// for a given schema, return main type fields, selector fields,\n// unique selector fields, orderBy fields, creatable fields, and updatable fields\nexport const getSchemaFields = (schema, typeName) => {\n  const fields = {\n    mainType: [],\n    create: [],\n    update: [],\n    selector: [],\n    selectorUnique: [],\n    orderBy: [],\n    readable: [],\n  };\n  const resolvers = [];\n\n  Object.keys(schema).forEach(fieldName => {\n    const field = schema[fieldName];\n    const fieldType = getGraphQLType(schema, fieldName);\n    const inputFieldType = getGraphQLType(schema, fieldName, true);\n\n    const {\n      canRead,\n      canCreate,\n      canUpdate,\n      viewableBy,\n      insertableBy,\n      editableBy,\n      description,\n      selectable,\n      unique,\n    } = field;\n    // only include fields that are viewable/insertable/editable and don't contain \"$\" in their name\n    // note: insertable/editable fields must be included in main schema in case they're returned by a mutation\n    // OpenCRUD backwards compatibility\n    if (\n      (canRead || canCreate || canUpdate || viewableBy || insertableBy || editableBy) &&\n      fieldName.indexOf('$') === -1\n    ) {\n      const fieldDescription = description;\n      const fieldDirective = isIntlField(field) ? '@intl' : '';\n      const fieldArguments = isIntlField(field) ? [{ name: 'locale', type: 'String' }] : [];\n\n      // if field is readable, make it filterable/orderable too\n      if (canRead || viewableBy) {\n        fields.readable.push({\n          name: fieldName,\n          type: fieldType,\n        });\n      }\n\n      // if field has a resolveAs, push it to schema\n      // note: resolveAs can be an array containing multiple resolver definitions\n      if (field.resolveAs) {\n\n        const resolveAsArray = Array.isArray(field.resolveAs) ? field.resolveAs : [field.resolveAs];\n\n        // unless addOriginalField option is disabled in one or more fields, also add original field to schema\n        const addOriginalField = resolveAsArray.every(resolveAs => resolveAs.addOriginalField !== false);\n        // note: do not add original field if resolved field has same name\n        if (addOriginalField && fieldType && field.resolveAs.fieldName && field.resolveAs.fieldName !== fieldName) {\n          fields.mainType.push({\n            description: fieldDescription,\n            name: fieldName,\n            args: fieldArguments,\n            type: fieldType,\n            directive: fieldDirective,\n          });\n        }\n\n        resolveAsArray.forEach(resolveAs => {\n          // get resolver name from resolveAs object, or else default to field name\n          const resolverName = resolveAs.fieldName || fieldName;\n\n          // use specified GraphQL type or else convert schema type\n          const fieldGraphQLType = resolveAs.type || fieldType;\n\n          // if resolveAs is an object, first push its type definition\n          // include arguments if there are any\n          // note: resolved fields are not internationalized\n          fields.mainType.push({\n            description: resolveAs.description,\n            name: resolverName,\n            args: resolveAs.arguments,\n            type: fieldGraphQLType,\n          });\n\n          // then build actual resolver object and pass it to addGraphQLResolvers\n          const resolver = {\n            [typeName]: {\n              [resolverName]: (document, args, context, info) => {\n                const { Users, currentUser } = context;\n                // check that current user has permission to access the original non-resolved field\n                const canReadField = Users.canReadField(currentUser, field, document);\n                const { resolver, relation } = resolveAs;\n                if (canReadField) {\n                  if (resolver) {\n                    return resolver(document, args, context, info);\n                  } else {\n                    return relations[relation]({\n                      document,\n                      args,\n                      context,\n                      info,\n                      fieldName,\n                      typeName: fieldGraphQLType,\n                    });\n                  }\n                } else {\n                  return null;\n                }\n              },\n            },\n          };\n          resolvers.push(resolver);\n        });\n      } else {\n        // try to guess GraphQL type\n        if (fieldType) {\n          fields.mainType.push({\n            description: fieldDescription,\n            name: fieldName,\n            args: fieldArguments,\n            type: fieldType,\n            directive: fieldDirective,\n          });\n        }\n      }\n\n      // OpenCRUD backwards compatibility\n      if (canCreate || insertableBy) {\n        fields.create.push({\n          name: fieldName,\n          type: inputFieldType,\n          required: !field.optional,\n        });\n      }\n      // OpenCRUD backwards compatibility\n      if (canUpdate || editableBy) {\n        fields.update.push({\n          name: fieldName,\n          type: inputFieldType,\n        });\n      }\n\n      // if field is i18nized, add foo_intl field containing all languages\n      // NOTE: not necessary anymore because intl fields are added by addIntlFields() in collections.js\n      // TODO: delete if not needed\n      // if (isIntlField(field)) {\n      //   // fields.mainType.push({\n      //   //   name: `${fieldName}_intl`,\n      //   //   type: '[IntlValue]',\n      //   // });\n      //   fields.create.push({\n      //     name: `${fieldName}_intl`,\n      //     type: '[IntlValueInput]',\n      //   });\n      //   fields.update.push({\n      //     name: `${fieldName}_intl`,\n      //     type: '[IntlValueInput]',\n      //   });\n      // }\n\n      if (selectable) {\n        fields.selector.push({\n          name: fieldName,\n          type: inputFieldType,\n        });\n      }\n\n      if (selectable && unique) {\n        fields.selectorUnique.push({\n          name: fieldName,\n          type: inputFieldType,\n        });\n      }\n    }\n  });\n\n  return {\n    fields,\n    resolvers,\n  };\n};\n\n*/\n/**\n * Generate graphQL types for the fields of a Vulcan schema\n */\n/* eslint-disable no-console */\nimport { isIntlField, isIntlDataField } from '../../modules/intl.js';\nimport { isBlackbox, isArrayChildField, unarrayfyFieldName, getFieldType, getFieldTypeName, getArrayChild, getNestedSchema } from '../../modules/simpleSchema_utils';\nimport { shouldAddOriginalField } from '../../modules/schema_utils';\nimport relations from './relations.js';\nimport { getGraphQLType } from '../../modules/graphql/utils';\n\nconst capitalize = word => {\n  if (!word) return word;\n  const [first, ...rest] = word;\n  return [first.toUpperCase(), ...rest].join('');\n};\n\n// get GraphQL type for a nested object (<MainTypeName><FieldName> e.g PostAuthor, EventAdress, etc.)\nexport const getNestedGraphQLType = (typeName, fieldName, isInput) =>\n  `${typeName}${capitalize(unarrayfyFieldName(fieldName))}${isInput ? 'Input' : ''}`;\n\n// NOTE: now lives in modules/graphql/utils.js so that it can be shared with client\n// TODO: clean up\n// get GraphQL type for a given schema and field name\n// export const getGraphQLType = ({ schema, fieldName, typeName, isInput = false, isParentBlackbox = false }) => {\n//   const field = schema[fieldName];\n\n//   if (field.typeName) return field.typeName; // respect typeName provided by user\n\n//   const fieldType = getFieldType(field);\n//   const fieldTypeName = getFieldTypeName(fieldType);\n\n//   // NOTE: we DON't USE isInputField! we don't want to match \"field.intl\", only \"field.intlData\"\n//   /**\n//    * Expected GraphQL Schema:\n//    * \n//    *   # The room name\n//   * name(locale: String): String @intl\n//   * # The room name\n//   * name_intl(locale: String): [IntlValue] @intl\n//   * \n//   * JS schema:\n//   * \n//   * name: {\n//   *   type: String,\n//   *   optional: false,\n//   *   canRead: ['guests'],\n//   *   canCreate: ['admins'],\n//   *   intl: true,\n//   * },\n//    */\n//   if (field.isIntlData) {\n//     return isInput ? '[IntlValueInput]' : '[IntlValue]';\n//   }\n\n//   switch (fieldTypeName) {\n//     case 'String':\n//       /*\n//       Getting Enums from allowed values is counter productive because enums syntax is limited\n//       @see https://github.com/VulcanJS/Vulcan/issues/2332\n//       if (hasAllowedValues(field) && isValidEnum(getAllowedValues(field))) {\n//         return getEnumType(typeName, fieldName);\n//       }*/\n//       return 'String';\n\n//     case 'Boolean':\n//       return 'Boolean';\n\n//     case 'Number':\n//       return 'Float';\n\n//     case 'SimpleSchema.Integer':\n//       return 'Int';\n\n//     // for arrays, look for type of associated schema field or default to [String]\n//     case 'Array':\n//       const arrayItemFieldName = `${fieldName}.$`;\n//       // note: make sure field has an associated array\n//       if (schema[arrayItemFieldName]) {\n//         // try to get array type from associated array\n//         const arrayItemType = getGraphQLType({\n//           schema,\n//           fieldName: arrayItemFieldName,\n//           typeName,\n//           isInput,\n//           isParentBlackbox: isParentBlackbox || isBlackbox(field) // blackbox field may not be nested items\n//         });\n//         return arrayItemType ? `[${arrayItemType}]` : null;\n//       }\n//       return null;\n\n//     case 'Object':\n//       // 4 cases: \n//       // - it's the child of a blackboxed array  => will be blackbox JSON\n//       // - a nested Schema, \n//       // - a referenced schema, or an actual JSON\n//       if (isParentBlackbox) return 'JSON';\n//       if (!isBlackbox(field) && fieldType._schema) {\n//         return getNestedGraphQLType(typeName, fieldName, isInput);\n//       }\n\n//       // referenced Schema\n//       if (/*field.type.definitions[0].blackbox && */field.typeName && field.typeName !== 'JSON') {\n//         return isInput ? field.typeName + 'Input' : field.typeName;\n//       }\n//       // blackbox JSON object\n//       return 'JSON';\n//     case 'Date':\n//       return 'Date';\n\n//     default:\n//       return null;\n//   }\n// };\n\n//const isObject = field => getFieldTypeName(getFieldType(field));\nconst hasTypeName = field => !!(field || {}).typeName;\n\nconst hasNestedSchema = field => !!getNestedSchema(field);\n\nconst hasArrayChild = (fieldName, schema) => !!getArrayChild(fieldName, schema);\n\nconst getArrayChildSchema = (fieldName, schema) => {\n  return getNestedSchema(getArrayChild(fieldName, schema));\n};\nconst hasArrayNestedChild = (fieldName, schema) =>\n  hasArrayChild(fieldName, schema) && !!getArrayChildSchema(fieldName, schema);\n\n//const getArrayChildTypeName = (fieldName, schema) =>\n//  (getArrayChild(fieldName, schema) || {}).typeName;\n//const hasArrayReferenceChild = (fieldName, schema) =>\n//  hasArrayChild(fieldName, schema) && !!getArrayChildTypeName(fieldName, schema);\n\nconst hasPermissions = field => field.canRead || field.canCreate || field.canUpdate;\nconst hasLegacyPermissions = field => {\n  const hasLegacyPermissions = field.viewableBy || field.insertableBy || field.editableBy;\n  if (hasLegacyPermissions)\n    console.warn(\n      'Some field is using legacy permission fields viewableBy, insertableBy and editableBy. Please replace those fields with canRead, canCreate and canUpdate.'\n    );\n  return hasLegacyPermissions;\n};\n\n// Generate GraphQL fields and resolvers for a field with a specific resolveAs\n// resolveAs allow to generate \"virtual\" fields that are queryable in GraphQL but does not exist in the database\nexport const getResolveAsFields = ({\n  typeName,\n  field,\n  fieldName,\n  fieldType,\n  fieldDescription,\n  fieldDirective,\n  fieldArguments,\n}) => {\n  const fields = {\n    mainType: [],\n  };\n  const resolvers = [];\n\n  const resolveAsArray = Array.isArray(field.resolveAs) ? field.resolveAs : [field.resolveAs];\n\n  // check if original (main schema) field should be added to GraphQL schema\n  const addOriginalField = shouldAddOriginalField(fieldName, field);\n  if (addOriginalField) {\n    fields.mainType.push({\n      description: fieldDescription,\n      name: fieldName,\n      args: fieldArguments,\n      type: fieldType,\n      directive: fieldDirective,\n    });\n  }\n\n  resolveAsArray.forEach(resolveAs => {\n    // get resolver name from resolveAs object, or else default to field name\n    const resolverName = resolveAs.fieldName || fieldName;\n\n    // use specified GraphQL type or else convert schema type\n    const fieldGraphQLType = resolveAs.typeName || resolveAs.type || fieldType;\n\n    // if resolveAs is an object, first push its type definition\n    // include arguments if there are any\n    // note: resolved fields are not internationalized\n    fields.mainType.push({\n      description: resolveAs.description,\n      name: resolverName,\n      args: resolveAs.arguments,\n      type: fieldGraphQLType,\n    });\n\n    // then build actual resolver object and pass it to addGraphQLResolvers\n    const resolver = {\n      [typeName]: {\n        [resolverName]: (document, args, context, info) => {\n          const { Users, currentUser } = context;\n          // check that current user has permission to access the original non-resolved field\n          const canReadField = Users.canReadField(currentUser, field, document);\n          const { resolver, relation } = resolveAs;\n          if (canReadField) {\n            if (resolver) {\n              return resolver(document, args, context, info);\n            } else if (relation) {\n              return relations[relation]({\n                document,\n                args,\n                context,\n                info,\n                fieldName,\n                typeName: fieldGraphQLType,\n              });\n            }\n          } else {\n            return null;\n          }\n        },\n      },\n    };\n    resolvers.push(resolver);\n  });\n  return { fields, resolvers };\n};\n\n// [Foo] => [CreateFoo]\nconst prefixType = (prefix, type) => {\n  if (!(type && type.length)) return type;\n  if (type[0] === '[') return `[${prefix}${type.slice(1, -1)}]`;\n  return prefix + type;\n};\n// [Foo] => [FooDataDinput]\nconst suffixType = (type, suffix) => {\n  if (!(type && type.length)) return type;\n  if (type[0] === '[') return `[${type.slice(1, -1)}${suffix}]`;\n  return type + suffix;\n};\n// handle querying/updating permissions\nexport const getPermissionFields = ({\n  field,\n  fieldName,\n  fieldType,\n  inputFieldType,\n  hasNesting = false,\n}) => {\n  const fields = {\n    create: [],\n    update: [],\n    selector: [],\n    selectorUnique: [],\n    sort: [],\n    readable: [],\n    filterable: [],\n  };\n  const {\n    canRead,\n    canCreate,\n    canUpdate,\n    viewableBy,\n    insertableBy,\n    editableBy,\n    selectable,\n    unique,\n    apiOnly,\n  } = field;\n  const createInputFieldType = hasNesting\n    ? suffixType(prefixType('Create', fieldType), 'DataInput')\n    : inputFieldType;\n  const updateInputFieldType = hasNesting\n    ? suffixType(prefixType('Update', fieldType), 'DataInput')\n    : inputFieldType;\n\n  // if field is readable, make it filterable/orderable too\n  if (canRead || viewableBy) {\n    fields.readable.push({\n      name: fieldName,\n      type: fieldType,\n    });\n    // we can only filter based on fields that actually exist in the db\n    if (!apiOnly) {\n      fields.filterable.push({\n        name: fieldName,\n        type: fieldType,\n      });\n    }\n  }\n\n  // OpenCRUD backwards compatibility\n  if (canCreate || insertableBy) {\n    fields.create.push({\n      name: fieldName,\n      type: createInputFieldType,\n      required: !field.optional,\n    });\n  }\n  // OpenCRUD backwards compatibility\n  if (canUpdate || editableBy) {\n    fields.update.push({\n      name: fieldName,\n      type: updateInputFieldType,\n    });\n  }\n\n  // if field is i18nized, add foo_intl field containing all languages\n  // NOTE: not necessary anymore because intl fields are added by addIntlFields() in collections.js\n  // TODO: delete if not needed\n  // if (isIntlField(field)) {\n  //   // fields.mainType.push({\n  //   //   name: `${ fieldName } _intl`,\n  //   //   type: '[IntlValue]',\n  //   // });\n  //   fields.create.push({\n  //     name: `${ fieldName } _intl`,\n  //     type: '[IntlValueInput]',\n  //   });\n  //   fields.update.push({\n  //     name: `${ fieldName } _intl`,\n  //     type: '[IntlValueInput]',\n  //   });\n  // }\n\n  if (selectable) {\n    fields.selector.push({\n      name: fieldName,\n      type: inputFieldType,\n    });\n  }\n\n  if (selectable && unique) {\n    fields.selectorUnique.push({\n      name: fieldName,\n      type: inputFieldType,\n    });\n  }\n\n  return fields;\n};\n\n// for a given schema, return main type fields, selector fields,\n// unique selector fields, sort fields, creatable fields, and updatable fields\nexport const getSchemaFields = (schema, typeName) => {\n  if (!schema) console.log('/////////////////////', typeName, '/////////////////////');\n  const fields = {\n    mainType: [],\n    create: [],\n    update: [],\n    selector: [],\n    selectorUnique: [],\n    sort: [],\n    enums: [],\n    readable: [],\n    filterable: [],\n  };\n  const nestedFieldsList = [];\n  const resolvers = [];\n\n  Object.keys(schema).forEach(fieldName => {\n    const field = schema[fieldName];\n    const fieldType = getGraphQLType({ schema, fieldName, typeName });\n    const inputFieldType = getGraphQLType({ schema, fieldName, typeName, isInput: true });\n\n    // find types that have a nested schema or have a reference to antoher type\n    const isNestedObject = hasNestedSchema(field);\n    // note: intl fields are an exception and are not considered as nested\n    const isNestedArray =\n      hasArrayNestedChild(fieldName, schema) && hasNestedSchema(getArrayChild(fieldName, schema)) && !isIntlField(field) && !isIntlDataField(field);\n    const isReferencedObject = hasTypeName(field);\n    const isReferencedArray = hasTypeName(getArrayChild(fieldName, schema));\n    const hasNesting = !isBlackbox(field) && (isNestedArray || isNestedObject || isReferencedObject || isReferencedArray);\n\n    // only include fields that are viewable/insertable/editable and don't contain \"$\" in their name\n    // note: insertable/editable fields must be included in main schema in case they're returned by a mutation\n    // OpenCRUD backwards compatibility\n    if ((hasPermissions(field) || hasLegacyPermissions(field)) && !isArrayChildField(fieldName)) {\n      const fieldDescription = field.description;\n      const fieldDirective = isIntlField(field) ? '@intl' : '';\n      const fieldArguments = isIntlField(field) ? [{ name: 'locale', type: 'String' }] : [];\n\n      // if field has a resolveAs, push it to schema\n      if (field.resolveAs) {\n        const { fields: resolveAsFields, resolvers: resolveAsResolvers } = getResolveAsFields({\n          typeName,\n          field,\n          fieldName,\n          fieldType,\n          fieldDescription,\n          fieldDirective,\n          fieldArguments,\n        });\n        resolvers.push(...resolveAsResolvers);\n        fields.mainType.push(...resolveAsFields.mainType);\n      } else {\n        // try to guess GraphQL type\n        if (fieldType) {\n          fields.mainType.push({\n            description: fieldDescription,\n            name: fieldName,\n            args: fieldArguments,\n            type: fieldType,\n            directive: fieldDirective,\n          });\n        }\n      }\n\n      // Support for enums from allowedValues has been removed (counter-productive)\n      // if field has allowedValues, add enum type\n      /*if (hasAllowedValues(field)) {\n        const allowedValues = getAllowedValues(field);\n        // TODO: we can't force value creation\n        //if (!isValidEnum(allowedValues)) throw new Error(`Allowed values of field ${ fieldName } can not be used as enum.\n        //One or more values are not respecting the Name regex`)\n \n        // ignore arrays containing invalid values\n        if (isValidEnum(allowedValues)) {\n          fields.enums.push({//\n            allowedValues,\n            typeName: getEnumType(typeName, fieldName)\n          });\n        } else {\n          console.warn(`Warning: Allowed values of field ${fieldName} can not be used as GraphQL Enum. One or more values are not respecting the Name regex.Consider normalizing allowedValues and using separate labels for displaying.`);\n        }\n      } \n      */\n\n      const permissionsFields = getPermissionFields({\n        field,\n        fieldName,\n        fieldType,\n        inputFieldType,\n        hasNesting,\n      });\n      fields.create.push(...permissionsFields.create);\n      fields.update.push(...permissionsFields.update);\n      fields.selector.push(...permissionsFields.selector);\n      fields.selectorUnique.push(...permissionsFields.selectorUnique);\n      fields.sort.push(...permissionsFields.sort);\n      fields.readable.push(...permissionsFields.readable);\n      fields.filterable.push(...permissionsFields.filterable);\n\n      // check for nested fields if the field does not reference an existing type\n      if (!field.typeName && isNestedObject) {\n        // TODO: reuse addTypeAndResolver on the nested schema instead?\n        //console.log('detected a nested field', fieldName);\n        const nestedSchema = getNestedSchema(field);\n        const nestedTypeName = getNestedGraphQLType(typeName, fieldName);\n        //const nestedInputTypeName = `${ nestedTypeName }Input`;\n        const nestedFields = getSchemaFields(nestedSchema, nestedTypeName);\n        // add the generated typeName to the info\n        nestedFields.typeName = nestedTypeName;\n        //nestedFields.inputTypeName = nestedInputTypeName;\n        nestedFieldsList.push(nestedFields);\n      }\n      // check if field is an array of objects if the field does not reference an existing type\n      if (isNestedArray && !getArrayChild(fieldName, schema).typeName) {\n        // TODO: reuse addTypeAndResolver on the nested schema instead?\n        //console.log('detected a field with an array child', fieldName);\n        const arrayNestedSchema = getArrayChildSchema(fieldName, schema);\n        const arrayNestedTypeName = getNestedGraphQLType(typeName, fieldName);\n        const arrayNestedFields = getSchemaFields(arrayNestedSchema, arrayNestedTypeName);\n        // add the generated typeName to the info\n        arrayNestedFields.typeName = arrayNestedTypeName;\n        nestedFieldsList.push(arrayNestedFields);\n      }\n    }\n  });\n  return {\n    fields,\n    nestedFieldsList,\n    resolvers,\n  };\n};\n\nexport default getSchemaFields;\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/graphql/typedefs.js",
    "content": "/**\n * Generate GraphQL typedefs\n */\n\n\n// schema generation\nconst generateQueryType = (queries = []) =>\n  queries.length === 0\n  ? ''\n  : `type Query {\n${queries\n      .map(\n        q =>\n          `${\n          q.description\n            ? `  # ${q.description}\n`\n            : ''\n          }  ${q.query}\n  `\n      )\n      .join('\\n')}\n}\n  `;\n\nconst generateMutationType = (mutations = []) =>\n  mutations.length === 0\n  ? ''\n  : `type Mutation {\n${mutations\n              .map(\n                m =>\n                  `${\n                    m.description\n                      ? `  # ${m.description}\n`\n                      : ''\n                  }  ${m.mutation}\n`\n              )\n              .join('\\n')}\n}\n`;\n\n// typeDefs\nexport const generateTypeDefs = (GraphQLSchema) => [\n  `\nscalar JSON\nscalar Date\n\n# see https://docs.hasura.io/1.0/graphql/manual/queries/query-filters.html\n\ninput String_Selector {\n  _eq: String\n  _gt: String\n  _gte: String\n  _in: [String!]\n  _nin: [String!]\n  _is_null: Boolean\n  _like: String\n  _lt: String\n  _lte: String\n  _neq: String\n  #_ilike: String\n  #_nilike: String\n  #_nlike: String\n  #_similar: String\n  #_nsimilar: String\n}\n\ninput String_Array_Selector {\n  _in: [String!]\n  _nin: [String!]\n  _contains: String\n  _contains_all: [String_Selector]\n}\n\ninput Int_Selector {\n  _eq: Int\n  _gt: Int\n  _gte: Int\n  _in: [Int!]\n  _nin: [Int!]\n  _is_null: Boolean\n  _lt: Int\n  _lte: Int\n  _neq: Int\n}\n\ninput Int_Array_Selector {\n  _in: [Int!]\n  _nin: [Int!]\n  _contains: Int_Selector\n  _contains_all: [Int_Selector]\n}\n\ninput Float_Selector {\n  _eq: Float\n  _gt: Float\n  _gte: Float\n  _in: [Float!]\n  _nin: [Float!]\n  _is_null: Boolean\n  _lt: Float\n  _lte: Float\n  _neq: Float\n}\n\ninput Float_Array_Selector {\n  _in: [Int!]\n  _nin: [Int!]\n  _contains: Float_Selector\n  _contains_all: [Float_Selector]\n}\n\ninput Boolean_Selector {\n  _eq: Boolean\n  _neq: Boolean\n}\n\ninput Boolean_Array_Selector {\n  _contains: Boolean_Selector\n}\n\ninput Date_Selector {\n  _eq: Date\n  _gt: Date\n  _gte: Date\n  _in: [Date!]\n  _nin: [Date!]\n  _is_null: Boolean\n  _lt: Date\n  _lte: Date\n  _neq: Date\n}\n\ninput Date_Array_Selector {\n  _contains: Date_Selector\n  _contains_all: [Date_Selector]\n}\n\n# column ordering options\nenum SortOptions {\n  asc\n  desc\n}\n\ninput OptionsInput {\n  # Whether to enable caching for this query\n  enableCache: Boolean\n  # For single document queries, return null instead of throwing MissingDocumentError\n  allowNull: Boolean\n}\n\n${GraphQLSchema.getAdditionalSchemas()}\n\n${GraphQLSchema.getCollectionsSchemas()}\n\n${generateQueryType(GraphQLSchema.queries)}\n\n${generateMutationType(GraphQLSchema.mutations)}\n\n`,\n];\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/intl.js",
    "content": "// see https://github.com/apollographql/graphql-tools/blob/master/docs/source/schema-directives.md#marking-strings-for-internationalization\n\nimport { addGraphQLDirectiveTransformer, addGraphQLSchema } from './graphql/index.js';\nimport { mapSchema, MapperKind } from '@graphql-tools/utils';\n\nimport { defaultFieldResolver } from 'graphql';\nimport { Collections } from '../modules/collections';\nimport { getSetting } from '../modules/settings';\nimport { debug } from '../modules/debug';\nimport Vulcan from '../modules/config';\nimport { isIntlField } from '../modules/intl';\nimport { Connectors } from './connectors';\nimport pickBy from 'lodash/pickBy';\n\n/*\n\nCreate GraphQL types\n\n*/\nconst intlValueSchemas = `type IntlValue {\n  locale: String\n  value: String\n}\ninput IntlValueInput{\n  locale: String\n  value: String\n}`;\naddGraphQLSchema(intlValueSchemas);\n\n/*\n\nTake an array of translations, a locale, and a default locale, and return a matching string\n\n*/\nconst getLocaleString = (translations, locale, defaultLocale) => {\n  const localeObject = translations.find(translation => translation.locale === locale);\n  const defaultLocaleObject = translations.find(translation => translation.locale === defaultLocale);\n  return (localeObject && localeObject.value) || (defaultLocaleObject && defaultLocaleObject.value);\n};\n\n/*\n\nGraphQL @intl directive resolver\n\n*/\n\nconst intlDirectiveTransformer = schema =>\n  mapSchema(schema, {\n    [MapperKind.OBJECT_FIELD]: fieldConfig => {\n      const { resolve = defaultFieldResolver } = fieldConfig;\n      const name = fieldConfig?.astNode?.name?.value;\n      fieldConfig.resolve = async function(source = {}, args = {}, context, info) {\n        const doc = source;\n        const fieldValue = await resolve(source, args, context, info);\n        const locale = args.locale || context.locale;\n        const defaultLocale = getSetting('locale');\n        const intlField = doc[`${name}_intl`];\n        // Return string in requested or default language, or else field's original value\n        return (intlField && getLocaleString(intlField, locale, defaultLocale)) || fieldValue;\n      };\n    },\n  });\n\naddGraphQLDirectiveTransformer(intlDirectiveTransformer);\n\naddGraphQLSchema('directive @intl on FIELD_DEFINITION');\n\n/*\n\nMigration function\n\n*/\nconst migrateIntlFields = async defaultLocale => {\n  if (!defaultLocale) {\n    throw new Error(\"Please pass the id of the locale to which to migrate your current content (e.g. migrateIntlFields('en'))\");\n  }\n\n  Collections.forEach(async collection => {\n    const schema = collection.simpleSchema()._schema;\n    const intlFields = pickBy(schema, isIntlField);\n    const intlFieldsNames = Object.keys(intlFields);\n\n    if (intlFieldsNames.length) {\n      // eslint-disable-next-line no-console\n      console.log(\n        `### Found ${intlFieldsNames.length} field to migrate for collection ${collection.options.collectionName}: ${intlFieldsNames.join(\n          ', '\n        )} ###\\n`\n      );\n\n      // const intlFieldsWithLocale = intlFieldsNames.map(f => `${f}_intl`);\n\n      // find all documents with one or more unmigrated intl fields\n      const selector = {\n        $or: intlFieldsNames.map(f => {\n          return {\n            $and: [{ [`${f}`]: { $exists: true } }, { [`${f}_intl`]: { $exists: false } }],\n          };\n        }),\n      };\n      const documentsToMigrate = await Connectors.find(collection, selector);\n\n      if (documentsToMigrate.length) {\n        console.log(`-> found ${documentsToMigrate.length} documents to migrate \\n`); // eslint-disable-line no-console\n        for (const doc of documentsToMigrate) {\n          console.log(`// Migrating document ${doc._id}`); // eslint-disable-line no-console\n          const modifier = { $push: {} };\n\n          intlFieldsNames.forEach(f => {\n            if (doc[f] && !doc[`${f}_intl`]) {\n              const translationObject = { locale: defaultLocale, value: doc[f] };\n              console.log(`-> Adding field ${f}_intl: ${JSON.stringify(translationObject)} `); // eslint-disable-line no-console\n              modifier.$push[`${f}_intl`] = translationObject;\n            }\n          });\n\n          if (!_.isEmpty(modifier.$push)) {\n            // update document\n            // eslint-disable-next-line no-await-in-loop\n            const n = await Connectors.update(collection, { _id: doc._id }, modifier);\n            console.log(`-> migrated ${n} documents \\n`); // eslint-disable-line no-console\n          }\n          console.log('\\n'); // eslint-disable-line no-console\n        }\n      } else {\n        console.log('-> found no documents to migrate.'); // eslint-disable-line no-console\n      }\n    }\n  });\n};\n\nVulcan.migrateIntlFields = migrateIntlFields;\n\n/*\n\nTake a header object, and figure out the locale\n\nAlso accepts userLocale to indicate the current user's preferred locale\n\n*/\nexport const getHeaderLocale = (headers, userLocale) => {\n  let cookieLocale, acceptedLocale, locale, localeMethod;\n\n  // get locale from cookies\n  if (headers?.['cookie']) {\n    const cookies = {};\n    headers['cookie'].split('; ').forEach(c => {\n      const cookieArray = c.split('=');\n      cookies[cookieArray[0]] = cookieArray[1];\n    });\n    cookieLocale = cookies.locale;\n  }\n\n  // get locale from accepted-language header\n  if (headers?.['accept-language']) {\n    const acceptedLanguages = headers['accept-language'].split(',').map(l => l.split(';')[0]);\n    acceptedLocale = acceptedLanguages[0]; // for now only use the highest-priority accepted language\n  }\n\n  if (headers?.locale) {\n    locale = headers.locale;\n    localeMethod = 'header';\n  } else if (cookieLocale) {\n    locale = cookieLocale;\n    localeMethod = 'cookie';\n  } else if (userLocale) {\n    locale = userLocale;\n    localeMethod = 'user';\n  } else if (acceptedLocale) {\n    locale = acceptedLocale;\n    localeMethod = 'browser';\n  } else {\n    locale = getSetting('locale', 'en-US');\n    localeMethod = 'setting';\n  }\n\n  debug(`// locale: ${locale} (via ${localeMethod})`);\n\n  return locale;\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/intl_polyfill.js",
    "content": "/*\n\nintl polyfill. See https://github.com/andyearnshaw/Intl.js/\n\n*/\n\nimport { getSetting, registerSetting } from '../modules/settings.js';\n\nregisterSetting('locale', 'en-US');\n\nimport areIntlLocalesSupported from 'intl-locales-supported'\n\nvar localesMyAppSupports = [\n  getSetting('locale', 'en-US')\n];\n\nif (global.Intl) {\n  // Determine if the built-in `Intl` has the locale data we need.\n  if (!areIntlLocalesSupported(localesMyAppSupports)) {\n    // `Intl` exists, but it doesn't have the data we need, so load the\n    // polyfill and replace the constructors with need with the polyfill's.\n    var IntlPolyfill = require('intl');\n    Intl.NumberFormat   = IntlPolyfill.NumberFormat;\n    Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;\n  }\n} else {\n  // No `Intl`, so use and load the polyfill.\n  global.Intl = require('intl');\n}"
  },
  {
    "path": "packages/vulcan-lib/lib/server/main.js",
    "content": "// import './oauth_config.js';\nimport './intl_polyfill.js';\nimport './site.js';\n\nimport './connectors/mongo.js';\n\nexport * from './graphql/index.js';\n\nexport * from './debug.js';\nexport * from './connectors.js';\nexport * from './query.js';\nexport * from '../modules/index.js';\nexport * from './mutators.js';\nexport * from './errors.js';\nexport * from './default_resolvers.js';\nexport * from './default_mutations.js';\n// TODO: what to do with this?\nexport * from './meteor_patch.js';\n//export * from './render_context.js';\nexport * from './utils.js';\nexport * from './intl.js';\nexport * from './accounts_helpers.js';\nexport * from './source_version.js';\nexport * from './caching.js';\n\nexport * from './apollo-server';\n\nimport './apollo-server/startup';\n\nexport * from './apollo-ssr';"
  },
  {
    "path": "packages/vulcan-lib/lib/server/meteor_patch.js",
    "content": "import { WebApp } from 'meteor/webapp';\n\n// NOTE: simplified version of webAppConnectHandlersUse that doesn't do anything\n// export const webAppConnectHandlersUse = (app, options) => {\n//   WebApp.connectHandlers.use(app);\n// };\n\n// TODO: figure out if still needed?\n// clever webAppConnectHandlersUse\nexport const webAppConnectHandlersUse = (name, route, fn, options) => {\n  // init\n  if (typeof name === 'function') {\n    options = route;\n    fn = name;\n    route = '/';\n    name = undefined;\n  } else if (name[0] === '/') {\n    options = fn;\n    fn = route;\n    route = name;\n    name = undefined;\n  } else if (typeof route === 'function') {\n    options = fn;\n    fn = route;\n    route = '/';\n  }\n  options = options || {};\n  route = options.route ? options.route : route;\n\n  // newfn\n  let done = false;\n  const newfn = (req, res, next) => {\n    if (!fn.stack && !fn._router && done && options.once) {\n      next();\n      return;\n    }\n    done = true;\n\n    fn(req, res, next);\n\n    if (!fn.stack && !fn._router && options.autoNext) {\n      next();\n    }\n  };\n\n  // use it\n  let connectHandlers;\n  if (options.raw) {\n    connectHandlers = WebApp.rawConnectHandlers;\n  } else {\n    connectHandlers = WebApp.connectHandlers;\n  }\n  connectHandlers.use(route, newfn);\n\n  // get handle\n  let handle;\n  if (options.unshift) {\n    const item = connectHandlers.stack.pop();\n    connectHandlers.stack.unshift(item);\n    handle = connectHandlers.stack[0].handle;\n  } else {\n    handle = connectHandlers.stack[connectHandlers.stack.length - 1].handle;\n  }\n\n  // copy options to handle\n  Object.keys(options).forEach((key) => {\n    handle[key] = options[key];\n  });\n};\n\n// NOTE: breaks /graphql endpoint in production\n// TODO: figure out what this does and if it's still needed?\n// webAppConnectHandlersUse(function sortConnectHandlersMiddleware(req, res, next) {\n//   WebApp.rawConnectHandlers.stack.forEach((item) => {\n//     if (isNaN(item.handle.order)) {\n//       item.handle.order = 100;\n//     }\n//   });\n//   WebApp.connectHandlers.stack.forEach((item) => {\n//     if (isNaN(item.handle.order)) {\n//       item.handle.order = 100;\n//     }\n//   });\n//   WebApp.rawConnectHandlers.stack.sort((a, b) => a.handle.order - b.handle.order);\n//   WebApp.connectHandlers.stack.sort((a, b) => a.handle.order - b.handle.order);\n// }, { order: 0, autoNext: true, once: true, unshift: true });\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/mutators.js",
    "content": "/*\n\nMutations have five steps:\n\n1. Validation\n\nIf the mutator call is not trusted (for example, it comes from a GraphQL mutation),\nwe'll run all validate steps:\n\n- Check that the current user has permission to insert/edit each field.\n- Add userId to document (insert only).\n- Run validation callbacks.\n\n2. Before Callbacks\n\nThe second step is to run the mutation argument through all the [before] callbacks.\n\n3. Operation\n\nWe then perform the insert/update/remove operation.\n\n4. After Callbacks\n\nWe then run the mutation argument through all the [after] callbacks.\n\n5. Async Callbacks\n\nFinally, *after* the operation is performed, we execute any async callbacks.\nBeing async, they won't hold up the mutation and slow down its response time\nto the client.\n\n*/\n\nimport { runCallbacks, runCallbacksAsync } from '../modules/index.js';\nimport {\n  validateDocument,\n  validateData,\n  dataToModifier,\n  modifierToData,\n} from '../modules/validation.js';\nimport { globalCallbacks } from '../modules/callbacks.js';\nimport { registerSetting } from '../modules/settings.js';\nimport { debug, debugGroup, debugGroupEnd } from '../modules/debug.js';\nimport { throwError } from './errors.js';\nimport { Connectors } from './connectors.js';\nimport pickBy from 'lodash/pickBy';\nimport clone from 'lodash/clone';\nimport isEmpty from 'lodash/isEmpty';\nimport get from 'lodash/get';\n\nregisterSetting('database', 'mongo', 'Which database to use for your back-end');\n\n/*\n\nCreate\n\n*/\nexport const createMutator = async ({\n  collection,\n  document: originalDocument,\n  data: originalData,\n  currentUser,\n  validate,\n  context = {},\n  contextName,\n}) => {\n  // OpenCRUD backwards compatibility: accept either data or document\n  // we don't want to modify the original document\n  let data = clone(originalData || originalDocument);\n\n  const { collectionName, typeName } = collection.options;\n  const schema = collection.simpleSchema()._schema;\n\n  // get currentUser from context if possible\n  if (!currentUser && context.currentUser) {\n    currentUser = context.currentUser;\n  }\n\n  startDebugMutator(collectionName, 'Create', { validate, data });\n\n  /*\n\n  Properties\n\n  Note: keep newDocument for backwards compatibility\n  \n  */\n  const properties = {\n    data,\n    originalData,\n    currentUser,\n    collection,\n    context,\n    document: data, // backwards compatibility\n    originalDocument, // backwards compatibility\n    newDocument: data, // backwards compatibility\n    schema,\n    contextName,\n  };\n\n  /*\n\n  Validation\n\n  */\n  if (validate) {\n    let validationErrors = [];\n    validationErrors = validationErrors.concat(validateDocument(data, collection, context, contextName));\n    // new callback API (Oct 2019)\n    validationErrors = await runCallbacks({\n      name: `${typeName.toLowerCase()}.create.validate`,\n      callbacks: get(collection, 'options.callbacks.create.validate', []),\n      iterator: validationErrors,\n      properties,\n    });\n    validationErrors = await runCallbacks({\n      name: '*.create.validate',\n      callbacks: get(globalCallbacks, 'create.validate', []),\n      iterator: validationErrors,\n      properties,\n    });\n    // run validation callbacks\n    validationErrors = await runCallbacks({\n      name: `${typeName.toLowerCase()}.create.validate`,\n      iterator: validationErrors,\n      properties,\n    });\n    validationErrors = await runCallbacks({\n      name: '*.create.validate',\n      iterator: validationErrors,\n      properties,\n    });\n    // OpenCRUD backwards compatibility\n    data = await runCallbacks(\n      `${collectionName.toLowerCase()}.new.validate`,\n      data,\n      currentUser,\n      validationErrors\n    );\n    if (validationErrors.length) {\n      console.log(validationErrors); // eslint-disable-line no-console\n      throwError({ id: 'app.validation_error', data: { break: true, errors: validationErrors } });\n    }\n  }\n  /*\n\n  userId\n\n  If user is logged in, check if userId field is in the schema and add it to document if needed\n\n  */\n  if (currentUser) {\n    const userIdInSchema = Object.keys(schema).find(key => key === 'userId');\n    if (!!userIdInSchema && !data.userId) data.userId = currentUser._id;\n  }\n  /* \n  \n  onCreate\n\n  note: cannot use forEach with async/await. \n  See https://stackoverflow.com/a/37576787/649299\n\n  note: clone arguments in case callbacks modify them\n\n  */\n  for (let fieldName of Object.keys(schema)) {\n    try {\n      let autoValue;\n      if (schema[fieldName].onCreate) {\n        // OpenCRUD backwards compatibility: keep both newDocument and data for now, but phase out newDocument eventually\n        autoValue = await schema[fieldName].onCreate(properties); // eslint-disable-line no-await-in-loop\n      } else if (schema[fieldName].onInsert) {\n        // OpenCRUD backwards compatibility\n        autoValue = await schema[fieldName].onInsert(clone(data), currentUser); // eslint-disable-line no-await-in-loop\n      }\n      if (typeof autoValue !== 'undefined') {\n        data[fieldName] = autoValue;\n      }\n    } catch(e) {\n      console.log(`// Autovalue error on field ${fieldName}`);\n      console.log(e);\n    }\n  }\n  // TODO: find that info in GraphQL mutations\n  // if (Meteor.isServer && this.connection) {\n  //   post.userIP = this.connection.clientAddress;\n  //   post.userAgent = this.connection.httpHeaders['user-agent'];\n  // }\n\n  /*\n\n  Before\n  \n  */\n  // new callback API (Oct 2019)\n  data = await runCallbacks({\n    name: `${typeName.toLowerCase()}.create.before`,\n    callbacks: get(collection, 'options.callbacks.create.before', []),\n    iterator: data,\n    properties,\n  });\n  data = await runCallbacks({\n    name: '*.create.before',\n    callbacks: get(globalCallbacks, 'create.before', []),\n    iterator: data,\n    properties,\n  });\n  // old callback API\n  data = await runCallbacks({\n    name: `${typeName.toLowerCase()}.create.before`,\n    iterator: data,\n    properties,\n  });\n  data = await runCallbacks({ name: '*.create.before', iterator: data, properties });\n  // OpenCRUD backwards compatibility\n  data = await runCallbacks(\n    `${collectionName.toLowerCase()}.new.before`,\n    data,\n    currentUser\n  );\n  data = await runCallbacks(`${collectionName.toLowerCase()}.new.sync`, data, currentUser);\n\n  /*\n\n  DB Operation\n\n  */\n  data._id = await Connectors.create(collection, data);\n\n  /*\n\n  After\n\n  */\n  // note: query for document to get fresh document with collection-hooks effects applied\n  let document = await Connectors.get(collection, data._id);\n  // new callback API (Oct 2019)\n  document = await runCallbacks({\n    name: `${typeName.toLowerCase()}.create.after`,\n    callbacks: get(collection, 'options.callbacks.create.after', []),\n    iterator: document,\n    properties,\n  });\n  document = await runCallbacks({\n    name: '*.create.after',\n    callbacks: get(globalCallbacks, 'create.after', []),\n    iterator: document,\n    properties,\n  });\n  // run any post-operation sync callbacks\n  document = await runCallbacks({\n    name: `${typeName.toLowerCase()}.create.after`,\n    iterator: document,\n    properties,\n  });\n  document = await runCallbacks({ name: '*.create.after', iterator: document, properties });\n  // OpenCRUD backwards compatibility\n  document = await runCallbacks(`${collectionName.toLowerCase()}.new.after`, document, currentUser);\n\n  // update document object in properties object\n  properties.document = document;\n\n  /*\n\n  Async\n\n  */\n  // new callback API (Oct 2019)\n  await runCallbacksAsync({\n    name: `${typeName.toLowerCase()}.create.async`,\n    callbacks: get(collection, 'options.callbacks.create.async', []),\n    properties,\n  });\n  await runCallbacksAsync({\n    name: '*.create.async',\n    callbacks: get(globalCallbacks, 'create.async', []),\n    properties,\n  });\n  // note: make sure properties.document is up to date\n  await runCallbacksAsync({\n    name: `${typeName.toLowerCase()}.create.async`,\n    properties,\n  });\n  await runCallbacksAsync({ name: '*.create.async', properties });\n  // OpenCRUD backwards compatibility\n  await runCallbacksAsync(\n    `${collectionName.toLowerCase()}.new.async`,\n    document,\n    currentUser,\n    collection\n  );\n\n  if (context.Users) {\n    document = context.Users.restrictViewableFields(currentUser, collection, document);\n  }\n\n  endDebugMutator(collectionName, 'Create', { document });\n\n  return { data: document };\n};\n\n/*\n\nUpdate\n\n*/\nexport const updateMutator = async ({\n  collection,\n  documentId,\n  selector,\n  data,\n  set = {},\n  unset = {},\n  currentUser,\n  validate,\n  context = {},\n  document: oldDocument,\n  contextName,\n}) => {\n  const { collectionName, typeName } = collection.options;\n  const schema = collection.simpleSchema()._schema;\n\n  // get currentUser from context if possible\n  if (!currentUser && context.currentUser) {\n    currentUser = context.currentUser;\n  }\n\n  // OpenCRUD backwards compatibility\n  selector = selector || { _id: documentId };\n  data = data || modifierToData({ $set: set, $unset: unset });\n\n  startDebugMutator(collectionName, 'Update', { selector, data });\n\n  if (isEmpty(selector)) {\n    throw new Error('Selector cannot be empty');\n  }\n\n  // get original document from database or arguments\n  oldDocument = oldDocument || (await Connectors.get(collection, selector));\n\n  if (!oldDocument) {\n    throw new Error(`Could not find document to update for selector: ${JSON.stringify(selector)}`);\n  }\n\n  // get a \"preview\" of the new document\n  let document = { ...oldDocument, ...data };\n  document = pickBy(document, f => f !== null);\n\n  /*\n\n  Properties\n\n  */\n  const properties = {\n    data,\n    originalData: clone(data),\n    oldDocument,\n    originalDocument: oldDocument,\n    document,\n    currentUser,\n    collection,\n    context,\n    schema,\n    contextName,\n  };\n\n  /*\n\n  Validation\n\n  */\n  if (validate) {\n    let validationErrors = [];\n\n    validationErrors = validationErrors.concat(validateData(data, document, collection, context, contextName));\n\n    // new callback API (Oct 2019)\n    validationErrors = await runCallbacks({\n      name: `${typeName.toLowerCase()}.update.validate`,\n      callbacks: get(collection, 'options.callbacks.update.validate', []),\n      iterator: validationErrors,\n      properties,\n    });\n    validationErrors = await runCallbacks({\n      name: '*.update.validate',\n      callbacks: get(globalCallbacks, 'update.validate', []),\n      iterator: validationErrors,\n      properties,\n    });\n    // old API\n    validationErrors = await runCallbacks({\n      name: `${typeName.toLowerCase()}.update.validate`,\n      iterator: validationErrors,\n      properties,\n    });\n    validationErrors = await runCallbacks({\n      name: '*.update.validate',\n      iterator: validationErrors,\n      properties,\n    });\n    // OpenCRUD backwards compatibility\n    data = modifierToData(\n      await runCallbacks(\n        `${collectionName.toLowerCase()}.edit.validate`,\n        dataToModifier(data),\n        document,\n        currentUser,\n        validationErrors\n      )\n    );\n\n    if (validationErrors.length) {\n      console.log(validationErrors); // eslint-disable-line no-console\n      throwError({ id: 'app.validation_error', data: { break: true, errors: validationErrors } });\n    }\n  }\n\n  /*\n\n  onUpdate\n\n  */\n  for (let fieldName of Object.keys(schema)) {\n    let autoValue;\n    if (schema[fieldName].onUpdate) {\n      autoValue = await schema[fieldName].onUpdate(properties); // eslint-disable-line no-await-in-loop\n    } else if (schema[fieldName].onEdit) {\n      // OpenCRUD backwards compatibility\n      // eslint-disable-next-line no-await-in-loop\n      autoValue = await schema[fieldName].onEdit(\n        dataToModifier(clone(data)),\n        oldDocument || document,\n        currentUser,\n        document\n      );\n    }\n    if (typeof autoValue !== 'undefined') {\n      data[fieldName] = autoValue;\n    }\n  }\n\n  /*\n\n  Before\n\n  */\n  // new callback API (Oct 2019)\n  data = await runCallbacks({\n    name: `${typeName.toLowerCase()}.update.before`,\n    callbacks: get(collection, 'options.callbacks.update.before', []),\n    iterator: data,\n    properties,\n  });\n  data = await runCallbacks({\n    name: '*.update.before',\n    callbacks: get(globalCallbacks, 'update.before', []),\n    iterator: data,\n    properties,\n  });\n  // old API\n  data = await runCallbacks({\n    name: `${typeName.toLowerCase()}.update.before`,\n    iterator: data,\n    properties,\n  });\n  data = await runCallbacks({ name: '*.update.before', iterator: data, properties });\n  // OpenCRUD backwards compatibility\n  data = modifierToData(\n    await runCallbacks(\n      `${collectionName.toLowerCase()}.edit.before`,\n      dataToModifier(data),\n      document,\n      currentUser,\n      document\n    )\n  );\n  data = modifierToData(\n    await runCallbacks(\n      `${collectionName.toLowerCase()}.edit.sync`,\n      dataToModifier(data),\n      document,\n      currentUser,\n      document\n    )\n  );\n\n  // update connector requires a modifier, so get it from data\n  const modifier = dataToModifier(data);\n\n  // remove empty modifiers\n  if (isEmpty(modifier.$set)) {\n    delete modifier.$set;\n  }\n  if (isEmpty(modifier.$unset)) {\n    delete modifier.$unset;\n  }\n\n  /*\n\n  DB Operation\n\n  */\n  if (!isEmpty(modifier)) {\n    // update document\n    await Connectors.update(collection, selector, modifier, { removeEmptyStrings: false });\n\n    // get fresh copy of document from db\n    document = await Connectors.get(collection, selector);\n\n    // TODO: add support for caching by other indexes to Dataloader\n    // https://github.com/VulcanJS/Vulcan/issues/2000\n    // clear cache if needed\n    if (selector.documentId && collection.loader) {\n      collection.loader.clear(selector.documentId);\n    }\n  }\n\n  /*\n\n  After\n\n  */\n  // new callback API (Oct 2019)\n  document = await runCallbacks({\n    name: `${typeName.toLowerCase()}.update.after`,\n    callbacks: get(collection, 'options.callbacks.update.after', []),\n    iterator: document,\n    properties,\n  });\n  document = await runCallbacks({\n    name: '*.update.after',\n    callbacks: get(globalCallbacks, 'update.after', []),\n    iterator: document,\n    properties,\n  });\n  // old API\n  document = await runCallbacks({\n    name: `${typeName.toLowerCase()}.update.after`,\n    iterator: document,\n    properties,\n  });\n  document = await runCallbacks({ name: '*.update.after', iterator: document, properties });\n  // OpenCRUD backwards compatibility\n  document = await runCallbacks(\n    `${collectionName.toLowerCase()}.edit.after`,\n    document,\n    oldDocument,\n    currentUser\n  );\n\n  /*\n\n  Async\n\n  */\n  // new callback API (Oct 2019)\n  await runCallbacksAsync({\n    name: `${typeName.toLowerCase()}.update.async`,\n    callbacks: get(collection, 'options.callbacks.update.async', []),\n    properties,\n  });\n  await runCallbacksAsync({\n    name: '*.update.async',\n    callbacks: get(globalCallbacks, 'update.async', []),\n    properties,\n  });\n  // run async callbacks\n  await runCallbacksAsync({ name: `${typeName.toLowerCase()}.update.async`, properties });\n  await runCallbacksAsync({ name: '*.update.async', properties });\n  // OpenCRUD backwards compatibility\n  await runCallbacksAsync(\n    `${collectionName.toLowerCase()}.edit.async`,\n    document,\n    oldDocument,\n    currentUser,\n    collection\n  );\n\n  endDebugMutator(collectionName, 'Update', { modifier });\n\n  // filter out non readable fields if appliable\n  if (context.Users) {\n    document = context.Users.restrictViewableFields(currentUser, collection, document);\n  }\n\n  return { data: document };\n};\n\n/*\n\nDelete\n\n*/\nexport const deleteMutator = async ({\n  collection,\n  documentId,\n  selector,\n  currentUser,\n  validate,\n  context = {},\n  document,\n}) => {\n  const { collectionName, typeName } = collection.options;\n  const schema = collection.simpleSchema()._schema;\n\n  // get currentUser from context if possible\n  if (!currentUser && context.currentUser) {\n    currentUser = context.currentUser;\n  }\n\n  // OpenCRUD backwards compatibility\n  selector = selector || { _id: documentId };\n\n  if (isEmpty(selector)) {\n    throw new Error('Selector cannot be empty');\n  }\n\n  document = document || (await Connectors.get(collection, selector));\n\n  if (!document) {\n    throw new Error(`Could not find document to delete for selector: ${JSON.stringify(selector)}`);\n  }\n\n  /*\n\n  Properties\n\n  */\n  const properties = { document, currentUser, collection, context, schema };\n\n  /*\n\n  Validation\n\n  */\n  if (validate) {\n    let validationErrors = [];\n\n    // new API (Oct 2019)\n    validationErrors = await runCallbacks({\n      name: `${typeName.toLowerCase()}.delete.validate`,\n      callbacks: get(collection, 'options.callbacks.delete.validate', []),\n      iterator: validationErrors,\n      properties,\n    });\n    validationErrors = await runCallbacks({\n      name: '*.delete.validate',\n      callbacks: get(globalCallbacks, 'delete.validate', []),\n      iterator: validationErrors,\n      properties,\n    });\n    // old API\n    validationErrors = await runCallbacks({\n      name: `${typeName.toLowerCase()}.delete.validate`,\n      iterator: validationErrors,\n      properties,\n    });\n    validationErrors = await runCallbacks({\n      name: '*.delete.validate',\n      iterator: validationErrors,\n      properties,\n    });\n    // OpenCRUD backwards compatibility\n    document = await runCallbacks(\n      `${collectionName.toLowerCase()}.remove.validate`,\n      document,\n      currentUser\n    );\n\n    if (validationErrors.length) {\n      console.log(validationErrors); // eslint-disable-line no-console\n      throwError({ id: 'app.validation_error', data: { break: true, errors: validationErrors } });\n    }\n  }\n\n  /*\n\n  onDelete\n\n  */\n  for (let fieldName of Object.keys(schema)) {\n    if (schema[fieldName].onDelete) {\n      await schema[fieldName].onDelete(properties); // eslint-disable-line no-await-in-loop\n    } else if (schema[fieldName].onRemove) {\n      // OpenCRUD backwards compatibility\n      await schema[fieldName].onRemove(document, currentUser); // eslint-disable-line no-await-in-loop\n    }\n  }\n\n  /*\n\n  Before\n\n  */\n  // new API (Oct 2019)\n  document = await runCallbacks({\n    name: `${typeName.toLowerCase()}.delete.before`,\n    callbacks: get(collection, 'options.callbacks.delete.before', []),\n    iterator: document,\n    properties,\n  });\n  document = await runCallbacks({\n    name: '*.delete.before',\n    callbacks: get(globalCallbacks, 'delete.before', []),\n    iterator: document,\n    properties,\n  });\n  // old API\n  document = await runCallbacks({\n    name: `${typeName.toLowerCase()}.delete.before`,\n    iterator: document,\n    properties,\n  });\n  document = await runCallbacks({ name: '*.delete.before', iterator: document, properties });\n  // OpenCRUD backwards compatibility\n  document = await runCallbacks(\n    `${collectionName.toLowerCase()}.remove.before`,\n    document,\n    currentUser\n  );\n  document = await runCallbacks(\n    `${collectionName.toLowerCase()}.remove.sync`,\n    document,\n    currentUser\n  );\n\n  /*\n\n  DB Operation\n\n  */\n  await Connectors.delete(collection, selector);\n\n  /*\n\n  After\n\n  */\n  // new API (Oct 2019)\n  document = await runCallbacks({\n    name: `${typeName.toLowerCase()}.delete.after`,\n    callbacks: get(collection, 'options.callbacks.delete.after', []),\n    iterator: document,\n    properties,\n  });\n  document = await runCallbacks({\n    name: '*.delete.after',\n    callbacks: get(globalCallbacks, 'delete.after', []),\n    iterator: document,\n    properties,\n  });\n  // old API\n  document = await runCallbacks({\n    name: `${typeName.toLowerCase()}.delete.after`,\n    iterator: document,\n    properties,\n  });\n  document = await runCallbacks({ name: '*.delete.after', iterator: document, properties });\n  // OpenCRUD backwards compatibility\n  document = await runCallbacks(\n    `${collectionName.toLowerCase()}.remove.after`,\n    document,\n    currentUser\n  );\n\n  // TODO: add support for caching by other indexes to Dataloader\n  // clear cache if needed\n  if (selector.documentId && collection.loader) {\n    collection.loader.clear(selector.documentId);\n  }\n\n  /*\n\n  Async\n\n  */\n  // new API (Oct 2019)\n  await runCallbacksAsync({\n    name: `${typeName.toLowerCase()}.delete.async`,\n    callbacks: get(collection, 'options.callbacks.delete.async', []),\n    properties,\n  });\n  await runCallbacksAsync({\n    name: '*.delete.async',\n    callbacks: get(globalCallbacks, 'delete.async', []),\n    properties,\n  });\n  // old API\n  await runCallbacksAsync({ name: `${typeName.toLowerCase()}.delete.async`, properties });\n  await runCallbacksAsync({ name: '*.delete.async', properties });\n  // OpenCRUD backwards compatibility\n  await runCallbacksAsync(\n    `${collectionName.toLowerCase()}.remove.async`,\n    document,\n    currentUser,\n    collection\n  );\n\n  endDebugMutator(collectionName, 'Delete');\n\n  // filter out non readable fields if appliable\n  if (context.Users) {\n    document = context.Users.restrictViewableFields(currentUser, collection, document);\n  }\n\n  return { data: document };\n};\n\n// OpenCRUD backwards compatibility\nexport const newMutation = createMutator;\nexport const editMutation = updateMutator;\nexport const removeMutation = deleteMutator;\nexport const newMutator = createMutator;\nexport const editMutator = updateMutator;\nexport const removeMutator = deleteMutator;\n\nconst startDebugMutator = (name, action, properties) => {\n  debug('');\n  debugGroup(`--------------- start \\x1b[36m${name} ${action} Mutator\\x1b[0m ---------------`);\n  Object.keys(properties).forEach(p => {\n    debug(`// ${p}: `, properties[p]);\n  });\n};\n\nconst endDebugMutator = (name, action, properties = {}) => {\n  Object.keys(properties).forEach(p => {\n    debug(`// ${p}: `, properties[p]);\n  });\n  debugGroupEnd();\n  debug(`--------------- end \\x1b[36m${name} ${action} Mutator\\x1b[0m ---------------`);\n  debug('');\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/query.js",
    "content": "/*\n\nRun a GraphQL request from the server with the proper context\n\n*/\nimport { graphql } from 'graphql';\nimport { Collections } from '../modules/collections.js';\nimport DataLoader from 'dataloader';\nimport findByIds from '../modules/findbyids.js';\nimport { extractFragmentName, getFragmentText } from '../modules/fragments.js';\nimport { getDefaultFragmentText } from '../modules/graphql/defaultFragment.js';\n\nimport { getSetting } from '../modules/settings';\nimport merge from 'lodash/merge';\nimport { singleClientTemplate } from '../modules/graphql_templates/index.js';\nimport { Utils } from './utils';\nimport { GraphQLSchema } from './graphql/index.js';\nimport { useQueryCache } from './caching.js';\nimport { expandQueryFragments } from '../modules/fragments.js';\n\n// note: if no context is passed, default to running requests with full admin privileges\nexport const runGraphQL = async (query, variables = {}, context = {}, options = {}) => {\n  const defaultContext = {\n    currentUser: { isAdmin: true },\n    locale: getSetting('locale'),\n  };\n  const { useCache = false, key } = options;\n  const queryContext = merge(defaultContext, context);\n  const executableSchema = GraphQLSchema.getExecutableSchema();\n\n  // within the scope of this specific request,\n  // decorate each collection with a new Dataloader object and add it to context\n  Collections.forEach(collection => {\n    collection.loader = new DataLoader(ids => findByIds(collection, ids, queryContext), {\n      cache: true,\n    });\n    queryContext[collection.options.collectionName] = collection;\n  });\n\n  const fullQueryContext = merge({}, queryContext, GraphQLSchema.context);\n\n  const queryWithFragments = expandQueryFragments(query);\n\n  // see http://graphql.org/graphql-js/graphql/#graphql\n  const result = useCache\n    ? await useQueryCache({ query: queryWithFragments, variables, context: fullQueryContext, key })\n    : await graphql(executableSchema, queryWithFragments, {}, fullQueryContext, variables);\n\n  if (result.errors) {\n    // eslint-disable-next-line no-console\n    console.error(`runGraphQL error: ${result.errors[0].message}`);\n    // eslint-disable-next-line no-console\n    console.error(result.errors);\n    throw new Error(result.errors[0].message);\n  }\n\n  return result;\n};\n\nexport const runQuery = runGraphQL; //backwards compatibility\n\n/*\n\nGiven a collection and a fragment, build a query to fetch one document. \nIf no fragment is passed, default to default fragment\n\n*/\nexport const buildQuery = (collection, { fragmentName, fragmentText }) => {\n  const collectionName = collection.options.collectionName;\n  const typeName = collection.options.typeName;\n\n  const defaultFragmentName = `${collectionName}DefaultFragment`;\n  const defaultFragmentText = getDefaultFragmentText(collection, {\n    onlyViewable: false,\n  });\n\n  // default to default name and text\n  let name = defaultFragmentName;\n  let text = defaultFragmentText;\n\n  if (fragmentName) {\n    // if fragmentName is passed, use that to get name\n    name = fragmentName;\n    // any registered fragment's text will be automatically added by runGraphQL()\n    text = '';\n  } else if (fragmentText) {\n    // if fragmentText is passed, use that to get name and text\n    name = extractFragmentName(fragmentText);\n    text = fragmentText;\n  }\n\n  const query = `${singleClientTemplate({\n    typeName,\n    fragmentName: name,\n  })}${text}`;\n\n  return query;\n};\n\nMeteor.startup(() => {\n  Collections.forEach(collection => {\n    const typeName = collection.options.typeName;\n\n    collection.queryOne = async (inputOrId, { fragmentName, fragmentText, context }) => {\n      let input = inputOrId;\n\n      if (typeof inputOrId === 'string') {\n        input = { id: inputOrId };\n      }\n\n      const query = buildQuery(collection, { fragmentName, fragmentText });\n      const result = await runGraphQL(query, { input }, context);\n      return result.data[Utils.camelCaseify(typeName)].result;\n    };\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/site.js",
    "content": "import { addGraphQLSchema, addGraphQLResolvers, addGraphQLQuery } from './graphql';\nimport { Utils } from '../modules/utils';\nimport { getSetting } from '../modules/settings.js';\nimport { getSourceVersion } from './source_version.js';\n\nconst siteSchema = `type Site {\n  title: String\n  url: String\n  logoUrl: String\n  sourceVersion: String\n}`;\naddGraphQLSchema(siteSchema);\n\nconst siteResolvers = {\n  Query: {\n    siteData(root, args, context) {\n      return {\n        title: getSetting('title'),\n        url: getSetting('siteUrl', Meteor.absoluteUrl()),\n        logoUrl: Utils.getLogoUrl(),\n        sourceVersion: getSourceVersion(),\n      };\n    },\n  },\n};\n\naddGraphQLResolvers(siteResolvers);\n\naddGraphQLQuery('siteData: Site');\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/source_version.js",
    "content": "import childProcess from 'child_process';\n\n/*\n\nGet latest commit hash from either Meteor.gitCommitHash or env variables (set with Mup for example)\nor current child process\n\nSee https://github.com/zodern/meteor-up/issues/807#issuecomment-346915622\nAnd https://github.com/meteor/meteor/pull/10442\n\n*/\nexport const getSourceVersion = () => {\n  if (Meteor && Meteor.gitCommitHash) {\n    return Meteor.gitCommitHash;\n  } else {\n    try {\n      return (\n        process.env.SOURCE_VERSION ||\n        childProcess\n          .execSync('git rev-parse HEAD')\n          .toString()\n          .trim()\n      );\n    } catch (error) {\n      return null;\n    }\n  }\n};\n"
  },
  {
    "path": "packages/vulcan-lib/lib/server/utils.js",
    "content": "import sanitizeHtml from 'sanitize-html';\nimport { Utils } from '../modules';\nimport { throwError } from './errors.js';\n\nUtils.sanitize = function(s) {\n  return sanitizeHtml(s, {\n    allowedTags: [\n      'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul',\n      'ol', 'nl', 'li', 'b', 'i', 'strong', 'em', 's', 'del', 'ins',\n      'code', 'hr', 'br', 'div', 'table', 'thead', 'caption',\n      'tbody', 'tr', 'th', 'td', 'pre', 'img'\n    ]\n  });\n};\n\nUtils.performCheck = (operation, user, checkedObject, context, documentId, operationName, collectionName) => {\n\n  if (!operation) {\n    throwError({ id: 'app.no_permissions_defined', data: { documentId, operationName } });\n  }\n\n  if (!checkedObject) {\n    throwError({ id: 'app.document_not_found', data: { documentId, operationName } });\n  }\n\n  if (!operation(user, checkedObject, context)) {\n    throwError({ id: 'app.operation_not_allowed', data: { documentId, operationName } });\n  }\n\n};\n\nexport { Utils };\n"
  },
  {
    "path": "packages/vulcan-lib/package.js",
    "content": "Package.describe({\n  name: 'vulcan:lib',\n  summary: 'Vulcan libraries.',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  // note: if used, accounts-base should be loaded before vulcan:lib\n  api.use('accounts-base@2.0.0', { weak: true });\n\n  var packages = [\n    // 'buffer@0.0.0', // see https://github.com/meteor/meteor/issues/8645\n\n    // Minimal Meteor packages\n\n    'meteor@1.9.3',\n    'static-html@1.2.2',\n    'standard-minifier-css@1.5.3',\n    'standard-minifier-js@2.4.1',\n    'es5-shim@4.8.0',\n    'ecmascript@0.12.4',\n    'shell-server@0.4.0',\n    'webapp@1.7.3',\n    'server-render@0.3.1',\n\n    // Other meteor-base package\n    // see https://github.com/meteor/meteor/blob/master/packages/meteor-base/package.js\n\n    'underscore@1.0.10',\n    'hot-code-push@1.0.4',\n    // 'ddp',\n\n    // Other packages\n\n    'mongo@1.10.1',\n    'check@1.3.1',\n    'http@2.0.0',\n    'email@2.0.0',\n    'random@1.2.0',\n    'apollo@4.0.0',\n\n    // Third-party packages\n\n    // 'aldeed:collection2-core@2.0.0',\n    'meteorhacks:picker@1.0.3',\n    'littledata:synced-cron@1.5.1',\n  ];\n\n  api.use(packages);\n\n  api.imply(packages);\n\n  api.export(['Vulcan']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n\nPackage.onTest(function(api) {\n  api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:test', 'vulcan:lib', 'vulcan:users']);\n  api.mainModule('./test/client/index.js', 'client');\n  api.mainModule('./test/server/index.js', 'server');\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/client/apolloClient.test.js",
    "content": "import expect from 'expect';\nimport {\n  registerStateLinkDefault,\n  getStateLinkDefaults,\n  registerStateLinkMutation,\n  getStateLinkMutations,\n  getStateLinkResolvers,\n  createStateLink,\n  createApolloClient,\n} from '../../lib/client/main.js';\nimport {\n  addToSet,\n} from '../../lib/client/apollo-client/updates'\n\ndescribe('vulcan-lib/apolloClient', function () {\n  describe('apollo-state-link', () => {\n    it('registerStateLink and retrieve a mutation', function () {\n      const dummyMutation = () => { };\n      registerStateLinkMutation({\n        name: 'dummyMutation',\n        mutation: dummyMutation,\n      });\n      const mutations = getStateLinkMutations();\n      expect(mutations['dummyMutation']).toEqual(dummyMutation);\n    });\n    it('register and retrieve a default value', function () {\n      const dummyDefault = () => { };\n      registerStateLinkDefault({\n        name: 'dummyDefault',\n        defaultValue: dummyDefault,\n      });\n      const defaults = getStateLinkDefaults();\n      expect(defaults['dummyDefault']).toEqual(dummyDefault);\n    });\n    it('register mutation and get resolvers', function () {\n      const dummyMutation = () => { };\n      registerStateLinkMutation({\n        name: 'dummyMutation',\n        mutation: dummyMutation,\n      });\n      const resolvers = getStateLinkResolvers();\n      expect(resolvers.Mutation['dummyMutation']).toEqual(dummyMutation);\n    });\n    it('create a stateLink', () => {\n      const stateLink = createStateLink({});\n      expect(stateLink).toBeDefined();\n    });\n  });\n  describe('apollo-client', function () {\n    it.skip('create a client', function () {\n      const apolloClient = createApolloClient();\n      expect(apolloClient).toBeDefined();\n    });\n  });\n\n  describe('updates for watched mutations', () => {\n    test('addToSet', () => {\n      const queryData = { results: [], totalCount: 0 }\n      const document = { _id: \"foobar\" }\n      const res = addToSet(queryData, document)\n      // reference should not be the same\n      expect(res.results === queryData.results).not.toBe(true)\n      expect(res.results).toHaveLength(1)\n      expect(res.results[0]).toEqual(document)\n      expect(res.totalCount).toEqual(1)\n    })\n\n  })\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/client/index.js",
    "content": "// import client only tests here\nimport './apolloClient.tests.js';\n// common tests\nimport '../index';"
  },
  {
    "path": "packages/vulcan-lib/test/components.test.js",
    "content": "import { mergeWithComponents } from '../lib/modules/components';\nimport { Components } from 'meteor/vulcan:core';\nimport expect from 'expect';\nimport { initComponentTest } from 'meteor/vulcan:test';\n\ninitComponentTest();\n\ndescribe('vulcan:lib/components', function () {\n    describe('mergeWithComponents', function () {\n        const TestComponent = () => 'foo';\n        const OverrideTestComponent = () => 'bar';\n        Components.TestComponent = TestComponent;\n        it('override existing components', function () {\n            const MyComponents = { TestComponent: OverrideTestComponent };\n            const MergedComponents = mergeWithComponents(MyComponents);\n            expect(MergedComponents.TestComponent).toEqual(OverrideTestComponent);\n            // eslint-disable-next-line\n            expect(MergedComponents.TestComponent()).toEqual('bar');\n        });\n        it('return \\'Components\\' if no components are provided', function () {\n            expect(mergeWithComponents()).toEqual(Components);\n            expect(mergeWithComponents().TestComponent).toEqual(TestComponent);\n        });\n\n    });\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/documentValidation.test.js",
    "content": "import { createDummyCollection } from \"meteor/vulcan:test\"\nimport { validateDocument, validateData } from \"../lib/modules/validation\"\nimport expect from 'expect'\nimport './routes.test';\nimport SimpleSchema from \"simpl-schema\"\nimport Users from \"meteor/vulcan:users\"\n\nconst test = it\n\nconst defaultContext = { Users }\ndescribe(\"vulcan:lib/validation\", () => {\n    describe(\"validate document permissions per field (on creation and update)\", () => {\n        test('no error if all fields are creatable', () => {\n            const collection = createDummyCollection({\n                schema: {\n                    foo: {\n                        type: String,\n                        canCreate: ['guests'],\n                        canUpdate: ['guests']\n                    }\n                }\n            })\n            // create\n            const errors = validateDocument({ foo: \"bar\" }, collection, defaultContext)\n            expect(errors).toHaveLength(0)\n            const updateErrors = validateData({ foo: \"bar\" }, { foo: \"bar\" }, collection, defaultContext)\n            expect(updateErrors).toHaveLength(0)\n        })\n        test('create error for non creatable field', () => {\n            const collection = createDummyCollection({\n                schema: {\n                    foo: {\n                        type: String,\n                        canCreate: ['members'],\n                        canUpdate: ['members']\n                    },\n                    bar: {\n                        type: String,\n                        canCreate: ['guests'],\n                        canUpdate: ['guests']\n                    }\n                }\n            })\n            const errors = validateDocument({ foo: \"bar\", bar: \"foo\" }, collection, defaultContext)\n            expect(errors).toHaveLength(1)\n            expect(errors[0]).toMatchObject({\n                id: \"errors.disallowed_property_detected\",\n                properties: { name: \"foo\" }\n            })\n            const updateErrors = validateData({ foo: \"bar\", bar: \"foo\" }, { foo: \"bar\", bar: \"foo\" }, collection, defaultContext)\n            expect(updateErrors).toHaveLength(1)\n            expect(updateErrors[0]).toMatchObject({\n                id: \"errors.disallowed_property_detected\",\n                properties: { name: \"foo\" }\n            })\n        })\n\n        test('create error for non creatable nested field (object)', () => {\n            const collection = createDummyCollection({\n                schema: {\n                    nested: {\n                        type: new SimpleSchema({\n                            foo: {\n                                type: String,\n                                canCreate: [\"members\"],\n                                canUpdate: [\"members\"]\n                            },\n                            zed: {\n                                optional: true,\n                                type: String,\n                                canCreate: [\"members\"],\n                                canUpdate: [\"members\"]\n                            },\n                            bar: {\n                                type: String,\n                                canCreate: [\"guests\"],\n                                canUpdate: [\"guests\"]\n                            },\n                        }),\n                        canCreate: [\"guests\"],\n                        canUpdate: [\"guests\"]\n                    }\n                }\n            })\n            // create\n            const errors = validateDocument({ nested: { foo: \"bar\", bar: \"foo\" } }, collection, defaultContext)\n            expect(errors).toHaveLength(1)\n            expect(errors[0]).toMatchObject({\n                id: \"errors.disallowed_property_detected\",\n                properties: { name: \"nested.foo\" }\n            })\n            // update with set and unset\n            const updateErrors = validateData({ nested: { foo: \"bar\", bar: \"foo\", zed: null } }, { nested: { foo: \"bar\", bar: \"foo\", zed: \"hello\" } }, collection, defaultContext)\n            expect(updateErrors).toHaveLength(2)\n            expect(updateErrors[0]).toMatchObject({\n                id: \"errors.disallowed_property_detected\",\n                properties: { name: \"nested.foo\" }\n            },\n                {\n                    id: \"errors.disallowed_property_detected\",\n                    properties: { name: \"nested.zed\" }\n                },\n            )\n        })\n        test('create error for non creatable nested field (array)', () => {\n            const collection = createDummyCollection({\n                schema: {\n                    nested: {\n                        type: Array,\n                        canCreate: [\"guests\"],\n                        canUpdate: [\"guests\"]\n                    },\n                    \"nested.$\": {\n                        type: new SimpleSchema({\n                            foo: {\n                                type: String,\n                                canCreate: [\"members\"],\n                                canUpdate: [\"members\"]\n                            },\n                            bar: {\n                                type: String,\n                                canCreate: [\"guests\"],\n                                canUpdate: [\"guests\"]\n                            }\n                        })\n                    }\n                }\n            })\n            const errors = validateDocument({ nested: [{ foo: \"bar\", bar: \"foo\" }] }, collection, defaultContext)\n            expect(errors).toHaveLength(1)\n            expect(errors[0]).toMatchObject({\n                id: \"errors.disallowed_property_detected\",\n                properties: { name: \"nested[0].foo\" }\n            })\n\n            const updateErrors = validateData({ nested: [{ foo: \"bar\", bar: \"foo\" }] }, { nested: [{ foo: \"bar\", bar: \"foo\" }] }, collection, defaultContext)\n            expect(updateErrors).toHaveLength(1)\n            expect(updateErrors[0]).toMatchObject({\n                id: \"errors.disallowed_property_detected\",\n                properties: { name: \"nested[0].foo\" }\n            })\n        })\n\n\n        test('ignore nested fields without permissions (use parent permissions)', () => {\n            const collection = createDummyCollection({\n                schema: {\n                    nested: {\n                        type: new SimpleSchema({\n                            nok: {\n                                type: String,\n                                canCreate: [\"members\"],\n                                canUpdate: [\"members\"]\n                            },\n                            ok: {\n                                type: String,\n                            },\n                            zed: {\n                                optional: true,\n                                type: String,\n                                canCreate: [\"members\"],\n                                canUpdate: [\"members\"]\n                            },\n                        }),\n                        canCreate: [\"guests\"],\n                        canUpdate: [\"guests\"]\n                    }\n                }\n            })\n            // create\n            const errors = validateDocument({ nested: { nok: \"bar\", ok: \"foo\" } }, collection, defaultContext)\n            expect(errors).toHaveLength(1)\n            expect(errors[0]).toMatchObject({\n                id: \"errors.disallowed_property_detected\",\n                properties: { name: \"nested.nok\" }\n            })\n            // update with set and unset\n            const updateErrors = validateData({ nested: { nok: \"bar\", ok: \"foo\", zed: null } }, { nested: { nok: \"bar\", ok: \"foo\", zed: \"hello\" } }, collection, defaultContext)\n            expect(updateErrors).toHaveLength(2)\n            expect(updateErrors[0]).toMatchObject({\n                id: \"errors.disallowed_property_detected\",\n                properties: { name: \"nested.nok\" }\n            },\n                {\n                    id: \"errors.disallowed_property_detected\",\n                    properties: { name: \"nested.zed\" }\n                },\n            )\n        })\n\n        test('do not check permissions of blackbox JSON', () => {\n            const collection = createDummyCollection({\n                schema: {\n                    nested: {\n                        type: new SimpleSchema({ foo: { type: String, canCreate: ['members'], canUpdate: ['members'] } }),\n                        blackbox: true,\n                        canCreate: [\"guests\"],\n                        canUpdate: [\"guests\"],\n                    },\n                }\n            })\n            const errors = validateDocument({ nested: { foo: \"bar\" } }, collection, defaultContext)\n            expect(errors).toHaveLength(0)\n\n            const updateErrors = validateData({ nested: { foo: \"bar\" } }, { nested: { foo: \"bar\" } }, collection, defaultContext)\n            expect(updateErrors).toHaveLength(0)\n        })\n        test('do not check native arrays', () => {\n            const collection = createDummyCollection({\n                schema: {\n                    array: {\n                        type: Array,\n                        canCreate: [\"guests\"],\n                        canUpdate: [\"guests\"]\n                    },\n                    \"array.$\": {\n                        type: Number\n                    }\n                }\n            })\n            const errors = validateDocument({ array: [1, 2, 3] }, collection, defaultContext)\n            expect(errors).toHaveLength(0)\n\n            const updateErrors = validateData({ array: [1, 2, 3] }, { array: [1, 2, 3] }, collection, defaultContext)\n            expect(updateErrors).toHaveLength(0)\n        })\n    })\n\n})"
  },
  {
    "path": "packages/vulcan-lib/test/handleOptions.test.js",
    "content": "import { extractCollectionInfo, extractFragmentInfo } from '../lib/modules/handleOptions';\nimport expect from 'expect';\n\ndescribe('vulcan:lib/handleOptions', function() {\n  const expectedCollectionName = 'COLLECTION_NAME';\n  const expectedCollection = { options: { collectionName: expectedCollectionName } };\n\n  it('get collectionName from collection', function() {\n    const options = { collection: expectedCollection };\n    const { collection, collectionName } = extractCollectionInfo(options);\n    expect(collection).toEqual(expectedCollection);\n    expect(collectionName).toEqual(expectedCollectionName);\n  });\n  it.skip('get collection from collectionName', function() {\n    // TODO: mock getCollection\n    const options = { collectionName: expectedCollectionName };\n    const { collection, collectionName } = extractCollectionInfo(options);\n    expect(collection).toEqual(expectedCollection);\n    expect(collectionName).toEqual(expectedCollectionName);\n  });\n  const expectedFragmentName = 'FRAGMENT_NAME';\n  const expectedFragment = { definitions: [{ name: { value: expectedFragmentName } }] };\n  it.skip('get fragment from fragmentName', function() {\n    // TODO: mock getCollection\n    const options = { fragmentName: expectedFragmentName };\n    const { fragment, fragmentName } = extractFragmentInfo(options);\n    expect(fragment).toEqual(expectedFragment);\n    expect(fragmentName).toEqual(expectedFragmentName);\n  });\n  it('get fragmentName from fragment', function() {\n    const options = { fragment: expectedFragment };\n    const { fragment, fragmentName } = extractFragmentInfo(options);\n    expect(fragment).toEqual(expectedFragment);\n    expect(fragmentName).toEqual(expectedFragmentName);\n  });\n  it.skip('get fragmentName and fragment from collectionName', function() {\n    // TODO: mock getFragment\n    // if options does not contain fragment, we get the collection default fragment based on its name\n    const options = {};\n    const { fragment, fragmentName } = extractFragmentInfo(options, expectedCollectionName);\n    expect(fragment).toEqual(expectedFragment);\n    expect(fragmentName).toEqual(expectedFragmentName);\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/index.js",
    "content": "import './components.test.js';\nimport './documentValidation.test';\nimport './handleOptions.test.js';\nimport './intl.test.js';\nimport './mongoParams.test';\nimport './reactive-state.test';\nimport './routes.test';\n//import './schema_utils.test';\nimport './utils.test.js';\n"
  },
  {
    "path": "packages/vulcan-lib/test/intl.test.js",
    "content": "import React from 'react';\nimport expect from 'expect';\nimport {addStrings, Strings, Utils} from 'meteor/vulcan:core';\nimport {formatLabel, getString} from '../lib/modules/intl';\nimport { shallow } from 'enzyme';\n\n// constants for formatLabel\nconst fieldName = 'testFieldName';\nconst fieldNameForSchema = 'fieldNameForSchema';\nconst fieldNameForGlobal = 'testFieldNameGlobal';\nconst fieldNameForCollection = 'testFieldNameCollection';\nconst unknownFieldName = 'unknownFieldName';\nconst collectionName = 'Tests';\nconst labelFromCollection = 'label from collection';\nconst labelFromGlobal = 'label from global';\nconst labelFromSchema = 'label from schema';\nconst labelFromFieldName = 'label from fieldName';\n\n// add the schema entries for all fields to test respect of the order too\nconst schema = {\n  [fieldName]: {\n    label: labelFromSchema,\n  },\n  [fieldNameForSchema]: {\n    label: labelFromSchema,\n  },\n  [fieldNameForGlobal]: {\n    label: labelFromSchema,\n  },\n  [fieldNameForCollection]: {\n    label: labelFromSchema,\n  },\n};\n\n// add the strings for formatLabel\naddStrings('en', {\n  // fieldName only\n  [fieldName]: labelFromFieldName,\n  // fieldName + global - we expect labelFromGlobal\n  [fieldNameForGlobal]: labelFromFieldName,\n  [`global.${fieldNameForGlobal}`]: labelFromGlobal,\n  // fieldName + global + collectionName - we expect labelFromCollection\n  [fieldNameForCollection]: labelFromFieldName,\n  [`global.${fieldNameForCollection}`]: labelFromGlobal,\n  [`${collectionName.toLowerCase()}.${fieldNameForCollection}`]: labelFromCollection,\n});\n\nconst intl = {\n  formatMessage: ({id, defaultMessage}, values = null) => {\n    return getString({id, defaultMessage, values, messages: Strings, locale: 'en'});\n  }\n}\n\ndescribe('vulcan:lib/intl', function () {\n\n  describe('formatLabel', function () {\n\n    it('return the fieldName when there is no matching string or label', function () {\n      const ENString = formatLabel({fieldName: unknownFieldName, schema, collectionName, intl});\n      expect(ENString).toEqual(Utils.camelToSpaces(unknownFieldName));\n    });\n\n    it('return the matching schema label when there is no matching string', function () {\n      const ENString = formatLabel({fieldName: fieldNameForSchema, schema, collectionName, intl});\n      expect(ENString).toEqual(schema[fieldName].label);\n    });\n\n    it('return the label from a matched `fieldName`', function () {\n      const ENString = formatLabel({fieldName, schema, collectionName, intl});\n      expect(ENString).toEqual(labelFromFieldName);\n    });\n\n    it('return the label from a matched `global.fieldName`', function () {\n      const ENString = formatLabel({fieldName: fieldNameForGlobal, schema, collectionName, intl});\n      expect(ENString).toEqual(labelFromGlobal);\n    });\n\n    it('return the label from a matched `collectionName.fieldName`', function () {\n      const ENString = formatLabel({fieldName: fieldNameForCollection, schema, collectionName, intl});\n      expect(ENString).toEqual(labelFromCollection);\n    });\n\n  });\n\n  describe('getString', function () {\n\n    it('returns a simple string', function () {\n      const id = 'simple';\n      const string = 'This is an intl string with no values';\n      const expected = string;\n      addStrings('en', {[id]: string});\n\n      const ENString = getString({id, messages: Strings, locale: 'en'});\n      expect(ENString).toEqual(expected);\n    });\n\n    it('substitutes string values passed', function () {\n      const id = 'withStringValue';\n      const string = 'This is an intl string with {type} values';\n      const values = {\n        type: 'string'\n      };\n      const expected = 'This is an intl string with string values';\n      addStrings('en', {[id]: string});\n\n      const ENString = getString({id, values, messages: Strings, locale: 'en'});\n      expect(ENString).toEqual(expected);\n    });\n\n    it('substitutes plural values passed', function () {\n      const id = 'withPluralValue';\n      const string = 'You have {itemCount, plural, =0 {no items} one {# item} other {# items}}.';\n      const expectedWithZero = 'You have no items.';\n      const expectedWithOne = 'You have 1 item.';\n      const expectedWithOther = 'You have 3 items.';\n      addStrings('en', {[id]: string});\n\n      const ENString1 = getString({id, values: { itemCount: 0 }, messages: Strings, locale: 'en'});\n      expect(ENString1).toEqual(expectedWithZero);\n\n      const ENString2 = getString({id, values: { itemCount: 1 }, messages: Strings, locale: 'en'});\n      expect(ENString2).toEqual(expectedWithOne);\n\n      const ENString3 = getString({id, values: { itemCount: 3 }, messages: Strings, locale: 'en'});\n      expect(ENString3).toEqual(expectedWithOther);\n    });\n\n    it('substitutes react node values passed', function () {\n      const id = 'withNodeValue';\n      const string = 'To learn more, see {link}';\n      const values = {\n        link: <a href=\"https://docs.vulcanjs.org/unit-testing.html\" className=\"link\">Vulcan Docs</a>,\n      };\n      const expected = ['To learn more, see ', values.link];\n      addStrings('en', {[id]: string});\n\n      const ENResult = getString({id, values, messages: Strings, locale: 'en'});\n      expect(Array.isArray(ENResult)).toBeTruthy();\n      expect(ENResult).toHaveLength(2);\n      expect(ENResult[0]).toEqual(expected[0]);\n\n      const wrapper = shallow(<span>{ENResult}</span>);\n      expect(wrapper.find('.link')).toHaveLength(1);\n    });\n\n  });\n\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/mongoParams.test.js",
    "content": "import expect from 'expect';\nimport {filterFunction} from '../lib/modules/mongoParams';\nimport {createDummyCollection, initServerTest} from 'meteor/vulcan:test';\n\nconst test = it;\n\nconst mayTheFourth = new Date('1977-05-04T22:00:00');\nconst summerSolstice = new Date('1972-06-20T12:41:00');\n\ndescribe('vulcan:lib/mongoParams', function () {\n\n  let collection;\n\n  before(async function () {\n    collection = createDummyCollection({\n      schema: {\n        _id: {\n          type: String,\n          canRead: ['admins'],\n        },\n        name: {\n          type: String,\n          canRead: ['admins'],\n        },\n        age: {\n          type: Number,\n          canRead: ['admins'],\n        },\n        withTheForce: {\n          type: Boolean,\n          canRead: ['admins'],\n        },\n        birthday: {\n          type: Date,\n          canRead: ['admins'],\n        },\n        friends: {\n          type: Array,\n          canRead: ['admins'],\n        },\n        'friends.$': {\n          type: String,\n          canRead: ['admins'],\n        },\n        agesOfFriends: {\n          type: Array,\n          canRead: ['admins'],\n        },\n        'agesOfFriends.$': {\n          type: Number,\n          canRead: ['admins'],\n        },\n        scores: {\n          type: Array,\n          canRead: ['admins'],\n        },\n        'scores.$': {\n          type: Number,\n          canRead: ['admins'],\n        },\n        forcesOfFriends: {\n          type: Array,\n          canRead: ['admins'],\n        },\n        'forcesOfFriends.$': {\n          type: Number,\n          canRead: ['admins'],\n        },\n        birthdaysOfFriends: {\n          type: Array,\n          canRead: ['admins'],\n        },\n        'birthdaysOfFriends.$': {\n          type: Number,\n          canRead: ['admins'],\n        },\n        scores: {\n          type: Array,\n          canRead: ['admins'],\n        },\n        'scores.$': {\n          type: Number,\n          canRead: ['admins'],\n        },\n        forcesOfFriends: {\n          type: Array,\n          canRead: ['admins'],\n        },\n        'forcesOfFriends.$': {\n          type: Number,\n          canRead: ['admins'],\n        },\n        birthdaysOfFriends: {\n          type: Array,\n          canRead: ['admins'],\n        },\n        'birthdaysOfFriends.$': {\n          type: Number,\n          canRead: ['admins'],\n        },\n      },\n      results: [{\n        _id: \"1\", name: 'Han', age: 140, withTheForce: false,\n        birthday: summerSolstice, friends: ['Leia', 'Luke'], scores: [1.1, 1.2],\n      }, {\n        _id: \"2\", name: 'Leia', age: 120, withTheForce: true,\n        birthday: mayTheFourth, friends: ['Luke'], scores: [1.1],\n      }, {\n        _id: \"3\", name: 'Luke', age: 100, withTheForce: true,\n        birthday: mayTheFourth, friends: ['Leia'], scores: [1.2],\n      }, {\n        _id: \"4\", name: 'Obi-Wan', age: null, withTheForce: true,\n        birthday: new Date('1496-05-04T22:00:00'),\n      },\n      ]\n    });\n  })\n\n  describe('string selector', async function () {\n\n    test('string selector _eq', async function () {\n      const input = {\n        filter: {\n          name: {_eq: 'Han'},\n        }\n      };\n      const expectedFilter = {name: {$eq: 'Han'}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string selector _gt', async function () {\n      const input = {\n        filter: {\n          name: {_gt: 'Han'},\n        }\n      };\n      const expectedFilter = {name: {$gt: 'Han'}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string selector _gte', async function () {\n      const input = {\n        filter: {\n          name: {_gte: 'Han'},\n        }\n      };\n      const expectedFilter = {name: {$gte: 'Han'}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string selector _in', async function () {\n      const input = {\n        filter: {\n          name: {_in: ['Luke', 'Han']},\n        }\n      };\n      const expectedFilter = {name: {$in: ['Luke', 'Han']}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string selector _nin', async function () {\n      const input = {\n        filter: {\n          name: {_nin: ['Han', 'Leia']},\n        }\n      };\n      const expectedFilter = {name: {$nin: ['Han', 'Leia']}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string selector _is_null', async function () {\n      const input = {\n        filter: {\n          name: {_is_null: true},\n        }\n      };\n      const expectedFilter = {name: {$exists: false}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string selector _like', async function () {\n      const input = {\n        filter: {\n          name: {_like: '^e'},\n        }\n      };\n      const expectedFilter = {name: {$regex: '\\\\^e', $options: 'i'}};\n      const mongoParams = await filterFunction(collection, input);\n      console.log('mongoParams.selector', mongoParams.selector);\n      console.log('expectedFilter', expectedFilter);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string selector _lt', async function () {\n      const input = {\n        filter: {\n          name: {_lt: 'Han'},\n        }\n      };\n      const expectedFilter = {name: {$lt: 'Han'}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string selector _lte', async function () {\n      const input = {\n        filter: {\n          name: {_lte: 'Han'},\n        }\n      };\n      const expectedFilter = {name: {$lte: 'Han'}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string selector _neq', async function () {\n      const input = {\n        filter: {\n          name: {_neq: 'Han'},\n        }\n      };\n      const expectedFilter = {name: {$ne: 'Han'}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n  });\n\n  describe('string array selector', async function () {\n\n    test('string array selector _in', async function () {\n      const input = {\n        filter: {\n          friends: {_in: ['Luke', 'Han']},\n        }\n      };\n      const expectedFilter = {friends: {$in: ['Luke', 'Han']}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string array selector _nin', async function () {\n      const input = {\n        filter: {\n          friends: {_nin: ['Luke', 'Han']},\n        }\n      };\n      const expectedFilter = {friends: {$nin: ['Luke', 'Han']}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string array selector _contains', async function () {\n      const input = {\n        filter: {\n          friends: {_contains: 'Han'},\n        }\n      };\n      const expectedFilter = {friends: {$elemMatch: {$eq: 'Han'}}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('string array selector _contains_all', async function () {\n      const input = {\n        filter: {\n          friends: {_contains_all: ['Leia', 'Luke']},\n        }\n      };\n      const expectedFilter = {friends: {$all: ['Leia', 'Luke']}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n  });\n\n  describe('int selector', async function () {\n\n    test('int selector _eq', async function () {\n      const input = {\n        filter: {\n          age: {_eq: 100},\n        }\n      };\n      const expectedFilter = {age: {$eq: 100}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('int selector _gt', async function () {\n      const input = {\n        filter: {\n          age: {_gt: 100},\n        }\n      };\n      const expectedFilter = {age: {$gt: 100}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('int selector _gte', async function () {\n      const input = {\n        filter: {\n          age: {_gte: 100},\n        }\n      };\n      const expectedFilter = {age: {$gte: 100}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('int selector _in', async function () {\n      const input = {\n        filter: {\n          age: {_in: [100, 120]},\n        }\n      };\n      const expectedFilter = {age: {$in: [100, 120]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('int selector _nin', async function () {\n      const input = {\n        filter: {\n          age: {_nin: [100, 120]},\n        }\n      };\n      const expectedFilter = {age: {$nin: [100, 120]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('int selector _is_null', async function () {\n      const input = {\n        filter: {\n          age: {_is_null: true},\n        }\n      };\n      const expectedFilter = {age: {$exists: false}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('int selector _lt', async function () {\n      const input = {\n        filter: {\n          age: {_lt: 120},\n        }\n      };\n      const expectedFilter = {age: {$lt: 120}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('int selector _lte', async function () {\n      const input = {\n        filter: {\n          age: {_lte: 120},\n        }\n      };\n      const expectedFilter = {age: {$lte: 120}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('int selector _neq', async function () {\n      const input = {\n        filter: {\n          age: {_neq: 120},\n        }\n      };\n      const expectedFilter = {age: {$ne: 120}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n  });\n\n  describe('int array selector', async function () {\n\n    test('int array selector _in', async function () {\n      const input = {\n        filter: {\n          agesOfFriends: {_in: [110, 120]},\n        }\n      };\n      const expectedFilter = {agesOfFriends: {$in: [110, 120]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('int array selector _nin', async function () {\n      const input = {\n        filter: {\n          agesOfFriends: {_nin: [110, 120]},\n        }\n      };\n      const expectedFilter = {agesOfFriends: {$nin: [110, 120]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('int array selector _contains', async function () {\n      const input = {\n        filter: {\n          agesOfFriends: {_contains: 110},\n        }\n      };\n      const expectedFilter = {agesOfFriends: {$elemMatch: {$eq: 110}}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('int array selector _contains_all', async function () {\n      const input = {\n        filter: {\n          agesOfFriends: {_contains_all: [110, 120]},\n        }\n      };\n      const expectedFilter = {agesOfFriends: {$all: [110, 120]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n  });\n\n  describe('float selector', async function () {\n\n    test('float selector _eq', async function () {\n      const input = {\n        filter: {\n          age: {_eq: 100},\n        }\n      };\n      const expectedFilter = {age: {$eq: 100}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('float selector _gt', async function () {\n      const input = {\n        filter: {\n          age: {_gt: 100},\n        }\n      };\n      const expectedFilter = {age: {$gt: 100}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('float selector _gte', async function () {\n      const input = {\n        filter: {\n          age: {_gte: 100},\n        }\n      };\n      const expectedFilter = {age: {$gte: 100}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('float selector _in', async function () {\n      const input = {\n        filter: {\n          age: {_in: [100, 120]},\n        }\n      };\n      const expectedFilter = {age: {$in: [100, 120]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('float selector _nin', async function () {\n      const input = {\n        filter: {\n          age: {_nin: [100, 120]},\n        }\n      };\n      const expectedFilter = {age: {$nin: [100, 120]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('float selector _is_null', async function () {\n      const input = {\n        filter: {\n          age: {_is_null: true},\n        }\n      };\n      const expectedFilter = {age: {$exists: false}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('float selector _lt', async function () {\n      const input = {\n        filter: {\n          age: {_lt: 120},\n        }\n      };\n      const expectedFilter = {age: {$lt: 120}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('float selector _lte', async function () {\n      const input = {\n        filter: {\n          age: {_lte: 120},\n        }\n      };\n      const expectedFilter = {age: {$lte: 120}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('float selector _neq', async function () {\n      const input = {\n        filter: {\n          age: {_neq: 120},\n        }\n      };\n      const expectedFilter = {age: {$ne: 120}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n  });\n\n  describe('float array selector', async function () {\n\n    test('float array selector _in', async function () {\n      const input = {\n        filter: {\n          scores: {_in: [1.1, 1.2]},\n        }\n      };\n      const expectedFilter = {scores: {$in: [1.1, 1.2]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('float array selector _nin', async function () {\n      const input = {\n        filter: {\n          scores: {_nin: [1.1, 1.2]},\n        }\n      };\n      const expectedFilter = {scores: {$nin: [1.1, 1.2]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('float array selector _contains', async function () {\n      const input = {\n        filter: {\n          scores: {_contains: 1.1},\n        }\n      };\n      const expectedFilter = {scores: {$elemMatch: {$eq: 1.1}}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('float array selector _contains_all', async function () {\n      const input = {\n        filter: {\n          scores: {_contains_all: [1.1, 1.2]},\n        }\n      };\n      const expectedFilter = {scores: {$all: [1.1, 1.2]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n  });\n\n  describe('boolean selector', async function () {\n\n    test('boolean selector _eq', async function () {\n      const input = {\n        filter: {\n          withTheForce: {_eq: true},\n        }\n      };\n      const expectedFilter = {withTheForce: {$eq: true}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('boolean selector _neq', async function () {\n      const input = {\n        filter: {\n          withTheForce: {_neq: true},\n        }\n      };\n      const expectedFilter = {withTheForce: {$ne: true}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n  });\n\n  describe('boolean array selector', async function () {\n\n    test('boolean array selector _contains', async function () {\n      const input = {\n        filter: {\n          forcesOfFriends: {_contains: true},\n        }\n      };\n      const expectedFilter = {forcesOfFriends: {$elemMatch: {$eq: true}}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n  });\n\n  describe('date selector', async function () {\n\n    test('date selector _eq', async function () {\n      const input = {\n        filter: {\n          birthday: {_eq: mayTheFourth},\n        }\n      };\n      const expectedFilter = {birthday: {$eq: mayTheFourth }};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('date selector _gt', async function () {\n      const input = {\n        filter: {\n          birthday: {_gt: mayTheFourth},\n        }\n      };\n      const expectedFilter = {birthday: {$gt: mayTheFourth}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('date selector _gte', async function () {\n      const input = {\n        filter: {\n          birthday: {_gte: mayTheFourth},\n        }\n      };\n      const expectedFilter = {birthday: {$gte: mayTheFourth}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('date selector _in', async function () {\n      const input = {\n        filter: {\n          birthday: {_in: [mayTheFourth, summerSolstice]},\n        }\n      };\n      const expectedFilter = {birthday: {$in: [mayTheFourth, summerSolstice]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('date selector _nin', async function () {\n      const input = {\n        filter: {\n          birthday: {_nin: [mayTheFourth, summerSolstice]},\n        }\n      };\n      const expectedFilter = {birthday: {$nin: [mayTheFourth, summerSolstice]}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('date selector _is_null', async function () {\n      const input = {\n        filter: {\n          birthday: {_is_null: true},\n        }\n      };\n      const expectedFilter = {birthday: {$exists: false}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('date selector _lt', async function () {\n      const input = {\n        filter: {\n          birthday: {_lt: mayTheFourth},\n        }\n      };\n      const expectedFilter = {birthday: {$lt: mayTheFourth}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('date selector _lte', async function () {\n      const input = {\n        filter: {\n          birthday: {_lte: mayTheFourth},\n        }\n      };\n      const expectedFilter = {birthday: {$lte: mayTheFourth}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('date selector _neq', async function () {\n      const input = {\n        filter: {\n          birthday: {_neq: mayTheFourth},\n        }\n      };\n      const expectedFilter = {birthday: {$ne: mayTheFourth}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n  });\n\n  describe('date array selector', async function () {\n\n    test('date array selector _contains', async function () {\n      const input = {\n        filter: {\n          birthdaysOfFriends: {_contains: mayTheFourth},\n        }\n      };\n      const expectedFilter = {birthdaysOfFriends: {$elemMatch: {$eq: mayTheFourth}}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('date array selector _contains_all', async function () {\n      const input = {\n        filter: {\n          birthdaysOfFriends: {_contains_all: ['Leia', 'Luke']},\n        }\n      };\n      const expectedFilter = {birthdaysOfFriends: {$all: ['Leia', 'Luke']}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n  });\n\n  describe('complex selectors', () => {\n\n    test('handle multiple filters', async () => {\n      const filter = {\n        _id: {_gte: 1, _lte: 5},\n      };\n      const input = {\n        filter,\n      };\n      const expectedFilter = {_id: {$gte: 1, $lte: 5}};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n    test('handle _and at root', async () => {\n      const filter = {\n        _and: [{name: {_gte: 'A'}}, {age: {_lte: 2}}],\n      };\n      const input = {\n        filter,\n      };\n      const expectedFilter = {$and: [{name: {$gte: 'A'}}, {age: {$lte: 2}}]};\n      const mongoParams = await filterFunction(collection, input);\n      expect(mongoParams.selector).toEqual(expectedFilter);\n    });\n\n  });\n\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/reactive-state.test.js",
    "content": "import expect from 'expect';\nimport {createReactiveState, getReactiveState} from \"../lib/modules/reactive-state\";\n\n\ndescribe('vulcan:lib/reactive-state', function () {\n\n  const scalarStateKey = 'clickCount';\n  let scalarState;\n\n  const stateKey = 'testState';\n  let objectState;\n  const schema = {\n    stringField: {\n      type: String,\n      defaultValue: 'One',\n    },\n    numField: {\n      type: Number,\n      defaultValue: 1,\n    },\n    arrayField: {\n      type: Array,\n      optional: true,\n      arrayItem: {\n        type: String,\n      },\n    },\n  };\n  const schemaKeys = ['stringField', 'numField', 'arrayField', 'arrayField.$'];\n  const defaultObject = {\n    stringField: 'One',\n    numField: 1,\n  }\n\n  describe('createReactiveState()', function () {\n\n    it('registers the given scalar state', function () {\n      const checkIfStateIsRegistered = () => {\n        getReactiveState(scalarStateKey);\n      };\n\n      expect(checkIfStateIsRegistered).toThrowError('no reactive state');\n\n      scalarState = createReactiveState({ stateKey: scalarStateKey, defaultValue: 0 });\n\n      expect(typeof scalarState).toEqual('function');\n      expect(scalarState()).toEqual(0);\n      expect(typeof scalarState.reactiveVar).toEqual('function');\n      expect(scalarState.reactiveVar()).toEqual(0);\n      expect(scalarState.stateKey).toEqual(scalarStateKey);\n      expect(scalarState.schema).toBeUndefined();\n      expect(scalarState.defaultValue).toEqual(0);\n    });\n\n    it('registers the given object state', function () {\n      const checkIfStateIsRegistered = () => {\n        getReactiveState(stateKey);\n      };\n\n      expect(checkIfStateIsRegistered).toThrowError('no reactive state');\n\n      objectState = createReactiveState({stateKey, schema});\n\n      expect(typeof objectState).toEqual('function');\n      expect(objectState()).toMatchObject(defaultObject);\n      expect(typeof objectState.reactiveVar).toEqual('function');\n      expect(objectState.reactiveVar()).toMatchObject(defaultObject);\n      expect(objectState.stateKey).toEqual(stateKey);\n      expect(objectState.schema._schemaKeys).toEqual(schemaKeys);\n      expect(objectState.defaultValue).toMatchObject(defaultObject);\n      expect(objectState()).toMatchObject(defaultObject);\n    });\n\n    it('throws an error for a duplicate key', function () {\n      const registerState = () => {\n        createReactiveState({stateKey, schema});\n      };\n\n      expect(registerState).toThrowError('already a reactive state');\n    });\n\n    it('ignores a duplicate key when the skipDuplicate option is used', function () {\n      const receivedState = createReactiveState({stateKey, schema, skipDuplicate: true});\n\n      expect(receivedState.stateKey).toEqual(stateKey);\n    });\n\n  });\n\n  describe('get ReactiveState value', function () {\n\n    it('returns the correct scalar state value', function () {\n      expect(scalarState()).toEqual(0);\n    });\n\n    it('returns the correct object state value', function () {\n      expect(objectState()).toEqual(defaultObject);\n    });\n\n  });\n\n  describe('setting ReactiveState value', function () {\n\n    it('resets the state to its default value when null is passed', function () {\n      objectState(null);\n      const receivedValue = objectState();\n\n      expect(receivedValue).toEqual(defaultObject);\n    });\n\n    it('sets the state to the given scalar value', function () {\n      scalarState(11);\n      const receivedValue = scalarState();\n\n      expect(receivedValue).toEqual(11);\n    });\n\n    it('sets the state to the given object value', function () {\n      const object2 = {\n        stringField: 'Two',\n        numField: 2,\n      }\n\n      objectState(object2);\n      const receivedValue = objectState();\n\n      expect(receivedValue).toEqual(object2);\n    });\n\n    it('sets only the given number field of an object state', function () {\n      const object3NumFieldSet = {\n        stringField: 'Two',\n        numField: 3,\n      }\n\n      objectState({numField: 3});\n      const receivedValue = objectState();\n\n      expect(receivedValue).toEqual(object3NumFieldSet);\n    });\n\n    it('sets only the given array field of an object state', function () {\n      const object3ArrayFieldSet = {\n        stringField: 'Two',\n        numField: 3,\n        arrayField: ['a'],\n      }\n\n      objectState({arrayField: ['a']});\n      const receivedValue = objectState();\n\n      expect(receivedValue).toEqual(object3ArrayFieldSet);\n    });\n\n    it('allows updating the value of an object state using a function', function () {\n      const object3ArrayFieldPushed = {\n        stringField: 'Two',\n        numField: 3,\n        arrayField: ['a', 'b', 'c'],\n      }\n\n      objectState(value => {\n        value.arrayField.push('b', 'c');\n        return value;\n      });\n      const receivedValue = objectState();\n\n      expect(receivedValue).toEqual(object3ArrayFieldPushed);\n    });\n\n  });\n\n  /*describe('useReactiveState()', function () {\n\n    it('returns props including the ReactiveState and functions to set and update it', function () {\n      objectState(null); // reset\n\n      const { testState } = useReactiveState({ stateKey });\n\n      expect(typeof testState).toEqual('function');\n      expect(testState.stateKey).toEqual(stateKey);\n      expect(testState.schema._schemaKeys).toEqual(schemaKeys);\n      expect(testState.defaultValue).toMatchObject(defaultObject);\n      expect(testState()).toMatchObject(defaultObject);\n    });\n\n    it('returns props with custom names when the propName option is passed', function () {\n      const { MyState } = useReactiveState({ stateKey, propName: 'MyState' });\n\n      expect(typeof MyState).toEqual('function');\n      expect(MyState.stateKey).toEqual(stateKey);\n      expect(MyState.schema._schemaKeys).toEqual(schemaKeys);\n      expect(MyState.defaultValue).toMatchObject(defaultObject);\n      expect(MyState()).toMatchObject(defaultObject);\n    });\n\n    it('returns a function that allows setting the state', function () {\n      const { MyState } = useReactiveState({ stateKey, propName: 'MyState' });\n\n      const object2 = {\n        stringField: 'Two',\n        numField: 2,\n      }\n\n      MyState(object2)\n      const receivedValue = MyState();\n\n      expect(receivedValue).toEqual(object2);\n    });\n\n  });*/\n\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/routes.test.js",
    "content": "import expect from 'expect'\nimport { addRoute, populateRoutesApp, emptyRoutes, Routes } from '../lib/modules/routes'\nconst Foo = () => 'foo'\ndescribe('vulcan:lib/routes', () => {\n    beforeEach(() => {\n        emptyRoutes()\n    })\n    it('add and retrieve a route', () => {\n        const route = {\n            name: 'foo',\n            path: '/coo',\n            component: Foo\n        }\n        addRoute(route)\n        populateRoutesApp()\n        expect(Routes['foo']).toEqual(route)\n    })\n    it('takes parent name into consideration', () => {\n        const parentRoute = {\n            name: 'parent',\n            path: '/parent',\n            component: Foo\n        }\n        const route = {\n            name: 'foo',\n            path: '/foo',\n            component: Foo\n        }\n        addRoute(parentRoute)\n        addRoute(route, 'parent')\n        populateRoutesApp()\n        expect(Routes['parent'].childRoutes).toEqual([route])\n    })\n    it('add array of routes', () => {\n        const route1 = {\n            name: 'foo1',\n            path: '/foo1',\n            component: Foo\n        }\n        const route2 = {\n            name: 'foo2',\n            path: '/foo2',\n            component: Foo\n        }\n        const routes = [route1, route2]\n        addRoute(routes)\n        populateRoutesApp()\n        expect(Routes['foo1']).toEqual(route1)\n        expect(Routes['foo2']).toEqual(route2)\n    })\n})"
  },
  {
    "path": "packages/vulcan-lib/test/schema_utils.test.js",
    "content": "import {\n  getCreateableFields,\n  getReadableFields,\n  getUpdateableFields,\n  getValidFields,\n} from '../lib/modules/schema_utils.js';\nimport expect from 'expect';\n\ndescribe('schema_utils', function () {\n  describe('fields extraction', function () {\n    describe('valid', function () {\n      it('remove invalid fields', function () {\n        const schema = {\n          validField: {},\n          arrayField: {},\n          // array child\n          'arrayField.$': {}\n        };\n        expect(getValidFields(schema)).toEqual(['validField', 'arrayField']);\n      });\n    });\n    describe('readable', function () {\n      it('get readable field', function () {\n        const schema = {\n          readable: { canRead: [] },\n          notReadble: {}\n        };\n        expect(getReadableFields(schema)).toEqual(['readable']);\n      });\n    });\n    describe('creatable', function () {\n      it('get creatable field', function () {\n        const schema = {\n          creatable: { canCreate: [] },\n          notCreatable: {}\n        };\n        expect(getCreateableFields(schema)).toEqual(['creatable']);\n      });\n    });\n    describe('updatable', function () {\n      it('get updatable field', function () {\n        const schema = {\n          updatable: { canUpdate: [] },\n          notUpdatable: {}\n        };\n        expect(getUpdateableFields(schema)).toEqual(['updatable']);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/server/apollo-server.test.js",
    "content": "import { createApolloServer, onStart } from '../../lib/server/apollo-server/apollo_server';\n//import initGraphQL from '../../lib/server/apollo-server/initGraphQL';\nimport { GraphQLSchema } from '../../lib/server/graphql';\nimport expect from 'expect';\nimport { executableSchema } from './fixtures/minimalSchema';\nimport { createTestClient } from 'apollo-server-testing';\n// import { createTestClient } from 'apollo-server-testing'\nimport { WebApp } from 'meteor/webapp';\nimport request from 'supertest';\n\nconst test = it; // TODO: just before we switch to jest\n// @see https://www.apollographql.com/docs/apollo-server/features/testing.html\n\ndescribe('apollo-server', function() {\n  let options;\n  before(function() {\n    // TODO: does not work in this test. This should setup the schema.\n    //   initGraphQL();\n\n    options = {\n      config: {}, //defaultConfig,\n      // Apollo options\n      apolloServerOptions: {\n        // TODO: check why this fails. One of the schema defined\n        // in one of the test file (when running createCollection in a test)\n        // is not working as expected\n        //schema: GraphQLSchema.getExecutableSchema(),\n        schema: executableSchema,\n        //formatError,\n        //tracing: getSetting('apolloTracing', Meteor.isDevelopment),\n        cacheControl: true,\n        //context\n      },\n      // Apollo applyMiddleware Option\n      apolloApplyMiddlewareOptions: {},\n    };\n  });\n  describe('createServer', function() {\n    test('init server', function() {\n      const server = createApolloServer(options);\n      expect(server).toBeDefined();\n    });\n  });\n\n  describe('body parser', () => {\n    test.skip('application/graphql', async () => {\n      const server = onStart();\n      const { query /*mutate*/ } = createTestClient(server);\n      const res = await query({\n        query: ``,\n        variables: {},\n      });\n      expect(res).toEqual({});\n    });\n  });\n\n  describe('cors', () => {\n    test.skip('cors', async () => {\n      //const corsDisallowed\n      const server = createApolloServer(options);\n      const { query, mutate } = createTestClient(server);\n      query({\n        query: ``,\n        variables: { id: 1 },\n      });\n      // mutate({mutation: ``, variables: {...}})\n      const res = await query({ query: GET_LAUNCH, variables: { id: 1 } });\n      expect(res).toEqual({});\n    });\n    test.skip('cors works with same origin', () => {});\n  });\n  describe('bodyParser', () => {\n    test.skip('answer to application/graphql calls', () => {\n      const server = createApolloServer(options);\n      expect(server).toBeDefined();\n    });\n  });\n  describe('setupWebApp', function() {});\n  describe('compute context', function() {\n    test.skip('initial context contains graphQLSchema context', function() {\n      before(() => {\n        // reinit the graphql schema\n        // NOTE: do NOT use initGraphQLTest => we only want to drop the graphql schema, as apolloServer will already reinit it during onStart call\n        GraphQLSchema.init();\n        // FIXME: the schema is not yet ready when tests are run\n        // it makes the bodyParser tests failing at first run even if they are valid\n        onStart();\n      });\n\n      // TODO: not yet working\n      // also can't configure HTTP request to change headers\n      //test('application/graphql', async () => {\n      //  const server = onStart()\n      //  const { query, /*mutate*/ } = createTestClient(server)\n      //  const res = await query({\n      //    query: `query currentUser {\n      //      currentUser {\n      //        _id\n      //      }\n      //    }`\n      //  })\n      //  expect(res).toEqual({})\n      //})\n\n      // Example HTTP integration test with usual supertest lib\n      // FIXME: test is passing but only after a reload\n      test.skip('application/graphql', async () => {\n        const res = await request(WebApp.connectHandlers)\n          .post('/graphql')\n          .send(\n            `\n          query currentUser {\n            currentUser {\n              _id\n            }\n          }`\n          )\n          .set('Content-Type', 'application/graphql');\n        const data = JSON.parse(res.text).data;\n        expect(data).toEqual({ currentUser: null });\n      });\n      // FIXME: test is passing but only after a reload\n      test.skip('application/json', async () => {\n        const res = await request(WebApp.connectHandlers)\n          .post('/graphql')\n          .send(\n            JSON.stringify({\n              query: `\n          query currentUser {\n            currentUser {\n              _id\n            }\n          }`,\n            })\n          )\n          .set('Content-Type', 'application/json');\n        //.expect('Content-Type', /json/)\n        const data = JSON.parse(res.text).data;\n        expect(data).toEqual({ currentUser: null });\n      });\n    });\n\n    /*\n  describe('cors', () => {\n    test('cors', async () => {\n      const corsDisallowed\n      const server = createApolloServer(options);\n      const { query, mutate } = createTestClient(server)\n      query({\n        query: ``,\n        variables: { id: 1 }\n      })\n      // mutate({mutation: ``, variables: {...}})\n      const res = await query({ query: GET_LAUNCH, variables: { id: 1 } });\n      expect(res).toEqual({})\n    })\n  })*/\n    describe('setupWebApp', function() {});\n    describe('compute context', function() {\n      test.skip('initial context contains graphQLSchema context', function() {\n        // TODO\n      });\n      test.skip('initial context is merged with provided context', function() {\n        // TODO\n      });\n      test.skip('data loaders are regenerated on each request', function() {\n        // TODO\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/server/apollo-ssr.test.js",
    "content": "import expect from 'expect';\nimport sinon from 'sinon';\nimport makePageRenderer from '../../lib/server/apollo-ssr/renderPage';\nimport React from 'react';\nimport ApolloState from '../../lib/server/apollo-ssr/components/ApolloState';\n//import { InjectData } from '../../lib/server/apollo-ssr';\nimport { initGraphQLTest } from 'meteor/vulcan:test';\nimport { mount } from 'enzyme';\n\nconst test = it;\n\nconst createDummySink = () => ({\n    result: {\n        body: '',\n        head: ''\n    },\n    request: {\n        url: 'foobar.com'\n    },\n    appendToHead(content) {\n        this.result.head += content;\n    },\n    appendToBody(content) {\n        this.result.body += content;\n    }\n});\ndescribe('vulcan:lib/renderPage', () => {\n    let renderPage;\n    before(() => {\n        initGraphQLTest();\n        // TODO: remove the apollo client warning by initing GraphQL\n        renderPage = makePageRenderer({\n            computeContext: () => ({\n                currentUser: null,\n                siteData: null\n            })\n        });\n    });\n\n    test('should render page', async () => {\n        const sink = createDummySink();\n        await renderPage(sink);\n        expect(sink.result.body).toMatch('<div id=\"react-app\">');\n        expect(sink.result.head).toMatch('<title');\n        expect(sink.result.head).not.toMatch('<head');\n    });\n    test('should NOT render an additional <head> tag', async () => {\n        const sink = createDummySink();\n        await renderPage(sink);\n        expect(sink.result.head).not.toMatch('<head');\n    })\n\n    test('should inject default data', async () => {\n        const sink = createDummySink();\n        await renderPage(sink);\n        expect(sink.result.head).toMatch(/<script type=\"text\\/inject-data\">(.*?)<\\/script>/);\n    });\n\n    test('do not inject data if cors are set', async () => {\n        const sink = createDummySink();\n        sink.responseHeaders = {\n            'access-control-allow-origin': 'whatever'\n        };\n        await renderPage(sink);\n        expect(sink.result.head).not.toMatch('type=\"text/inject-data');\n    });\n\n    describe('ApolloState', () => {\n        // TODO: a better test would be replacing the App component\n        // temporarily with a component that add <script>window.HACKED=1</script>\n        // to apollo state, and run renderPage directly\n        // That would mean creating a helper to replace App easily when rendering\n        //@see https://medium.com/node-security/the-most-common-xss-vulnerability-in-react-js-applications-2bdffbcc1fa0\n        test('serialize apollo state', async () => {\n            const wrapper = mount(<ApolloState initialState={\n                {\n                    hack: '<script>window.HACKED=1</script>'\n                }\n            } />);\n            expect(wrapper.find('script')).toHaveLength(1);\n            const script = wrapper.find('script');\n            expect(script.text()).not.toMatch('<script');\n            expect(script.text()).not.toMatch('<');\n        });\n    });\n});"
  },
  {
    "path": "packages/vulcan-lib/test/server/fixtures/minimalSchema.js",
    "content": "// blatantly stolen from https://www.apollographql.com/docs/graphql-tools/generate-schema.html\nimport find from 'lodash/find';\nimport filter from 'lodash/filter';\nimport { makeExecutableSchema } from '@graphql-tools/schema';\n\nconst typeDefs = `\n  type Author {\n    id: Int!\n    firstName: String\n    lastName: String\n    posts: [Post]\n  }\n\n  type Post {\n    id: Int!\n    title: String\n    author: Author\n    votes: Int\n  }\n\n  # the schema allows the following query:\n  type Query {\n    posts: [Post]\n    author(id: Int!): Author\n  }\n\n  # this schema allows the following mutation:\n  type Mutation {\n    upvotePost (\n      postId: Int!\n    ): Post\n  }\n`;\n\n// example data\nconst authors = [\n  { id: 1, firstName: 'Tom', lastName: 'Coleman' },\n  { id: 2, firstName: 'Sashko', lastName: 'Stubailo' },\n  { id: 3, firstName: 'Mikhail', lastName: 'Novikov' },\n];\n\nconst posts = [\n  { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 },\n  { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 },\n  { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 },\n  { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 },\n];\n\nconst resolvers = {\n  Query: {\n    posts: () => posts,\n    author: (_, { id }) => find(authors, { id }),\n  },\n\n  Mutation: {\n    upvotePost: (_, { postId }) => {\n      const post = find(posts, { id: postId });\n      if (!post) {\n        throw new Error(`Couldn't find post with id ${postId}`);\n      }\n      post.votes += 1;\n      return post;\n    },\n  },\n\n  Author: {\n    posts: author => filter(posts, { authorId: author.id }),\n  },\n\n  Post: {\n    author: post => find(authors, { id: post.authorId }),\n  },\n};\n\nexport const executableSchema = makeExecutableSchema({\n  typeDefs,\n  resolvers,\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/server/fragments.test.js",
    "content": "import expect from 'expect'\n\nimport SimpleSchema from 'simpl-schema';\nimport { createDummyCollection, normalizeGraphQLSchema } from 'meteor/vulcan:test'\nimport { getDefaultFragmentText } from '../../lib/modules/graphql/defaultFragment';\nconst test = it\n\nconst fooCollection = (schema) => createDummyCollection({\n    collectionName: 'Foos',\n    typeName: 'Foo',\n    resolvers: null,\n    mutations: null,\n    schema\n});\n\ndescribe('default fragment generation', () => {\n    test('generate default fragment for basic collection', () => {\n        const collection = fooCollection({\n            foo: {\n                type: String,\n                canRead: ['guests']\n            },\n            bar: {\n                type: String,\n                canRead: ['guests']\n            }\n\n        });\n        const fragment = getDefaultFragmentText(collection);\n        const normalizedFragment = normalizeGraphQLSchema(fragment);\n        expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { foo bar }');\n    });\n    test('generate default fragment with nested object', () => {\n        const collection = fooCollection({\n            foo: {\n                type: String,\n                canRead: ['guests']\n            },\n            nestedField: {\n                canRead: ['guests'],\n                type: new SimpleSchema({\n                    bar: {\n                        type: String,\n                        canRead: ['guests']\n                    }\n                })\n            }\n        });\n        const fragment = getDefaultFragmentText(collection);\n        const normalizedFragment = normalizeGraphQLSchema(fragment);\n        expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { foo nestedField { bar } }');\n    });\n    test('generate default fragment with blackbox JSON object (no nesting)', () => {\n        const collection = fooCollection({\n            foo: {\n                type: String,\n                canRead: ['guests']\n            },\n            object: {\n                canRead: ['guests'],\n                type: Object\n            }\n        });\n        const fragment = getDefaultFragmentText(collection);\n        const normalizedFragment = normalizeGraphQLSchema(fragment);\n        expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { foo object }');\n    });\n    test('generate default fragment with nested array of objects', () => {\n        const collection = fooCollection({\n            arrayField: {\n                type: Array,\n                canRead: ['admins']\n\n            },\n            'arrayField.$': {\n                type: new SimpleSchema({\n                    subField: {\n                        type: String,\n                        canRead: ['admins']\n                    }\n                }),\n                canRead: ['admins']\n            }\n        });\n        const fragment = getDefaultFragmentText(collection);\n        const normalizedFragment = normalizeGraphQLSchema(fragment);\n        expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { arrayField { subField } }');\n    });\n    test('generate default fragment with array of native values', () => {\n        const collection = fooCollection({\n            arrayField: {\n                type: Array,\n                canRead: ['admins']\n\n            },\n            'arrayField.$': {\n                type: Number,\n                canRead: ['admins']\n            }\n        });\n        const fragment = getDefaultFragmentText(collection);\n        const normalizedFragment = normalizeGraphQLSchema(fragment);\n        expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { arrayField }');\n\n    });\n    test('return fieldName for intl fields even if they are objects or arrays', () => {\n        const collection = fooCollection({\n            foo_intl: {\n                type: Array,\n                canRead: ['guests']\n            },\n            \"foo_intl.$\": {\n                type: String,\n                canRead: ['guests']\n            },\n            bar_intl: {\n                type: Object,\n                canRead: ['guests']\n            },\n        });\n        const fragment = getDefaultFragmentText(collection);\n        const normalizedFragment = normalizeGraphQLSchema(fragment);\n        expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { foo_intl{ locale value } bar_intl{ locale value } }');\n    });\n\n    test('do not generate subfield for blackboxed array', () => {\n        const collection = fooCollection({\n            foo: {\n                type: Array,\n                canRead: ['guests'],\n                blackbox: true\n            },\n            \"foo.$\": {\n                type: new SimpleSchema({\n                    bar: {\n                        type: String,\n                        canRead: ['guests']\n                    }\n                }),\n                canRead: ['guests']\n            },\n        });\n        const fragment = getDefaultFragmentText(collection);\n        const normalizedFragment = normalizeGraphQLSchema(fragment);\n        expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { foo }');\n\n    })\n\n    describe('resolveAs', () => {\n        test('ignore resolved fields with a an unknown type', () => {\n            const collection = fooCollection({\n                // ignored in default fragments because we don't know People type\n                object: {\n                    type: Object,\n                    canRead: ['admins'],\n                    resolveAs: {\n                        fieldName: 'resolvedObject',\n                        type: 'People',\n                        resolver: () => (null)\n                    }\n                },\n                // dummy field to avoid empty fragment\n                foo: {\n                    type: String,\n                    canRead: ['admins']\n                }\n            });\n            const fragment = getDefaultFragmentText(collection);\n            const normalizedFragment = normalizeGraphQLSchema(fragment);\n            expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { object foo }');\n        })\n        test('add original field with resolveAs as a default', () => {\n            const collection = fooCollection({\n                json: {\n                    type: Object,\n                    canRead: ['admins'],\n                    resolveAs: {\n                        fieldName: 'resolvedJSON',\n                        type: 'JSON',\n                        resolver: () => null,\n                    }\n                },\n            });\n            const fragment = getDefaultFragmentText(collection);\n            const normalizedFragment = normalizeGraphQLSchema(fragment);\n            expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { json }');\n        });\n        test('do not add original field if at least one addOriginalField is false', () => {\n            const collection = fooCollection({\n                // ignored in default fragments\n                foo: {\n                    type: String,\n                    canRead: ['admins'],\n                    resolveAs: [{\n                        fieldName: 'resolvedObject',\n                        type: 'String',\n                        resolver: () => (null)\n                    }, {\n                        fieldName: 'anotherResolvedObject',\n                        type: 'String', resolver: () => null,\n                        addOriginalField: false\n                    }]\n                },\n            });\n            const fragment = getDefaultFragmentText(collection);\n            expect(fragment).toBeNull() // resolved field are not yet present in the fragment so it's null\n            //const normalizedFragment = normalizeGraphQLSchema(fragment);\n            //expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { resolvedObject anotherResolvedObject }');\n        })\n\n    })\n    test('ignore referenced schemas', () => {\n        const collection = fooCollection({\n            field: {\n                type: String,\n                canRead: ['admins']\n            },\n            // ignored in default fragments\n            address: {\n                type: Object,\n                typeName: 'Address',\n                canRead: ['admins'],\n            },\n        });\n        const fragment = getDefaultFragmentText(collection);\n        const normalizedFragment = normalizeGraphQLSchema(fragment);\n        expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { field }');\n    });\n    test('ignore referenced schemas in array child', () => {\n        const collection = fooCollection({\n            field: {\n                type: String,\n                canRead: ['admins']\n            },\n            emails: {\n                type: Array,\n                optional: true,\n                canRead: ['admin']\n            },\n            'emails.$': {\n                type: Object,\n                typeName: 'UserEmail',\n                optional: true,\n            },\n        });\n        const fragment = getDefaultFragmentText(collection);\n        const normalizedFragment = normalizeGraphQLSchema(fragment);\n        expect(normalizedFragment).toMatch('fragment FoosDefaultFragment on Foo { field }');\n    });\n});"
  },
  {
    "path": "packages/vulcan-lib/test/server/graphql.test.js",
    "content": "import expect from 'expect';\n\nimport { GraphQLSchema } from '../../lib/server/graphql';\nimport initGraphQL from '../../lib/server/apollo-server/initGraphQL';\n//import { getIntlString } from '../../lib/modules/intl'\nimport { addIntlFields } from '../../lib/modules/collections';\n\n//import collectionToGraphQL from '../../lib/modules/graphql/collectionToSchema';\nimport collectionToGraphQL from '../../lib/server/graphql/collection';\nimport { getSchemaFields } from '../../lib/server/graphql/schemaFields';\nimport { getGraphQLType } from '../..//lib/modules/graphql/utils.js';\nimport { generateResolversFromSchema } from '../../lib/server/graphql/resolvers';\nimport SimpleSchema from 'simpl-schema';\nimport { createDummyCollection, normalizeGraphQLSchema } from 'meteor/vulcan:test';\nimport Users from 'meteor/vulcan:users';\n\nconst test = it;\n\nconst fooCollection = schema =>\n  createDummyCollection({\n    collectionName: 'Foos',\n    typeName: 'Foo',\n    resolvers: null,\n    mutations: null,\n    schema,\n  });\n\ndescribe('vulcan:lib/graphql', function() {\n  // TODO: handle the graphQL init better to fix those tests\n  it.skip('throws if graphql schema is not initialized', function() {\n    expect(() => GraphQLSchema.getSchema()).toThrow();\n  });\n  it.skip('throws if executable schema is not initialized', function() {\n    expect(() => GraphQLSchema.getExecutableSchema()).toThrow();\n  });\n  it('can access the graphql schema', function() {\n    GraphQLSchema.init();\n    initGraphQL();\n    expect(GraphQLSchema.getSchema()).toBeDefined();\n  });\n  it('can access the executable graphql schema', function() {\n    GraphQLSchema.init();\n    initGraphQL();\n    expect(GraphQLSchema.getExecutableSchema()).toBeDefined();\n  });\n\n  describe('generateResolversFromSchema - generate a secure resolver for each field', () => {\n    const context = {\n      currentUser: null,\n      Users,\n    };\n    test('get the resolvers for a field', () => {\n      const resolvers = generateResolversFromSchema(\n        new SimpleSchema({\n          foo: {\n            type: String,\n            canRead: ['guests'],\n          },\n        })\n      );\n      const fooResolver = resolvers['foo'];\n      expect(fooResolver).toBeInstanceOf(Function);\n      expect(fooResolver({ foo: 'bar' }, null, context)).toEqual('bar');\n    });\n    test('ignore non readable fields', () => {\n      const resolvers = generateResolversFromSchema(\n        new SimpleSchema({\n          foo: {\n            type: String,\n            canRead: ['admins'],\n          },\n        })\n      );\n      const fooResolver = resolvers['foo'];\n      expect(fooResolver({ foo: 'bar' }, null, context)).toBeNull();\n    });\n    test('convert undefined fields into null', () => {\n      const resolvers = generateResolversFromSchema(\n        new SimpleSchema({\n          foo: {\n            type: String,\n            canRead: ['admins'],\n          },\n        })\n      );\n      const fooResolver = resolvers['foo'];\n      expect(fooResolver({ foo2: 'bar' }, null, context)).toBeNull();\n    });\n    test('do NOT convert other falsy fields into null', () => {\n      const resolvers = generateResolversFromSchema(\n        new SimpleSchema({\n          foo: {\n            type: Number,\n            canRead: ['guests'],\n          },\n        })\n      );\n      const fooResolver = resolvers['foo'];\n      expect(fooResolver({ foo: 0 }, null, context)).toEqual(0);\n    });\n  });\n\n  describe('schemaFields - graphql fields generation from simple schema', () => {\n    describe('getGraphQLType - associate a graphQL type to a field', () => {\n      test('return nested type for nested objects', () => {\n        const schema = new SimpleSchema({\n          nestedField: {\n            type: new SimpleSchema({\n              firstNestedField: {\n                type: String,\n              },\n              secondNestedField: {\n                type: Number,\n              },\n            }),\n          },\n        })._schema;\n        const type = getGraphQLType({ schema, fieldName: 'nestedField', typeName: 'Foo' });\n        expect(type).toBe('FooNestedField');\n      });\n      test('return JSON for nested objects with blackbox option', () => {\n        const schema = new SimpleSchema({\n          nestedField: {\n            optional: true,\n            blackbox: true,\n            type: new SimpleSchema({\n              firstNestedField: {\n                type: String,\n              },\n              secondNestedField: {\n                type: Number,\n              },\n            }),\n          },\n        })._schema;\n        const type = getGraphQLType({ schema, fieldName: 'nestedField', typeName: 'Foo' });\n        expect(type).toBe('JSON');\n      });\n      test('return JSON for nested objects that are actual JSON objects', () => {\n        const schema = new SimpleSchema({\n          nestedField: {\n            type: Object,\n          },\n        })._schema;\n        const type = getGraphQLType({ schema, fieldName: 'nestedField', typeName: 'Foo' });\n        expect(type).toBe('JSON');\n      });\n      test('return JSON for child of blackboxed array', () => {\n        const schema = new SimpleSchema({\n          arrayField: {\n            type: Array,\n            blackbox: true,\n          },\n          'arrayField.$': {\n            type: new SimpleSchema({\n              someField: {\n                type: String,\n              },\n            }),\n          },\n        })._schema;\n        const type = getGraphQLType({ schema, fieldName: 'arrayField', typeName: 'Foo' });\n        expect(type).toBe('[JSON]');\n      });\n\n      test('return JSON for input type if provided typeName is JSON', () => {\n        const schema = new SimpleSchema({\n          nestedField: {\n            type: Object,\n            typeName: 'JSON',\n          },\n        })._schema;\n        const inputType = getGraphQLType({ schema, fieldName: 'nestedField', typeName: 'Foo', isInput: true });\n        expect(inputType).toBe('JSON');\n      });\n\n      test('return nested  array type for arrays of nested objects', () => {\n        const schema = new SimpleSchema({\n          arrayField: {\n            type: Array,\n            canRead: ['admins'],\n          },\n          'arrayField.$': {\n            type: new SimpleSchema({\n              firstNestedField: {\n                type: String,\n              },\n              secondNestedField: {\n                type: Number,\n              },\n            }),\n          },\n        })._schema;\n        const type = getGraphQLType({ schema, fieldName: 'arrayField', typeName: 'Foo' });\n        expect(type).toBe('[FooArrayField]');\n      });\n      test('return basic array type for array of primitives', () => {\n        const schema = new SimpleSchema({\n          arrayField: {\n            type: Array,\n            canRead: ['admins'],\n          },\n          'arrayField.$': {\n            type: String,\n          },\n        })._schema;\n        const type = getGraphQLType({ schema, fieldName: 'arrayField', typeName: 'Foo' });\n        expect(type).toBe('[String]');\n      });\n\n      test('return JSON if blackbox is true', () => {});\n    });\n\n    describe('getSchemaFields - get the fields to add to graphQL schema', () => {\n      test('fields without permissions are ignored', () => {\n        const schema = new SimpleSchema({\n          field: {\n            type: String,\n            canRead: ['admins'],\n          },\n          ignoredField: {\n            type: String,\n          },\n        })._schema;\n        const fields = getSchemaFields(schema, 'Foo');\n        const mainType = fields.fields.mainType;\n        expect(mainType).toHaveLength(1);\n        expect(mainType[0].name).toEqual('field');\n      });\n      test('nested fields without permissions are ignored', () => {\n        const schema = new SimpleSchema({\n          nestedField: {\n            type: new SimpleSchema({\n              firstNestedField: {\n                type: String,\n                canRead: ['admins'],\n              },\n              ignoredNestedField: {\n                type: Number,\n              },\n            }),\n            canRead: ['admins'],\n          },\n        })._schema;\n        const fields = getSchemaFields(schema, 'Foo');\n        const nestedFields = fields.nestedFieldsList[0];\n        // one field in the nested object\n        expect(nestedFields.fields.mainType).toHaveLength(1);\n        expect(nestedFields.fields.mainType[0].name).toEqual('firstNestedField');\n      });\n      test('generate fields for nested objects', () => {\n        const schema = new SimpleSchema({\n          nestedField: {\n            type: new SimpleSchema({\n              firstNestedField: {\n                type: String,\n                canRead: ['admins'],\n              },\n              secondNestedField: {\n                type: Number,\n                canRead: ['admins'],\n              },\n            }),\n            canRead: ['admins'],\n          },\n        })._schema;\n\n        const fields = getSchemaFields(schema, 'Foo');\n        // one nested object\n        expect(fields.nestedFieldsList).toHaveLength(1);\n        const nestedFields = fields.nestedFieldsList[0];\n        expect(nestedFields.typeName).toEqual('FooNestedField');\n        // one field in the nested object\n        expect(nestedFields.fields.mainType).toHaveLength(2);\n        expect(nestedFields.fields.mainType[0].name).toEqual('firstNestedField');\n        expect(nestedFields.fields.mainType[1].name).toEqual('secondNestedField');\n      });\n    });\n  });\n  describe('collection to GraphQL schema and type', () => {\n    describe('basic', () => {\n      test('generate a type for a simple collection', () => {\n        const collection = fooCollection({\n          field: {\n            type: String,\n            canRead: ['admins'],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        expect(res.graphQLSchema).toBeDefined();\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { field: String }');\n      });\n      test('use provided graphQL type if any', () => {\n        const collection = createDummyCollection({\n          collectionName: 'Foos',\n          typeName: 'Foo',\n          schema: {\n            field: {\n              type: String,\n              typeName: 'StringEnum',\n              canRead: ['admins'],\n            },\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        expect(res.graphQLSchema).toBeDefined();\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { field: StringEnum }');\n      });\n    });\n    describe('nested objects and arrays', () => {\n      test('generate type for a nested field', () => {\n        const collection = fooCollection({\n          nestedField: {\n            type: new SimpleSchema({\n              subField: {\n                type: String,\n                canRead: ['admins'],\n              },\n            }),\n            canRead: ['admins'],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { nestedField: FooNestedField }');\n        expect(normalizedSchema).toMatch('type FooNestedField { subField: String }');\n      });\n      test('generate graphQL type for array of nested objects', () => {\n        const collection = fooCollection({\n          arrayField: {\n            type: Array,\n            canRead: ['admins'],\n          },\n          'arrayField.$': {\n            type: new SimpleSchema({\n              subField: {\n                type: String,\n                canRead: ['admins'],\n              },\n            }),\n            canRead: ['admins'],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { arrayField: [FooArrayField] }');\n        expect(normalizedSchema).toMatch('type FooArrayField { subField: String }');\n      });\n      test('ignore field if parent is blackboxed', () => {\n        const collection = fooCollection({\n          blocks: {\n            type: Array,\n            canRead: ['admins'],\n            blackbox: true,\n          },\n          'blocks.$': {\n            type: new SimpleSchema({\n              addresses: {\n                type: Array,\n                canRead: ['admins'],\n              },\n              'addresses.$': {\n                type: new SimpleSchema({\n                  street: {\n                    type: String,\n                    canRead: ['adminst'],\n                  },\n                }),\n                canRead: ['admins'],\n              },\n            }),\n            canRead: ['admins'],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { blocks: [JSON] }');\n      });\n    });\n\n    describe('nesting with referenced field', () => {\n      test('use referenced graphQL type if provided for nested object', () => {\n        const collection = fooCollection({\n          nestedField: {\n            type: Object,\n            blackbox: true,\n            typeName: 'AlreadyRegisteredNestedType',\n            canRead: ['admins'],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { nestedField: AlreadyRegisteredNestedType }');\n        expect(normalizedSchema).not.toMatch('FooNestedField');\n      });\n\n      // TODO: does this test case make any sense?\n      test('do NOT generate graphQL type if an existing graphQL type is referenced', () => {\n        const collection = fooCollection({\n          nestedField: {\n            type: new SimpleSchema({\n              subField: {\n                type: String,\n                canRead: ['admins'],\n              },\n            }),\n            typeName: 'AlreadyRegisteredNestedType',\n            canRead: ['admins'],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { nestedField: AlreadyRegisteredNestedType }');\n        expect(normalizedSchema).not.toMatch('FooNestedField');\n      });\n      test('do NOT generate graphQL type for array of nested objects if an existing graphQL type is referenced', () => {\n        const collection = fooCollection({\n          arrayField: {\n            type: Array,\n            canRead: ['admins'],\n          },\n          'arrayField.$': {\n            typeName: 'AlreadyRegisteredType',\n            type: new SimpleSchema({\n              subField: {\n                type: String,\n                canRead: ['admins'],\n              },\n            }),\n            canRead: ['admins'],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { arrayField: [AlreadyRegisteredType] }');\n        expect(normalizedSchema).not.toMatch('type FooArrayField { subField: String }');\n      });\n    });\n\n    describe('intl', () => {\n      test('generate type for intl fields', () => {\n        const collection = fooCollection(\n          addIntlFields(\n            // we need to do this manually, it is handled by a callback when creating the collection\n            {\n              intlField: {\n                intl: true,\n                type: String,\n                canRead: ['admins'],\n              },\n            }\n          )\n        );\n        const res = collectionToGraphQL(collection);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch(\n          'type Foo { intlField(locale: String): String @intl intlField_intl(locale: String): [IntlValue] @intl }'\n        );\n      });\n      test.skip('generate type for array of intl fields', () => {\n        const collection = fooCollection(\n          addIntlFields(\n            // we need to do this manually, it is handled by a callback when creating the collection\n            {\n              arrayField: {\n                type: Array,\n                canRead: ['admins'],\n              },\n              'arrayField.$': {\n                type: String,\n                intl: true,\n                canRead: ['admins'],\n              },\n            }\n          )\n        );\n        const res = collectionToGraphQL(collection);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { arrayField: [[IntlValue]] }');\n      });\n      test('generate correct type for nested intl fields', () => {\n        const collection = fooCollection({\n          nestedField: {\n            type: new SimpleSchema(\n              addIntlFields(\n                // we need to do this manually, it is handled by a callback when creating the collection\n                {\n                  intlField: {\n                    type: String,\n                    intl: true,\n                    canRead: ['admins'],\n                  },\n                }\n              )\n            ),\n            canRead: ['admins'],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { nestedField: FooNestedField }');\n        expect(normalizedSchema).toMatch(\n          'type FooNestedField { intlField(locale: String): String @intl intlField_intl(locale: String): [IntlValue] @intl }'\n        );\n      });\n    });\n\n    describe('resolveAs', () => {\n      test('generate a type for a field with resolveAs', () => {\n        const collection = fooCollection({\n          field: {\n            type: String,\n            canRead: ['admins'],\n            resolveAs: {\n              fieldName: 'field',\n              type: 'Bar',\n              resolver: async (user, args, { Users }) => {\n                return 'bar';\n              },\n            },\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        expect(res.graphQLSchema).toBeDefined();\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { field: Bar }');\n      });\n      test('generate a type for a field with addOriginalField=true', () => {\n        const collection = fooCollection({\n          field: {\n            type: String,\n            optional: true,\n            canRead: ['admins'],\n            resolveAs: {\n              fieldName: 'resolvedField',\n              type: 'Bar',\n              resolver: (collection, args, context) => {\n                return 'bar';\n              },\n              addOriginalField: true,\n            },\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        expect(res.graphQLSchema).toBeDefined();\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { field: String resolvedField: Bar }');\n      });\n      test('generate a type for a field with addOriginalField=true for at least one resolver of an array of resolveAs', () => {\n        const collection = fooCollection({\n          field: {\n            type: String,\n            optional: true,\n            canRead: ['admins'],\n            resolveAs: [\n              {\n                fieldName: 'resolvedField',\n                type: 'Bar',\n                resolver: () => 'bar',\n                addOriginalField: true,\n              },\n              {\n                fieldName: 'anotherResolvedField',\n                type: 'Bar',\n                resolver: () => 'bar',\n              },\n            ],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        expect(res.graphQLSchema).toBeDefined();\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { field: String resolvedField: Bar anotherResolvedField: Bar }');\n      });\n    });\n\n    /*\n    Feature removed\n    generating enums from allowed values automatically => bad idea, could be a manual helper instead\n    describe('enums', () => {\n      test('don\\'t generate enum type when some values are not allowed', () => {\n        const collection = fooCollection({\n          withAllowedField: {\n            type: String,\n            canRead: ['admins'],\n            allowedValues: ['français', 'bar'] // \"ç\" is not accepted, Enum must be a name\n          }\n        });\n        const res = collectionToGraphQL(collection);\n        expect(res.graphQLSchema).toBeDefined();\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { withAllowedField: String }');\n        expect(normalizedSchema).not.toMatch('type Foo { withAllowedField: FooWithAllowedFieldEnum }');\n        expect(normalizedSchema).not.toMatch('enum FooWithAllowedFieldEnum { français bar }');\n      });\n      test('fail when allowedValues are not string', () => {\n        const collection = fooCollection({\n          withAllowedField: {\n            type: String,\n            canRead: ['admins'],\n            allowedValues: [0, 1] // \"ç\" is not accepted, Enum must be a name\n          }\n        });\n        expect(() => collectionToGraphQL(collection)).toThrow();\n      });\n      test('generate enum type when allowedValues is defined and field is a string', () => {\n        const collection = fooCollection({\n          withAllowedField: {\n            type: String,\n            canRead: ['admins'],\n            allowedValues: ['foo', 'bar']\n          }\n        });\n        const res = collectionToGraphQL(collection);\n        expect(res.graphQLSchema).toBeDefined();\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { withAllowedField: FooWithAllowedFieldEnum }');\n        expect(normalizedSchema).toMatch('enum FooWithAllowedFieldEnum { foo bar }');\n      });\n      test('generate enum type for nested objects', () => {\n        test('generate enum type when allowedValues is defined and field is a string', () => {\n          const collection = fooCollection({\n            nestedField: {\n              type: new SimpleSchema({\n                withAllowedField: {\n                  type: String,\n                  allowedValues: ['foo', 'bar'],\n                  canRead: ['admins'],\n                }\n              }),\n              canRead: ['admins'],\n            }\n          });\n          const res = collectionToGraphQL(collection);\n          expect(res.graphQLSchema).toBeDefined();\n          // debug\n          //console.log(res.graphQLSchema);\n          const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n          expect(normalizedSchema).toMatch('type Foo { nestedField { withAllowedField: FooNestedFieldWithAllowedFieldEnum } }');\n          expect(normalizedSchema).toMatch('enum FooNestedFieldWithAllowedFieldEnum { foo bar }');\n        });\n     \n      });\n     \n      test('2 level of nesting', () => {\n        const collection = fooCollection({\n          entrepreneurLifeCycleHistory: {\n            type: Array,\n            optional: true,\n            canRead: ['admins', 'mods'],\n            //onUpdate: entLifecycleHistoryOnUpdate,\n          },\n          'entrepreneurLifeCycleHistory.$': {\n            type: new SimpleSchema(\n              {\n                entrepreneurLifeCycleState: {\n                  type: String,\n                  // canCreate: ['admins', 'mods'],\n                  canRead: ['admins', 'mods'],\n                  // canUpdate: ['admins', 'mods'],\n                  input: 'select',\n                  options: [\n                    { value: 'booster', label: 'Booster' },\n                    { value: 'explorer', label: 'Explorer' },\n                    { value: 'starter', label: 'Starter' },\n                    { value: 'tester', label: 'Tester' },\n                  ],\n                  allowedValues: ['booster', 'explorer', 'starter', 'tester'],\n                },\n              }\n            )\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        expect(res.graphQLSchema).toBeDefined();\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('type Foo { entrepreneurLifeCycleHistory: [FooEntrepreneurLifeCycleHistory]');\n        expect(normalizedSchema).toMatch('type FooEntrepreneurLifeCycleHistory { entrepreneurLifeCycleState: FooEntrepreneurLifeCycleHistoryEntrepreneurLifeCycleStateEnum');\n        expect(normalizedSchema).toMatch('enum FooEntrepreneurLifeCycleHistoryEntrepreneurLifeCycleStateEnum { booster explorer starter tester }');\n      });\n     \n      test(\"support enum type in array children\", () => {\n        throw new Error(\"test not written yet\")\n        const schema = {\n          arrayField : { ... }\n          \"arrayField.$\": {\n            type: String,\n            allowedValues: [...] // whatever\n          } \n        }\n      })\n    });\n    */\n\n    describe('mutation inputs', () => {\n      test('generate creation input', () => {\n        const collection = fooCollection({\n          field: {\n            type: String,\n            canRead: ['admins'],\n            canCreate: ['admins'],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('input CreateFooInput { data: CreateFooDataInput!');\n        expect(normalizedSchema).toMatch('input CreateFooDataInput { field: String }');\n      });\n      test('generate inputs for nested objects', () => {\n        const collection = fooCollection({\n          nestedField: {\n            type: new SimpleSchema({\n              someField: {\n                type: String,\n                canRead: ['admins'],\n                canCreate: ['admins'],\n              },\n            }),\n            canRead: ['admins'],\n            canCreate: ['admins'],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        // TODO: not 100% of the expected result\n        expect(normalizedSchema).toMatch('input CreateFooInput { data: CreateFooDataInput!');\n        expect(normalizedSchema).toMatch('input CreateFooDataInput { nestedField: CreateFooNestedFieldDataInput }');\n        expect(normalizedSchema).toMatch('input CreateFooNestedFieldDataInput { someField: String }');\n      });\n      test('generate inputs for array of nested objects', () => {\n        const collection = fooCollection({\n          arrayField: {\n            type: Array,\n            canRead: ['admins'],\n            canCreate: ['admins'],\n          },\n          'arrayField.$': {\n            canRead: ['admins'],\n            canCreate: ['admins'],\n            type: new SimpleSchema({\n              someField: {\n                type: String,\n                canRead: ['admins'],\n                canCreate: ['admins'],\n              },\n            }),\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        // TODO: not 100% sure of the syntax\n        expect(normalizedSchema).toMatch('input CreateFooInput { data: CreateFooDataInput!');\n        expect(normalizedSchema).toMatch('input CreateFooDataInput { arrayField: [CreateFooArrayFieldDataInput] }');\n        expect(normalizedSchema).toMatch('input CreateFooArrayFieldDataInput { someField: String }');\n      });\n      test('do NOT generate new inputs for array of JSON', () => {\n        const collection = fooCollection({\n          arrayField: {\n            type: Array,\n            canRead: ['admins'],\n            canCreate: ['admins'],\n          },\n          'arrayField.$': {\n            canRead: ['admins'],\n            canCreate: ['admins'],\n            type: Object,\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        // TODO: not 100% sure of the syntax\n        expect(normalizedSchema).toMatch('input CreateFooDataInput { arrayField: [JSON] }');\n        expect(normalizedSchema).not.toMatch('CreateJSONDataInput');\n      });\n      test('do NOT generate new inputs for blackboxed array', () => {\n        const collection = fooCollection({\n          arrayField: {\n            type: Array,\n            canRead: ['admins'],\n            canCreate: ['admins'],\n            blackbox: true,\n          },\n          'arrayField.$': {\n            canRead: ['admins'],\n            canCreate: ['admins'],\n            type: new SimpleSchema({\n              foo: {\n                type: String,\n                canRead: ['admins'],\n                canUpdate: ['admins'],\n              },\n            }),\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('input CreateFooDataInput { arrayField: [JSON] }');\n        expect(normalizedSchema).not.toMatch('CreateJSONDataInput');\n      });\n\n      test('do NOT generate new inputs for nested objects if a type is provided', () => {\n        const collection = fooCollection({\n          nestedField: {\n            type: new SimpleSchema({\n              someField: {\n                type: String,\n                canRead: ['admins'],\n                canCreate: ['admins'],\n              },\n            }),\n            typeName: 'AlreadyRegisteredType',\n            canRead: ['admins'],\n            canCreate: ['admins'],\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        // TODO: not 100% of the expected result\n        expect(normalizedSchema).toMatch('input CreateFooDataInput { nestedField: CreateAlreadyRegisteredTypeDataInput }');\n        expect(normalizedSchema).not.toMatch('CreateFooNestedFieldDataInput');\n      });\n      test('do NOT generate new inputs for array of objects if typeName is provided', () => {\n        const collection = fooCollection({\n          arrayField: {\n            type: Array,\n            canRead: ['admins'],\n            canCreate: ['admins'],\n          },\n          'arrayField.$': {\n            canRead: ['admins'],\n            canCreate: ['admins'],\n            typeName: 'AlreadyRegisteredType',\n            type: new SimpleSchema({\n              someField: {\n                type: String,\n                canRead: ['admins'],\n                canCreate: ['admins'],\n              },\n            }),\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        // debug\n        //console.log(res.graphQLSchema);\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        // TODO: not 100% sure of the syntax\n        expect(normalizedSchema).toMatch('input CreateFooDataInput { arrayField: [CreateAlreadyRegisteredTypeDataInput] }');\n        expect(normalizedSchema).not.toMatch('CreateFooArrayFieldDataInput');\n      });\n\n      test('ignore resolveAs', () => {\n        const collection = fooCollection({\n          nestedField: {\n            canRead: ['admins'],\n            canCreate: ['admins'],\n            type: new SimpleSchema({\n              someField: {\n                type: String,\n                optional: true,\n                canRead: ['admins'],\n                resolveAs: {\n                  fieldName: 'resolvedField',\n                  type: 'Bar',\n                  resolver: (collection, args, context) => {\n                    return 'bar';\n                  },\n                },\n              },\n            }),\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        expect(res.graphQLSchema).toBeDefined();\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).not.toMatch('input CreateFooNestedFieldDataInput');\n      });\n      test('ignore resolveAs with addOriginalField when generating nested create input', () => {\n        const collection = fooCollection({\n          nestedField: {\n            canRead: ['admins'],\n            canCreate: ['admins'],\n            type: new SimpleSchema({\n              someField: {\n                type: String,\n                optional: true,\n                canRead: ['admins'],\n                canCreate: ['admins'],\n                resolveAs: {\n                  fieldName: 'resolvedField',\n                  type: 'Bar',\n                  resolver: (collection, args, context) => {\n                    return 'bar';\n                  },\n                  addOriginalField: true,\n                },\n              },\n            }),\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        expect(res.graphQLSchema).toBeDefined();\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).toMatch('input CreateFooInput { data: CreateFooDataInput!');\n        expect(normalizedSchema).toMatch('input CreateFooDataInput { nestedField: CreateFooNestedFieldDataInput }');\n        expect(normalizedSchema).toMatch('input CreateFooNestedFieldDataInput { someField: String }');\n      });\n\n      test('do not generate generic input type for direct nested arrays or objects (only appliable to referenced types)', () => {\n        // TODO: test is over complex because of a previous misunderstanding, can be simplified\n        const collection = fooCollection({\n          arrayField: {\n            type: Array,\n            optional: true,\n            canRead: ['admins'],\n            canCreate: ['admins'],\n            canUpdate: ['admins'],\n          },\n          'arrayField.$': {\n            type: new SimpleSchema({\n              someFieldId: {\n                type: String,\n                optional: true,\n                canRead: ['admins'],\n                resolveAs: {\n                  fieldName: 'someField',\n                  type: 'User',\n                  resolver: (collection, args, context) => {\n                    return { foo: 'bar' };\n                  },\n                  addOriginalField: true,\n                },\n              },\n            }),\n          },\n        });\n        const res = collectionToGraphQL(collection);\n        expect(res.graphQLSchema).toBeDefined();\n        const normalizedSchema = normalizeGraphQLSchema(res.graphQLSchema);\n        expect(normalizedSchema).not.toMatch('input FooArrayFieldInput');\n      });\n    });\n  });\n\n  describe('resolvers', () => {\n    test.skip('use default resolvers if none is specified', () => {});\n    test.skip('do not add default resolvers if \"null\" is specified', () => {});\n    test.skip('use provided resolvers if any', () => {});\n  });\n  describe('mutations', () => {\n    test.skip('use default resolvers if none is specified', () => {});\n    test.skip('do not add default resolvers if \"null\" is specified', () => {});\n    test.skip('use provided resolvers if any', () => {});\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/server/index.js",
    "content": "// common tests\nimport '../index';\n\nimport './graphql.test';\nimport './apollo-server.test';\nimport './mutators.test';\nimport './apollo-ssr.test';\nimport './mutations.test';\nimport './resolvers.test';\nimport './fragments.test';\n"
  },
  {
    "path": "packages/vulcan-lib/test/server/mutations.test.js",
    "content": "import { getNewDefaultMutations } from '../../lib/server/default_mutations2';\nimport SimpleSchema from 'simpl-schema';\nimport Users from 'meteor/vulcan:users';\n\nimport expect from 'expect';\nimport { createDummyCollection } from 'meteor/vulcan:test';\nconst test = it;\n\n\ndescribe('vulcan:lib/default_mutations', function () {\n\n    it('returns mutations', function () {\n        const mutations = getNewDefaultMutations({\n            typeName: 'Foo',\n            collectionName: 'Foos',\n            options: {}\n        });\n        expect(mutations.create).toBeDefined();\n        expect(mutations.update).toBeDefined();\n        expect(mutations.delete).toBeDefined();\n    });\n\n    describe('delete mutation', () => {\n        const foo = { _id: 'foo' };\n        const Foos = createDummyCollection({\n            results: {\n                findOne: foo\n            },\n            collectionName: 'Foos',\n            schema: { _id: { type: String, canRead: ['admins'] } }\n        })\n        const context = {\n            Users,/*: {\n                options: {\n                    collectionName: 'Users',\n                    typeName: 'User'\n                },\n                simpleSchema: () => new SimpleSchema({ _id: { type: String, canRead: ['admins'] } }),\n                restrictViewableFields: (currentUser, collection, doc) => doc\n            },*/\n            Foos,\n            currentUser: {\n                isAdmin: true,\n                groups: ['admins']\n            }\n        };\n        const mutations = getNewDefaultMutations({\n            typeName: 'Foo',\n            collectionName: 'Foos',\n            options: {}\n        });\n        // We do not need this test anymore because the delete mutator (called by the mutation)\n        // will test the selector itself (selector should return a document, otherwise it is ignored)\n        //test('refuse deletion if selector is empty', async () => {\n        //    const { delete: deleteMutation } = mutations;\n        //    const emptySelector = {};\n        //    const nullSelector = { documentId: null };\n        //    const validIdSelector = { _id: 'foobar' };\n        //    const validDocIdSelector = { documentId: 'foobar' };\n        //    const validSlugSelector = { slug: 'foobar' };\n        //\n        //            // const { mutation } = deleteMutation; // won't work because \"this\" must equal deleteMutation to access \"check\"\n        //            await expect(deleteMutation.mutation(null, { input: { selector: emptySelector } }, context)).rejects.toThrow();\n        //            await expect(deleteMutation.mutation(null, { input: { selector: nullSelector } }, context)).rejects.toThrow();\n        //\n        //            await expect(deleteMutation.mutation(null, { input: { selector: validIdSelector } }, context)).resolves.toEqual({ data: foo });\n        //            await expect(deleteMutation.mutation(null, { input: { selector: validDocIdSelector } }, context)).resolves.toEqual({ data: foo });\n        //            await expect(deleteMutation.mutation(null, { input: { selector: validSlugSelector } }, context)).resolves.toEqual({ data: foo });\n        //        });\n\n    });\n\n\n});"
  },
  {
    "path": "packages/vulcan-lib/test/server/mutators.test.js",
    "content": "import expect from 'expect';\nimport sinon from 'sinon/pkg/sinon.js';\n\nimport { createMutator, updateMutator, deleteMutator } from '../../lib/server/mutators';\n//import StubCollections from 'meteor/hwillson:stub-collections';\nimport Users from 'meteor/vulcan:users';\n\nconst test = it; // TODO: just before we switch to jest\n\n// stub collection\nimport {\n  getDefaultResolvers,\n  getDefaultMutations,\n  addCallback,\n  removeAllCallbacks, createCollection,\n} from 'meteor/vulcan:core';\nimport {\n  initServerTest\n} from 'meteor/vulcan:test';\n\nconst createDummyCollection = (typeName, schema) =>\n  createCollection({\n    collectionName: typeName + 's',\n    typeName,\n    schema,\n    resolvers: getDefaultResolvers(typeName + 's'),\n    mutations: getDefaultMutations(typeName + 's'),\n  });\nconst foo2Schema = {\n  _id: {\n    type: String,\n    canRead: ['guests'],\n    optional: true,\n  },\n  foo2: {\n    type: String,\n    canCreate: ['guests'],\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n  },\n  // generated by a callback\n  after: {\n    required: false,\n    type: String,\n    canCreate: ['guests'],\n    canRead: ['guests'],\n    canUpdate: ['guests']\n  },\n  // generated by onCreate/onUpdate\n  publicAuto: {\n    optional: true,\n    type: String,\n    canCreate: ['guests'],\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n    onCreate: () => {\n      return 'CREATED';\n    },\n    onUpdate: () => {\n      return 'UPDATED';\n    },\n    onDelete: () => {\n      return 'DELETED';\n    }\n  },\n  privateAuto: {\n    optional: true,\n    type: String,\n    canCreate: ['admins'],\n    canRead: ['admins'],\n    canUpdate: ['admins'],\n    onCreate: () => {\n      return 'CREATED';\n    },\n    onUpdate: () => {\n      return 'UPDATED';\n    },\n    onDelete: () => {\n      return 'DELETED';\n    }\n  }\n};\nlet Foo2s = createDummyCollection('Foo2', foo2Schema);\n\n\ndescribe('vulcan:lib/mutators', function () {\n\n  let defaultArgs;\n  let createArgs;\n  let updateArgs;\n  before(async function () {\n    initServerTest();\n  })\n\n  beforeEach(function () {\n    removeAllCallbacks('foo2.create.after');\n    removeAllCallbacks('foo2.create.before');\n    removeAllCallbacks('foo2.create.async');\n    defaultArgs = {\n      collection: Foo2s,\n      document: { foo2: 'bar' },\n      currentUser: null,\n      validate: () => true,\n      context: {\n        Users\n      }\n    };\n    createArgs = {\n      ...defaultArgs,\n    };\n    updateArgs = {\n      ...defaultArgs\n\n    };\n  });\n\n  describe('basic', () => {\n    test('should run createMutator', async function () {\n      const { data: resultDocument } = await createMutator({\n        ...createArgs,\n        document: { foo2: 'bar' }\n      });\n      expect(resultDocument).toBeDefined();\n    });\n    test('create should not mutate the provided data', async () => {\n      const foo = { foo2: 'foo' };\n      const fooCopy = { ...foo };\n      const { data: resultDocument } = await createMutator({ ...createArgs, document: foo });\n      expect(foo).toEqual(fooCopy);\n    });\n    test('update should not mutate the provided data', async () => {\n      const fooUpdate = { foo2: 'fooUpdate' };\n      const fooUpdateCopy = { ...fooUpdate };\n      const { data: foo } = await createMutator({ ...createArgs, document: { foo2: 'foo' } });\n      const { data: resultDocument } = await updateMutator({\n        ...updateArgs,\n        documentId: foo._id,\n        data: fooUpdate,\n      });\n      expect(fooUpdate).toEqual(fooUpdateCopy);\n    });\n  });\n\n  describe('delete mutator', () => {\n    test('refuse deletion if selector is empty', async () => {\n      const emptySelector = {};\n\n      const defaultParams = {\n        collection: Foo2s\n      }\n\n      await expect(deleteMutator({ ...defaultParams, selector: emptySelector })).rejects.toThrow();\n    });\n    test('refuse deletion if doucment is not found', async () => {\n      const nullSelector = { documentId: null };\n\n      const defaultParams = {\n        collection: {\n          ...Foo2s,\n          // document not found\n          findOne: () => null\n        }\n      }\n\n      await expect(deleteMutator({ ...defaultParams, selector: nullSelector })).rejects.toThrow();\n\n    })\n    test('accept valid deletions', async () => {\n      const validIdSelector = { _id: 'foobar' };\n      const validDocIdSelector = { documentId: 'foobar' };\n      const validSlugSelector = { slug: 'foobar' };\n      const foo = { hello: \"world\" }\n\n      const defaultParams = {\n        collection: { ...Foo2s, findOne: () => foo, remove: () => foo }\n      }\n\n      await expect(deleteMutator({ ...defaultParams, selector: validIdSelector })).resolves.toEqual({ data: foo });\n      await expect(deleteMutator({ ...defaultParams, selector: validDocIdSelector })).resolves.toEqual({ data: foo });\n      await expect(deleteMutator({ ...defaultParams, selector: validSlugSelector })).resolves.toEqual({ data: foo });\n\n    })\n  })\n\n  describe('onCreate/onUpdate/onDelete', () => {\n    test('run onCreate callbacks during creation and assign returned value', async () => {\n      const { data: resultDocument } = await createMutator({\n        ...createArgs,\n        document: { foo2: 'bar' }\n      });\n      expect(resultDocument.publicAuto).toEqual('CREATED');\n    });\n    test('run onUpdate callback during update and assign returned value', async () => {\n      const { data: foo } = await createMutator({ ...createArgs, document: { foo2: 'bar' } });\n      const { data: resultDocument } = await updateMutator({\n        ...updateArgs,\n        documentId: foo._id,\n        data: { foo2: 'update' },\n      });\n      expect(resultDocument.publicAuto).toEqual('UPDATED');\n    });\n\n    test('keep auto generated private fields ', async () => {\n      const { data: resultDocument } = await createMutator({\n        ...defaultArgs,\n        document: { foo2: 'bar' }\n      });\n      expect(resultDocument.privateAuto).not.toBeDefined();\n    });\n    test('keep auto generated private fields ', async () => {\n      const { data: foo } = await createMutator({ ...defaultArgs, document: { foo2: 'bar' } });\n      const { data: resultDocument } = await updateMutator({\n        ...defaultArgs,\n        documentId: foo._id,\n        data: { foo2: 'update' }\n      });\n      expect(resultDocument.privateAuto).not.toBeDefined();\n\n    });\n  });\n  describe('permissions', () => {\n    test('filter out non allowed field before returning new document', async () => {\n      const { data: resultDocument } = await createMutator({\n        ...defaultArgs,\n        document: { foo2: 'bar' }\n      });\n      expect(resultDocument.privateAuto).not.toBeDefined();\n    });\n    test('filter out non allowed field before returning updated document', async () => {\n      const { data: foo } = await createMutator({ ...defaultArgs, document: { foo2: 'bar' } });\n      const { data: resultDocument } = await updateMutator({\n        ...defaultArgs,\n        documentId: foo._id,\n        data: { foo2: 'update' }\n      });\n      expect(resultDocument.privateAuto).not.toBeDefined();\n    });\n    test('filter out non allowed field before returning deleted document', async () => {\n      const { data: foo } = await createMutator({ ...defaultArgs, document: { foo2: 'bar' } });\n      const { data: resultDocument } = await deleteMutator({\n        ...defaultArgs,\n        selector: {\n          documentId: foo._id,\n        }\n      });\n      expect(resultDocument.privateAuto).not.toBeDefined();\n    });\n\n  });\n\n  describe('create callbacks', () => {\n    // before\n    test.skip('run before callback before document is saved', function () {\n      // TODO get the document in the database\n    });\n    //after\n    test('run after callback  before document is returned', async function () {\n      const afterSpy = sinon.spy();\n      addCallback('foo2.create.after', (document) => {\n        afterSpy();\n        document.after = true;\n        return document;\n      });\n      const { data: resultDocument } = await createMutator({ ...createArgs, document: { foo2: 'bar' } });\n      expect(afterSpy.calledOnce).toBe(true);\n      expect(resultDocument.after).toBe(true);\n    });\n    // async\n    test('run async callback', async function () {\n      // TODO need a sinon stub\n      const asyncSpy = sinon.spy();\n      addCallback('foo2.create.async', (properties) => {\n        asyncSpy(properties);\n        // TODO need a sinon stub\n        //expect(originalData.after).toBeUndefined()\n      });\n      const { data: resultDocument } = await createMutator({ ...createArgs, document: { foo2: 'bar' } });\n      expect(asyncSpy.calledOnce).toBe(true);\n    });\n    test.skip('provide initial data to async callbacks', async function () {\n      const asyncSpy = sinon.spy();\n      addCallback('foo2.create.after', (document) => {\n        document.after = true;\n        return document;\n      });\n      addCallback('foo2.create.async', (properties) => {\n        asyncSpy(properties);\n        // TODO need a sinon stub\n        //expect(originalData.after).toBeUndefined()\n      });\n      const { data: resultDocument } = await createMutator({ ...createArgs, document: { foo2: 'bar' } });\n      expect(asyncSpy.calledOnce).toBe(true);\n      // TODO: check result\n    });\n\n    test('should run createMutator', async function () {\n      const { data: resultDocument } = await createMutator(defaultArgs);\n      expect(resultDocument).toBeDefined();\n    });\n    // before\n    test.skip('run before callback before document is saved', function () {\n      // TODO get the document in the database\n    });\n    //after\n    test('run after callback  before document is returned', async function () {\n      const afterSpy = sinon.spy();\n      addCallback('foo2.create.after', (document) => {\n        afterSpy();\n        document.after = true;\n        return document;\n      });\n      const { data: resultDocument } = await createMutator(defaultArgs);\n      expect(afterSpy.calledOnce).toBe(true);\n      expect(resultDocument.after).toBe(true);\n    });\n    // async\n    test('run async callback', async function () {\n      // TODO need a sinon stub\n      const asyncSpy = sinon.spy();\n      addCallback('foo2.create.async', (properties) => {\n        asyncSpy(properties);\n        // TODO need a sinon stub\n        //expect(originalData.after).toBeUndefined()\n      });\n      const { data: resultDocument } = await createMutator(defaultArgs);\n      expect(asyncSpy.calledOnce).toBe(true);\n    });\n    test.skip('provide initial data to async callbacks', async function () {\n      const asyncSpy = sinon.spy();\n      addCallback('foo2.create.after', (document) => {\n        document.after = true;\n        return document;\n      });\n      addCallback('foo2.create.async', (properties) => {\n        asyncSpy(properties);\n        // TODO need a sinon stub\n        //expect(originalData.after).toBeUndefined()\n      });\n      const { data: resultDocument } = await createMutator(defaultArgs);\n      expect(asyncSpy.calledOnce).toBe(true);\n      // TODO: check result\n    });\n\n  });\n\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/server/resolvers.test.js",
    "content": "import { createDummyCollection, isoCreateCollection } from 'meteor/vulcan:test';\n//import { createCollection } from 'meteor/vulcan:lib';\nimport Users from 'meteor/vulcan:users';\nimport expect from 'expect';\nimport { getNewDefaultResolvers } from '../../lib/server/default_resolvers2';\nimport sinon from 'sinon';\n\ndescribe('vulcan:core/default_resolvers', function() {\n  const resolversOptions = {\n    typeName: 'Dummy',\n    collectionName: 'Dummies',\n    options: {},\n  };\n  // TODO: build helpers to mock collections\n  const buildContext = ({ usersMocks = {}, currentUser = adminUser, ...otherProps }) => ({\n    Users,\n    currentUser,\n    ...otherProps,\n  });\n  // TODO: what's the name of this argument? handles cache\n  const lastArg = { cacheControl: {} };\n  // eslint-disable-next-line no-unused-vars\n  const loggedInUser = { _id: 'foobar', groups: [], isAdmin: false };\n  // eslint-disable-next-line no-unused-vars\n  const adminUser = { _id: 'foobar', groups: [], isAdmin: true };\n  const getSingleResolver = () => getNewDefaultResolvers(resolversOptions).single.resolver;\n  const getMultiResolver = () => getNewDefaultResolvers(resolversOptions).multi.resolver;\n\n  describe('single', function() {\n    it('defines the correct fields', function() {\n      const { single } = getNewDefaultResolvers(resolversOptions);\n      const { description, resolver } = single;\n      expect(description).toBeDefined();\n      expect(resolver).toBeDefined();\n      expect(resolver).toBeInstanceOf(Function);\n    });\n\n    // TODO: the current behaviour is not consistent, could be improved\n    // @see https://github.com/VulcanJS/Vulcan/issues/2118\n    it.skip('return null if documentId is undefined in selector', function() {\n      const resolver = getSingleResolver();\n      // no documentId\n      const input = { selector: {} };\n      // non empty db\n      const context = buildContext({\n        Dummies: createDummyCollection({\n          results: { load: { _id: 'my-document' } },\n        }),\n      });\n      const res = resolver(null, { input }, context, lastArg);\n      return expect(res).resolves.toEqual({ result: null });\n    });\n    it('return document in case of success', function() {\n      const resolver = getSingleResolver();\n      const documentId = 'my-document';\n      const document = { _id: documentId };\n      const input = { selector: { documentId } };\n      // non empty db\n      const context = buildContext({\n        Dummies: createDummyCollection({\n          results: { findOne: document },\n        }),\n      });\n      const res = resolver(null, { input }, context, lastArg);\n      return expect(res).resolves.toEqual({ result: document });\n    });\n    it('return null if failure to find doc but allowNull is true', function() {\n      const resolver = getSingleResolver();\n      const documentId = 'bad-document';\n      const input = { selector: { documentId }, allowNull: true };\n      // empty db\n      const context = buildContext({\n        Dummies: createDummyCollection({}),\n      });\n      const res = resolver(null, { input }, context, lastArg);\n      return expect(res).resolves.toEqual({ result: null });\n    });\n    it('throws if documentId is defined but does not match any document', function() {\n      const resolver = getSingleResolver();\n      const documentId = 'bad-document';\n      const input = { selector: { documentId } };\n      // empty db\n      const context = buildContext({\n        Dummies: createDummyCollection({}),\n      });\n      return expect(resolver(null, { input }, context, lastArg)).rejects.toThrow();\n    });\n    describe('filtering', () => {\n      const schema = {\n        adminOnlyField: {\n          type: String,\n          canRead: ['admins'],\n        },\n        year: {\n          type: Number,\n          canRead: 'owners',\n        },\n        userId: {\n          type: String,\n          canRead: ['guests'],\n        },\n      };\n      it('throws if filtering on field never readable by user, whatever the document is', () => {\n        const resolver = getSingleResolver();\n        const filter = {\n          adminOnyField: { _gte: 'hello' },\n        };\n        const input = { selector: {}, filter, allowNull: true };\n        const doc = { userId: '1', year: 3 };\n        // empty db\n        const context = buildContext({\n          Dummies: createDummyCollection({\n            schema,\n            results: {\n              load: doc, // load is used when using _id\n              findOne: doc, // get is used with custom selectors => uses findOne under the hood\n            },\n          }),\n          currentUser: { _id: '2' },\n        });\n        return expect(resolver(null, { input }, context, lastArg)).rejects.toThrow();\n      });\n      it('return null if filtering on field readable by owners but user is not owner', () => {\n        const resolver = getSingleResolver();\n        const filter = {\n          year: { _gte: 1, _lte: 5 },\n        };\n        const input = { selector: {}, filter, allowNull: true };\n        const doc = { userId: '1', year: 3 };\n        // empty db\n        const context = buildContext({\n          Dummies: createDummyCollection({\n            schema,\n            results: {\n              load: doc, // load is used when using _id\n              findOne: doc, // get is used with custom selectors => uses findOne under the hood\n            },\n          }),\n          currentUser: { _id: '2' },\n        });\n        return expect(resolver(null, { input }, context, lastArg)).resolves.toEqual({ result: null });\n      });\n      it('return doc if filtering on field readable by owners and user is owner', () => {\n        const resolver = getSingleResolver();\n        const filter = {\n          year: { _gte: 1, _lte: 5 },\n        };\n        const input = { selector: {}, filter };\n        const doc = { userId: '1', year: 3 };\n        // empty db\n        const context = buildContext({\n          Dummies: createDummyCollection({\n            schema,\n            results: {\n              load: doc, // load is used when using _id\n              findOne: doc, // get is used with custom selectors => uses findOne under the hood\n            },\n          }),\n          currentUser: { _id: '1' },\n        });\n        const res = resolver(null, { input }, context, lastArg);\n        return expect(res).resolves.toEqual({ result: doc });\n      });\n    });\n  });\n\n  describe('multi', () => {\n    it('defines the correct fields', function() {\n      const { multi } = getNewDefaultResolvers(resolversOptions);\n      const { description, resolver } = multi;\n      expect(description).toBeDefined();\n      expect(resolver).toBeDefined();\n      expect(resolver).toBeInstanceOf(Function);\n    });\n    it('get documents', () => {\n      const resolver = getMultiResolver();\n      const dbDocuments = [\n        {\n          _id: '1',\n        },\n        {\n          _id: '2',\n        },\n      ];\n      const input = { terms: {} };\n      // non empty db\n      const context = buildContext({\n        Dummies: createDummyCollection({\n          results: { find: dbDocuments },\n        }),\n        currentUser: adminUser,\n      });\n      const res = resolver(null, { input }, context, lastArg);\n      return expect(res).resolves.toMatchObject({ results: dbDocuments });\n    });\n    describe('security', () => {\n      it('filter out unallowed documents', () => {\n        const resolver = getMultiResolver();\n        const doc2 = { _id: '2' };\n        const dbDocuments = [\n          {\n            _id: '1',\n          },\n          doc2,\n        ];\n        const input = { terms: {} };\n        const context = buildContext({\n          // non empty db\n          Dummies: createDummyCollection({\n            results: {\n              find: dbDocuments,\n            },\n            options: {\n              permissions: {\n                // filter out doc 1\n                canRead: ({ document: { _id } }) => _id !== '1',\n              },\n            },\n          }),\n        });\n        const res = resolver(null, { input }, context, lastArg);\n        return expect(res).resolves.toMatchObject({ results: [doc2] });\n      });\n      it('filter out restricted fields from retrieved documents', () => {\n        const resolver = getMultiResolver();\n        // foo does not exist in the schema\n        const doc1 = { _id: '1', foo: 'bar' };\n        const doc2 = { _id: '2', foo: 'bar' };\n        const dbDocuments = [doc1, doc2];\n        const input = { terms: {} };\n        const context = buildContext({\n          // non empty db\n          Dummies: createDummyCollection({\n            results: {\n              find: dbDocuments,\n            },\n          }),\n        });\n        const res = resolver(null, { input }, context, lastArg);\n        return expect(res).resolves.toMatchObject({\n          results: [{ _id: '1' }, { _id: '2' }],\n        });\n      });\n    });\n    // @see https://5da5072ecae7f900081d6d9a--happy-villani-6ca506.netlify.com/\n    describe('user defined search', () => {\n      // TODO: this is a unit test based on props but an integration test\n      // with mongo would be more efficient\n      it('filter documents based on user input', async () => {\n        const resolver = getMultiResolver();\n        const input = {\n          filter: { year: { _gte: 2000 } },\n        };\n        // TODO: creating a spy on find is tedious, use integration test instead\n        const findSpy = sinon.spy(() => ({ fetch: () => [], count: () => 0 }));\n        const context = buildContext({\n          Dummies: createDummyCollection({\n            schema: {\n              _id: { type: String, canRead: ['admins'] },\n              year: { type: Number, canRead: ['admins'] },\n            },\n            find: findSpy,\n          }),\n        });\n        const res = await resolver(null, { input }, context, lastArg);\n        // TODO:\n        expect(findSpy.getCall(0).args[0]).toMatchObject({\n          year: { $gte: 2000 },\n        });\n      });\n      // TODO: API changed, this test is not valid anymore\n      it.skip('detect invalid filters', async () => {\n        const resolver = getMultiResolver();\n        const input = {\n          // gte is not valid, _gte is correct\n          filter: { year: { gte: 2000 } },\n        };\n        // TODO: creating a spy on find is tedious, use integration test instead\n        const findSpy = sinon.spy(() => ({ fetch: () => [], count: () => 0 }));\n        const context = buildContext({\n          Dummies: createDummyCollection({\n            schema: {\n              _id: { type: String, canRead: ['guests'] },\n              year: { type: Number, canRead: ['guests'] },\n            },\n            find: findSpy,\n          }),\n        });\n        await expect(resolver(null, { input }, context, lastArg)).rejects.toThrow();\n      });\n      // important to avoid indirect access to the value (filtering is indirectly equivalent to reading)\n      it('throw if field in filter is never-readable', async () => {\n        const resolver = getMultiResolver();\n        const input = {\n          filter: { year: { _gte: 2000 } },\n        };\n        const findSpy = sinon.spy(() => ({ fetch: () => [], count: () => 0 }));\n        const context = buildContext({\n          Dummies: createDummyCollection({\n            schema: {\n              _id: { type: String, canRead: ['admins', 'members'] },\n              year: { type: Number, canRead: ['admins'] },\n            },\n            find: findSpy,\n          }),\n          currentUser: loggedInUser, // not an admin so can't filter on year,\n        });\n        await expect(resolver(null, { input }, context, lastArg)).rejects.toThrow();\n      });\n      it('apply document based canRead functions to filtered documents', () => {\n        const resolver = getMultiResolver();\n        const doc1 = { userId: '1', year: 3 };\n        const doc2 = { userId: '2', year: 3 }; // filter is applied to year, but user cannot read the year of this document because he is not owner\n        const filter = {\n          year: { _gte: 1, _lte: 5 },\n        };\n        const input = { selector: {}, filter };\n        // empty db\n        const context = buildContext({\n          Dummies: createDummyCollection({\n            schema: {\n              year: {\n                type: Number,\n                canRead: 'owners',\n              },\n              userId: {\n                type: String,\n                canRead: ['guests'],\n              },\n            },\n            results: {\n              find: [doc1, doc2],\n            },\n          }),\n          currentUser: { _id: '1' },\n        });\n        return expect(resolver(null, { input }, context, lastArg)).resolves.toEqual({ results: [doc1] });\n      });\n      // seems to work eventually...\n      /*\n      it('runs integration test', async () => {\n        const Foobars = createCollection({\n          collectionName: 'Foobars',\n          typeName: 'Foobar',\n          schema: { _id: { type: String, canRead: ['admins'] } }\n        })\n        await Foobars.insert({ _id: '1' })\n        const res = await Foobars.find().fetch()\n        console.log(res)\n        await Foobars.rawCollection().drop()\n      })\n      */\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-lib/test/utils.test.js",
    "content": "import {Utils} from '../lib/modules/utils';\nimport expect from 'expect';\nimport {createDummyCollection} from \"meteor/vulcan:test\"\nimport SimpleSchema from \"simpl-schema\"\n\n// prepare Jest migration\nconst test = it\n\ndescribe('vulcan:lib/utils', function () {\n\n  const collection = {\n    findOne: function ({slug}) {\n      switch (slug) {\n        case 'duplicate-name':\n          return {\n            _id: 'duplicate-name',\n            name: 'Duplicate name',\n            slug: 'duplicate-name',\n          };\n        case 'triplicate-name':\n          return {\n            _id: 'triplicate-name',\n            name: 'Triplicate name',\n            slug: 'triplicate-name',\n          };\n        case 'triplicate-name-1':\n          return {\n            _id: 'triplicate-name-1',\n            name: 'Triplicate name',\n            slug: 'triplicate-name-1',\n          };\n        case 'renamed-name':\n          return {\n            _id: 'renamed-name',\n            name: 'RENAMED NAME',\n            slug: 'renamed-name',\n          };\n        default:\n          return null;\n      }\n    }\n  };\n\n  describe('Utils.getUnusedSlug()', async function () {\n\n    it('returns the same slug when there are no conflicts', function () {\n      const slug = 'unique-name';\n      const unusedSlug = Utils.getUnusedSlug(collection, slug);\n\n      expect(unusedSlug).toEqual(slug);\n    });\n\n    it('appends integer to slug when there is a conflict', function () {\n      const slug = 'duplicate-name';\n      const unusedSlug = Utils.getUnusedSlug(collection, slug);\n\n      expect(unusedSlug).toEqual(slug + '-1');\n    });\n\n    it('appends incremented integer to slug when there is a conflict', function () {\n      const slug = 'triplicate-name';\n      const unusedSlug = Utils.getUnusedSlug(collection, slug);\n\n      expect(unusedSlug).toEqual(slug + '-2');\n    });\n\n    it('returns the same slug when the conflict has the same _id', function () {\n      // This tests the case where a document is renamed, but its slug remains the same\n      // For example 'RENAMED NAME' is changed to 'Renamed name'; the slug should not increment\n      const slug = 'renamed-name';\n      const documentId = 'renamed-name';\n      const unusedSlug = Utils.getUnusedSlug(collection, slug, documentId);\n\n      expect(unusedSlug).toEqual(slug);\n    });\n\n    it('appends integer to slug when the conflict has the same _id, but it’s not passed to getUnusedSlug', function () {\n      // This tests the case where a document is renamed, but its slug remains the same\n      // For example 'RENAMED NAME' is changed to 'Renamed name'; the slug should not increment\n      const slug = 'renamed-name';\n      const unusedSlug = Utils.getUnusedSlug(collection, slug);\n\n      expect(unusedSlug).toEqual(slug + '-1');\n    });\n\n  });\n\n  describe(\"Utils.convertDates()\", () => {\n\n    it(\"convert date string to object\", () => {\n      const Dummies = createDummyCollection({\n        schema: {\n          begin: {\n            type: Date,\n          }\n        }\n      })\n      const now = new Date()\n      const res = Utils.convertDates(Dummies, {begin: now.toISOString()})\n      expect(res.begin).toBeInstanceOf(Date)\n    })\n\n    it(\"convert date string in nested objects\", () => {\n      const Dummies = createDummyCollection({\n        schema: {\n          nested: {\n            type: new SimpleSchema({\n              begin: {\n                type: Date,\n              }\n            })\n          }\n        }\n      })\n      const now = new Date()\n      const res = Utils.convertDates(Dummies, {nested: {begin: now.toISOString()}})\n      expect(res.nested.begin).toBeInstanceOf(Date)\n    })\n\n    it(\"convert date string in arrays of nested objects\", () => {\n      const Dummies = createDummyCollection({\n        schema: {\n          array: {\n            type: Array,\n          },\n          \"array.$\": {\n            type: new SimpleSchema({\n              begin: {\n                type: Date,\n              }\n            })\n          }\n        }\n      })\n      const now = new Date()\n      const res = Utils.convertDates(Dummies, {array: [{begin: now.toISOString()}]})\n      expect(res.array[0].begin).toBeInstanceOf(Date)\n    })\n\n  })\n\n  describe('Utils.pluralize()', () => {\n\n    test('force a plural for words where plural = singular', () => {\n      const peoples = Utils.pluralize(\"people\")\n      expect(peoples).toEqual(\"peoples\")\n    })\n\n  })\n\n  describe('Utils.isEmptyOrUndefined()', () => {\n\n    it('reports not empty for non-empty string values', () => {\n      const result = Utils.isEmptyOrUndefined('abc');\n      expect(result).toEqual(false);\n    })\n\n    it('reports not empty for string values of zero length', () => {\n      const result = Utils.isEmptyOrUndefined('');\n      expect(result).toEqual(true);\n    })\n\n    it('reports not empty for number values', () => {\n      const result = Utils.isEmptyOrUndefined(1);\n      expect(result).toEqual(false);\n    })\n\n    it('reports not empty for the number zero', () => {\n      const result = Utils.isEmptyOrUndefined(1);\n      expect(result).toEqual(false);\n    })\n\n    it('reports empty for undefined values', () => {\n      let value;\n      const result = Utils.isEmptyOrUndefined(value);\n      expect(result).toEqual(true);\n    })\n\n    it('reports empty for null values', () => {\n      const value = null;\n      const result = Utils.isEmptyOrUndefined(value);\n      expect(result).toEqual(true);\n    })\n\n    it('reports not empty for non-empty objects', () => {\n      const result = Utils.isEmptyOrUndefined({ key: 'abc' });\n      expect(result).toEqual(false);\n    })\n\n    it('reports empty for empty objects', () => {\n      const result = Utils.isEmptyOrUndefined({});\n      expect(result).toEqual(true);\n    })\n\n    it('reports not empty for dates', () => {\n      const result = Utils.isEmptyOrUndefined(new Date());\n      expect(result).toEqual(false);\n    })\n\n    it('reports not empty for empty dates', () => {\n      const result = Utils.isEmptyOrUndefined(new Date(0));\n      expect(result).toEqual(false);\n    })\n\n    it('reports not empty for non-empty arrays', () => {\n      const result = Utils.isEmptyOrUndefined(['abc']);\n      expect(result).toEqual(false);\n    })\n\n    it('reports empty for empty arrays', () => {\n      const result = Utils.isEmptyOrUndefined([]);\n      expect(result).toEqual(true);\n    })\n\n    it('reports not empty for regular expressions', () => {\n      const result = Utils.isEmptyOrUndefined(/^e/);\n      expect(result).toEqual(false);\n    })\n\n  })\n\n});\n"
  },
  {
    "path": "packages/vulcan-newsletter/.gitignore",
    "content": ".build*\n"
  },
  {
    "path": "packages/vulcan-newsletter/README.md",
    "content": "# Vulcan Newsletter\n\nThis package schedules an automatic newsletter digest.\n\n![Newsletter](http://f.cl.ly/items/0V0F351k1R1i3L1k1D0J/telescope-newsletter.png)\n\n### Install\n\n1. `mrt add vulcan-newsletter`.\n2. Go to the Vulcan settings page and add your MailChimp API key and List ID. \n\n### Dependencies\n\n- [meteor-mailchimp](https://github.com/MiroHibler/meteor-mailchimp/)\n- [synced-cron](https://github.com/littledata/meteor-synced-cron)\n- [handlebars-server](https://github.com/EventedMind/meteor-handlebars-server)\n- [meteor-npm](https://github.com/arunoda/meteor-npm/)\n\n### Settings\n\n- **Show Banner**: \n- **MailChimp API Key**: \n- **MailChimp List ID**: \n- **Newsletter Frequency**: Choose from every day, three times a week, and once a week. Note that changes to this setting require you to restart your app to take effect. \n- **Posts Per Newsletter**: how many posts each newsletter should contain. \n\nNote that for this package to work properly, you'll also need to fill in the **Default Email** setting. \n\n### How It Works\n\nThe package works with [MailChimp](http://mailchimp.com), which means you'll need to fill in an API key and List ID in your Vulcan app's settings panel. \n\nEvery `x` days, it builds a digest consisting of the top `y` items posted in the past `x` days that haven't yet been sent out in a newsletter. \n\nIt then creates a campaign in MailChimp and schedules it to be sent out **one hour later**, and sends you a confirmation email (to give you some time to check that everything looks good).\n\n### Test Routes\n\nIf you want to preview your email templates, you can do so at the following routes: \n\n- **Digest**: [http://localhost:3000/email/campaign](http://localhost:3000/email/campaign)\n- **Confirmation**: [http://localhost:3000/email/digest-confirmation](http://localhost:3000/email/digest-confirmation)\n\n(Replace `http://localhost:3000` with your app's URL)\n\n### Newsletter Sign-Up Banner\n\nThis package also includes a newsletter sign-up banner that uses the MailChimp API to add people to your list. \n\n![Newsletter Banner](http://f.cl.ly/items/3k282w2b0I1U3y200944/telescope-newsletter-banner.png)\n"
  },
  {
    "path": "packages/vulcan-newsletter/lib/client/main.js",
    "content": "import Newsletters from '../modules/index.js';\n\nexport default Newsletters;"
  },
  {
    "path": "packages/vulcan-newsletter/lib/components/NewsletterSubscribe.jsx",
    "content": "import { Components, registerComponent, withMutation, withMessages } from 'meteor/vulcan:core';\nimport React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\n\n// this component is used as a custom controller in user's account edit (cf. ./custom_fields.js)\nclass NewsletterSubscribe extends PureComponent {\n  \n  // check if fields is true or falsy (no value = not subscribed)\n  isSubscribed = () => {\n    return !!this.props.value;\n  }\n\n  subscribeUnsubscribe = async () => {\n    \n    const { path, updateCurrentValues, throwError } = this.props;\n    \n    const user = this.props.document;\n    const mutationName = this.isSubscribed() ? 'removeUserNewsletter' : 'addUserNewsletter';\n    const mutation = this.props[mutationName];\n\n    try {\n\n      await mutation({userId: user._id});\n\n      updateCurrentValues({ [path]: !this.isSubscribed() });\n      \n      // display a nice message to the client\n      this.props.flash({ id: 'newsletter.subscription_updated', type: 'success'});\n      \n    } catch(error) {\n      throwError(error);\n    }\n  }\n  \n  render() {\n\n    return (\n      <div className=\"form-group row\">\n        <label className=\"control-label col-sm-3\"></label>\n          <div className=\"col-sm-9\">\n            <Components.Button\n              className=\"newsletter-button\"\n              onClick={this.subscribeUnsubscribe}\n              variant=\"primary\"\n            >\n              <Components.FormattedMessage id={this.isSubscribed() ? 'newsletter.unsubscribe' : 'newsletter.subscribe'}/>\n            </Components.Button>\n        </div>\n      </div>\n    );\n  }\n}\n\nconst addOptions = {name: 'addUserNewsletter', args: {userId: 'String'}};\nconst removeOptions = {name: 'removeUserNewsletter', args: {userId: 'String'}};\n\nregisterComponent('NewsletterSubscribe', NewsletterSubscribe, withMutation(addOptions), withMutation(removeOptions), withMessages);\n"
  },
  {
    "path": "packages/vulcan-newsletter/lib/modules/collection.js",
    "content": "import { createCollection } from 'meteor/vulcan:core';\nimport schema from './schema';\n\nconst Newsletters = createCollection({\n  collectionName: 'Newsletters',\n\n  typeName: 'Newsletter',\n\n  resolvers: null,\n\n  mutations: null,\n  \n  schema,\n\n  generateGraphQLSchema: false\n});\n\nexport default Newsletters;\n"
  },
  {
    "path": "packages/vulcan-newsletter/lib/modules/custom_fields.js",
    "content": "// import Users from 'meteor/vulcan:users';\n\n// Users.addField([\n//   {\n//     fieldName: 'newsletter_subscribeToNewsletter',\n//     fieldSchema: {\n//       label: 'Subscribe to Newsletter',\n//       type: Boolean,\n//       optional: true,\n//       defaultValue: false,\n//       canCreate: ['members'],\n//       canUpdate: ['members'],\n//       canRead: ['guests'],\n//       input: 'NewsletterSubscribe',\n//       group: {\n//         name: 'newsletter',\n//         label: 'Newsletter',\n//         order: 3\n//       },\n//     }\n//   },\n// ]);\n"
  },
  {
    "path": "packages/vulcan-newsletter/lib/modules/fragments.js",
    "content": "// import { extendFragment } from 'meteor/vulcan:core';\n\n// extendFragment('UsersCurrent', `\n//   newsletter_subscribeToNewsletter\n// `);"
  },
  {
    "path": "packages/vulcan-newsletter/lib/modules/i18n.js",
    "content": "import { addStrings } from 'meteor/vulcan:core';\n\naddStrings('en', {\n  'newsletter': 'Newsletter',\n  'newsletter.subscribe': 'Subscribe',\n  'newsletter.unsubscribe': 'Unsubscribe',\n  'newsletter.subscribe_prompt': 'Subscribe to the newsletter',\n  'newsletter.email': 'Your email',\n  'newsletter.success_message': 'Thanks for subscribing!',\n  'newsletter.subscription_updated': 'Newsletter subscription updated.',\n  'newsletter.subscription_failed': 'Subscription failed. Are your API keys configured in your settings file?',\n\n  'newsletter.error_invalid_email': 'Sorry, that doesn\\'t look like a valid email.',\n  'newsletter.error_already_subscribed': 'Sorry, it looks like you\\'re already subscribed to the list.',\n  'newsletter.error_has_unsubscribed': 'Sorry, it looks like you\\'ve previously unsubscribed from the list, and we\\'re not able to re-subscribe you automatically.',\n  'newsletter.error_subscription_failed': 'Sorry, your subscription failed ({message}).',\n});\n"
  },
  {
    "path": "packages/vulcan-newsletter/lib/modules/index.js",
    "content": "import Newsletters from './collection.js';\n\nimport './custom_fields.js';\nimport './fragments.js';\nimport './i18n.js';\n\nimport '../components/NewsletterSubscribe.jsx';\n\nexport default Newsletters;"
  },
  {
    "path": "packages/vulcan-newsletter/lib/modules/schema.js",
    "content": "const schema = {\n  _id: {\n    type: String,\n  },\n  createdAt: {\n    type: Date,\n    optional: true,\n  },\n  userId: {\n    type: String,\n    optional: true,\n  },\n  scheduledAt: {\n    type: Date,\n    optional: true,\n  },\n  subject: {\n    type: String,\n    optional: true,\n  },\n  data: {\n    type: String,\n    optional: true,\n  },\n  html: {\n    type: String,\n    optional: true,\n  },\n  provider: {\n    type: String,\n    optional: true,\n  },\n};\n\nexport default schema;"
  },
  {
    "path": "packages/vulcan-newsletter/lib/server/callbacks.js",
    "content": "import Users from 'meteor/vulcan:users';\nimport { addCallback, getSetting, registerSetting } from 'meteor/vulcan:core';\nimport Newsletters from '../modules/collection.js';\n\nregisterSetting('newsletter.autoSubscribe', false, 'Automatically subscribe every new user to your newsletter');\n\nfunction subscribeUserOnProfileCompletion (user) {\n  if (!!getSetting('newsletter.autoSubscribe') && !!Users.getEmail(user)) {\n    try {\n      Newsletters.subscribeUser(user, false);\n    } catch (error) {\n      console.log('// Newsletter Error:') // eslint-disable-line\n      console.log(error) // eslint-disable-line\n    }\n  }\n  return user;\n}\naddCallback('users.profileCompleted.async', subscribeUserOnProfileCompletion);\n"
  },
  {
    "path": "packages/vulcan-newsletter/lib/server/cron.js",
    "content": "import { SyncedCron } from 'meteor/littledata:synced-cron';\nimport moment from 'moment';\nimport Newsletters from '../modules/collection.js';\nimport { getSetting, registerSetting } from 'meteor/vulcan:core';\n\nconst defaultFrequency = [1]; // every monday\nconst defaultTime = '00:00'; // GMT\n\nregisterSetting('newsletter.frequency', defaultFrequency, 'Which days to send the newsletter on (1 = Monday, 7 = Sunday)');\nregisterSetting('newsletter.time', defaultTime, 'Time to send the newsletter on (ex: “16:30”)');\nregisterSetting('newsletter.enabledInDev', false, 'Enable the newsletter in development');\nregisterSetting('newsletter.enabled', false, 'Enable the newsletter');\n\nSyncedCron.options = {\n  log: true,\n  collectionName: 'cronHistory',\n  utc: false,\n  collectionTTL: 172800\n};\n\nconst addZero = num => {\n  return num < 10 ? '0'+num : num;\n};\n\nvar getSchedule = function (parser) {\n  var frequency = getSetting('newsletter.frequency', defaultFrequency);\n  var recur = parser.recur();\n  var schedule;\n\n  // Default is once a week (Mondays)\n  if (!!frequency) {\n    const frequencyArray = Array.isArray(frequency) ? frequency : _.toArray(frequency);\n    schedule = recur.on(frequencyArray).dayOfWeek();\n  }\n  else {\n    schedule = recur.on(2).dayOfWeek();\n  }\n\n  const offsetInMinutes = new Date().getTimezoneOffset();\n  const GMTtime = moment.duration(getSetting('newsletter.time', defaultTime));\n  const serverTime = GMTtime.subtract(offsetInMinutes, 'minutes');\n  const serverTimeString = addZero(serverTime.hours()) + ':' + addZero(serverTime.minutes());\n\n  // console.log(\"// scheduled for: (GMT): \"+getSetting('newsletterTime', defaultTime));\n  // console.log(\"// server offset (minutes): \"+offsetInMinutes);\n  // console.log(\"// server scheduled time (minutes): \"+serverTime.asMinutes());\n  // console.log(\"// server scheduled time: \"+serverTimeString);\n\n  return schedule.on(serverTimeString).time();\n};\n\nMeteor.methods({\n  getNextJob: function () {\n    var nextJob = SyncedCron.nextScheduledAtDate('scheduleNewsletter');\n    console.log(nextJob); // eslint-disable-line\n    return nextJob;\n  }\n});\n\nvar addJob = function () {\n  SyncedCron.add({\n    name: 'scheduleNewsletter',\n    schedule: function(parser) {\n      // parser is a later.parse object\n      return getSchedule(parser);\n    },\n    job: function() {\n      // only schedule newsletter campaigns in production\n      if (process.env.NODE_ENV === 'production' || getSetting('newsletter.enabledInDev', false)) {\n        console.log(\"// Scheduling newsletter…\"); // eslint-disable-line\n        console.log(new Date()); // eslint-disable-line\n        Newsletters.send();\n      }\n    }\n  });\n};\n\nMeteor.startup(function () {\n  if (getSetting('newsletter.enabled', false)) {\n    addJob();\n  }\n});\n"
  },
  {
    "path": "packages/vulcan-newsletter/lib/server/integrations/emailoctopus.js",
    "content": "/* eslint-disable no-console */\n\n// newsletter scheduling with MailChimp\n\nimport moment from 'moment';\nimport { getSetting, registerSetting, throwError } from 'meteor/vulcan:core';\nimport Newsletters from '../../modules/collection.js';\nimport fetch from 'node-fetch';\n\nregisterSetting('emailoctopus', null, 'EmailOctopus settings');\n\n/*\n\nAPI\n\n*/\n\nconst settings = getSetting('emailoctopus');\n\nif (settings) {\n  const { apiKey, listId, fromName, fromEmail } = settings;\n\n  /*\n\n  Methods\n\n  */\n\n  Newsletters.emailoctopus = {\n    // add a user to a MailChimp list.\n    // called when a new user is created, or when an existing user fills in their email\n    async subscribe(email, confirm = false) {\n\n      try {\n        // subscribe user\n        const body = {\n          api_key: apiKey,\n          email_address: email,\n          // status: 'SUBSCRIBED'\n        }\n        const subscribe = await fetch(`https://emailoctopus.com/api/1.5/lists/${listId}/contacts`, {\n          method: 'post',\n          body: JSON.stringify(body),\n          headers: {'Content-Type': 'application/json'}\n        });\n        const json = await subscribe.json();\n        if (json.error) {\n          throw json.error;\n        }\n        // const subscribe = await mailchimp.post(`/lists/${listId}/members`, subscribeOptions);\n        // const subscribe = callSyncAPI('lists', 'subscribe', subscribeOptions);\n        return { result: 'subscribed', ...json };\n      } catch (error) {\n        const name = error.code;\n        const message = error.message;\n        throwError({ id: name, message, data: { path: 'newsletter_subscribeToNewsletter', message } });\n      }\n    },\n\n    // remove a user to a MailChimp list.\n    // called from the user's account\n    async unsubscribe(email) {\n      // not available\n      throw Error(`Unsubscribe not implemented yet`);\n    },\n\n    async send({ subject, text, html, isTest = false }) {\n      // not available\n      throw Error(`EmailOctopus API doesn't support sending campaigns currently (June 2020)`);\n    },\n  };\n}\n"
  },
  {
    "path": "packages/vulcan-newsletter/lib/server/integrations/index.js",
    "content": "export * from './mailchimp.js';\nexport * from './emailoctopus.js';\n"
  },
  {
    "path": "packages/vulcan-newsletter/lib/server/integrations/mailchimp.js",
    "content": "/* eslint-disable no-console */\n\n// newsletter scheduling with MailChimp\n\nimport moment from 'moment';\nimport { getSetting, registerSetting, throwError } from 'meteor/vulcan:core';\nimport Newsletters from '../../modules/collection.js';\nimport Mailchimp from 'mailchimp-api-v3';\n\nregisterSetting('mailchimp', null, 'MailChimp settings');\n\n/*\n\nAPI\n\n*/\n\nconst settings = getSetting('mailchimp');\n\nif (settings) {\n  const { apiKey, listId, fromName, fromEmail } = settings;\n\n  var mailchimp = new Mailchimp(apiKey);\n\n  // const mailChimpAPI = new MailChimpNPM.MailChimpAPI(apiKey, { version : '2.0' });\n\n  // const callSyncAPI = ( section, method, options, callback ) => {\n  //   const wrapped = Meteor.wrapAsync( mailChimpAPI.call, mailChimpAPI );\n  //   return wrapped( section, method, options );\n  // };\n\n  /*\n\n  Methods\n\n  */\n\n  Newsletters.mailchimp = {\n    // add a user to a MailChimp list.\n    // called when a new user is created, or when an existing user fills in their email\n    async subscribe(email, confirm = false) {\n      try {\n        const subscribeOptions = {\n          email_address: email,\n          status: 'subscribed',\n        };\n        // subscribe user\n        const subscribe = await mailchimp.post(`/lists/${listId}/members`, subscribeOptions);\n        // const subscribe = callSyncAPI('lists', 'subscribe', subscribeOptions);\n        return { result: 'subscribed', ...subscribe };\n      } catch (error) {\n        console.log(error);\n        let name;\n        const message = error.message;\n        if (error.code == 214) {\n          name = 'has_unsubscribed';\n          //} else if (error.code != 214) { // TODO should get the right code for already_subscribed\n          //  name = 'already_subscribed';\n        } else {\n          name = 'subscription_failed';\n        }\n        throwError({ id: name, message, data: { path: 'newsletter_subscribeToNewsletter', message } });\n      }\n    },\n\n    // remove a user to a MailChimp list.\n    // called from the user's account\n    async unsubscribe(email) {\n      try {\n        const subscribeOptions = {\n          email_address: email,\n          status: 'unsubscribed',\n        };\n        // unsubscribe user\n        const subscribe = await mailchimp.post(`/lists/${listId}/members`, subscribeOptions);\n        return { result: 'unsubscribed', ...subscribe };\n      } catch (error) {\n        throw new Error('unsubscribe-failed', error.message);\n      }\n    },\n\n    async send({ subject, text, html, isTest = false }) {\n      const campaignCreationOptions = {\n        type: 'regular',\n        recipients: {\n          list_id: listId,\n        },\n        settings: {\n          subject_line: subject,\n          title: subject,\n          reply_to: fromEmail,\n          from_name: fromName,\n        },\n        // content: {\n        //   html: html,\n        //   text: text,\n        // },\n      };\n\n      // create campaign\n      const createdCampaign = await mailchimp.post('campaigns', campaignCreationOptions);\n\n      const campaignContentOptions = {\n        html: html,\n        plain_text: text,\n      };\n\n      // eslint-disable-next-line\n      const editedCampaign = await mailchimp.put(`/campaigns/${createdCampaign.id}/content`, campaignContentOptions);\n\n      const scheduledMoment = moment()\n        .utcOffset(0)\n        .add(1, 'hours');\n\n      // note: we always schedule on the hour\n      const scheduledTime = scheduledMoment.format('YYYY-MM-DDTHH:00:00');\n\n      const scheduleOptions = {\n        schedule_time: scheduledTime,\n      };\n\n      // schedule campaign\n      const scheduledCampaign = await mailchimp.post(`/campaigns/${createdCampaign.id}/actions/schedule`, scheduleOptions);\n\n      console.log('// Newsletter scheduled for ' + scheduledTime);\n      console.log(scheduledCampaign);\n\n      return scheduledCampaign;\n    },\n  };\n}\n"
  },
  {
    "path": "packages/vulcan-newsletter/lib/server/integrations/sample.js",
    "content": "/*\n\nThis is a sample template for future integrations. \n\n*/\n\nimport { getSetting, regiserSetting } from 'meteor/vulcan:core';\nimport Newsletters from '../../modules/collection.js';\n\nregiserSetting('providerName');\n\n/*\n\nAPI\n\n*/\n\nconst settings = getSetting('providerName');\n\nif (settings) {\n\n  const {server, apiKey, /* listId, somethingElse */ } = settings;\n  // eslint-disable-next-line no-undef\n  const MyProviderAPI = new ProviderAPI(server, apiKey);\n\n  const subscribeSync = options => {\n    try {\n      const wrapped = Meteor.wrapAsync( MyProviderAPI.subscribe, MyProviderAPI );\n      return wrapped( options );\n    } catch ( error ) {\n      // eslint-disable-next-line no-console\n      console.log(error);\n    }\n  };\n\n  const unsubscribeSync = options => {\n    try {\n      const wrapped = Meteor.wrapAsync( MyProviderAPI.unsubscribe, MyProviderAPI );\n      return wrapped( options );\n    } catch ( error ) {\n      // eslint-disable-next-line no-console\n      console.log(error);\n    }\n  };\n\n  const sendSync = options => {\n    try {\n      const wrapped = Meteor.wrapAsync( MyProviderAPI.send, MyProviderAPI );\n      return wrapped( options );\n    } catch ( error ) {\n      // eslint-disable-next-line no-console\n      console.log(error);\n    }\n  };\n\n  /*\n\n  Methods\n\n  */\n\n  Newsletters['providerName'] = {\n\n    subscribe(email) {\n      return subscribeSync({email});\n    },\n\n    unsubscribe(email) {\n      return unsubscribeSync({email});\n    },\n\n    send({ subject, text, html, isTest = false }) {\n      const options = {\n        subject,\n        text,\n        html\n      };\n      return sendSync(options);\n    }\n\n  };\n\n}"
  },
  {
    "path": "packages/vulcan-newsletter/lib/server/integrations/sendy.js",
    "content": "import Sendy from 'sendy-api'; // see https://github.com/igord/sendy-api\nimport { getSetting, registerSetting } from 'meteor/vulcan:core';\nimport Newsletters from '../../modules/collection.js';\n\nregisterSetting('sendy', null, 'Sendy settings');\n\n/*\n\nAPI\n\n*/\n\nconst settings = getSetting('sendy');\n\nif (settings) {\n  \n  const { server, apiKey, listId, fromName, fromEmail, replyTo } = settings;\n  const SendyAPI = new Sendy(server, apiKey);\n\n  const subscribeSync = options => {\n    try {\n      const wrapped = Meteor.wrapAsync( SendyAPI.subscribe, SendyAPI );\n      return wrapped( options );\n    } catch ( error ) {\n      // eslint-disable-next-line no-console\n      console.log('// Sendy API error');\n      // eslint-disable-next-line no-console\n      console.log(error);\n      if (error.message === 'Already subscribed.') {\n        return {result: 'already-subscribed'};\n      }\n    }\n  };\n\n  const unsubscribeSync = options => {\n    try {\n      const wrapped = Meteor.wrapAsync( SendyAPI.unsubscribe, SendyAPI );\n      return wrapped( options );\n    } catch ( error ) {\n      // eslint-disable-next-line no-console\n      console.log('// Sendy API error');\n      // eslint-disable-next-line no-console\n      console.log(error);\n    }\n  };\n\n  const createCampaignSync = options => {\n    try {\n      const wrapped = Meteor.wrapAsync( SendyAPI.createCampaign, SendyAPI );\n      return wrapped( options );\n    } catch ( error ) {\n      // eslint-disable-next-line no-console\n      console.log('// Sendy API error');\n      // eslint-disable-next-line no-console\n      console.log(error);\n    }\n  };\n\n  /*\n\n  Methods\n\n  */\n\n  Newsletters.sendy = {\n\n    subscribe(email) {\n      return subscribeSync({email, list_id: listId});\n    },\n\n    unsubscribe(email) {\n      return unsubscribeSync({email, list_id: listId});\n    },\n\n    send({ subject, text, html, isTest = false }) {\n      const params = {\n        from_name: fromName,\n        from_email: fromEmail,\n        reply_to: replyTo,\n        title: subject,\n        subject: subject,\n        plain_text: text,\n        html_text: html,\n        send_campaign: !isTest,\n        list_ids: listId\n      };\n      return createCampaignSync(params);\n    }\n\n  };\n\n}"
  },
  {
    "path": "packages/vulcan-newsletter/lib/server/main.js",
    "content": "import Newsletters from '../modules/index.js';\n\nexport * from './newsletters.js';\nexport * from './cron.js';\nexport * from './mutations.js';\nexport * from './callbacks.js';\n\n// import './integrations/sendy.js';\nexport * from './integrations/index.js';\n\nexport default Newsletters;"
  },
  {
    "path": "packages/vulcan-newsletter/lib/server/mutations.js",
    "content": "import Users from 'meteor/vulcan:users';\nimport { addGraphQLSchema, addGraphQLMutation, addGraphQLResolvers, Connectors } from 'meteor/vulcan:core';\nimport { subscribeUser, subscribeEmail, send, unsubscribeUser } from './newsletters';\n\nexport const sendNewsletter = async (root, { newsletterId }, context) => {\n  if (Users.isAdmin(context.currentUser)) {\n    const response = await send({ newsletterId });\n    return response;\n  } else {\n    throw new Error({ id: 'app.noPermission' });\n  }\n};\n\nexport const testNewsletter = async (root, { newsletterId }, context) => {\n  if (Users.isAdmin(context.currentUser)) {\n    const response = await send({ newsletterId, isTest: true });\n    return response;\n  } else {\n    throw new Error({ id: 'app.noPermission' });\n  }\n};\n\nexport const addUserNewsletter = async (root, { userId }, context) => {\n  const currentUser = context.currentUser;\n  const user = await Connectors.get(Users, userId);\n  if (!user || !Users.options.mutations.edit.check(currentUser, user, context)) {\n    throw new Error({ id: 'app.noPermission' });\n  }\n  return await subscribeUser(user, false);\n};\n\nexport const addEmailNewsletter = async (root, { email }, context) => {\n  return await subscribeEmail(email, true);\n};\n\nexport const removeUserNewsletter = async (root, { userId }, context) => {\n  const currentUser = context.currentUser;\n  const user = await Connectors.get(Users, userId);\n  if (!user || !Users.options.mutations.edit.check(currentUser, user, context)) {\n    throw new Error({ id: 'app.noPermission' });\n  }\n\n  try {\n    return await unsubscribeUser(user);\n  } catch (error) {\n    const errorMessage = error.message.includes('subscription-failed') ? { id: 'newsletter.subscription_failed' } : error.message;\n    throw new Error(errorMessage);\n  }\n};\n\nexport const addNewsletterMutations = () => {\n\n  const newsletterResponseSchema = `type NewsletterResponse {\n    email: String\n    success: JSON\n    error: String\n  }`;\n  addGraphQLSchema(newsletterResponseSchema);\n\n  addGraphQLMutation('sendNewsletter(newsletterId: String) : Newsletter');\n  addGraphQLMutation('testNewsletter(newsletterId: String) : Newsletter');\n  addGraphQLMutation('addUserNewsletter(userId: String) : NewsletterResponse');\n  addGraphQLMutation('addEmailNewsletter(email: String) : NewsletterResponse');\n  addGraphQLMutation('removeUserNewsletter(userId: String) : NewsletterResponse');\n\n  const resolver = {\n    Mutation: {\n      sendNewsletter,\n      testNewsletter,\n      addUserNewsletter,\n      addEmailNewsletter,\n      removeUserNewsletter,\n    },\n  };\n\n  addGraphQLResolvers(resolver);\n};\n"
  },
  {
    "path": "packages/vulcan-newsletter/lib/server/newsletters.js",
    "content": "import Users from 'meteor/vulcan:users';\nimport VulcanEmail from 'meteor/vulcan:email';\nimport { SyncedCron } from 'meteor/littledata:synced-cron';\nimport Newsletters from '../modules/collection.js';\nimport { Utils, getSetting, registerSetting, runCallbacksAsync, Connectors } from 'meteor/vulcan:core';\n\nregisterSetting('newsletter.provider', 'mailchimp', 'Newsletter provider');\nregisterSetting('defaultEmail', null, 'Email newsletter confirmations will be sent to');\n\nconst provider = getSetting('newsletter.provider', 'mailchimp'); // default to MailChimp\n\n/*\n\nsubscribeUser\n\nsubscribeEmail\n\nunsubscribeUser\n\nunsubscribeEmail\n\ngetSubject\n\nbuild\n\ngetNext\n\ngetLast\n\nsend\n\n*/\n\nexport const testProvider = () => {\n  if (!Newsletters[provider]) {\n    throw new Error(`Could not find newsletter provider “${provider}”. Please make sure your settings are configured correctly.`);\n  }\n};\n\n/**\n * @summary Subscribe a user to the newsletter\n * @param {Object} user\n * @param {Boolean} confirm\n */\nexport const subscribeUser = async (user, confirm = false) => {\n  testProvider();\n  const email = Users.getEmail(user);\n  if (!email) {\n    throw 'User must have an email address';\n  }\n\n  // eslint-disable-next-line no-console\n  console.log(`// Adding ${email} to ${provider} list…`);\n  const result = await Newsletters[provider].subscribe(email, confirm);\n  // eslint-disable-next-line no-console\n  if (result) {\n    console.log('-> added');\n  }\n  await Connectors.update(Users, user._id, { $set: { newsletter_subscribeToNewsletter: true } });\n  return { email, success: result };\n};\nNewsletters.subscribeUser = subscribeUser;\n\n/**\n * @summary Subscribe an email to the newsletter\n * @param {String} email\n */\nexport const subscribeEmail = async (email, confirm = false) => {\n  testProvider();\n  // eslint-disable-next-line no-console\n  console.log(`// Adding ${email} to ${provider} list…`);\n  const result = await Newsletters[provider].subscribe(email, confirm);\n  // eslint-disable-next-line no-console\n  if (result) {\n    console.log('-> added');\n    return { email, success: result };\n  }\n};\nNewsletters.subscribeEmail = subscribeEmail;\n\n/**\n * @summary Unsubscribe a user from the newsletter\n * @param {Object} user\n */\nexport const unsubscribeUser = async user => {\n  testProvider();\n  const email = Users.getEmail(user);\n  if (!email) {\n    throw 'User must have an email address';\n  }\n\n  // eslint-disable-next-line no-console\n  console.log('// Removing \"' + email + '\" from list…');\n  Newsletters[provider].unsubscribe(email);\n  await Connectors.update(Users, user._id, { $set: { newsletter_subscribeToNewsletter: false } });\n};\nNewsletters.unsubscribeUser = unsubscribeUser;\n\n/**\n * @summary Unsubscribe an email from the newsletter\n * @param {String} email\n */\nexport const unsubscribeEmail = email => {\n  testProvider();\n  // eslint-disable-next-line no-console\n  console.log('// Removing \"' + email + '\" from list…');\n  Newsletters[provider].unsubscribe(email);\n};\nNewsletters.unsubscribeEmail = unsubscribeEmail;\n/**\n * @summary Build a newsletter subject from an array of posts\n * (Called from Newsletter.send)\n * @param {Array} posts\n */\nexport const getSubject = posts => {\n  const subject = posts.map((post, index) => (index > 0 ? `, ${post.title}` : post.title)).join('');\n  return Utils.trimWords(subject, 15);\n};\nNewsletters.getSubject = getSubject;\n\n/**\n * @summary Get info about the next scheduled newsletter\n */\nexport const getNext = () => {\n  var nextJob = SyncedCron.nextScheduledAtDate('scheduleNewsletter');\n  return nextJob;\n};\nNewsletters.getNext = getNext;\n\n/**\n * @summary Get the last sent newsletter\n */\nexport const getLast = () => {\n  return Newsletters.findOne({}, { sort: { createdAt: -1 } });\n};\nNewsletters.getLast = getLast;\n\n/**\n * @summary Send the newsletter\n * @param {Boolean} isTest\n */\nexport const send = async ({ newsletterId, isTest = false }) => {\n  testProvider();\n  let result = { _id: newsletterId };\n\n  if (!newsletterId) {\n    throw new Error('You must specify a newsletterId argument');\n  }\n\n  const newsletter = await Connectors.get(Newsletters, { _id: newsletterId });\n\n  const newsletterEmail = VulcanEmail.emails.newsletter;\n  // if newsletter document already has its own subject, html, and data use them;\n  // else get them from email object\n  const email = await VulcanEmail.build({ emailName: 'newsletter', variables: { terms: { view: 'newsletter' } } });\n\n  const { subject, html, data } = { ...email, ...newsletter };\n  const text = VulcanEmail.generateTextVersion(html);\n\n  if (!newsletterEmail.isValid || newsletterEmail.isValid(data)) {\n    // eslint-disable-next-line no-console\n    console.log('// Sending newsletter…');\n    // eslint-disable-next-line no-console\n    console.log('// Subject: ' + subject);\n\n    try {\n      const sendResult = await Newsletters[provider].send({ subject, text, html, isTest });\n      // console.log('// newsletter sending success!');\n      // console.log(sendResult);\n\n      //   // send confirmation email\n      //   const confirmationHtml = VulcanEmail.getTemplate('newsletterConfirmation')({\n      //     time: createdAt.toString(),\n      //     newsletterLink: sendResult.archive_url,\n      //     subject: subject,\n      //   });\n      //   VulcanEmail.send(getSetting('defaultEmail'), 'Newsletter scheduled', VulcanEmail.buildTemplate(confirmationHtml));\n\n      await runCallbacksAsync('newsletter.send.async', email);\n\n      // status 2 = scheduled\n      result = { scheduledAt: new Date(), status: 2 };\n    } catch (error) {\n      console.log('// Newsletter sending error!');\n      console.log(error);\n      // status 4 = error\n      result = { error, status: 4 };\n    }\n\n    await Connectors.update(Newsletters, { _id: newsletterId }, { $set: result });\n    return result;\n  }\n};\nNewsletters.send = send;"
  },
  {
    "path": "packages/vulcan-newsletter/package.js",
    "content": "Package.describe({\n  name: 'vulcan:newsletter',\n  summary: 'Vulcan email newsletter package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9', 'vulcan:email@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "packages/vulcan-newsletter/scss.json",
    "content": "{\n  \"enableAutoprefixer\": true,\n  \"outputStyle\": \"compressed\",\n  \"sourceComments\": true,\n  \"sourceMap\": true\n}"
  },
  {
    "path": "packages/vulcan-payments/README.md",
    "content": "# Vulcan Payments\n\nThis package helps you process charges with Vulcan. It currently only supports Stripe, but other payment processors may be supported in the future (PRs welcome!). \n\n## Overview\n\nThis package does the following things: \n\n- Provide a button that triggers the [Stripe Checkout](https://stripe.com/checkout) form.\n- Once the form is submitted, trigger a GraphQL mutation that will:\n  - Perform the charge.\n  - Create a new Charge.\n  - Modify a document associated with the charge.\n- The mutation then returns a document associated with the charge to the client.\n\n## Settings\n\nStripe requires the following public setting in your `settings.json`. \n\n- `public.stripe.publishableKey`: your publishable Stripe key.\n\nAs well as the following private setting (can be stored in the setting's root or on `private`): \n\n- `stripe.secretKey`: your Stripe secret key. \n\n```\n{\n  \"public\": {\n    \"stripe\": {\n      \"publishableKey\": \"pk_test_K0rkFDrT0jj4NqG5Dumr3RaU\"\n      }\n    }\n  },\n  \"stripe\": {\n    \"secretKey\": \"sk_test_sfdhj34jdsfxhjs234sd0K\",\n    \"createPlans\": false\n  },\n}\n```\n\n## Charges\n\nCharges are stored in the database with the following fields:\n\n- `_id`: the charge's id. \n- `createdAt`: the charge's timestamp.\n- `userId`: the Vulcan `_id` of the user performing the purchase. \n- `tokenId`: the charge token's id. \n- `productKey`: the key corresponding to the product being purchased, as defined with `addProduct`.\n- `type`: the type of charge (currently only `stripe` is supported).\n- `test`: whether the operation is a test or not. \n- `data`: a JSON object containing all charge data generated by the payment processor.\n- `properties`: a JSON object containing any custom properties passed by the client. \n- `ip`: the IP address of the client performing the purchase. \n\n## Products\n\nA product is a type of purchase a user can make. It has a `name`, `amount` (in cents), `currency`, and `description`.\n\nNew products are defined using the `addProduct` function, which takes two arguments. The first argument is a unique **product key** used to identify the product. The second argument can be an object (for \"static\" products like a subscription):\n\n```\nimport { addProduct } from 'meteor/vulcan:payments';\n\naddProduct('membership', {\n  'amount': 25000,\n  'currency': 'USD',\n  'description': 'Become a paid member.'\n});\n```\n\nOr it can be a function (for \"dynamic\" products like in an e-commerce site) that takes the associated document (i.e. the product being sold) as argument and returns an object:\n\n```\nimport { addProduct } from 'meteor/vulcan:payments';\n\naddProduct('book', book => ({\n  'name': book.title,\n  'amount': book.price,\n  'currency': 'USD',\n  'description': book.description\n}));\n```\n\nMake sure you define your products in a location accessible to both client and server, in order to access them both on the front-end to configure Stripe Checkout, and in the back-end to perform the actual charge. \n\n## Checkout Component\n\n```js\n<Components.Checkout \n    productKey=\"jobPosting\"\n    associatedCollection={Jobs}\n    associatedId={job._id}\n    callback={setToPaid}\n    button={<Button className=\"buy-job-button\" variant=\"primary\">Complete Payment</Button>}\n  />\n```\n\n- `productKey`: The key of the product to buy. \n- `button`: The button that triggers the Stripe Checkout overlay.\n- `associatedCollection`: the associated collection.\n- `associatedDocument`: the associated `document`. \n- `callback`: a callback function that runs once the charge is successful (takes the `charge` as result argument).\n- `fragment`: a GraphQL fragment specifying the fields expected in return after the charge. \n- `fragmentName`: a registeredGraphQL fragment name.\n- `properties`: any other properties you want to pass on to `createChargeMutation` on the server. \n\n## Associating a Collection Document\n\nThe Vulcan Charge package requires associating a document with a purchase, typically the item being paid for. For example, maybe you want people to buy access to a file hosted on your servers, and give them download access once the transaction is complete. \n\nThe `associatedCollection` and `associatedId` props give you an easy way to implement this by automatically setting a `chargeIds` field on the document once the charge succeeds. \n\nFor example, if you pass `associatedCollection={Jobs}` and `associatedId=\"foo123\"` to the Checkout component, the resulting charge's `_id` will automatically be added to a `chargeIds` array on job `foo123`. \n\nThe `createChargeMutation` GraphQL mutation will then return that job according to the `fragment` property specified. \n\nNote: you will need to make sure that your collection accepts this `chargeIds` field. For example:\n\n```js\nJobs.addField([\n  {\n    fieldName: 'chargeIds',\n    fieldSchema: {\n      type: Array,\n      optional: true,\n    }\n  },\n  {\n    fieldName: 'chargeIds.$',\n    fieldSchema: {\n      type: String,\n      optional: true,\n    }\n  }\n]);\n```\n\n#### The \"Chargeable\" Type\n\nIn order to be able to return any associated document, the package creates a new `Chargeable` GraphQL type that is an union of every collection's types. \n\n## Post-Charge Updates\n\nThe best way to update a document based on a successful charge is by using the `collection.charge.sync` callback. \n\nCallback functions on this hook will run with a MongoDB `modifier` as the first argument (although note that only `$set` and `$unset` operations are supported here), the `document` associated with the charge as their second argument, and the `charge` object as their third argument. \n\nBecause the callback is added in a **sync** manner, the final document returned by the `createChargeMutation` mutation will include any new values set by the callback hook. \n\n#### Example 1: Setting a job offer as paid\n\n```js\nimport { addCallback } from 'meteor/vulcan:core';\n\nfunction setToPaidOnCharge (modifier, job, charge) {\n  modifier.$set.status = 'paid';\n  return modifier;\n}\n\naddCallback('jobs.charge.sync', setToPaidOnCharge);\n```\n\n#### Example 2: Adding a user to a group\n\n```js\nimport { addCallback } from 'meteor/vulcan:core';\n\nfunction makePaidMember (modifier, user, charge) {\n  modifier.$set.groups = [...user.groups, 'paidMembers'];\n  return modifier;\n}\n\naddCallback('users.charge.sync', makePaidMember);\n```\n\n#### Example 3: Giving a user access to a specific document\n\nWe'll pass the `videoId` property to our `Checkout` component (`property={{videoId: video._id}}`) to make it accessible as `charge.properties.videoId` inside the callback: \n\n```js\nimport { addCallback } from 'meteor/vulcan:core';\n\nfunction giveAccessToVideo (modifier, user, charge) {\n  const videoId = charge.properties.videoId;\n  modifier.$set.accessibleVideos = [...user.accessibleVideos, videoId];\n  return modifier;\n}\n\naddCallback('users.charge.sync', giveAccessToVideo);\n```\n\n"
  },
  {
    "path": "packages/vulcan-payments/lib/client/main.js",
    "content": "export * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-payments/lib/components/ChargesDashboard.jsx",
    "content": "import React from 'react';\nimport { registerComponent, Components } from 'meteor/vulcan:lib';\n\n// import { Link } from 'react-router-dom';\n// const AssociatedDocument = ({ document }) => {\n//   <Link to={document.pageUrl}>{document._id}</Link>\n// }\n\nconst StripeId = ({ document }) => \n  <a href={document.stripeChargeUrl} target=\"_blank\" rel=\"noopener noreferrer\">{document.stripeId}</a>;\n\nconst ChargesDashboard = props =>\n  <div className=\"charges\">\n    <Components.Datatable\n      showSearch={false}\n      showEdit={false}\n      showNew={false}\n      collectionName=\"Charges\"\n      options={{\n        fragmentName: 'ChargeFragment'\n      }}\n      columns={[\n        {\n          name: 'createdAtFormattedShort',\n          label: 'Created At',\n        },\n        'user',\n        'amount',\n        'type',\n        'source',\n        'productKey',\n        'test',\n        'properties',\n        {\n          name: 'stripeId',\n          component: StripeId\n        },\n      ]}\n    />\n  </div>;\n\nregisterComponent('ChargesDashboard', ChargesDashboard);"
  },
  {
    "path": "packages/vulcan-payments/lib/components/Checkout.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport StripeCheckout from 'react-stripe-checkout';\nimport { Components, registerComponent, getSetting, withCurrentUser, withMessages } from 'meteor/vulcan:core';\nimport Users from 'meteor/vulcan:users';\nimport classNames from 'classnames';\nimport withPaymentAction from '../containers/withPaymentAction.js';\nimport { Products } from '../modules/products.js';\n\nconst stripeSettings = getSetting('stripe');\n\nclass Checkout extends React.Component {\n\n  constructor() {\n    super();\n    this.onToken = this.onToken.bind(this);\n    this.state = {\n      loading: false,\n      mounted: false\n    };\n  }\n\n  handleOpen = () => {\n    if (this.props.onClick) {\n      this.props.onClick();\n    }\n  }\n  \n  onToken(token) {\n\n    const {paymentActionMutation, productKey, associatedCollection, associatedDocument, callback, successCallback, errorCallback, properties, currentUser, flash, coupon} = this.props;\n\n    this.setState({ loading: true });\n\n    const args = {\n      token, \n      userId: currentUser._id, \n      productKey,\n      associatedCollection: associatedCollection._name,\n      associatedId: associatedDocument._id,\n      properties,\n      coupon,\n    };\n\n    paymentActionMutation(args).then(response => {\n\n      // not needed because we just unmount the whole component:\n      this.setState({ loading: false });\n\n      // support both names for backwards compatibility\n      const callbackFunction = successCallback || callback;\n      if (callbackFunction) {\n        callbackFunction(response);\n      }else{\n        flash({id: 'payments.payment_succeeded', type: 'success'});\n      }\n    \n    }).catch((error) => {\n\n      const { graphQLErrors } = error;\n\n      /*\n\n      Pass full graphQLErrors array instead of \"dumbed-down\" error object\n      See https://github.com/apollographql/apollo-link/issues/1022#issuecomment-517809093\n\n      */\n     \n      // eslint-disable-next-line no-console\n      console.log(graphQLErrors);\n\n      if (errorCallback) {\n        errorCallback(graphQLErrors);\n      } else {\n        flash({message: error.message, type: 'error'});\n      }\n    });\n\n  }\n\n  render() {\n\n    const { productKey, currentUser, button, coupon, associatedDocument, customAmount } = this.props;\n  \n    const sampleProduct = {\n      amount: 10000,\n      name: 'My Cool Product',\n      description: 'This product is awesome.',\n      currency: 'USD',\n    };\n\n    // get the product from Products (either object or function applied to doc)\n    // or default to sample product\n    const definedProduct = Products[productKey];\n    const product = typeof definedProduct === 'function' ? definedProduct(associatedDocument) : definedProduct || sampleProduct;\n\n    // if product has initial amount, use it  (for subscription products)\n    let checkoutAmount = customAmount || ( product.initialAmount ? product.initialAmount + product.amount : product.amount );\n\n    if (coupon && product.coupons && product.coupons[coupon]) {\n      checkoutAmount -= product.coupons[coupon];\n    }\n\n    return (\n      <div className={classNames('stripe-checkout', {'checkout-loading': this.state.loading})}>\n        <StripeCheckout\n          opened={this.handleOpen}\n          token={this.onToken}\n          stripeKey={Meteor.isDevelopment || stripeSettings.alwaysUseTest ? stripeSettings.publishableKeyTest : stripeSettings.publishableKey}\n          ComponentClass=\"div\"\n          name={product.name}\n          description={product.description}\n          amount={checkoutAmount}\n          currency={product.currency}\n          email={Users.getEmail(currentUser)}\n          allowRememberMe\n          >\n          {button ? button :\n            <button className=\"btn btn-primary\">\n              Buy\n            </button>\n          }\n        </StripeCheckout>\n        {this.state.loading ? <Components.Loading /> : null}\n      </div>\n    );\n  }\n}\n\nCheckout.propTypes = {\n  productKey: PropTypes.string,\n  currentUser: PropTypes.object, \n  button: PropTypes.object, \n  coupon: PropTypes.string,\n  associatedDocument: PropTypes.object,\n  customAmount: PropTypes.number,\n  onClick: PropTypes.func,\n};\n\nconst WrappedCheckout = (props) => {\n  const { fragment, fragmentName } = props;\n  const WrappedCheckout = withPaymentAction({fragment, fragmentName})(Checkout);\n  return <WrappedCheckout {...props}/>;\n};\n\nregisterComponent('Checkout', WrappedCheckout, withCurrentUser, withMessages);\n\nexport default WrappedCheckout;"
  },
  {
    "path": "packages/vulcan-payments/lib/containers/withPaymentAction.js",
    "content": "import { graphql } from '@apollo/client/react/hoc';\nimport gql from 'graphql-tag';\nimport { getFragment, getFragmentName } from 'meteor/vulcan:core';\n\nexport default function withPaymentAction(options) {\n\n  const fragment = options.fragment || getFragment(options.fragmentName);\n  const fragmentName = getFragmentName(fragment) || fragmentName;\n\n  const mutation = gql`\n    mutation paymentActionMutation($token: JSON, $userId: String, $productKey: String, $associatedCollection: String, $associatedId: String, $properties: JSON, $coupon: String) {\n      paymentActionMutation(token: $token, userId: $userId, productKey: $productKey, associatedCollection: $associatedCollection, associatedId: $associatedId, properties: $properties, coupon: $coupon) {\n        __typename\n        ...${fragmentName}\n      }\n    }\n    ${fragment}\n  `;\n\n  return graphql(mutation, {\n    alias: 'withPaymentAction',\n    props: ({ownProps, mutate}) => ({\n      paymentActionMutation: (vars) => {\n        return mutate({ \n          variables: vars,\n        });\n      }\n    }),\n  });\n}"
  },
  {
    "path": "packages/vulcan-payments/lib/modules/charges/collection.js",
    "content": "// The main Charges collection definition file.\nimport { createCollection, getDefaultResolvers } from 'meteor/vulcan:core';\nimport schema from './schema.js';\nimport Users from 'meteor/vulcan:users';\n\nconst Charges = createCollection({\n  collectionName: 'Charges',\n\n  typeName: 'Charge',\n\n  schema,\n\n  resolvers: getDefaultResolvers('Charges'),\n\n  mutations: null,\n\n  defaultInput: {\n    sort: {\n      createdAt: 'desc',\n    },\n  },\n});\n\nCharges.addDefaultView(terms => {\n  return {\n    options: { sort: { createdAt: -1 } },\n  };\n});\n\nCharges.checkAccess = (currentUser, charge) => {\n  return Users.isAdmin(currentUser);\n};\n\nexport default Charges;\n"
  },
  {
    "path": "packages/vulcan-payments/lib/modules/charges/schema.js",
    "content": "import moment from 'moment';\n\nconst schema = {\n\n  // default properties\n\n  _id: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n  },\n  createdAt: {\n    type: Date,\n    optional: true,\n    canRead: ['admins'],\n    onCreate: () => {\n      return new Date();\n    },\n  },\n  userId: {\n    type: String,\n    optional: true,\n    canRead: ['admins'],\n    resolveAs: {\n      fieldName: 'user',\n      type: 'User',\n      resolver: async (post, args, { currentUser, Users }) => {\n        const user = await Users.loader.load(post.userId);\n        return Users.restrictViewableFields(currentUser, Users, user);\n      },\n      addOriginalField: true\n    },\n  },\n  \n  // custom properties\n\n  type: {\n    type: String,\n    optional: true,\n    canRead: ['admins'],\n  },\n\n  associatedCollection: {\n    type: String,\n    canRead: ['admins'],\n    optional: true,\n  },\n\n  associatedId: {\n    type: String,\n    canRead: ['admins'],\n    optional: true,\n  },\n\n  tokenId: {\n    type: String,\n    optional: false,\n  },\n\n  productKey: {\n    type: String,\n    canRead: ['admins'],\n    optional: true,\n  },\n\n  source: {\n    type: String,\n    canRead: ['admins'],\n    optional: false,\n  },\n\n  test: {\n    type: Boolean,\n    canRead: ['admins'],\n    optional: true,\n  },\n\n  data: {\n    type: Object,\n    // canRead: ['admins'], // for security's sake don't expose this through GraphQL API\n    blackbox: true,\n  },\n\n  properties: {\n    type: Object,\n    canRead: ['admins'],\n    blackbox: true,\n  },\n\n  ip: {\n    type: String,\n    canRead: ['admins'],\n    optional: true,\n  },\n\n  amount: {\n    type: Number,\n    optional: true,\n    canRead: ['admins'],\n    onCreate: ({ data: charge }) => charge.data && charge.data.amount,\n    // resolveAs: {\n    //   type: 'Int',\n    //   resolver: charge => {console.log(charge); return charge.data && charge.data.amount},\n    // }  \n  },\n\n  stripeId: {\n    type: String,\n    optional: true,\n    canRead: ['admins'],\n    onCreate: ({ data: charge }) => charge.data && charge.data.id,\n    // resolveAs: {\n    //   type: 'String',\n    //   resolver: (charge, args, context) => {\n    //     return charge.data && charge.data.id;\n    //   }\n    // } \n  },\n\n  stripeChargeUrl: {\n    type: String,\n    optional: true,\n    canRead: ['admins'],\n    resolveAs: {\n      type: 'String',\n      resolver: (charge, args, context) => {\n        return `https://dashboard.stripe.com/payments/${charge.stripeId}`;\n      }\n    } \n  },\n\n  // doesn't work yet\n\n  // associatedDocument: {\n  //   type: Object,\n  //   canRead: ['admins'],\n  //   optional: true,\n  //   resolveAs: {\n  //     type: 'Chargeable',\n  //     resolver: (charge, args, context) => {\n  //       const collection = getCollection(charge.associatedCollection);\n  //       return collection.loader.load(charge.associatedId);\n  //     }\n  //   } \n  // },\n\n};\n\nexport default schema;\n"
  },
  {
    "path": "packages/vulcan-payments/lib/modules/components.js",
    "content": "import '../components/Checkout.jsx';\nimport '../components/ChargesDashboard.jsx';\n"
  },
  {
    "path": "packages/vulcan-payments/lib/modules/custom_fields.js",
    "content": "import Users from 'meteor/vulcan:users';\n\nUsers.addField([\n  {\n    fieldName: 'stripeCustomerId',\n    fieldSchema: {\n      type: String,\n      optional: true,\n    },\n  },\n]);\n"
  },
  {
    "path": "packages/vulcan-payments/lib/modules/fragments.js",
    "content": "import { registerFragment } from 'meteor/vulcan:core';\n\nregisterFragment(`\n  fragment ChargeFragment on Charge {\n    _id\n    createdAt\n    createdAtFormatted(format: \"dddd, MMMM Do YYYY\")\n    createdAtFormattedShort: createdAtFormatted(format: \"YYYY/MM/DD, hh:mm\")\n    user{\n      _id\n      slug\n      username\n      displayName\n      pageUrl\n      pagePath\n      emailHash\n      avatarUrl\n    }\n    type\n    source\n    productKey\n    test\n    associatedCollection\n    associatedId\n\n    # doesn't work with unions, maybe try interface?\n    # associatedDocument{\n    #   _id\n    #  pageUrl\n    # }\n\n    amount\n    properties\n    stripeId\n    stripeChargeUrl\n  }\n`);"
  },
  {
    "path": "packages/vulcan-payments/lib/modules/i18n.js",
    "content": "import { addStrings } from 'meteor/vulcan:core';\n\naddStrings('en', {\n  'payments.payment_succeeded': 'Thanks, your payment has succeeded.',\n  'payments.error': 'Sorry, something went wrong.',\n});\n"
  },
  {
    "path": "packages/vulcan-payments/lib/modules/index.js",
    "content": "\nimport './components.js';\nimport './routes.js';\nimport './fragments.js';\nimport './i18n.js';\nimport './custom_fields.js';\n\nimport Charges from './charges/collection.js';\n\nexport { Charges };\nexport * from './products.js';\n"
  },
  {
    "path": "packages/vulcan-payments/lib/modules/products.js",
    "content": "export const Products = {};\n\nexport const addProduct = (productKey, product, productType = 'product') => {\n\n  let productWithType; \n\n  // if product is a function, set it to a new function that returns the same thing except\n  // with `type` set to `productType`\n  if (typeof product === 'function') {\n    productWithType = document => {\n      const returnValue = product(document);\n      returnValue.type = productType;\n      return returnValue;\n    };\n  } else {\n    productWithType = product;\n    productWithType.type = productType;\n  }\n\n  Products[productKey] = productWithType;\n};\n\nexport const addSubscriptionProduct = (productKey, product) => {\n  addProduct(productKey, product, 'subscription');\n};\n"
  },
  {
    "path": "packages/vulcan-payments/lib/modules/routes.js",
    "content": "import { addRoute } from 'meteor/vulcan:core';\n\naddRoute([\n  {name:'checkoutTest', path: '/checkout-test', componentName: 'Checkout', layoutName: 'AdminLayout'},\n  {name:'chargesDashboard', path: '/charges', componentName: 'ChargesDashboard', layoutName: 'AdminLayout'},\n]);\n"
  },
  {
    "path": "packages/vulcan-payments/lib/server/integrations/stripe.js",
    "content": "/*\n\nStripe charge lifecycle\n\n### From a GraphQL Mutation ###\n\n1. paymentActionMutation GraphQL mutation is received\n\n2. receiveAction is called\n\n  -> [stripe.receive.sync] callback on metadata object\n  -> [stripe.receive.async] callback\n\n| for one-time charges\n\n3. createCharge is called\n\n  -> [stripe.charge.async] callback\n\n| for subscriptions\n\n3. createSubscription is called\n\n  -> [stripe.subscribe.async] callback\n\n4. processAction is called\n\n  -> [stripe.process.sync] callback\n  -> [stripe.process.async] callback\n\n### From a Stripe Webhook ###\n\n1. `/stripe` endpoint is triggered\n\n2. processAction is called\n\n*/\n\nimport {\n  webAppConnectHandlersUse,\n  debug,\n  debugGroup,\n  debugGroupEnd,\n  getSetting,\n  registerSetting,\n  createMutator,\n  updateMutator,\n  Collections,\n  registerCallback,\n  runCallbacks,\n  runCallbacksAsync,\n  Connectors,\n} from 'meteor/vulcan:core';\nimport express from 'express';\nimport Stripe from 'stripe';\nimport Charges from '../../modules/charges/collection.js';\nimport Users from 'meteor/vulcan:users';\nimport { Products } from '../../modules/products.js';\nimport { Promise } from 'meteor/promise';\n\nregisterSetting('stripe', null, 'Stripe settings');\nregisterSetting('stripe.publishableKey', null, 'Publishable key', true);\nregisterSetting('stripe.publishableKeyTest', null, 'Publishable key (test)', true);\nregisterSetting('stripe.secretKey', null, 'Secret key');\nregisterSetting('stripe.secretKeyTest', null, 'Secret key (test)');\nregisterSetting('stripe.endpointSecret', null, 'Endpoint secret for webhook');\nregisterSetting('stripe.alwaysUseTest', false, 'Always use test keys in all environments', true);\n\nconst stripeSettings = getSetting('stripe');\n\n// initialize Stripe\nconst keySecret =\n  Meteor.isDevelopment || stripeSettings && stripeSettings.alwaysUseTest\n    ? stripeSettings && stripeSettings.secretKeyTest\n    : stripeSettings && stripeSettings.secretKey;\nexport const stripe = new Stripe(keySecret);\n\nconst sampleProduct = {\n  amount: 10000,\n  name: 'My Cool Product',\n  description: 'This product is awesome.',\n  currency: 'USD',\n};\n\n/*\n\nReceive the action and call the appropriate handler\n\n*/\nexport const receiveAction = async (args, context) => {\n  let collection,\n    document,\n    returnDocument = {};\n\n  const { userId, productKey, associatedCollection, associatedId, properties } = args;\n\n  if (!stripeSettings) {\n    throw new Error('Please fill in your Stripe settings');\n  }\n\n  // if an associated collection name and document id have been provided,\n  // get the associated collection and document\n  if (associatedCollection && associatedId) {\n    collection = _.findWhere(Collections, { _name: associatedCollection });\n    document = await Connectors.get(collection, associatedId);\n  }\n\n  // get the product from Products (either object or function applied to doc)\n  // or default to sample product\n  const definedProduct = Products[productKey];\n  const product =\n    typeof definedProduct === 'function'\n      ? definedProduct(document)\n      : definedProduct || sampleProduct;\n\n  // get the user performing the transaction\n  const user = await Connectors.get(Users, userId);\n\n  // create metadata object\n  let metadata = {\n    userId: userId,\n    userName: Users.getDisplayName(user),\n    userProfile: Users.getProfileUrl(user, true),\n    productKey,\n    ...properties,\n  };\n\n  if (associatedCollection && associatedId) {\n    metadata.associatedCollection = associatedCollection;\n    metadata.associatedId = associatedId;\n  }\n\n  metadata = await runCallbacks('stripe.receive.sync', metadata, {\n    user,\n    product,\n    collection,\n    document,\n    args,\n    context,\n  });\n\n  if (product.type === 'subscription') {\n    // if product is a subscription product, subscribe user to its plan\n    returnDocument = await createSubscription({\n      user,\n      product,\n      collection,\n      document,\n      metadata,\n      args,\n      context,\n    });\n  } else {\n    // else, perform charge\n    returnDocument = await createCharge({\n      user,\n      product,\n      collection,\n      document,\n      metadata,\n      args,\n      context,\n    });\n  }\n\n  runCallbacks('stripe.receive.async', {\n    metadata,\n    user,\n    product,\n    collection,\n    document,\n    args,\n    context,\n  });\n  return returnDocument;\n};\n\n/*\n\nUpdate/retrieve or create a Stripe customer\n\n*/\nexport const getCustomer = async (user, token) => {\n  const { id } = token;\n\n  let customer;\n  const customerOptions = {};\n  if (id) {\n    customerOptions.source = id;\n  }\n\n  try {\n    // update customer with latest payment source and get customer object in return (if it exists)\n    customer = await stripe.customers.update(user.stripeCustomerId, customerOptions);\n  } catch (error) {\n    // if user doesn't have a stripeCustomerId; or if id doesn't match up with Stripe, create new customer object\n    customerOptions.email = user.email;\n    customer = await stripe.customers.create(customerOptions);\n\n    // add stripe customer id to user object\n    await updateMutator({\n      collection: Users,\n      documentId: user._id,\n      data: { stripeCustomerId: customer.id },\n      validate: false,\n    });\n  }\n\n  return customer;\n};\n\n/*\n\nCreate one-time charge. \n\n*/\nexport const createCharge = async ({\n  user,\n  product,\n  collection,\n  document,\n  metadata,\n  args,\n  context,\n}) => {\n  const { token, /* userId, productKey, associatedId, properties, */ coupon } = args;\n\n  const customer = await getCustomer(user, token);\n\n  let amount = product.amount;\n\n  // apply discount coupon and add it to metadata, if there is one\n  if (coupon && product.coupons && product.coupons[coupon]) {\n    amount -= product.coupons[coupon];\n    metadata.coupon = coupon;\n    metadata.discountAmount = product.coupons[coupon];\n  }\n\n  // gather charge data\n  const chargeData = {\n    amount,\n    description: product.description,\n    currency: product.currency,\n    customer: customer.id,\n    metadata,\n  };\n\n  // create Stripe charge\n  const charge = await stripe.charges.create(chargeData);\n\n  charge.objectType = 'charge';\n\n  runCallbacksAsync('stripe.charge.async', {\n    charge,\n    collection,\n    document,\n    args,\n    user,\n    context,\n  });\n\n  return processAction({\n    collection,\n    document,\n    stripeObject: charge,\n    args,\n    user,\n    context,\n  });\n};\n\n/*\n\nSubscribe a user to a Stripe plan\n\n*/\nexport const createSubscription = async ({\n  user,\n  product,\n  collection,\n  document,\n  metadata,\n  args,\n  context,\n}) => {\n  let returnDocument = document;\n\n  let invoiceItemId;\n\n  try {\n    const customer = await getCustomer(user, args.token);\n    // if product has an initial cost,\n    // create an invoice item and attach it to the customer first\n    // see https://stripe.com/docs/subscriptions/invoices#adding-invoice-items\n    if (product.initialAmount) {\n      // eslint-disable-next-line no-unused-vars\n      const initialInvoiceItem = await stripe.invoiceItems.create({\n        customer: customer.id,\n        amount: product.initialAmount,\n        currency: product.currency,\n        description: product.initialAmountDescription,\n      });\n      invoiceItemId = initialInvoiceItem.id;\n    }\n\n    // eslint-disable-next-line no-unused-vars\n    const subscription = await stripe.subscriptions.create({\n      customer: customer.id,\n      items: [{ plan: product.plan }],\n      metadata,\n      ...product.subscriptionProperties,\n    });\n\n    subscription.objectType = 'subscription';\n\n    // // if an associated collection and id have been provided,\n    // // update the associated document\n    // if (collection && document) {\n\n    //   let modifier = {\n    //     $set: {},\n    //     $unset: {}\n    //   }\n\n    //   // run collection.subscribe.sync callbacks\n    //   modifier = runCallbacks(`${collection._name}.subscribe.sync`, modifier, document, subscription, user);\n\n    //   returnDocument = await editMutation({\n    //     collection,\n    //     documentId: document._id,\n    //     set: modifier.$set,\n    //     unset: modifier.$unset,\n    //     validate: false\n    //   });\n\n    //   returnDocument.__typename = collection.typeName;\n\n    // }\n\n    runCallbacksAsync('stripe.subscribe.async', {\n      subscription,\n      collection,\n      returnDocument,\n      args,\n      user,\n      context,\n    });\n\n    returnDocument = await processAction({\n      collection,\n      document,\n      stripeObject: subscription,\n      args,\n      user,\n      context,\n    });\n\n    return returnDocument;\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.log('// Stripe createSubscription error');\n    // eslint-disable-next-line no-console\n    console.log(error);\n    /*\n\n    If an invoice item was created, cancel it to avoid having invoice items\n    pile up and be charged during future payment attempts.\n\n    */\n    if (invoiceItemId) {\n      try {\n        await stripe.invoiceItems.del(invoiceItemId);\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.log(`// Error while attempting to delete invoice item ID ${invoiceItemId}`);\n        // eslint-disable-next-line no-console\n        console.log(error);\n        throw error;\n      }\n    }\n    runCallbacksAsync('stripe.error.async', {\n      action: 'subscription',\n      collection,\n      returnDocument,\n      args,\n      user,\n      context,\n    });\n    throw error;\n  }\n};\n\n// create a stripe plan\n// plan is used as the unique ID and is not needed for creating a plan\nconst createPlan = async ({\n  // Extract all the known properties for the stripe api\n  // Evertying else goes in the metadata field\n  plan: id,\n  currency,\n  interval,\n  name,\n  amount,\n  interval_count,\n  statement_descriptor,\n  context,\n  ...metadata\n}) =>\n  stripe.plans.create({\n    id,\n    currency,\n    interval,\n    amount,\n    interval_count,\n    product: {\n      name,\n      statement_descriptor,\n      metadata,\n    },\n    metadata,\n  });\n\nexport const createSubscriptionPlan = async maybePlanObject =>\n  typeof maybePlanObject === 'object' && createPlan(maybePlanObject);\nconst retrievePlan = async planObject => stripe.plans.retrieve(planObject.plan);\nexport const retrieveSubscriptionPlan = async maybePlanObject =>\n  typeof maybePlanObject === 'object' && retrievePlan(maybePlanObject);\nconst createOrRetrievePlan = async planObject => {\n  return retrievePlan(planObject).catch(error => {\n    // Plan does not exist, create it\n    if (error.statusCode === 404) {\n      // eslint-disable-next-line no-console\n      console.log(\n        `Creating subscription plan ${planObject.plan} for ${(planObject.amount &&\n          (planObject.amount / 100).toLocaleString('en-US', {\n            style: 'currency',\n            currency: planObject.currency,\n          })) ||\n          'free'}`\n      );\n      return createPlan(planObject);\n    }\n    // Something else went wrong\n    // eslint-disable-next-line no-console\n    console.error(error);\n    throw error;\n  });\n};\nexport const createOrRetrieveSubscriptionPlan = async maybePlanObject =>\n  typeof maybePlanObject === 'object' && createOrRetrievePlan(maybePlanObject);\n\n/*\n\nProcess charges, subscriptions, etc. on Vulcan's side\n\n*/\nexport const processAction = async ({\n  collection,\n  document,\n  stripeObject,\n  args,\n  user,\n  context,\n}) => {\n  debug('');\n  debugGroup('--------------- start\\x1b[35m processAction \\x1b[0m ---------------');\n  debug(`Collection: ${collection.options.collectionName}`);\n  debug(`documentId: ${document._id}`);\n  debug(`Charge: ${stripeObject}`);\n\n  let returnDocument = {};\n\n  // make sure charge hasn't already been processed\n  // (could happen with multiple endpoints listening)\n\n  const existingCharge = await Connectors.get(Charges, {\n    'data.id': stripeObject.id,\n  });\n\n  if (existingCharge) {\n    // eslint-disable-next-line no-console\n    console.log(\n      `// Charge with Stripe id ${stripeObject.id} already exists in db; aborting processAction`\n    );\n    return collection && document ? document : {};\n  }\n\n  const {\n    token,\n    userId,\n    productKey,\n    associatedCollection,\n    associatedId,\n    properties,\n    livemode,\n  } = args;\n\n  // create charge document for storing in our own Charges collection\n  const chargeDoc = {\n    createdAt: new Date(),\n    userId,\n    type: stripeObject.objectType,\n    source: 'stripe',\n    test: !livemode,\n    data: stripeObject,\n    associatedCollection,\n    associatedId,\n    properties,\n    productKey,\n  };\n\n  if (token) {\n    chargeDoc.tokenId = token.id;\n    chargeDoc.test = !token.livemode; // get livemode from token if provided\n    chargeDoc.ip = token.client_ip;\n  }\n  // insert\n  const chargeSavedData = await createMutator({\n    collection: Charges,\n    data: chargeDoc,\n    validate: false,\n  });\n  const chargeSaved = chargeSavedData.data;\n\n  // if an associated collection and id have been provided,\n  // update the associated document\n  if (collection && document) {\n    // note: assume a single document can have multiple successive charges associated to it\n    const chargeIds = document.chargeIds\n      ? [...document.chargeIds, chargeSaved._id]\n      : [chargeSaved._id];\n\n    let data = { chargeIds };\n\n    // run collection.charge.sync callbacks\n    data = await runCallbacks({\n      name: 'stripe.process.sync',\n      iterator: data,\n      properties: { collection, document, chargeDoc, user },\n    });\n\n    context.event = 'stripe.process.sync';\n    context.chargeDoc = chargeDoc;\n\n    const updateResult = await updateMutator({\n      collection,\n      documentId: associatedId,\n      data,\n      validate: false,\n      context,\n    });\n\n    returnDocument = updateResult.data;\n\n    returnDocument.__typename = collection.typeName;\n  }\n\n  runCallbacksAsync('stripe.process.async', {\n    collection,\n    returnDocument,\n    chargeDoc,\n    user,\n    context,\n  });\n\n  debugGroupEnd();\n  debug('--------------- end\\x1b[35m processAction \\x1b[0m ---------------');\n  debug('');\n\n  return returnDocument;\n};\n\n/*\n\nWebhooks with Express\n\n*/\n\n// see https://github.com/stripe/stripe-node/blob/master/examples/webhook-signing/express.js\n\nconst app = express();\n\n// Add the raw text body of the request to the `request` object\nfunction addRawBody(req, res, next) {\n  req.setEncoding('utf8');\n\n  var data = '';\n\n  req.on('data', function(chunk) {\n    data += chunk;\n  });\n\n  req.on('end', function() {\n    req.rawBody = data;\n\n    next();\n  });\n}\n\napp.post('/stripe', addRawBody, async function(req, res) {\n  // eslint-disable-next-line no-console\n  console.log('////////////// stripe webhook');\n\n  const sig = req.headers['stripe-signature'];\n\n  try {\n    const event = stripe.webhooks.constructEvent(req.rawBody, sig, stripeSettings.endpointSecret);\n\n    // eslint-disable-next-line no-console\n    console.log('event ///////////////////');\n    // eslint-disable-next-line no-console\n    console.log(event);\n\n    switch (event.type) {\n      case 'charge.succeeded':\n        // eslint-disable-next-line no-console\n        console.log('////// charge succeeded');\n\n        const charge = event.data.object;\n\n        charge.objectType = 'charge';\n\n        // eslint-disable-next-line no-console\n        console.log(charge);\n\n        try {\n          // look up corresponding invoice\n          const invoice = await stripe.invoices.retrieve(charge.invoice);\n          // eslint-disable-next-line no-console\n          console.log('////// invoice');\n          // eslint-disable-next-line no-console\n          console.log(invoice);\n\n          // look up corresponding subscription\n          const subscription = await stripe.subscriptions.retrieve(invoice.subscription);\n          // eslint-disable-next-line no-console\n          console.log('////// subscription');\n          // eslint-disable-next-line no-console\n          console.log(subscription);\n\n          const { userId, productKey, associatedCollection, associatedId } = subscription.metadata;\n\n          if (associatedCollection && associatedId) {\n            const collection = _.findWhere(Collections, {\n              _name: associatedCollection,\n            });\n            const document = await Connectors.get(collection, associatedId);\n\n            // make sure document actually exists\n            if (!document) {\n              throw new Error(\n                `Could not find ${associatedCollection} document with id ${associatedId} associated with subscription id ${\n                  subscription.id\n                }; Not processing charge.`\n              );\n            }\n\n            const args = {\n              userId,\n              productKey,\n              associatedCollection,\n              associatedId,\n              livemode: subscription.livemode,\n            };\n\n            processAction({ collection, document, stripeObject: charge, args });\n          }\n        } catch (error) {\n          // eslint-disable-next-line no-console\n          console.log('// Stripe webhook error');\n          // eslint-disable-next-line no-console\n          console.log(error);\n        }\n\n        break;\n    }\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.log('///// Stripe webhook error');\n    // eslint-disable-next-line no-console\n    console.log(error);\n  }\n\n  res.sendStatus(200);\n});\n\nwebAppConnectHandlersUse(Meteor.bindEnvironment(app), {\n  name: 'stripe_endpoint',\n  order: 100,\n});\n\n// Picker.middleware(bodyParser.json());\n\n// Picker.route('/stripe', async function(params, req, res, next) {\n\n//   console.log('////////////// stripe webhook')\n\n//   console.log(req)\n//   const sig = req.headers['stripe-signature'];\n//   const body = req.body;\n\n//   console.log('sig ///////////////////')\n//   console.log(sig)\n\n//   console.log('body ///////////////////')\n//   console.log(body)\n\n//   console.log('rawBody ///////////////////')\n//   console.log(req.rawBody)\n\n//   try {\n//     const event = stripe.webhooks.constructEvent(req.rawBody, sig, stripeSettings.endpointSecret);\n//     console.log('event ///////////////////')\n//     console.log(event)\n//   } catch (error) {\n//     console.log('///// Stripe webhook error')\n//     console.log(error)\n//   }\n\n//    // Retrieve the request's body and parse it as JSON\n//    switch (body.type) {\n\n//     case 'charge.succeeded':\n\n//       console.log('////// charge succeeded')\n//       // console.log(body)\n\n//       const charge = body.data.object;\n\n//       try {\n\n//         // look up corresponding invoice\n//         const invoice = await stripe.invoices.retrieve(body.data.object.invoice);\n//         console.log('////// invoice')\n\n//         // look up corresponding subscription\n//         const subscription = await stripe.subscriptions.retrieve(invoice.subscription);\n//         console.log('////// subscription')\n//         console.log(subscription)\n\n//         const { userId, productKey, associatedCollection, associatedId } = subscription.metadata;\n\n//         if (associatedCollection && associatedId) {\n//           const collection = _.findWhere(Collections, {_name: associatedCollection});\n//           const document = collection.findOne(associatedId);\n\n//           const args = {\n//             userId,\n//             productKey,\n//             associatedCollection,\n//             associatedId,\n//             livemode: subscription.livemode,\n//           }\n\n//           processAction({ collection, document, charge, args});\n\n//         }\n//       } catch (error) {\n//         console.log('// Stripe webhook error')\n//         console.log(error)\n//       }\n\n//       break;\n\n//    }\n\n//   res.statusCode = 200;\n//   res.end();\n\n// });\n\nMeteor.startup(() => {\n  registerCallback({\n    name: 'stripe.receive.sync',\n    description: \"Modify any metadata before calling Stripe's API\",\n    arguments: [\n      { metadata: 'Metadata about the action' },\n      { user: 'The user' },\n      { product: 'Product created with addProduct' },\n      { collection: 'Associated collection of the charge' },\n      { document: 'Associated document in collection to the charge' },\n      { args: 'Original mutation arguments' },\n    ],\n    runs: 'sync',\n    newSyntax: true,\n    returns: 'The modified metadata to be sent to Stripe',\n  });\n\n  registerCallback({\n    name: 'stripe.receive.async',\n    description: \"Run after calling Stripe's API\",\n    arguments: [\n      { metadata: 'Metadata about the charge' },\n      { user: 'The user' },\n      { product: 'Product created with addProduct' },\n      { collection: 'Associated collection of the charge' },\n      { document: 'Associated document in collection to the charge' },\n      { args: 'Original mutation arguments' },\n    ],\n    runs: 'sync',\n    newSyntax: true,\n  });\n\n  registerCallback({\n    name: 'stripe.charge.async',\n    description: 'Perform operations immediately after the stripe subscription has completed',\n    arguments: [\n      { charge: 'The charge' },\n      { collection: 'Associated collection of the subscription' },\n      { document: 'Associated document in collection to the charge' },\n      { args: 'Original mutation arguments' },\n      { user: 'The user' },\n    ],\n    runs: 'async',\n    newSyntax: true,\n  });\n\n  registerCallback({\n    name: 'stripe.subscribe.async',\n    description: 'Perform operations immediately after the stripe subscription has completed',\n    arguments: [\n      { subscription: 'The subscription' },\n      { collection: 'Associated collection of the subscription' },\n      { document: 'Associated document in collection to the charge' },\n      { args: 'Original mutation arguments' },\n      { user: 'The user' },\n    ],\n    runs: 'async',\n    newSyntax: true,\n  });\n\n  registerCallback({\n    name: 'stripe.process.sync',\n    description: 'Modify any metadata before sending the charge to stripe',\n    arguments: [\n      {\n        modifier: 'The modifier object used to update the associated collection',\n      },\n      { collection: 'Collection associated to the product' },\n      { document: 'Associated document' },\n      { chargeDoc: \"Charge document returned by Stripe's API\" },\n      { user: 'The user' },\n    ],\n    runs: 'sync',\n    returns: 'The modified arguments to be sent to stripe',\n  });\n\n  registerCallback({\n    name: 'stripe.process.async',\n    description: 'Modify any metadata before sending the charge to stripe',\n    arguments: [\n      { collection: 'Collection associated to the product' },\n      { document: 'Associated document' },\n      { chargeDoc: \"Charge document returned by Stripe's API\" },\n      { user: 'The user' },\n    ],\n    runs: 'async',\n    returns: 'The modified arguments to be sent to stripe',\n  });\n\n  // Create plans if they don't exist\n  if (stripeSettings && stripeSettings.createPlans) {\n    // eslint-disable-next-line no-console\n    console.log('Creating stripe plans...');\n    Promise.awaitAll(\n      Object.keys(Products)\n        // Filter out function type products and those without a plan defined (non-subscription)\n        .filter(productKey => typeof Products[productKey] === 'object' && Products[productKey].plan)\n        .map(productKey => createOrRetrievePlan(Products[productKey]))\n    );\n    // eslint-disable-next-line no-console\n    console.log('Finished creating stripe plans.');\n  }\n});\n"
  },
  {
    "path": "packages/vulcan-payments/lib/server/main.js",
    "content": "export * from '../modules/index.js';\n\nimport './mutations.js';\nexport * from './integrations/stripe.js';\n"
  },
  {
    "path": "packages/vulcan-payments/lib/server/mutations.js",
    "content": "import { addGraphQLSchema, addGraphQLResolvers, addGraphQLMutation, Collections, addCallback } from 'meteor/vulcan:core';\n// import Users from 'meteor/vulcan:users';\nimport { receiveAction } from '../server/integrations/stripe.js';\n\nconst resolver = {\n  Mutation: {\n    async paymentActionMutation(root, args, context) {\n      return await receiveAction(args, context);\n    },\n  },\n};\naddGraphQLResolvers(resolver);\naddGraphQLMutation('paymentActionMutation(token: JSON, userId: String, productKey: String, associatedCollection: String, associatedId: String, properties: JSON, coupon: String) : Chargeable');\n\nfunction CreateChargeableUnionType() {\n  const chargeableSchema = `union Chargeable = ${Collections.map(collection => collection.typeName).join(' | ')}`;\n  addGraphQLSchema(chargeableSchema);\n  return {};\n}\naddCallback('graphql.init.before', CreateChargeableUnionType);\n\nconst resolverMap = {\n  Chargeable: {\n    __resolveType(obj, context, info){\n      return obj.__typename || null;\n    },\n  },\n};\naddGraphQLResolvers(resolverMap);"
  },
  {
    "path": "packages/vulcan-payments/lib/stylesheets/style.scss",
    "content": ".stripe-checkout{\n  position: relative;\n  display: inline-block;\n  .spinner{\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    margin-left: -20px;\n    margin-top: -5px;\n    display: none;\n  }\n  &.checkout-loading{\n    div:first-child{\n      opacity: 0.2;\n      pointer-events: none;\n    }\n    .spinner{\n      display: flex;\n    }\n  }\n}"
  },
  {
    "path": "packages/vulcan-payments/package.js",
    "content": "Package.describe({\n  name: 'vulcan:payments',\n  summary: 'Vulcan payments package',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['promise@0.11.2', 'vulcan:core@=1.16.9', 'vulcan:scss@4.12.0']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n\n  api.addFiles(['lib/stylesheets/style.scss']);\n});\n"
  },
  {
    "path": "packages/vulcan-redux/README.md",
    "content": "Redux package."
  },
  {
    "path": "packages/vulcan-redux/lib/client/main.js",
    "content": "import './reduxInitialState';\n\nexport setupRedux from './setupRedux';\n\nexport * from '../modules/index';"
  },
  {
    "path": "packages/vulcan-redux/lib/client/reduxInitialState.js",
    "content": "import { runCallbacks } from 'meteor/vulcan:lib';\nimport setupRedux from './setupRedux';\n\nMeteor.startup(() => {\n  const initialState = runCallbacks({name: 'redux.initialState', item: {}});\n  setupRedux(initialState);\n});"
  },
  {
    "path": "packages/vulcan-redux/lib/client/setupRedux.js",
    "content": "import React from 'react';\nimport { Provider } from 'react-redux';\nimport { addCallback } from 'meteor/vulcan:core';\nimport { initStore } from '../modules/redux';\n\nconst setupRedux = initialState => {\n  const store = initStore(initialState);\n  addCallback('router.client.wrapper', function ReduxStoreProvider(app) {\n    return <Provider store={store}>{app}</Provider>;\n  });\n};\nexport default setupRedux;\n"
  },
  {
    "path": "packages/vulcan-redux/lib/modules/index.js",
    "content": "export * from './redux.js';\n"
  },
  {
    "path": "packages/vulcan-redux/lib/modules/redux.js",
    "content": "import { createStore, applyMiddleware, combineReducers } from 'redux';\nimport { compose } from 'meteor/vulcan:lib';\nimport _isEmpty from 'lodash/isEmpty';\n// TODO: now we should add some callback call to add the store to\n// Apollo SSR + client side too\n\n// create store, and implement reload function\nexport const configureStore = (\n  reducers = getReducers,\n  initialState = {},\n  middlewares = getMiddlewares\n) => {\n  let getReducers;\n  if (typeof reducers === 'function') {\n    getReducers = reducers;\n    reducers = getReducers();\n  }\n  if (typeof reducers === 'object') {\n    // allow to tolerate empty reducers\n    //@see https://github.com/reduxjs/redux/issues/968\n    reducers = !_isEmpty(reducers) ? combineReducers(reducers) : () => {};\n  }\n  let getMiddlewares;\n  if (typeof middlewares === 'function') {\n    getMiddlewares = middlewares;\n    middlewares = getMiddlewares();\n  }\n  middlewares = Array.isArray(middlewares) ? middlewares : [middlewares];\n  const store = createStore(\n    // reducers\n    reducers,\n    // initial state\n    initialState,\n    // middlewares\n    compose(\n      applyMiddleware(...middlewares),\n      typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__\n        ? window.__REDUX_DEVTOOLS_EXTENSION__()\n        : f => f\n    )\n  );\n  store.reload = function reload(options = {}) {\n    if (typeof options.reducers === 'function') {\n      getReducers = options.reducers;\n      options.reducers = undefined;\n    }\n    if (!options.reducers && getReducers) {\n      options.reducers = getReducers();\n    }\n    if (options.reducers) {\n      reducers =\n        typeof options.reducers === 'object' ? combineReducers(options.reducers) : options.reducers;\n    }\n    this.replaceReducer(reducers);\n    return store;\n  };\n  return store;\n};\n\n// action\n// **Notes: client side, addAction to browser**\n// **Notes: server side, addAction to server share with every req**\nlet actions = {};\nexport const addAction = addedAction => {\n  actions = { ...actions, ...addedAction };\n  return actions;\n};\nexport const getActions = () => actions;\n// reducers\n// **Notes: client side, addReducer to browser**\n// **Notes: server side, addReducer to server share with every req**\nlet reducers = {};\n\nexport const addReducer = addedReducer => {\n  reducers = { ...reducers, ...addedReducer };\n  return reducers;\n};\nexport const getReducers = () => reducers;\n// middlewares\n// **Notes: client side, addMiddleware to browser**\n// **Notes: server side, addMiddleware to server share with every req**\nlet middlewares = [];\n\nexport const addMiddleware = (middlewareOrMiddlewareArray, options = {}) => {\n  const addedMiddleware = Array.isArray(middlewareOrMiddlewareArray)\n    ? middlewareOrMiddlewareArray\n    : [middlewareOrMiddlewareArray];\n  if (options.unshift) {\n    middlewares = [...addedMiddleware, ...middlewares];\n  } else {\n    middlewares = [...middlewares, ...addedMiddleware];\n  }\n  return middlewares;\n};\nexport const getMiddlewares = () => middlewares;\n\nlet store;\nexport const initStore = initialState => {\n  if (!store) {\n    store = configureStore(getReducers, initialState, []);\n  }\n  return store;\n};\nexport const getStore = () => {\n  return store;\n};\n"
  },
  {
    "path": "packages/vulcan-redux/lib/server/main.js",
    "content": "import './reduxInitialState';\n\nexport setupRedux from './setupRedux';\n\nexport * from '../modules/index';"
  },
  {
    "path": "packages/vulcan-redux/lib/server/reduxInitialState.js",
    "content": "import { runCallbacks } from 'meteor/vulcan:lib';\nimport setupRedux from './setupRedux';\n\nMeteor.startup(() => {\n  const initialState = runCallbacks({name: 'redux.initialState', item: {}});\n  setupRedux(initialState);\n});"
  },
  {
    "path": "packages/vulcan-redux/lib/server/setupRedux.js",
    "content": "import React from 'react';\nimport { Provider } from 'react-redux';\nimport { addCallback } from 'meteor/vulcan:core';\nimport { initStore } from '../modules/redux';\n\nconst setupRedux = initialState => {\n  const store = initStore(initialState);\n  addCallback('router.server.wrapper', function ReduxStoreProvider(app) {\n    return <Provider store={store}>{app}</Provider>;\n  });\n};\nexport default setupRedux;\n"
  },
  {
    "path": "packages/vulcan-redux/package.js",
    "content": "Package.describe({\n  name: 'vulcan:redux',\n  summary: 'Add Redux to Vulcan.',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n\nPackage.onTest(function(api) {\n  api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:core']);\n  api.mainModule('./test/client/index.js', 'client');\n  api.mainModule('./test/server/index.js', 'server');\n});\n"
  },
  {
    "path": "packages/vulcan-redux/test/client/index.js",
    "content": "import './initialState.test.js';\n"
  },
  {
    "path": "packages/vulcan-redux/test/client/initialState.test.js",
    "content": "import expect from 'expect';\nimport setupRedux from '../../lib/client/setupRedux.js';\nimport { getStore, addReducer } from '../../lib/modules/redux';\n\ndescribe('vulcan-redux/setupRedux', function () {\n  /* describe('redux should init with empty initialState', () => {\n    it('default initialisation', function() {\n      setupRedux();\n      const initialState = getStore().getState();\n      expect(initialState).toBeUndefined;\n    });\n  }); */\n  describe('redux should init with initialState', () => {\n    it('initial value', function () {\n      addReducer({\n        stage: (state = 0, action) => {\n          return state;\n        },\n      });\n      setupRedux({ stage: 1 });\n      const initialState = getStore().getState();\n      expect(initialState).toMatchObject({ stage: 1 });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-redux/test/server/index.js",
    "content": "// import './initialState.test.js';\nimport './initialStateWithValue.test.js';\n"
  },
  {
    "path": "packages/vulcan-redux/test/server/initialState.test.js",
    "content": "import expect from 'expect';\nimport setupRedux from '../../lib/server/setupRedux.js';\nimport { getStore } from '../../lib/modules/redux';\n\ndescribe('vulcan-redux/setupRedux', function() {\n  describe('server : redux should init with empty initialState', () => {\n    it('default initialisation', function() {\n      setupRedux();\n      const initialState = getStore().getState();\n      expect(initialState).toBeUndefined;\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-redux/test/server/initialStateWithValue.test.js",
    "content": "import expect from 'expect';\nimport setupRedux from '../../lib/server/setupRedux.js';\nimport { getStore, addReducer } from '../../lib/modules/redux';\n\ndescribe('vulcan-redux/setupRedux', function() {\n  describe('server : redux should init with initialState', () => {\n    it('initial value', function() {\n      addReducer({\n        stage: (state = 0, action) => {\n          return state;\n        },\n      });\n      setupRedux({ stage: 1 });\n      const initialState = getStore().getState();\n      expect(initialState).toMatchObject({ stage: 1 });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-scss/.github/workflows/comment-issue.yml",
    "content": "name: Add immediate comment on new issues\n\non:\n  issues:\n    types: [opened]\n\njobs:\n  createComment:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Create Comment\n        uses: peter-evans/create-or-update-comment@v1.4.2\n        with:\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            Thank you for submitting this issue!\n            \n            We, the Members of Meteor Community Packages take every issue seriously.\n            Our goal is to provide long-term lifecycles for packages and keep up\n            with the newest changes in Meteor and the overall NodeJs/JavaScript ecosystem.\n            \n            However, we contribute to these packages mostly in our free time.\n            Therefore, we can't guarantee your issues to be solved within certain time.\n            \n            If you think this issue is trivial to solve, don't hesitate to submit\n            a pull request, too! We will accompany you in the process with reviews and hints\n            on how to get development set up.\n            \n            Please also consider sponsoring the maintainers of the package.\n            If you don't know who is currently maintaining this package, just leave a comment\n            and we'll let you know\n"
  },
  {
    "path": "packages/vulcan-scss/.gitignore",
    "content": "\n.DS_Store\n\nmeteor/\n.build*\n.idea\n.npm\n"
  },
  {
    "path": "packages/vulcan-scss/.travis.yml",
    "content": "language: node_js\nsudo: required\nnode_js:\n  - \"12\"\n  - \"14\"\nbefore_install:\n  - \"curl -L http://git.io/ejPSng | /bin/sh\"\n"
  },
  {
    "path": "packages/vulcan-scss/.versions",
    "content": "allow-deny@2.0.0\nbabel-compiler@7.11.0\nbabel-runtime@1.5.2\nbase64@1.0.13\nbinary-heap@1.0.12\nblaze@3.0.0\nboilerplate-generator@2.0.0\ncaching-compiler@2.0.0\ncallback-hook@1.6.0\ncheck@1.4.2\ncore-runtime@1.0.0\nddp@1.4.2\nddp-client@3.0.0\nddp-common@1.4.3\nddp-server@3.0.0\ndiff-sequence@1.1.3\ndynamic-import@0.7.4\necmascript@0.16.9\necmascript-runtime@0.8.2\necmascript-runtime-client@0.12.2\necmascript-runtime-server@0.11.1\nejson@1.1.4\nfacts-base@1.0.2\nfetch@0.1.5\nvulcan:scss@4.17.0-rc.0\ngeojson-utils@1.0.12\nhtmljs@2.0.1\nid-map@1.2.0\ninter-process-messaging@0.1.2\nlocal-test:vulcan:scss@4.17.0-rc.0\nlogging@1.3.5\nmeteor@2.0.0\nminimongo@2.0.0\nmodern-browsers@0.1.11\nmodules@0.20.1\nmodules-runtime@0.13.2\nmongo@2.0.0\nmongo-decimal@0.1.4-beta300.7\nmongo-dev-server@1.1.1\nmongo-id@1.0.9\nnpm-mongo@4.17.3\nobserve-sequence@2.0.0\nordered-dict@1.2.0\npromise@1.0.0\nrandom@1.2.2\nreact-fast-refresh@0.2.9\nreactive-var@1.0.13\nreload@1.3.2\nretry@1.1.1\nroutepolicy@1.1.2\nsocket-stream-client@0.5.3\ntest-helpers@2.0.0\ntinytest@1.3.0\ntracker@1.3.4\ntypescript@5.4.3\nunderscore@1.6.4\nwebapp@2.0.0\nwebapp-hashing@1.1.2\n"
  },
  {
    "path": "packages/vulcan-scss/ISSUE_TEMPLATE.md",
    "content": "- [ ] Feature request\n- [ ] Bug report\n- [ ] Question\n\n<!--\nNote that the meteor build pipeline consists out of multiple steps (compiling, minification, injecting in the application etc.)\nThis plugin is only concerned with the first step: compilation of scss to css. The files we compile are fed to us by the meteor build tool. Therefore issures regarding anything but the compilation step should not be filed here.\n-->\n\n`meteor` version:\n\n`vulcan:scss` version:\n"
  },
  {
    "path": "packages/vulcan-scss/LICENSE.txt",
    "content": "Copyright (c) 2013 Mathew Hartley\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "packages/vulcan-scss/README.md",
    "content": "# Sass for Meteor\nThis is a Sass build plugin for Meteor. It compiles Sass files with node-sass.\n\n**Note that due to a limitation in libsass, there is no support for imports with indented syntax (sass). Indented syntax does work on the top-level. A version based on dart-sass is in the works which should remove this limitation.**\n\n**Meteor 1.7 introduced a change in how node_modules are handled, if you want to import sass from a node_module you need to symlink the package in your imports directory (more information below)**\n\n## Installation\n\nInstall using Meteor's package management system:\n\n```bash\n> meteor add vulcan:scss\n```\n\nIf you want to use it for your package, add it in your package control file's\n`onUse` block:\n\n```javascript\nPackage.onUse(function (api) {\n  ...\n  api.use('vulcan:scss');\n  ...\n});\n```\n\n## Compatibility\n<table>\n<thead>\n<tr><th>Meteor Version</th><th>Recommended vulcan:scss version</th></tr>\n</thead>\n<tbody>\n<tr><td>1.0 - 1.1</td><td>3.2.0</td></tr>\n<tr><td>1.2 - 1.3.1</td><td>3.4.2</td></tr>\n<tr><td>1.3.2+</td><td>3.8.0_1</td></tr>\n<tr><td>1.4.0</td><td>3.8.1</td></tr>\n<tr><td>1.4.1+</td><td>4.5.4</td></tr>\n<tr><td>1.6+</td><td>4.12.0</td></tr>\n</tbody>\n</table>\n\nSince `meteor 1.4.1+` (`vulcan:scss 3.9.0+`), we do not have prebuild binaries anymore. You are required to set up the [required toolchain](https://github.com/nodejs/node-gyp) yourselves.\n\n## Usage\nWithout any additional configuration after installation, this package automatically finds all `.scss` and `.sass` files in your project, compiles them with [node-sass](https://github.com/sass/node-sass), and includes the resulting CSS in the application bundle that Meteor sends to the client. The files can be anywhere in your project.\n\n### File types\n\nThere are two different types of files recognized by this package:\n\n- Sass sources (all `*.scss` and `*.sass` files that are not imports)\n- Sass imports/partials, which are:\n  * files that are prefixed with an underscore `_`\n  * marked as `isImport: true` in the package's `package.js` file:\n    `api.addFiles('x.scss', 'client', {isImport: true})`\n  * Starting from Meteor 1.3, all files in a directory named `imports/`\n\nThe source files are compiled automatically (eagerly loaded). The imports are not loaded by\nthemselves; you need to import them from one of the source files to use them.\n\nThe imports are intended to keep shared mixins and variables for your project,\nor to allow your package to provide several components which your package's\nusers can opt into one by one.\n\nEach compiled source file produces a separate CSS file.  (The\n`standard-minifiers` package merges them into one file afterwards.)\n\n### Importing\n\nYou can use the regular `@import` syntax to import any Sass files: sources or\nimports.\n\nBesides the usual way of importing files based on the relative path in the same\npackage (or app), you can also import files from other packages or apps with the\nfollowing syntax.\n\nImporting styles from a different package:\n\n```scss\n@import \"{my-package:pretty-buttons}/buttons/_styles.scss\";\n\n.my-button {\n  // use the styles imported from a package\n  @extend .pretty-button;\n}\n```\n\nImporting styles from the target app:\n\n```scss\n@import \"{}/client/styles/imports/colors.scss\";\n\n.my-nav {\n  // use a color from the app style pallete\n  background-color: @primary-branding-color;\n}\n```\n\nThis can also conveniently be used to import styles from npm modules for example:\n```scss\n@import \"{}/node_modules/module-name/stylesheet\";\n```\n\nNote that **Meteor 1.7** introduced a change so that files in `node_modules` aren't automatically compiled any more.\nThis requires you to add a symlink inside the `imports` directory to the pacakge in order for compilation to work.\nE.g.\n\n```\nmeteor npm install the-package\ncd imports\nln -s ../node_modules/the-package .\n```\n\nSee the [meteor changelog](https://github.com/meteor/meteor/blob/devel/History.md) for more information.\n\n#### Global include path\n\nSometimes a 3rd party module uses import paths that assume that the compiler is\nconfigured with specific `includePaths` option (e.g. Ionic, Bootstrap, etc.):\n```scss\n@import \"ionicons-icons\"; // This file is actually placed in another npm module!\n```\n\nCreate a configuration file named \"`scss-config.json`\" at the root of your Meteor\nproject to specify include paths that the compiler should use as an extra\npossibility to resolve import paths:\n```json\n{\n  \"includePaths\": [\n    \"{}/node_modules/ionicons/dist/scss/\"\n  ]\n}\n```\n\n\n### Sourcemaps\nThese are on by default.\n\n### Autoprefixer\nAs of Meteor 1.2 autoprefixer should preferably be installed as a separate plugin. You can do so by running:\n\n```\nmeteor remove standard-minifiers\nmeteor add seba:minifiers-autoprefixer@0.0.2\n```\n\nIn a Meteor 1.3+ project, do the same by running:\n```\nmeteor remove standard-minifier-css\nmeteor add seba:minifiers-autoprefixer\n```\n\n## LibSass vs Ruby Sass\nPlease note that this project uses [LibSass](https://github.com/hcatlin/libsass). LibSass is a C++ implementation of the Ruby Sass compiler. It has most of the features of the Ruby version, but not all of them. Things are improving, so please be patient. Before you ask, I have no intention of making a version of this package that links to the Ruby version instead.\n\nFor a quick rundown on what libsass does and doesn't (currently) do, [check here](http://sass-compatibility.github.io/).\n"
  },
  {
    "path": "packages/vulcan-scss/package.js",
    "content": "Package.describe({\n  summary: \"Clone of fourseven:scss\",\n  version: \"4.17.0-rc.0\",\n  git: \"https://github.com/Meteor-Community-Packages/meteor-scss.git\",\n  name: \"vulcan:scss\",\n});\n\nPackage.registerBuildPlugin({\n  name: \"compileScssBatch\",\n  use: [\"caching-compiler@1.2.2\", \"ecmascript@0.16.2\"],\n  sources: [\"plugin/compile-scss.js\"],\n  npmDependencies: {\n    sass: \"1.77.8\",\n    \"@babel/runtime\": \"7.24.5\",\n  },\n});\n\nPackage.onUse(function (api) {\n  api.versionsFrom([\"2.8.0\", \"3.0.1\"]);\n  api.use(\"isobuild:compiler-plugin@1.0.0\");\n});\n\nPackage.onTest(function (api) {\n  api.versionsFrom([\"2.8.0\", \"3.0.1\"]);\n  api.use([\"test-helpers\", \"tinytest\"]);\n\n  api.use([\"vulcan:scss\"]);\n\n  // Tests for .scss\n  api.addFiles([\n    \"test/scss/_emptyimport.scss\",\n    \"test/scss/_not-included.scss\",\n    \"test/scss/_top.scss\",\n    \"test/scss/_top3.scss\",\n    \"test/scss/empty.scss\",\n    \"test/scss/dir/_in-dir.scss\",\n    \"test/scss/dir/_in-dir2.scss\",\n    \"test/scss/dir/root.scss\",\n    \"test/scss/dir/subdir/_in-subdir.scss\",\n  ]);\n\n  api.addFiles(\"test/scss/top2.scss\", \"client\", { isImport: true });\n\n  // Test for includePaths\n  api.addFiles([\n    \"test/include-paths/include-paths.scss\",\n    \"test/include-paths/modules/module/_module.scss\",\n  ]);\n\n  api.mainModule(\"tests.js\", \"client\");\n});\n"
  },
  {
    "path": "packages/vulcan-scss/plugin/compile-scss.js",
    "content": "import sass from \"sass\";\nimport { promisify } from \"util\";\nconst path = Plugin.path;\nconst fs = Plugin.fs;\n\nconst compileSass = promisify(sass.render);\nconst { includePaths } = _getConfig(\"scss-config.json\");\nconst _includePaths = Array.isArray(includePaths) ? includePaths : [];\n\nPlugin.registerCompiler(\n  {\n    extensions: [\"scss\", \"sass\"],\n    archMatching: \"web\",\n  },\n  () => new SassCompiler()\n);\n\nconst convertToStandardPath = function convertToStandardPath(osPath) {\n  if (process.platform === \"win32\") {\n    // return toPosixPath(osPath, partialPath);\n    // p = osPath;\n    // Sometimes, you can have a path like \\Users\\IEUser on windows, and this\n    // actually means you want C:\\Users\\IEUser\n    if (osPath[0] === \"\\\\\") {\n      osPath = process.env.SystemDrive + osPath;\n    }\n\n    osPath = osPath.replace(/\\\\/g, \"/\");\n    if (osPath[1] === \":\") {\n      // transform \"C:/bla/bla\" to \"/c/bla/bla\"\n      osPath = `/${osPath[0]}${osPath.slice(2)}`;\n    }\n\n    return osPath;\n  }\n\n  return osPath;\n};\n\nconst rootDir = convertToStandardPath((process.env.PWD || process.cwd()) + \"/\");\n\n// CompileResult is {css, sourceMap}.\nclass SassCompiler extends MultiFileCachingCompiler {\n  constructor() {\n    super({\n      compilerName: \"sass\",\n      defaultCacheSize: 1024 * 1024 * 10,\n    });\n  }\n\n  getCacheKey(inputFile) {\n    return inputFile.getSourceHash();\n  }\n\n  compileResultSize(compileResult) {\n    return (\n      compileResult.css.length + this.sourceMapSize(compileResult.sourceMap)\n    );\n  }\n\n  // The heuristic is that a file is an import (ie, is not itself processed as a\n  // root) if it matches _*.sass, _*.scss\n  // This can be overridden in either direction via an explicit\n  // `isImport` file option in api.addFiles.\n  isRoot(inputFile) {\n    const fileOptions = inputFile.getFileOptions();\n\n    if (fileOptions.hasOwnProperty(\"isImport\")) {\n      return !fileOptions.isImport;\n    }\n\n    const pathInPackage = inputFile.getPathInPackage();\n    return !this.hasUnderscore(pathInPackage);\n  }\n\n  hasUnderscore(file) {\n    return path.basename(file).startsWith(\"_\");\n  }\n\n  compileOneFileLater(inputFile, getResult) {\n    inputFile.addStylesheet(\n      {\n        path: inputFile.getPathInPackage(),\n      },\n      async () => {\n        const result = await getResult();\n        return (\n          result && {\n            data: result.css,\n            sourceMap: result.sourceMap,\n          }\n        );\n      }\n    );\n  }\n\n  async compileOneFile(inputFile, allFiles) {\n    const referencedImportPaths = [];\n\n    var totalImportPath = [];\n    var sourceMapPaths = [`.${inputFile.getDisplayPath()}`];\n\n    const addUnderscore = (file) => {\n      if (!this.hasUnderscore(file)) {\n        file = path.join(path.dirname(file), `_${path.basename(file)}`);\n      }\n      return file;\n    };\n\n    const getRealImportPath = (importPath) => {\n      const isAbsolute = importPath.startsWith(\"/\");\n\n      //SASS has a whole range of possible import files from one import statement, try each of them\n      const possibleFiles = [];\n\n      //If the referenced file has no extension, try possible extensions, starting with extension of the parent file.\n      let possibleExtensions = [\"scss\", \"sass\", \"css\"];\n\n      if (!importPath.match(/\\.s?(a|c)ss$/)) {\n        possibleExtensions = [\n          inputFile.getExtension(),\n          ...possibleExtensions.filter((e) => e !== inputFile.getExtension()),\n        ];\n        for (const extension of possibleExtensions) {\n          possibleFiles.push(`${importPath}.${extension}`);\n        }\n      } else {\n        possibleFiles.push(importPath);\n      }\n\n      //Try files prefixed with underscore\n      for (const possibleFile of possibleFiles) {\n        if (!this.hasUnderscore(possibleFile)) {\n          possibleFiles.push(addUnderscore(possibleFile));\n        }\n      }\n\n      //Try if one of the possible files exists\n      for (const possibleFile of possibleFiles) {\n        if (\n          (isAbsolute && fileExists(possibleFile)) ||\n          (!isAbsolute && allFiles.has(possibleFile))\n        ) {\n          return { absolute: isAbsolute, path: possibleFile };\n        }\n      }\n      //Nothing found...\n      return null;\n    };\n\n    const fixTilde = function (thePath) {\n      let newPath = thePath;\n      // replace ~ with {}/....\n      if (newPath.startsWith(\"~\")) {\n        newPath = newPath.replace(\"~\", \"{}/node_modules/\");\n      }\n\n      // add {}/ if starts with node_modules\n      if (!newPath.startsWith(\"{\")) {\n        if (newPath.startsWith(\"node_modules\")) {\n          newPath = \"{}/\" + newPath;\n        }\n        if (newPath.startsWith(\"/node_modules\")) {\n          newPath = \"{}\" + newPath;\n        }\n      }\n\n      return newPath;\n    };\n\n    //Handle import statements found by the sass compiler, used to handle cross-package imports\n    const importer = function (url, prev, done) {\n      prev = convertToStandardPath(prev);\n      prev = fixTilde(prev);\n      if (!totalImportPath.length) {\n        totalImportPath.push(prev);\n      }\n\n      if (prev !== undefined) {\n        // iterate backwards over totalImportPath and remove paths that don't equal the prev url\n        for (let i = totalImportPath.length - 1; i >= 0; i--) {\n          // check if importPath contains prev, if it doesn't, remove it. Up until we find a path that does contain it\n          if (totalImportPath[i] == prev) {\n            break;\n          } else {\n            // remove last item (which has to be item i because we are iterating backwards)\n            totalImportPath.splice(i, 1);\n          }\n        }\n      }\n      let importPath = convertToStandardPath(url);\n      importPath = fixTilde(importPath);\n      for (let i = totalImportPath.length - 1; i >= 0; i--) {\n        if (importPath.startsWith(\"/\") || importPath.startsWith(\"{\")) {\n          break;\n        }\n        // 'path' is the nodejs path module\n        importPath = path.join(path.dirname(totalImportPath[i]), importPath);\n      }\n\n      let accPosition = importPath.indexOf(\"{\");\n      if (accPosition > -1) {\n        importPath = importPath.substr(accPosition, importPath.length);\n      }\n\n      // TODO: This fix works.. BUT if you edit the scss/css file it doesn't recompile! Probably because of the absolute path problem\n      if (importPath.startsWith(\"{\")) {\n        // replace {}/node_modules/ for rootDir + \"node_modules/\"\n        importPath = importPath.replace(\n          /^(\\{\\}\\/node_modules\\/)/,\n          rootDir + \"node_modules/\"\n        );\n        // importPath = importPath.replace('{}/node_modules/', rootDir + \"node_modules/\");\n        if (importPath.endsWith(\".css\")) {\n          // .css files aren't in allFiles. Replace {}/ for absolute path.\n          importPath = importPath.replace(/^(\\{\\}\\/)/, rootDir);\n        }\n      }\n\n      try {\n        let parsed = getRealImportPath(importPath);\n        if (!parsed) {\n          parsed = _getRealImportPathFromIncludes(url, getRealImportPath);\n        }\n        if (!parsed) {\n          //Nothing found...\n          throw new Error(\n            `File to import: ${url} not found in file: ${\n              totalImportPath[totalImportPath.length - 2]\n            }`\n          );\n        }\n        totalImportPath.push(parsed.path);\n\n        if (parsed.absolute) {\n          sourceMapPaths.push(parsed.path);\n          done({\n            contents: fs.readFileSync(parsed.path, \"utf8\"),\n            file: parsed.path,\n          });\n        } else {\n          referencedImportPaths.push(parsed.path);\n          sourceMapPaths.push(decodeFilePath(parsed.path));\n          done({\n            contents: allFiles.get(parsed.path).getContentsAsString(),\n            file: parsed.path,\n          });\n        }\n      } catch (e) {\n        return done(e);\n      }\n    };\n\n    //Start compile sass (async)\n    const options = {\n      sourceMap: true,\n      sourceMapContents: true,\n      sourceMapEmbed: false,\n      sourceComments: false,\n      omitSourceMapUrl: true,\n      sourceMapRoot: \".\",\n      indentedSyntax: inputFile.getExtension() === \"sass\",\n      outFile: `.${inputFile.getBasename()}`,\n      importer,\n      includePaths: [],\n      precision: 10,\n    };\n\n    options.file = this.getAbsoluteImportPath(inputFile);\n\n    options.data = inputFile.getContentsAsBuffer().toString(\"utf8\");\n\n    //If the file is empty, options.data is an empty string\n    // In that case options.file will be used by node-sass,\n    // which it can not read since it will contain a meteor package or app reference '{}'\n    // This is one workaround, another one would be to not set options.file, in which case the importer 'prev' will be 'stdin'\n    // However, this would result in problems if a file named stdín.scss would exist.\n    // Not the most elegant of solutions, but it works.\n    if (!options.data.trim()) {\n      options.data = \"$fakevariable_ae7bslvbp2yqlfba : blue;\";\n    }\n\n    let output;\n    try {\n      output = await compileSass(options);\n    } catch (e) {\n      inputFile.error({\n        message: `Scss compiler error: ${e.formatted}\\n`,\n        sourcePath: inputFile.getDisplayPath(),\n      });\n      return null;\n    }\n    //End compile sass\n\n    //Start fix sourcemap references\n    if (output.map) {\n      const map = JSON.parse(output.map.toString(\"utf-8\"));\n      map.sources = sourceMapPaths;\n      output.map = map;\n    }\n    //End fix sourcemap references\n\n    const compileResult = {\n      css: output.css.toString(\"utf-8\"),\n      sourceMap: output.map,\n    };\n    return { compileResult, referencedImportPaths };\n  }\n\n  addCompileResult(inputFile, compileResult) {\n    inputFile.addStylesheet({\n      data: compileResult.css,\n      path: `${inputFile.getPathInPackage()}.css`,\n      sourceMap: compileResult.sourceMap,\n    });\n  }\n}\n\nfunction _getRealImportPathFromIncludes(importPath, getRealImportPathFn) {\n  let possibleFilePath, foundFile;\n\n  for (let includePath of _includePaths) {\n    possibleFilePath = path.join(includePath, importPath);\n    foundFile = getRealImportPathFn(possibleFilePath);\n\n    if (foundFile) {\n      return foundFile;\n    }\n  }\n\n  return null;\n}\n\n/**\n * Build a path from current process working directory (i.e. meteor project\n * root) and specified file name, try to get the file and parse its content.\n * @param configFileName\n * @returns {{}}\n * @private\n */\nfunction _getConfig(configFileName) {\n  const appdir = process.env.PWD || process.cwd();\n  const custom_config_filename = path.join(appdir, configFileName);\n  let userConfig = {};\n\n  if (fileExists(custom_config_filename)) {\n    userConfig = fs.readFileSync(custom_config_filename, {\n      encoding: \"utf8\",\n    });\n    userConfig = JSON.parse(userConfig);\n  } else {\n    //console.warn('Could not find configuration file at ' + custom_config_filename);\n  }\n  return userConfig;\n}\n\nfunction decodeFilePath(filePath) {\n  const match = filePath.match(/{(.*)}\\/(.*)$/);\n  if (!match) {\n    throw new Error(`Failed to decode sass path: ${filePath}`);\n  }\n\n  if (match[1] === \"\") {\n    // app\n    return match[2];\n  }\n\n  return `packages/${match[1]}/${match[2]}`;\n}\n\nfunction fileExists(file) {\n  if (fs.statSync) {\n    try {\n      fs.statSync(file);\n    } catch (e) {\n      return false;\n    }\n    return true;\n  } else if (fs.existsSync) {\n    return fs.existsSync(file);\n  }\n}\n"
  },
  {
    "path": "packages/vulcan-scss/scss-config.json",
    "content": "{\n  \"includePaths\": [\n    \"{local-test:vulcan:scss}/test/include-paths/modules/module\"\n  ]\n}\n"
  },
  {
    "path": "packages/vulcan-scss/test/include-paths/include-paths.scss",
    "content": "@import \"_module\";\n"
  },
  {
    "path": "packages/vulcan-scss/test/include-paths/modules/module/_module.scss",
    "content": ".from-include-paths {\n  border-bottom-style: outset;\n}\n"
  },
  {
    "path": "packages/vulcan-scss/test/scss/_emptyimport.scss",
    "content": ""
  },
  {
    "path": "packages/vulcan-scss/test/scss/_not-included.scss",
    "content": "/* This scss file isn't included by any main file */\n.scss-el0 { border-style: ridge; }"
  },
  {
    "path": "packages/vulcan-scss/test/scss/_top.scss",
    "content": "$scss-el1-style: dotted;\n\n@import \"top3\";\n@import \"emptyimport.scss\";\n\n// Make sure regular CSS import doesn't make the compiler explode\n@import url(\"http://hello.myfonts.net/count/2c4b9d\");"
  },
  {
    "path": "packages/vulcan-scss/test/scss/_top3.scss",
    "content": "$scss-el6-style: inset;"
  },
  {
    "path": "packages/vulcan-scss/test/scss/dir/_in-dir.scss",
    "content": "$scss-el3-style: solid;"
  },
  {
    "path": "packages/vulcan-scss/test/scss/dir/_in-dir2.scss",
    "content": "$scss-el4-style: double;"
  },
  {
    "path": "packages/vulcan-scss/test/scss/dir/root.scss",
    "content": "@import \"../_top.scss\";\n@import \"{local-test:vulcan:scss}/test/scss/top2\";\n@import \"_in-dir\";\n@import \"./in-dir2\";\n@import \"subdir/in-subdir.scss\";\n\n\n.scss-el1 { border-style: $scss-el1-style; }\n.scss-el2 { border-style: $scss-el2-style; }\n.scss-el3 { border-style: $scss-el3-style; }\n.scss-el4 { border-style: $scss-el4-style; }\n.scss-el5 { border-style: $scss-el5-style; }\n.scss-el6 { border-style: $scss-el6-style; }"
  },
  {
    "path": "packages/vulcan-scss/test/scss/dir/subdir/_in-subdir.scss",
    "content": "$scss-el5-style: groove;"
  },
  {
    "path": "packages/vulcan-scss/test/scss/empty.scss",
    "content": ""
  },
  {
    "path": "packages/vulcan-scss/test/scss/top2.scss",
    "content": "$scss-el2-style: dashed;"
  },
  {
    "path": "packages/vulcan-scss/tests.js",
    "content": "Tinytest.add(\"sass/scss - imports\", function (test) {\n  var div = document.createElement('div');\n  document.body.appendChild(div);\n\n  var prefixes = ['scss'];\n\n  try {\n    var t = function (className, style) {\n      prefixes.forEach(function(prefix){\n        div.className = prefix + '-' + className;\n\n        // Read 'border-top-style' instead of 'border-style' (which is set\n        // by the stylesheet) because only the individual styles are computed\n        // and can be retrieved. Trying to read the synthetic 'border-style'\n        // gives an empty string.\n        test.equal(getStyleProperty(div, 'border-top-style'), style,  div.className);\n      });\n\n    };\n    t('el1', 'dotted');\n    t('el2', 'dashed');\n    t('el3', 'solid');\n    t('el4', 'double');\n    t('el5', 'groove');\n    t('el6', 'inset');\n\n    // This is assigned to 'ridge' in not-included.s(a|c)ss, which is ... not\n    // included. So that's why it should be 'none'.  (This tests that we don't\n    // process non-main files.)\n    t('el0', 'none');\n  } finally {\n    document.body.removeChild(div);\n  }\n});\n\n\nTinytest.add('sass/scss - import from includePaths', function (test) {\n\n  var div = document.createElement('div');\n\n  document.body.appendChild(div);\n\n  try {\n\n    div.className = 'from-include-paths';\n\n    test.equal(getStyleProperty(div, 'border-bottom-style'), 'outset',  div.className);\n\n  } finally {\n\n    document.body.removeChild(div);\n\n  }\n\n});\n"
  },
  {
    "path": "packages/vulcan-styled-components/README.md",
    "content": "Styled components package."
  },
  {
    "path": "packages/vulcan-styled-components/lib/client/main.js",
    "content": "export * from '../modules/index';\n"
  },
  {
    "path": "packages/vulcan-styled-components/lib/modules/index.js",
    "content": ""
  },
  {
    "path": "packages/vulcan-styled-components/lib/server/main.js",
    "content": "import setupStyledComponents from './setupStyledComponents';\nsetupStyledComponents();\n\nexport * from '../modules/index';\n"
  },
  {
    "path": "packages/vulcan-styled-components/lib/server/setupStyledComponents.js",
    "content": "// Setup SSR\nimport { ServerStyleSheet } from 'styled-components';\nimport { addCallback } from 'meteor/vulcan:core';\n\nconst setupStyledComponents = () => {\n  addCallback('router.server.renderWrapper', function collectStyles(app, { context }) {\n    const stylesheet = new ServerStyleSheet();\n    // @see https://www.styled-components.com/docs/advanced/#example\n    const wrappedApp = stylesheet.collectStyles(app);\n    // store the stylesheet to reuse it later\n    context.stylesheet = stylesheet;\n    return wrappedApp;\n  });\n\n  addCallback('router.server.postRender', function appendStyleTags(sink, { context }) {\n    sink.appendToHead(context.stylesheet.getStyleTags());\n    return sink;\n  });\n};\n\nexport default setupStyledComponents;\n"
  },
  {
    "path": "packages/vulcan-styled-components/package.js",
    "content": "Package.describe({\n  name: 'vulcan:styled-components',\n  summary: 'Add Styled Components to Vulcan.',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "packages/vulcan-subscribe/README.md",
    "content": "# vulcan:subscribe\n\nThis optional package for Vulcan lets your users subscribe to the different domains (collections) of your application.\n\n### Dependencies & usage\nExplicit dependency on `vulcan:users` to enable permissions.\n\nIf `vulcan:posts` is enabled, your users will be able to subscribe to:\n* new posts from users they follow (subscribed to)\n* new comments on a post they are subscribed to\n\nIf `vulcan:categories` is enabled, your users will be able to subscribe to new posts in a category.\n\n### Basic usage\n\nThis package gives you access to several methods of the type `collection.subscribe` & `collection.unsubscribe`. Default group of users can subscribe to any activated domain (see above) with the following methods (action) :\n```\nusers.subscribe\nusers.unsubscribe\nposts.subscribe\nposts.unsubscribe\ncategories.subscribe\ncategories.unsubscribe\n```\n\nThis package also provides a reusable component called `SubscribeTo` to subscribe to an document of collection.\n\nThis component takes the `document` as a props. It can trigger any method described below:\n\n```jsx\n// for example, in PostItem.jsx\n<Components.SubscribeTo document={post} />\n\n// for example, in UsersProfile.jsx\n<Components.SubscribeTo document={user} />\n\n// for example, in Category.jsx\n<Components.SubscribeTo document={category} />\n```\n\n### Extend to other collections than Users, Posts, Categories\nThis package exports a function called `subscribeMutationsGenerator` that takes a collection as an argument and create the associated methods code :\n\n```js\n// in my custom package\nimport subscribeMutationsGenerator from 'meteor/vulcan:subscribe';\nimport Movies from './collection.js';\n\n// the function creates the code and give it to the graphql server\nsubscribeMutationsGenerator(Movies);\n```\n\nThis will creates for you the mutations `moviesSubscribe` & `moviesUnsubscribe` than can be used in the `SubscribeTo` component:\n```jsx\n// in my custom component\n<Components.SubscribeTo document={movie} />\n```\n\nYou'll also need to write the relevant callbacks, custom fields & permissions to run whenever a user is subscribed to your custom collection's item. See these files for inspiration.\n*Note: it's more or less always the same thing*\n\n* Custom fields: https://github.com/TelescopeJS/Telescope/blob/devel/packages/vulcan-subscribe/lib/custom_fields.js#L47-L75\n* Callbacks: https://github.com/TelescopeJS/Telescope/blob/devel/packages/vulcan-subscribe/lib/callbacks.js#L13-L36\n* Permissions: https://github.com/TelescopeJS/Telescope/blob/devel/packages/vulcan-subscribe/lib/permissions.js\n\n### Reusable component to show a list of subscribed items\n\nThere was formerly a component that showed a list of subscribed posts. While reducing the depencies to other packages, it broke. It's on the roadmap to re-enable it. Feel free to discuss about it [on the Slack channel](http://slack.telescopeapp.org/) if you want to build it!\n\nOriginal PR & discussion can be found here: https://github.com/TelescopeJS/Telescope/pull/1425\n"
  },
  {
    "path": "packages/vulcan-subscribe/lib/callbacks.js",
    "content": "import { createNotification } from 'meteor/vulcan:notifications';\nimport Users from 'meteor/vulcan:users';\nimport { addCallback } from 'meteor/vulcan:core';\n\n// TODO: don't import these callbacks server-side (reduce bundle size of what's sent to the client)\n// note: even if all these callbacks are async, they are imported on the client so they pop in the cheatsheet when debug is enabled\n\n// note: leverage weak dependencies on packages\nconst Comments = Package['vulcan:comments'] ? Package['vulcan:comments'].default : null;\nconst Posts = Package['vulcan:posts'] ? Package['vulcan:posts'].default : null;\nconst Categories = Package['vulcan:categories'] ? Package['vulcan:categories'].default : null;\n\n/**\n * @summary Notify users subscribed to 'another user' whenever another user posts\n */\nfunction SubscribedCategoriesNotifications (post) {\n\n  if (Meteor.isServer && !!post.categories && !!post.categories.length) {\n    // get the subscribers of the different categories from the post's categories\n    const subscribers = post.categories\n                              // find the category from its id\n                              .map(categoryId => Categories.findOne({_id: categoryId}))\n                              // clean the array if none subscribe to this category\n                              .filter(category => typeof category.subscribers !== 'undefined' || !!category.subscribers)\n                              // build the subscribers list interested in these categories\n                              .reduce((subscribersList, category) => [...subscribersList, ...category.subscribers], []);\n\n    let userIdsNotified = [];\n    const notificationData = {\n      post: _.pick(post, '_id', 'userId', 'title', 'url', 'author')\n    };\n\n    if (!!subscribers && !!subscribers.length) {\n      // remove userIds of users that have already been notified and of post's author \n      let subscriberIdsToNotify = _.uniq(_.difference(subscribers, userIdsNotified, [post.userId]));\n      \n      createNotification(subscriberIdsToNotify, 'newPost', notificationData);\n\n      userIdsNotified = userIdsNotified.concat(subscriberIdsToNotify);\n    }\n  }\n}\n\n/**\n * @summary Notify users subscribed to the comment's thread\n */\nfunction SubscribedPostNotifications (comment) {\n    // note: dummy content has disableNotifications set to true\n  if (Meteor.isServer && !comment.disableNotifications) {\n\n    const post = Posts.findOne(comment.postId);\n\n    let userIdsNotified = [];\n    const notificationData = {\n      comment: _.pick(comment, '_id', 'userId', 'author', 'htmlBody', 'postId'),\n      post: _.pick(post, '_id', 'userId', 'title', 'url')\n    };\n\n    if (!!post.subscribers && !!post.subscribers.length) {\n      // remove userIds of users that have already been notified\n      // and of comment author (they could be replying in a thread they're subscribed to)\n      let subscriberIdsToNotify = _.difference(post.subscribers, userIdsNotified, [comment.userId]);\n      createNotification(subscriberIdsToNotify, 'newCommentSubscribed', notificationData);\n\n      userIdsNotified = userIdsNotified.concat(subscriberIdsToNotify);\n    }\n  }\n}\n\n/**\n * @summary Notify users subscribed to 'another user' whenever another user posts\n */\nfunction SubscribedUsersNotifications (post) {\n  if (Meteor.isServer) {\n\n    let userIdsNotified = [];\n    const notificationData = {\n      post: _.pick(post, '_id', 'userId', 'title', 'url', 'author')\n    };\n\n    const user = Users.findOne({_id: post.userId});\n\n    if (!!user.subscribers && !!user.subscribers.length) {\n      // remove userIds of users that have already been notified and of post's author \n      let subscriberIdsToNotify = _.difference(user.subscribers, userIdsNotified, [user._id]);\n      \n      createNotification(subscriberIdsToNotify, 'newPost', notificationData);\n\n      userIdsNotified = userIdsNotified.concat(subscriberIdsToNotify);\n    }\n  }\n}\n\nif (!!Posts && !!Comments) {\n  addCallback('comments.new.async', SubscribedPostNotifications);\n}\n\nif (!!Posts) {\n  addCallback('posts.new.async', SubscribedUsersNotifications);\n}\n\nif (!!Posts && !!Categories) {\n  addCallback('posts.new.async', SubscribedCategoriesNotifications);\n}\n"
  },
  {
    "path": "packages/vulcan-subscribe/lib/components/SubscribeTo.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { graphql } from '@apollo/client/react/hoc';\nimport { compose } from 'meteor/vulcan:lib';\nimport gql from 'graphql-tag';\nimport Users from 'meteor/vulcan:users';\nimport { Components, withCurrentUser, withMessages, registerComponent, Utils } from 'meteor/vulcan:core';\n\n// boolean -> unsubscribe || subscribe\nconst getSubscribeAction = subscribed => subscribed ? 'unsubscribe' : 'subscribe'; \n\nclass SubscribeToActionHandler extends PureComponent {\n\n  constructor(props, context) {\n    super(props, context);\n\n    this.onSubscribe = this.onSubscribe.bind(this);\n    \n    this.state = {\n      subscribed: !!Users.isSubscribedTo(props.currentUser, props.document, props.documentType),\n    };\n  }\n\n  async onSubscribe(e) {\n    try {\n      e.preventDefault();\n\n      const { document, documentType } = this.props;\n      const action = getSubscribeAction(this.state.subscribed);\n      \n      // todo: change the mutation to auto-update the user in the store\n      await this.setState(prevState => ({subscribed: !prevState.subscribed}));\n\n      // mutation name will be for example postsSubscribe\n      await this.props[`${documentType + Utils.capitalize(action)}`]({documentId: document._id});\n\n      // success message will be for example posts.subscribed\n      this.props.flash(this.context.intl.formatMessage(\n        {id: `${documentType}.${action}d`}, \n        // handle usual name properties\n        {name: document.name || document.title || document.displayName}\n      ), 'success');\n      \n\n    } catch(error) {\n      this.props.flash(error.message, 'error');\n    }\n  }\n\n  render() {\n    const { currentUser, document, documentType } = this.props;\n    const { subscribed } = this.state;\n    \n    const action = `${documentType}.${getSubscribeAction(subscribed)}`;\n    \n    // can't subscribe to yourself or to own post (also validated on server side)\n    if (!currentUser || !document || (documentType === 'posts' && document.userId === currentUser._id) || (documentType === 'users' && document._id === currentUser._id)) {\n      return null;\n    }\n\n    const className = this.props.className ? this.props.className : '';\n    \n    return Users.canDo(currentUser, action) ? <a className={className} onClick={this.onSubscribe}><Components.FormattedMessage id={action} /></a> : null;\n  }\n\n}\n\nSubscribeToActionHandler.propTypes = {\n  document: PropTypes.object.isRequired,\n  className: PropTypes.string,\n  currentUser: PropTypes.object,\n};\n\nSubscribeToActionHandler.contextTypes = {\n  intl: intlShape\n};\n\nconst subscribeMutationContainer = ({documentType, actionName}) => graphql(gql`\n  mutation ${documentType + actionName}($documentId: String) {\n    ${documentType + actionName}(documentId: $documentId) {\n      _id\n      subscribedItems\n    }\n  }\n`, {\n  props: ({ownProps, mutate}) => ({\n    [documentType + actionName]: vars => {\n      return mutate({ \n        variables: vars,\n      });\n    },\n  }),\n});\n\nconst SubscribeTo = props => {\n  \n  const documentType = Utils.getCollectionNameFromTypename(props.document.__typename);\n  \n  const withSubscribeMutations = ['Subscribe', 'Unsubscribe'].map(actionName => subscribeMutationContainer({documentType, actionName})); \n  \n  const EnhancedHandler = compose(...withSubscribeMutations)(SubscribeToActionHandler);\n  \n  return <EnhancedHandler {...props} documentType={documentType} />;\n};\n\nregisterComponent('SubscribeTo', SubscribeTo, withCurrentUser, withMessages);\n"
  },
  {
    "path": "packages/vulcan-subscribe/lib/custom_fields.js",
    "content": "import Users from 'meteor/vulcan:users';\n\n// note: leverage weak dependencies on packages\nconst Posts = Package['vulcan:posts'] ? Package['vulcan:posts'].default : null;\nconst Categories = Package['vulcan:categories'] ? Package['vulcan:categories'].default : null;\n\nUsers.addField([\n  {\n    fieldName: 'subscribedItems',\n    fieldSchema: {\n      type: Object,\n      optional: true,\n      blackbox: true,\n      hidden: true, // never show this\n    }\n  },\n  {\n    fieldName: 'subscribers',\n    fieldSchema: {\n      type: Array,\n      optional: true,\n      hidden: true, // never show this,\n    }\n  },\n  {\n    fieldName: 'subscribers.$',\n    fieldSchema: {\n      type: String,\n      optional: true,\n      hidden: true, // never show this,\n    }\n  },\n  {\n    fieldName: 'subscriberCount',\n    fieldSchema: {\n      type: Number,\n      optional: true,\n      hidden: true, // never show this\n    }\n  }\n]);\n\n// check if vulcan:posts exists, if yes, add the custom fields to Posts\nif (!!Posts) {\n\n  Posts.addField([\n    {\n      fieldName: 'subscribers',\n      fieldSchema: {\n        type: Array,\n        optional: true,\n        hidden: true, // never show this\n      }\n    },\n    {\n      fieldName: 'subscribers.$',\n      fieldSchema: {\n        type: String,\n        optional: true,\n        hidden: true, // never show this\n      }\n    },\n    {\n      fieldName: 'subscriberCount',\n      fieldSchema: {\n        type: Number,\n        optional: true,\n        hidden: true, // never show this\n      }\n    }\n  ]);\n\n}\n\n// check if vulcan:categories exists, if yes, add the custom fields to Categories\nif (!!Categories) {\n\n  Categories.addField([\n    {\n      fieldName: 'subscribers',\n      fieldSchema: {\n        type: Array,\n        optional: true,\n        hidden: true, // never show this\n      }\n    },\n    {\n      fieldName: 'subscribers.$',\n      fieldSchema: {\n        type: String,\n        optional: true,\n        hidden: true, // never show this\n      }\n    },\n    {\n      fieldName: 'subscriberCount',\n      fieldSchema: {\n        type: Number,\n        optional: true,\n        hidden: true, // never show this\n      }\n    }\n  ]);\n\n}\n"
  },
  {
    "path": "packages/vulcan-subscribe/lib/fragments.js",
    "content": "import { extendFragment } from 'meteor/vulcan:core';\n\nextendFragment('UsersCurrent', `\n  subscribedItems\n`);"
  },
  {
    "path": "packages/vulcan-subscribe/lib/helpers.js",
    "content": "import Users from 'meteor/vulcan:users';\nimport { Utils } from 'meteor/vulcan:core';\n\nUsers.isSubscribedTo = (user, document) => {\n\n  if (!user || !document) {\n    // should return an error\n    return false;\n  }\n  \n  const { __typename, _id: itemId } = document;\n  const documentType = Utils.capitalize(Utils.getCollectionNameFromTypename(__typename));\n  \n  if (user.subscribedItems && user.subscribedItems[documentType]) {\n    return !!user.subscribedItems[documentType].find(subscribedItems => subscribedItems.itemId === itemId);\n  } else {\n    return false;\n  }\n};\n"
  },
  {
    "path": "packages/vulcan-subscribe/lib/modules.js",
    "content": "import './callbacks.js';\nimport './custom_fields.js';\nimport './helpers.js';\nimport subscribeMutationsGenerator from './mutations.js';\nimport './views.js';\nimport './permissions.js';\nimport './fragments.js';\n\nimport './components/SubscribeTo.jsx';\n\nexport default subscribeMutationsGenerator;\n"
  },
  {
    "path": "packages/vulcan-subscribe/lib/mutations.js",
    "content": "import Users from 'meteor/vulcan:users';\nimport { Utils, addGraphQLMutation, addGraphQLResolvers, Connectors } from 'meteor/vulcan:core';\n\n/**\n * @summary Verify that the un/subscription can be performed\n * @param {String} action\n * @param {Collection} collection\n * @param {String} itemId\n * @param {Object} user\n * @returns {Object} collectionName, fields: object, item, hasSubscribedItem: boolean\n */\nconst prepareSubscription = (action, collection, itemId, user) => {\n\n  // get item's collection name\n  const collectionName = collection._name.slice(0,1).toUpperCase() + collection._name.slice(1);\n\n  // get item data\n  const item = collection.findOne(itemId);\n\n  // there no user logged in or no item, abort process\n  if (!user || !item) {\n    return false;\n  }\n\n  // edge case: Users collection\n  if (collectionName === 'Users') {\n    // someone can't subscribe to themself, abort process\n    if (item._id === user._id) {\n      return false;\n    }\n  } else {\n    // the item's owner is the subscriber, abort process\n    if (item.userId && item.userId === user._id) {\n      return false;\n    }\n  }\n\n  // assign the right fields depending on the collection\n  const fields = {\n    subscribers: 'subscribers',\n    subscriberCount: 'subscriberCount',\n  };\n\n  // return true if the item has the subscriber's id in its fields\n  const hasSubscribedItem = !!_.deep(item, fields.subscribers) && _.deep(item, fields.subscribers) && _.deep(item, fields.subscribers).indexOf(user._id) !== -1;\n\n  // assign the right update operator and count depending on the action type\n  const updateQuery = action === 'subscribe' ? {\n    findOperator: '$ne', // where 'IT' isn't...\n    updateOperator: '$addToSet', // ...add 'IT' to the array...\n    updateCount: 1, // ...and log the addition +1\n  } : {\n    findOperator: '$eq', // where 'IT' is...\n    updateOperator: '$pull', // ...remove 'IT' from the array...\n    updateCount: -1, // ...and log the subtraction -1\n  };\n\n  // return the utility object to pursue\n  return {\n    collectionName,\n    fields,\n    item,\n    hasSubscribedItem,\n    ...updateQuery,\n  };\n};\n\n/**\n * @summary Perform the un/subscription after verification: update the collection item & the user\n * @param {String} action\n * @param {Collection} collection\n * @param {String} itemId\n * @param {Object} user: current user (xxx: legacy, to replace with this.userId)\n * @returns {Boolean}\n */\nconst performSubscriptionAction = (action, collection, itemId, user) => {\n\n  // subscription preparation to verify if can pursue and give shorthand variables\n  const subscription = prepareSubscription(action, collection, itemId, user);\n\n  // Abort process if the situation matches one of these cases:\n  // - subscription preparation failed (ex: no user, no item, subscriber is author's item, ... see all cases above)\n  // - the action is subscribe but the user has already subscribed to this item\n  // - the action is unsubscribe but the user hasn't subscribed to this item\n  if (!subscription || (action === 'subscribe' && subscription.hasSubscribedItem) || (action === 'unsubscribe' && !subscription.hasSubscribedItem)) {\n    throw Error(Utils.encodeIntlError({id: 'app.mutation_not_allowed', value: 'Already subscribed'}));\n  }\n\n  // shorthand for useful variables\n  const { collectionName, fields, item, findOperator, updateOperator, updateCount } = subscription;\n\n  // Perform the action, eg. operate on the item's collection\n  const result = Connectors.update(collection, {\n    _id: itemId,\n    // if it's a subscription, find  where there are not the user (ie. findOperator = $ne), else it will be $in\n    [fields.subscribers]: { [findOperator]: user._id }\n  }, {\n    // if it's a subscription, add a subscriber (ie. updateOperator = $addToSet), else it will be $pull\n    [updateOperator]: { [fields.subscribers]: user._id },\n    // if it's a subscription, the count is incremented of 1, else decremented of 1\n    $inc: { [fields.subscriberCount]: updateCount },\n  });\n\n  // log the operation on the subscriber if it has succeeded\n  if (result > 0) {\n    // id of the item subject of the action\n    let loggedItem = {\n      itemId: item._id,\n    };\n\n    // in case of subscription, log also the date\n    if (action === 'subscribe') {\n      loggedItem = {\n        ...loggedItem,\n        subscribedAt: new Date()\n      };\n    }\n\n    // update the user's list of subscribed items\n    Users.update({\n      _id: user._id\n    }, {\n      [updateOperator]: { [`subscribedItems.${collectionName}`]: loggedItem }\n    });\n\n    const updatedUser = Users.findOne({_id: user._id}, {fields: {_id:1, subscribedItems: 1}});\n    \n    return updatedUser;\n  } else {\n    throw Error(Utils.encodeIntlError({id: 'app.something_bad_happened'}));\n  }\n};\n\n/**\n * @summary Generate mutations 'collection.subscribe' & 'collection.unsubscribe' automatically\n * @params {Array[Collections]} collections\n */\n const subscribeMutationsGenerator = (collection) => {\n\n   // generic mutation function calling the performSubscriptionAction\n   const genericMutationFunction = (collectionName, action) => {\n     // return the method code\n     return function(root, { documentId }, context) {\n       \n       // extract the current user & the relevant collection from the graphql server context\n       const { currentUser, [Utils.capitalize(collectionName)]: collection } = context;\n       \n       // permission check\n       if (!Users.canDo(context.currentUser, `${collectionName}.${action}`)) {\n         throw new Error(Utils.encodeIntlError({id: 'app.noPermission'}));\n       }\n       \n       // do the actual subscription action\n       return performSubscriptionAction(action, collection, documentId, currentUser);\n     };\n   };\n\n   const collectionName = collection._name;\n   \n   // add mutations to the schema\n   addGraphQLMutation(`${collectionName}Subscribe(documentId: String): User`),\n   addGraphQLMutation(`${collectionName}Unsubscribe(documentId: String): User`);\n   \n   // create an object of the shape expected by mutations resolvers\n   addGraphQLResolvers({\n     Mutation: {\n       [`${collectionName}Subscribe`]: genericMutationFunction(collectionName, 'subscribe'),\n       [`${collectionName}Unsubscribe`]: genericMutationFunction(collectionName, 'unsubscribe'),\n     },\n   });\n   \n   \n };\n\n// Finally. Add the mutations to the Meteor namespace 🖖\n\n// vulcan:users is a dependency of this package, it is alreay imported\nsubscribeMutationsGenerator(Users);\n\n\n// note: leverage weak dependencies on packages\nconst Posts = Package['vulcan:posts'] ? Package['vulcan:posts'].default : null;\n// check if vulcan:posts exists, if yes, add the mutations to Posts\nif (!!Posts) {\n  subscribeMutationsGenerator(Posts);\n}\n\n// check if vulcan:categories exists, if yes, add the mutations to Categories\nconst Categories = Package['vulcan:categories'] ? Package['vulcan:categories'].default : null;\nif (!!Categories) {\n  subscribeMutationsGenerator(Categories);\n}\n\nexport default subscribeMutationsGenerator;\n"
  },
  {
    "path": "packages/vulcan-subscribe/lib/permissions.js",
    "content": "import Users from 'meteor/vulcan:users';\n\nconst membersActions = [\n  'posts.subscribe',\n  'posts.unsubscribe',\n  'users.subscribe',\n  'users.unsubscribe',\n  'categories.subscribe',\n  'categories.unsubscribe',\n];\n\nUsers.groups.members.can(membersActions);\n"
  },
  {
    "path": "packages/vulcan-subscribe/lib/views.js",
    "content": "// import Users from 'meteor/vulcan:users';\n// \n// if (typeof Package['vulcan:posts'] !== \"undefined\") {\n//   import Posts from \"meteor/vulcan:posts\";\n// \n//   Posts.views.add(\"userSubscribedPosts\", function (terms) {\n//     var user = Users.findOne(terms.userId),\n//         postsIds = [];\n// \n//     if (user && user.subscribedItems && user.subscribedItems.Posts) {\n//       postsIds = _.pluck(user.subscribedItems.Posts, \"itemId\");\n//     }\n// \n//     return {\n//       selector: {_id: {$in: postsIds}},\n//       options: {limit: 5, sort: {postedAt: -1}}\n//     };\n//   });\n// }\n"
  },
  {
    "path": "packages/vulcan-subscribe/package.js",
    "content": "Package.describe({\n  name: 'vulcan:subscribe',\n  summary: 'Subscribe to posts, users, etc. to be notified of new activity',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use([\n    'vulcan:core@=1.16.9',\n    // dependencies on posts, categories are done with nested imports to reduce explicit dependencies\n  ]);\n\n  api.use(['vulcan:posts@=1.16.9', 'vulcan:comments@=1.16.9', 'vulcan:categories@=1.16.9'], {\n    weak: true,\n  });\n\n  api.mainModule('lib/modules.js', ['client']);\n  api.mainModule('lib/modules.js', ['server']);\n});\n"
  },
  {
    "path": "packages/vulcan-test/README.md",
    "content": "Test package."
  },
  {
    "path": "packages/vulcan-test/lib/client/initComponentTest.js",
    "content": "import commonInitComponentTest from '../modules/initComponentTest';\n\nconst initComponentTest = () => {\n    commonInitComponentTest();\n};\n\nexport default initComponentTest;"
  },
  {
    "path": "packages/vulcan-test/lib/client/main.js",
    "content": "export * from '../modules';\nexport { default as initComponentTest } from './initComponentTest';"
  },
  {
    "path": "packages/vulcan-test/lib/modules/createDummyCollection.js",
    "content": "import SimpleSchema from 'simpl-schema';\n// return a collection object for unit testing\nconst createDummyCollection = ({\n    collectionName = 'Dummies',\n    typeName = 'Dummy',\n    mutations,\n    resolvers,\n    options = {\n        permissions: {\n            canRead: ['admins', 'members', 'guests'],\n            canUpdate: ['members', 'admins'],\n            canCreate: ['members', 'admins'],\n            canDelete: ['members', 'admins']\n        }\n    },\n    schema = {\n        _id: {\n            type: String, canRead: ['admins']\n        }\n    },\n    // results to various calls\n    results = {\n        find: [],\n        findOne: null,\n        load: null\n    },\n    ...otherFields\n}) => {\n    const Dummies = {\n        typeName,\n        options: { collectionName, typeName, mutations, resolvers, ...options },\n        simpleSchema: () => new SimpleSchema(schema),\n        find: () => ({\n            fetch: () => results.find,\n            count: () => results.length\n        }),\n        findOne: () => results.findOne,\n        loader: {\n            load: () => results.load,\n            prime: () => { }\n        },\n        remove: () => 1,\n        ...otherFields\n    };\n    return Dummies;\n};\nexport default createDummyCollection;"
  },
  {
    "path": "packages/vulcan-test/lib/modules/graphqlSchema.js",
    "content": "// allow to easily test regex on a graphql string\n// all blanks and series of blanks are replaces by one single space\nexport const normalizeGraphQLSchema = gqlSchema => gqlSchema.replace(/\\s+/g, ' ').trim();"
  },
  {
    "path": "packages/vulcan-test/lib/modules/index.js",
    "content": "export { MockedProvider as MockedProvider } from '@apollo/client/testing';\nexport { default as createDummyCollection } from './createDummyCollection';\nexport * from './graphqlSchema';"
  },
  {
    "path": "packages/vulcan-test/lib/modules/initComponentTest.js",
    "content": "/**\n * Initialize components\n * Must be called AFTER components registration\n */\nimport Enzyme from 'enzyme';\n// TODO: must be updated depending on the React version\n// @see https://www.npmjs.com/package/enzyme-adapter-react-16\nimport Adapter from 'enzyme-adapter-react-16';\nimport { populateComponentsApp, initializeFragments } from 'meteor/vulcan:lib';\n\nconst initComponentTest = () => {\n  // setup enzyme\n  Enzyme.configure({ adapter: new Adapter() });\n  //\n  // and then load them in the app so that <Component.Whatever /> is defined\n  // we need registered fragments to be initialized because populateComponentsApp will run\n  // hocs, like withUpdate, that rely on fragments\n  initializeFragments();\n  // actually fills the Components object\n  populateComponentsApp();\n};\nexport default initComponentTest;\n"
  },
  {
    "path": "packages/vulcan-test/lib/server/initComponentTest.js",
    "content": "// setup JSDOM server side for testing (necessary for Enzyme to mount)\nimport jsdom from 'jsdom-global';\nimport commonInitComponentTest from '../modules/initComponentTest';\n\nconst initComponentTest = () => {\n    // init a JSDOM to allow rendering server side\n    jsdom('', {\n        runScripts: 'outside-only'\n    });\n    commonInitComponentTest();\n};\n\nexport default initComponentTest;"
  },
  {
    "path": "packages/vulcan-test/lib/server/initGraphQLTest.js",
    "content": "/**\n * Init tests that require a valid schema, like testing Apollo SSR\n */\nimport { GraphQLSchema, addGraphQLSchema } from 'meteor/vulcan:lib/lib/server/graphql';\nimport initGraphQL from 'meteor/vulcan:lib/lib/server/apollo-server/initGraphQL';\nconst initGraphQLTest = () => {\n    GraphQLSchema.init();\n    // schema must never be empty\n    /*addGraphQLSchema(`\n    type Query {\n            currentUser: JSON\n            siteData: JSON\n        }\n        `);*/\n    initGraphQL();\n};\n\nexport default initGraphQLTest;"
  },
  {
    "path": "packages/vulcan-test/lib/server/initServerTest.js",
    "content": "/**\n * Enable server side tests\n */\nimport { runCallbacks } from 'meteor/vulcan:lib';\n\nexport default ( )=> {\n    runCallbacks('app.startup');\n};"
  },
  {
    "path": "packages/vulcan-test/lib/server/isoCreateCollection.js",
    "content": "/**\n * Drop a collection if it already exists\n * before creating it\n * \n * Thus this function can be replayed indefinitely\n * Note: this function is async contrary to createCollection\n * \n * FIXME: does not work yet\n */\nimport {\n    createCollection,\n    getCollection\n} from 'meteor/vulcan:core';\n\nexport default async (params) => {\n    const ExistingColl = getCollection(params.collectionName);\n    if (ExistingColl) {\n        try {\n            await ExistingColl.rawCollection().drop();\n        } catch (err) {\n            // if collection has been dropped already\n            // mongo will return \"ns not found\"\n            // can happen when tests are run in parallel\n            if (err.codeName !== 'NamespaceNotFound') {\n                throw err;\n            }\n        }\n    }\n    return createCollection(params);\n};"
  },
  {
    "path": "packages/vulcan-test/lib/server/main.js",
    "content": "export * from '../modules';\nexport { default as isoCreateCollection } from './isoCreateCollection';\nexport { default as initServerTest } from './initServerTest';\nexport { default as initComponentTest } from './initComponentTest';\nexport { default as initGraphQLTest } from './initGraphQLTest';\n\n// init test in any case\nimport { default as initServerTest } from './initServerTest';\n\ninitServerTest();"
  },
  {
    "path": "packages/vulcan-test/package.js",
    "content": "Package.describe({\n  name: 'vulcan:test',\n  summary: 'Vulcan test helpers',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:core@=1.16.9', 'vulcan:lib@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/README.md",
    "content": "Vulcan users package, used internally. "
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/client/main.js",
    "content": "export * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/backoffice/BackofficeNavbar.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:lib';\nimport Navbar from 'react-bootstrap/Navbar';\n\nconst BackofficeNavbar = ({ onClick, basePath }) => {\n  return (\n    <Navbar collapseOnSelect bg=\"dark\" variant=\"dark\" expand=\"md\">\n      <Navbar.Toggle onClick={onClick} style={{ display: 'block', marginRight: '10px' }} />\n      <Navbar.Brand href={basePath}>Backoffice Admin</Navbar.Brand>\n    </Navbar>\n  );\n};\n\nregisterComponent('VulcanBackofficeNavbar', BackofficeNavbar);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/backoffice/BackofficePageLayout.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:lib';\n\nconst styles = {\n  pageLayout: {\n    display: 'flex',\n    flexDirection: 'column',\n    height: '100vh',\n    overflow: 'hidden',\n  },\n};\n\nconst BackofficePageLayout = ({ children }) => {\n  return <div style={styles.pageLayout}>{children}</div>;\n};\n\nregisterComponent('VulcanBackofficePageLayout', BackofficePageLayout);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/backoffice/BackofficeVerticalMenuLayout.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:lib';\n\nconst styles = {\n  wrapper: {\n    display: 'flex',\n    overflow: 'auto',\n    height: '100%',\n  },\n  side: {\n    top: '0',\n    left: '0',\n    height: '100%',\n    overflowX: 'hidden',\n    backgroundColor: '#f7f7f7',\n    borderRight: '1px solid #ececec',\n    padding: '0',\n    transition: 'all 0.5s ease-out',\n  },\n  sideOpen: {\n    minWidth: '200px',\n    width: '200px',\n    visibility: 'visible',\n  },\n  sideClosed: {\n    minWidth: '0',\n    width: '0',\n    visibility: 'hidden',\n  },\n  main: {\n    flexGrow: '1',\n    overflow: 'auto',\n  },\n  margin: {\n    margin: '16px',\n  },\n};\n\nconst BackofficeVerticalMenuLayout = ({ side, main, open }) => {\n  return (\n    <div style={styles.wrapper}>\n      <div style={open ? { ...styles.side, ...styles.sideOpen } : { ...styles.side, ...styles.sideClosed }}>{side}</div>\n\n      <div style={styles.main}>\n        <div style={styles.margin}>{main}</div>\n      </div>\n    </div>\n  );\n};\n\nregisterComponent('VulcanBackofficeVerticalMenuLayout', BackofficeVerticalMenuLayout);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Autocomplete.jsx",
    "content": "import { AsyncTypeahead } from 'react-bootstrap-typeahead'; // ES2015\nimport React, { useState } from 'react';\nimport { registerComponent, expandQueryFragments, mergeWithComponents } from 'meteor/vulcan:core';\nimport { useLazyQuery } from '@apollo/client';\nimport gql from 'graphql-tag';\n\nconst Autocomplete = props => {\n  const {\n    queryData,\n    updateCurrentValues,\n    refFunction,\n    path,\n    inputProperties = {},\n    itemProperties = {},\n    autocompleteQuery,\n    optionsFunction,\n    formComponents,\n  } = props;\n\n  const Components = mergeWithComponents(formComponents);\n\n  const { value, label } = inputProperties;\n\n  // store current autocomplete query string in local component state\n  const [queryString = 'xohaskjdhaskdjalh', setQueryString] = useState();\n\n  // get component's autocomplete query and use it to load autocomplete suggestions\n  // note: we use useLazyQuery because \n  // we don't want to trigger the query until the user has actually typed in something\n  const [loadAutocompleteOptions, { loading, error, data }] = useLazyQuery(gql(expandQueryFragments(autocompleteQuery())), {\n    variables: { queryString },\n  });\n\n  if (error) {\n    throw new Error(error);\n  }\n  // apply options function to data to get suggestions in { value, label } pairs\n  const autocompleteOptions = data && optionsFunction({ data });\n\n  // apply same function to loaded data; filter by current value to avoid displaying any\n  // extra unwanted items if field-level data loading loaded too many items\n  // note: should be an array even if there is only one item in it\n  const selectedItem = queryData ? optionsFunction({ data: queryData }).filter(d => value === d.value) : [];\n\n  return (\n    <Components.FormItem path={path} label={label} {...itemProperties}>\n      <AsyncTypeahead\n        {...inputProperties}\n        multiple={false}\n        onChange={selected => {\n          if (selected.length === 0) {\n            updateCurrentValues({ [path]: null });\n          } else {\n            const selectedId = selected[0].value;\n            updateCurrentValues({ [path]: selectedId });\n          }\n        }}\n        options={autocompleteOptions}\n        id={path}\n        ref={refFunction}\n        onSearch={queryString => {\n          setQueryString(queryString);\n          loadAutocompleteOptions();\n        }}\n        isLoading={loading}\n        selected={selectedItem}\n      />\n    </Components.FormItem>\n  );\n};\n\nregisterComponent('FormComponentAutocomplete', Autocomplete);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/AutocompleteMultiple.jsx",
    "content": "import { AsyncTypeahead } from 'react-bootstrap-typeahead'; // ES2015\nimport React, { useState } from 'react';\nimport { mergeWithComponents, registerComponent, expandQueryFragments } from 'meteor/vulcan:core';\nimport { useLazyQuery } from '@apollo/client';\nimport gql from 'graphql-tag';\n\nconst MultiAutocomplete = props => {\n  const {\n    queryData,\n    updateCurrentValues,\n    refFunction,\n    path,\n    inputProperties = {},\n    itemProperties = {},\n    autocompleteQuery,\n    optionsFunction,\n    formComponents,\n  } = props;\n\n  const Components = mergeWithComponents(formComponents);\n\n  const { value, label } = inputProperties;\n\n  // store current autocomplete query string in local component state\n  const [queryString, setQueryString] = useState();\n\n  // get component's autocomplete query and use it to load autocomplete suggestions\n  // note: we use useLazyQuery because \n  // we don't want to trigger the query until the user has actually typed in something\n  const [loadAutocompleteOptions, { loading, error, data }] = useLazyQuery(gql(expandQueryFragments(autocompleteQuery())), {\n    variables: { queryString },\n  });\n\n  if (error) {\n    throw new Error(error);\n  }\n  // apply options function to data to get suggestions in { value, label } pairs\n  const autocompleteOptions = data && optionsFunction({ data });\n\n  // apply same function to loaded data; filter by current value to avoid displaying any\n  // extra unwanted items if field-level data loading loaded too many items\n  const selectedItems = queryData && optionsFunction({ data: queryData }).filter(d => value.includes(d.value));\n\n  // console.log(queryData)\n  // console.log(queryData && optionsFunction({ data: queryData }));\n  // console.log(value)\n  // console.log(selectedItems)\n  \n  return (\n    <Components.FormItem path={path} label={label} {...itemProperties}>\n      <AsyncTypeahead\n        {...inputProperties}\n        multiple\n        onChange={selected => {\n          const selectedIds = selected.map(({ value }) => value);\n          updateCurrentValues({ [path]: selectedIds });\n        }}\n        options={autocompleteOptions}\n        id={path}\n        ref={refFunction}\n        onSearch={queryString => {\n          setQueryString(queryString);\n          loadAutocompleteOptions();\n        }}\n        isLoading={loading}\n        selected={selectedItems}\n      />\n    </Components.FormItem>\n  );\n};\n\nregisterComponent('FormComponentMultiAutocomplete', MultiAutocomplete);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Checkbox.jsx",
    "content": "import React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nconst CheckboxComponent = ({ refFunction, path, inputProperties = {}, itemProperties = {}, formComponents }) => {\n  const Components = mergeWithComponents(formComponents);\n  return (\n    <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n      <Form.Check {...inputProperties} id={path} ref={refFunction} checked={!!inputProperties.value} />\n    </Components.FormItem>\n  );\n};\n\nregisterComponent('FormComponentCheckbox', CheckboxComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Checkboxgroup.jsx",
    "content": "import React, { useState } from 'react';\nimport Form from 'react-bootstrap/Form';\nimport { registerComponent, mergeWithComponents } from 'meteor/vulcan:core';\nimport without from 'lodash/without';\nimport uniq from 'lodash/uniq';\nimport isEmpty from 'lodash/isEmpty';\n\n// this marker is used to identify \"other\" values\nexport const otherMarker = '[other]';\n\n// check if a string is an \"other\" value\nexport const isOtherValue = s => s && typeof s === 'string' && s.substr(0, otherMarker.length) === otherMarker;\n\n// remove the \"other\" marker from a string\nexport const removeOtherMarker = s => s && typeof s === 'string' && s.substr(otherMarker.length);\n\n// add the \"other\" marker to a string\nexport const addOtherMarker = s => `${otherMarker}${s}`;\n\n// return array of values without the \"other\" value\nexport const removeOtherValue = a => {\n  return a.filter(s => !isOtherValue(s));\n};\n\nconst OtherComponent = ({ value, path, updateCurrentValues, formComponents }) => {\n  const Components = mergeWithComponents(formComponents);\n\n  const otherValue = removeOtherMarker(value.find(isOtherValue));\n  // get copy of checkbox group values with \"other\" value removed\n  const withoutOtherValue = removeOtherValue(value);\n\n  // keep track of whether \"other\" field is shown or not\n  const [showOther, setShowOther] = useState(!!otherValue);\n\n  // keep track of \"other\" field value locally\n  const [textFieldValue, setTextFieldValue] = useState(otherValue);\n\n  // textfield properties\n  const textFieldInputProperties = {\n    name,\n    value: textFieldValue,\n    onChange: event => {\n      const fieldValue = event.target.value;\n      // first, update local state\n      setTextFieldValue(fieldValue);\n      // then update global form state\n      const newValue = isEmpty(fieldValue) ? withoutOtherValue : [...withoutOtherValue, addOtherMarker(fieldValue)];\n      updateCurrentValues({ [path]: newValue });\n    },\n  };\n  const textFieldItemProperties = { layout: 'elementOnly' };\n\n  return (\n    <div className=\"form-option-other\">\n      <Form.Check\n        layout=\"elementOnly\"\n        label={'Other'}\n        value={showOther}\n        checked={showOther}\n        onClick={event => {\n          const isChecked = event.target.checked;\n          setShowOther(isChecked);\n          if (isChecked) {\n            // if checkbox is checked and textfield has value, update global form state with current textfield value\n            if (textFieldValue) {\n              updateCurrentValues({ [path]: [...withoutOtherValue, addOtherMarker(textFieldValue)] });\n            }\n          } else {\n            // if checkbox is unchecked, also clear out field value from global form state\n            updateCurrentValues({ [path]: withoutOtherValue });\n          }\n        }}\n      />\n      {showOther && <Components.FormComponentText inputProperties={textFieldInputProperties} itemProperties={textFieldItemProperties} />}\n    </div>\n  );\n};\n\n// note: treat checkbox group the same as a nested component, using `path`\nconst CheckboxGroupComponent = ({\n  refFunction,\n  label,\n  path,\n  value,\n  formType,\n  disabled,\n  updateCurrentValues,\n  inputProperties,\n  itemProperties = {},\n  formComponents,\n}) => {\n  const Components = mergeWithComponents(formComponents);\n\n  const { options = [], name } = inputProperties;\n\n  // get rid of duplicate values; or any values that are not included in the options provided\n  // (unless they have the \"other\" marker)\n  value = value ? uniq(value.filter(v => isOtherValue(v) || options.map(o => o.value).includes(v))) : [];\n\n  const hasValue = value.length > 0;\n\n  // if this is a \"new document\" form check options' \"checked\" property to populate value\n  if (formType === 'new' && value.length === 0) {\n    const checkedValues = _.where(options, { checked: true }).map(option => option.value);\n    if (checkedValues.length) {\n      value = checkedValues;\n    }\n  }\n\n  return (\n    <Components.FormItem path={path} label={label} {...itemProperties}>\n      <div className=\"form-item-options\">\n        {options.map((option, i) => {\n          const isChecked = value.includes(option.value);\n          const checkClass = hasValue ? (isChecked ? 'form-check-checked' : 'form-check-unchecked') : '';\n          return (\n            <Form.Check\n              {...inputProperties}\n              name={name}\n              layout=\"elementOnly\"\n              key={i}\n              label={<Components.FormOptionLabel option={option} name={name} />}\n              value={isChecked}\n              checked={isChecked}\n              id={`${path}.${i}`}\n              path={`${path}.${i}`}\n              ref={refFunction}\n              onChange={event => {\n                const isChecked = event.target.checked;\n                const newValue = isChecked ? [...value, option.value] : without(value, option.value);\n                updateCurrentValues({ [path]: newValue });\n              }}\n              className={checkClass}\n            />\n          );\n        })}\n        {itemProperties.showOther && (\n          <OtherComponent value={value} path={path} updateCurrentValues={updateCurrentValues} formComponents={formComponents} />\n        )}\n      </div>\n    </Components.FormItem>\n  );\n};\n\nregisterComponent('FormComponentCheckboxGroup', CheckboxGroupComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Date.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport DateTimePicker from 'react-datetime';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nclass DateComponent extends PureComponent {\n  constructor(props) {\n    super(props);\n    this.updateDate = this.updateDate.bind(this);\n  }\n\n  updateDate(date) {\n    this.props.updateCurrentValues({ [this.props.path]: date });\n  }\n\n  render() {\n    const { inputProperties, disabled = false, formComponents  } = this.props;\n\n    const Components = mergeWithComponents(formComponents);\n\n    const date = this.props.value\n      ? typeof this.props.value === 'string'\n        ? new Date(this.props.value)\n        : this.props.value\n      : null;\n\n    return (\n      <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...this.props.itemProperties}>\n        <DateTimePicker\n          value={date}\n          timeFormat={false}\n          // newDate argument is a Moment object given by react-datetime\n          onChange={newDate => this.updateDate(newDate)}\n          inputProps={{ name: this.props.name, disabled }}\n        />\n      </Components.FormItem>\n    );\n  }\n}\n\nDateComponent.propTypes = {\n  input: PropTypes.any,\n  datatype: PropTypes.any,\n  group: PropTypes.any,\n  label: PropTypes.string,\n  name: PropTypes.string,\n  value: PropTypes.any,\n};\n\nDateComponent.contextTypes = {\n  updateCurrentValues: PropTypes.func,\n};\n\nexport default DateComponent;\n\nregisterComponent('FormComponentDate', DateComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Date2.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\nimport moment from 'moment';\n\nconst isEmptyValue = value =>\n  typeof value === 'undefined' ||\n  value === null ||\n  value === '' ||\n  (Array.isArray(value) && value.length === 0);\n\nconst isValidYear = year => year && year.toString().length === 4;\nconst isValidDay = day => day && day.toString().length <= 2;\n\nclass DateComponent2 extends PureComponent {\n\n  /*\n\n  Keep initial local state blank so that form state values are used instead\n\n  */\n  state = {}\n\n  /*\n\n  Transform the value received from props into\n  three year/month/day properties, or else default to \n  empty strings for all three\n\n  */\n  getDateObject = value => {\n    const mDate = !isEmptyValue(value) && moment(value);\n    return mDate ? {\n      year: mDate.format('YYYY'),\n      month: mDate.format('MMMM'),\n      day: mDate.format('D'),\n    } : {\n      year: '',\n      month: '',\n      day: '',\n    };\n  }\n\n  updateDate = date => {\n    const { value, path } = this.props;\n    const newState = { ...this.state, ...date };\n    const { year, month, day } = newState;\n\n    let newDate;\n    if (isEmptyValue(value)) { // if there is no date value yet\n      if (isValidYear(year) && month && isValidDay(day)) {\n        // wait until we have all three valid values to update the date in the form state\n        newDate = moment()\n          .year(year)\n          .month(month)\n          .date(day);\n        this.props.updateCurrentValues({ [path]: newDate.toDate() });\n        // clear our the local component state to avoid storing outdated or conflicting values\n        this.setState({ year: undefined, month: undefined, day: undefined });\n      } else {\n        // otherwise only update local state\n        this.setState(date);\n      }\n    } else {\n      // there is currently a date value in the form state\n      newDate = moment(this.props.value);\n\n      // by default, update all three values in local state\n      const updateStateObject = { ...date };\n\n      // update all three values separately; clear local state when updating a value in form state\n      if (isValidYear(year)) {\n        newDate.year(year);\n        updateStateObject.year = undefined;\n      }\n      if (month) {\n        newDate.month(month);\n        updateStateObject.month = undefined;\n      }\n      if (isValidDay(day)) {\n        newDate.date(day);\n        updateStateObject.day = undefined;\n      }\n      this.props.updateCurrentValues({ [path]: newDate.toDate() });\n      this.setState(updateStateObject);\n    }\n  };\n\n  render() {\n    const { path, value, inputProperties, itemProperties, formCopmonents } = this.props;\n    const s = this.state;\n    const p = this.getDateObject(value);\n\n    const Components = mergeWithComponents(formComponents);\n\n    /*\n\n    For values: if local *state* is defined we use that, else\n    we use value from form state passed through *props* and \n    split into month/day/year via getDateObject()\n\n    */\n    const monthProperties = {\n      name: `${path}.month`,\n      options: moment.months().map((m, i) => ({ label: m, value: m })),\n      value: typeof s.month === 'undefined' ? p.month : s.month,\n      onChange: e => {\n        this.updateDate({ month: e.target.value });\n      },\n    };\n\n    const dayProperties = {\n      name: `${path}.day`,\n      maxLength: 2,\n      value: typeof s.day === 'undefined' ? p.day : s.day,\n      onChange: e => {\n        this.updateDate({ day: e.target.value });\n      },\n    };\n\n    const yearProperties = {\n      name: `${path}.year`,\n      maxLength: 4,\n      value: typeof s.year === 'undefined' ? p.year : s.year,\n      onChange: e => {\n        this.updateDate({ year: e.target.value });\n      },\n    };\n\n    return (\n      <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n        <div style={{ display: 'flex' }}>\n          <div>\n            <label>\n              <Components.FormattedMessage id=\"forms.month\" />\n            </label>\n            <Components.FormComponentSelect\n              inputProperties={monthProperties}\n              datatype={[{ type: String }]}\n            />\n          </div>\n          <div style={{ marginLeft: 10, width: 60 }}>\n            <label>\n              <Components.FormattedMessage id=\"forms.day\" />\n            </label>\n            <Components.FormComponentText inputProperties={dayProperties} />\n          </div>\n          <div style={{ marginLeft: 10, width: 80 }}>\n            <label>\n              <Components.FormattedMessage id=\"forms.year\" />\n            </label>\n            <Components.FormComponentText inputProperties={yearProperties} />\n          </div>\n        </div>\n      </Components.FormItem>\n    );\n  }\n}\n\nexport default DateComponent2;\n\nregisterComponent('FormComponentDate2', DateComponent2);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Datetime.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport DateTimePicker from 'react-datetime';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nclass DateTime extends PureComponent {\n  constructor(props) {\n    super(props);\n    this.updateDate = this.updateDate.bind(this);\n  }\n\n  updateDate(date) {\n    this.context.updateCurrentValues({ [this.props.path]: date });\n  }\n\n  render() {\n\n    const { inputProperties, disabled = false, formComponents } = this.props;\n\n    const Components = mergeWithComponents(formComponents);\n\n    const date = this.props.value\n      ? typeof this.props.value === 'string'\n        ? new Date(this.props.value)\n        : this.props.value\n      : null;\n\n    return (\n      <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...this.props.itemProperties}>\n        <DateTimePicker\n          value={date}\n          // newDate argument is a Moment object given by react-datetime\n          onChange={newDate => this.updateDate(newDate._d)}\n          format={'x'}\n          inputProps={{ name: this.props.name, disabled }}\n        />\n      </Components.FormItem>\n    );\n  }\n}\n\nDateTime.propTypes = {\n  input: PropTypes.any,\n  datatype: PropTypes.any,\n  group: PropTypes.any,\n  label: PropTypes.string,\n  name: PropTypes.string,\n  value: PropTypes.any,\n};\n\nDateTime.contextTypes = {\n  updateCurrentValues: PropTypes.func,\n};\n\nexport default DateTime;\n\nregisterComponent('FormComponentDateTime', DateTime);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Default.jsx",
    "content": "import React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nconst Default = ({ refFunction, inputProperties = {}, itemProperties = {}, formComponents }) => {\n  const Components = mergeWithComponents(formComponents);\n  return (\n    <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n      <Form.Control {...inputProperties} ref={refFunction} type=\"text\" />\n    </Components.FormItem>\n  );\n};\n\nregisterComponent('FormComponentDefault', Default);\nregisterComponent('FormComponentText', Default);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Email.jsx",
    "content": "import React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nconst EmailComponent = ({ refFunction, inputProperties, itemProperties, formComponents }) => {\n  const Components = mergeWithComponents(formComponents);\n  return (\n    <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n      <Form.Control {...inputProperties} ref={refFunction} type=\"email\" />\n    </Components.FormItem>\n  );\n};\n\nregisterComponent('FormComponentEmail', EmailComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/FormComponentInner.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { Components, registerComponent, instantiateComponent, whitelistInputProps } from 'meteor/vulcan:core';\nimport classNames from 'classnames';\n\nclass FormComponentInner extends PureComponent {\n\n  getProperties = () => {\n    const { handleChange, inputType, itemProperties, help, description, loading, submitForm, formComponents, intlKeys } = this.props;\n    const properties = {\n      ...this.props,\n\n      inputProperties: {\n        ...whitelistInputProps(this.props),\n        onChange: event => {\n          // FormComponent's handleChange expects value as argument; look in target.checked or target.value\n          const inputValue = inputType === 'checkbox' ? event.target.checked : event.target.value;\n          handleChange(inputValue);\n        },\n        onKeyPress: event => {\n          if (event.key === 'Enter' && inputType !== 'textarea') {\n            submitForm();\n          }\n        },\n      },\n\n      itemProperties: {\n        ...itemProperties,\n        intlKeys,\n        Components: formComponents,\n        description: description || help,\n        loading,\n      },\n    };\n    return properties;\n  };\n\n  render() {\n    const {\n      inputClassName,\n      name,\n      input,\n      inputType,\n      beforeComponent,\n      afterComponent,\n      errors,\n      showCharsRemaining,\n      charsRemaining,\n      renderComponent,\n      formComponents,\n    } = this.props;\n\n    const FormComponents = formComponents;\n\n    const hasErrors = errors && errors.length;\n\n    const inputName = typeof input === 'function' ? input.name : inputType;\n    const inputClass = classNames('form-input', inputClassName, `input-${name}`, `form-component-${inputName || 'default'}`, {\n      'input-error': hasErrors,\n    });\n    const properties = this.getProperties();\n\n    const FormInput = this.props.formInput;\n\n    return (\n      <div className={inputClass}>\n        {instantiateComponent(beforeComponent, properties)}\n        <FormInput {...properties} />\n        {hasErrors ? <FormComponents.FieldErrors errors={errors} /> : null}\n        <Components.FormClear {...properties} />\n        {showCharsRemaining && <div className={classNames('form-control-limit', { danger: charsRemaining < 10 })}>{charsRemaining}</div>}\n        {instantiateComponent(afterComponent, properties)}\n      </div>\n    );\n  }\n}\n\nFormComponentInner.propTypes = {\n  inputClassName: PropTypes.string,\n  name: PropTypes.string.isRequired,\n  input: PropTypes.any,\n  beforeComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),\n  afterComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),\n  clearField: PropTypes.func.isRequired,\n  errors: PropTypes.array.isRequired,\n  help: PropTypes.node,\n  handleChange: PropTypes.func.isRequired,\n  showCharsRemaining: PropTypes.bool.isRequired,\n  charsRemaining: PropTypes.number,\n  charsCount: PropTypes.number,\n  charsMax: PropTypes.number,\n  inputComponent: PropTypes.func,\n};\n\nFormComponentInner.contextTypes = {\n  intl: intlShape,\n};\n\nFormComponentInner.displayName = 'FormComponentInner';\n\nregisterComponent('FormComponentInner', FormComponentInner);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/FormControl.jsx",
    "content": "import React from 'react';\nimport FormControl from 'react-bootstrap/FormControl';\nimport { registerComponent } from 'meteor/vulcan:lib';\n\nregisterComponent('FormControl', FormControl);"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/FormDescription.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport Form from 'react-bootstrap/Form';\n\nconst FormDescription = ({ description }) => {\n  return (\n    <Form.Text>\n      <div dangerouslySetInnerHTML={{ __html: description }} />\n    </Form.Text>\n  );\n};\n\nregisterComponent('FormDescription', FormDescription);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/FormElement.jsx",
    "content": "// import React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport { registerComponent } from 'meteor/vulcan:core';\n\nregisterComponent('FormElement', Form);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/FormGroupDefault.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, Utils } from 'meteor/vulcan:core';\nimport classNames from 'classnames';\nimport { registerComponent } from 'meteor/vulcan:core';\n\nconst FormGroupHeader = ({ toggle, collapsed, label }) => (\n  <div className=\"form-section-heading\" onClick={toggle}>\n    <h3 className=\"form-section-heading-title\">{label}</h3>\n    <span className=\"form-section-heading-toggle\">\n      {collapsed ? (\n        <Components.IconRight height={16} width={16}/>\n      ) : (\n        <Components.IconDown height={16} width={16}/>\n      )}\n    </span>\n  </div>\n);\nFormGroupHeader.propTypes = {\n  toggle: PropTypes.func.isRequired,\n  label: PropTypes.string.isRequired,\n  collapsed: PropTypes.bool,\n  group: PropTypes.object,\n};\nregisterComponent({ name: 'FormGroupHeader', component: FormGroupHeader });\n\nconst FormGroupLayout = ({ children, label, anchorName, heading, collapsed, hidden, group, hasErrors }) => (\n  <div className={classNames('form-section', `form-section-${anchorName}`, \n    `form-section-${Utils.slugify(label)}`, hidden && 'form-section-hidden')}>\n    <a name={anchorName}/>\n    {heading}\n    <div\n      className={classNames({\n        'form-section-collapsed': collapsed && !hasErrors\n      })}\n    >\n      {children}\n    </div>\n  </div>\n);\nFormGroupLayout.propTypes = {\n  label: PropTypes.string,\n  anchorName: PropTypes.string,\n  heading: PropTypes.node,\n  collapsed: PropTypes.bool,\n  hidden: PropTypes.bool,\n  group: PropTypes.object,\n  hasErrors: PropTypes.bool,\n  children: PropTypes.node\n};\nregisterComponent({ name: 'FormGroupLayout', component: FormGroupLayout });\n\nconst IconRight = ({ width = 24, height = 24 }) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={width}\n    height={height}\n    viewBox=\"0 0 24 24\"\n  >\n    <polyline\n      fill=\"none\"\n      stroke=\"#000\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      strokeMiterlimit=\"10\"\n      points=\"5.5,23.5 18.5,12 5.5,0.5\"\n      id=\"Outline_Icons\"\n    />\n    <rect fill=\"none\" width=\"24\" height=\"24\" id=\"Frames-24px\"/>\n  </svg>\n);\n\nregisterComponent('IconRight', IconRight);\n\nconst IconDown = ({ width = 24, height = 24 }) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={width}\n    height={height}\n    viewBox=\"0 0 24 24\"\n  >\n    <polyline\n      fill=\"none\"\n      stroke=\"#000\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      strokeMiterlimit=\"10\"\n      points=\"0.501,5.5 12.001,18.5 23.501,5.5\"\n      id=\"Outline_Icons\"\n    />\n    <rect fill=\"none\" width=\"24\" height=\"24\" id=\"Frames-24px\"/>\n  </svg>\n);\n\nregisterComponent('IconDown', IconDown);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/FormInputLoading.jsx",
    "content": "import React from 'react';\nimport { registerComponent, Components } from 'meteor/vulcan:core';\n\nconst FormInputLoading = ({ loading, children }) => (\n  <div className={`form-input-loading form-input-loading-${loading ? 'isLoading' : 'notLoading'}`}>\n    <div className=\"form-input-loading-inner\">{children}</div>\n    {loading && (\n      <div className=\"form-input-loading-loader\">\n        <Components.Loading />\n      </div>\n    )}\n  </div>\n);\n\nregisterComponent('FormInputLoading', FormInputLoading);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/FormItem.jsx",
    "content": "/*\n\nLayout for a single form item\n\n*/\n\nimport React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport Row from 'react-bootstrap/Row';\nimport Col from 'react-bootstrap/Col';\nimport { registerComponent, mergeWithComponents } from 'meteor/vulcan:core';\n\nconst FormItem = props => {\n  const {\n    path,\n    label,\n    children,\n    beforeInput,\n    afterInput,\n    description,\n    layout = 'horizontal',\n    loading,\n    Components: formComponents,\n    ...rest\n  } = props;\n\n  const Components = mergeWithComponents(formComponents);\n\n  const innerComponent = loading ? <Components.FormInputLoading loading={loading}>{children}</Components.FormInputLoading> : children;\n\n  if (layout === 'inputOnly' || !label) {\n    // input only layout\n    return (\n      <Form.Group controlId={path}>\n        {beforeInput}\n        {innerComponent}\n        {afterInput}\n        {description && <Components.FormDescription {...props} />}\n      </Form.Group>\n    );\n  } else if (layout === 'vertical') {\n    // vertical layout\n    return (\n      <Form.Group controlId={path}>\n        <Components.FormLabel {...props} />\n        <div className=\"form-item-contents\">\n          <div className=\"form-item-input\">\n            {beforeInput}\n            {innerComponent}\n            {afterInput}\n          </div>\n          {description && <Components.FormDescription {...props} />}\n        </div>\n      </Form.Group>\n    );\n  } else {\n    // horizontal layout (default)\n    return (\n      <Form.Group as={Row} controlId={path}>\n        <Components.FormLabel layout=\"horizontal\" {...props} />\n        <Col sm={9}>\n          {beforeInput}\n          {innerComponent}\n          {afterInput}\n          {description && <Components.FormDescription {...props} />}\n        </Col>\n      </Form.Group>\n    );\n  }\n};\n\nregisterComponent('FormItem', FormItem);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/FormLabel.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport Form from 'react-bootstrap/Form';\n\nconst FormLabel = ({ label: Label, layout }) => {\n  const labelProps = layout === 'horizontal' ? { column: true, sm: 3 } : {};\n  return (\n    <Form.Label {...labelProps}>\n      {typeof Label === 'function' ? <Label {...labelProps} /> : <span dangerouslySetInnerHTML={{ __html: Label }} />}\n    </Form.Label>\n  );\n};\n\nregisterComponent('FormLabel', FormLabel);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Likert.jsx",
    "content": "import React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nconst getRange = (n = 10) => [...Array(n).keys()].map(i => i + 1).map(i => ({ value: i, label: i }));\n\nconst Likert = ({ refFunction, path, updateCurrentValues, inputProperties, itemProperties = {}, formComponents }) => {\n  const Components = mergeWithComponents(formComponents);\n  const { options = [], value } = inputProperties;\n  const hasValue = value !== '';\n  return (\n    <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n      <div className=\"likert-scale\">\n        <div className=\"likert-row\">\n          <div />\n            {getRange().map((rating, i) => (\n              <div key={i} className=\"likert-row-cell\">\n                {i+1}\n              </div>\n            ))}\n        </div>\n        {options.map((option, i) => {\n          const optionPath = `${path}.${option.value}`;\n          const optionValue = value && value[option.value];\n          return (\n            <div key={i} className=\"likert-row\">\n              <div className=\"likert-row-heading\">{option.label}</div>\n              {/* <div className=\"likert-row-contents\"> */}\n                {getRange().map((rating, i) => {\n                  const isChecked = optionValue === rating.value;\n                  const checkClass = hasValue ? (isChecked ? 'form-check-checked' : 'form-check-unchecked') : '';\n                  return (\n                    <div key={i} className=\"likert-row-cell\">\n                      <Form.Check\n                        {...inputProperties}\n                        key={i}\n                        layout=\"elementOnly\"\n                        type=\"radio\"\n                        label={rating.label}\n                        value={rating.value}\n                        name={optionPath}\n                        id={optionPath}\n                        path={optionPath}\n                        ref={refFunction}\n                        checked={isChecked}\n                        className={checkClass}\n                        onChange={() => {\n                          updateCurrentValues({ [optionPath]: rating.value });\n                        }}\n                      />\n                    </div>\n                  );\n                })}\n              {/* </div> */}\n            </div>\n          );\n        })}\n      </div>\n    </Components.FormItem>\n  );\n};\n\nregisterComponent('FormComponentLikert', Likert);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Number.jsx",
    "content": "import React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nconst NumberComponent = ({ refFunction, inputProperties, itemProperties, formComponents }) => {\n  const Components = mergeWithComponents(formComponents);\n  return (\n    <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n      <Form.Control {...inputProperties} ref={refFunction} type=\"number\" />\n    </Components.FormItem>\n  );\n};\n\nregisterComponent('FormComponentNumber', NumberComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Password.jsx",
    "content": "import React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nconst Password = ({ refFunction, inputProperties, itemProperties, formComponents }) => {\n  const Components = mergeWithComponents(formComponents);\n  return (\n    <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n      <Form.Control {...inputProperties} ref={refFunction} type=\"password\" />\n    </Components.FormItem>\n  );\n};\n\nregisterComponent('FormComponentPassword', Password);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Radiogroup.jsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport Form from 'react-bootstrap/Form';\nimport { registerComponent, mergeWithComponents } from 'meteor/vulcan:core';\nimport isEmpty from 'lodash/isEmpty';\nimport { isOtherValue, removeOtherMarker, addOtherMarker } from './Checkboxgroup.jsx';\n\nconst OtherComponent = ({ value, path, updateCurrentValues, formComponents }) => {\n  const Components = mergeWithComponents(formComponents);\n  const otherValue = removeOtherMarker(value);\n\n  // keep track of whether \"other\" field is shown or not\n  const [showOther, setShowOther] = useState(isOtherValue(value));\n\n  // keep track of \"other\" field value locally\n  const [textFieldValue, setTextFieldValue] = useState(otherValue);\n\n  // whenever value changes (and is not empty), if it's not an \"other\" value\n  // this means another option has been selected and we need to uncheck the \"other\" radio button\n  useEffect(() => {\n    if (value) {\n      setShowOther(isOtherValue(value));\n    }\n  }, [value]);\n\n  // textfield properties\n  const textFieldInputProperties = {\n    name: path,\n    value: textFieldValue,\n    onChange: event => {\n      const fieldValue = event.target.value;\n      // first, update local state\n      setTextFieldValue(fieldValue);\n      // then update global form state\n      const newValue = isEmpty(fieldValue) ? null : addOtherMarker(fieldValue);\n      updateCurrentValues({ [path]: newValue });\n    },\n  };\n  const textFieldItemProperties = { layout: 'elementOnly' };\n\n  return (\n    <div className=\"form-option-other\">\n      <Form.Check\n        name={path}\n        layout=\"elementOnly\"\n        label={'Other'}\n        value={showOther}\n        checked={showOther}\n        type=\"radio\"\n        onClick={event => {\n          const isChecked = event.target.checked;\n          // clear any previous values to uncheck all other checkboxes\n          updateCurrentValues({ [path]: null });\n          setShowOther(isChecked);\n        }}\n      />\n      {showOther && <Components.FormComponentText inputProperties={textFieldInputProperties} itemProperties={textFieldItemProperties} />}\n    </div>\n  );\n};\n\nconst RadioGroupComponent = ({\n  refFunction,\n  path,\n  updateCurrentValues,\n  inputProperties,\n  itemProperties = {},\n  formComponents,\n}) => {\n  const Components = mergeWithComponents(formComponents);\n  const { options = [], value } = inputProperties;\n  const hasValue = value !== '';\n  return (\n    <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n      {options.map((option, i) => {\n        const isChecked = value === option.value;\n        const checkClass = hasValue ? (isChecked ? 'form-check-checked' : 'form-check-unchecked') : '';\n        return (\n          <Form.Check\n            {...inputProperties}\n            key={i}\n            layout=\"elementOnly\"\n            type=\"radio\"\n            label={<Components.FormOptionLabel option={option} />}\n            value={option.value}\n            name={path}\n            id={`${path}.${i}`}\n            path={`${path}.${i}`}\n            ref={refFunction}\n            checked={isChecked}\n            className={checkClass}\n          />\n        );\n      })}\n      {itemProperties.showOther && (\n        <OtherComponent value={value} path={path} updateCurrentValues={updateCurrentValues} formComponents={formComponents} />\n      )}\n    </Components.FormItem>\n  );\n};\n\nregisterComponent('FormComponentRadioGroup', RadioGroupComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Select.jsx",
    "content": "import React from 'react';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport Form from 'react-bootstrap/Form';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\n// copied from vulcan:forms/utils.js to avoid extra dependency\nconst getFieldType = datatype => datatype && datatype[0].type;\n\nconst SelectComponent = ({ refFunction, inputProperties, itemProperties, datatype, options, formComponents }, { intl }) => {\n  const Components = mergeWithComponents(formComponents);\n  const noneOption = {\n    label: intl.formatMessage({ id: 'forms.select_option' }),\n    value: getFieldType(datatype) === String || getFieldType(datatype) === Number ? '' : null, // depending on field type, empty value can be '' or null\n    disabled: true,\n  };\n  let otherOptions = Array.isArray(options) && options.length ? options : [];\n  const allOptions = [noneOption, ...otherOptions];\n  const { options: deleteOptions, ...newInputProperties } = inputProperties;\n  return (\n    <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n      <Form.Control as=\"select\" {...newInputProperties} ref={refFunction}>\n        {allOptions.map(({ value, label, intlId, ...rest }) => (\n          <option key={value} value={value} {...rest}>\n            {label}\n          </option>\n        ))}\n      </Form.Control>\n    </Components.FormItem>\n  );\n};\n\nSelectComponent.contextTypes = {\n  intl: intlShape,\n};\n\nregisterComponent('FormComponentSelect', SelectComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/SelectMultiple.jsx",
    "content": "import React from 'react';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport Form from 'react-bootstrap/Form';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nconst SelectMultipleComponent = ({ refFunction, inputProperties, itemProperties, formComponents }, { intl }) => {\n  inputProperties.multiple = true;\n  const Components = mergeWithComponents(formComponents);\n  return (\n    <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n      <Form.Control as=\"select\" {...inputProperties} ref={refFunction} />\n    </Components.FormItem>\n  );\n};\n\nSelectMultipleComponent.contextTypes = {\n  intl: intlShape,\n};\n\nregisterComponent('FormComponentSelectMultiple', SelectMultipleComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/StaticText.jsx",
    "content": "import React from 'react';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nconst parseUrl = value => {\n  return value && value.toString().slice(0, 4) === 'http' ? (\n    <a href={value} target=\"_blank\" rel=\"noopener noreferrer\">\n      {value}\n    </a>\n  ) : (\n    value\n  );\n};\n\nconst StaticComponent = ({ inputProperties, itemProperties, formComponents }) => {\n  const Components = mergeWithComponents(formComponents);\n  return (\n  <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n    <div style={{ paddingTop: 'calc(.375rem + 1px)', paddingBottom: 'calc(.375rem + 1px)' }}>{parseUrl(inputProperties.value)}</div>\n  </Components.FormItem>\n)};\n\nregisterComponent('FormComponentStaticText', StaticComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Textarea.jsx",
    "content": "import React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nconst TextareaComponent = ({ refFunction, inputProperties = {}, itemProperties = {}, formComponents }) => {\n  const Components = mergeWithComponents(formComponents);\n  return (\n  <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n    <Form.Control as=\"textarea\" ref={refFunction} {...inputProperties} />\n  </Components.FormItem>\n)};\n\nregisterComponent('FormComponentTextarea', TextareaComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Time.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport DateTimePicker from 'react-datetime';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nclass Time extends PureComponent {\n  constructor(props) {\n    super(props);\n    this.updateDate = this.updateDate.bind(this);\n  }\n\n  updateDate(mDate) {\n    // if this is a properly formatted moment date, update time\n    if (typeof mDate === 'object') {\n      this.context.updateCurrentValues({ [this.props.path]: mDate.format('HH:mm') });\n    }\n  }\n\n  render() {\n\n    const { inputProperties, formComponents } = this.props;\n    const Components = mergeWithComponents(formComponents);\n\n    const date = new Date();\n\n    // transform time string into date object to work inside datetimepicker\n    const time = this.props.value;\n    if (time) {\n      date.setHours(parseInt(time.substr(0, 2)), parseInt(time.substr(3, 5)));\n    } else {\n      date.setHours(0, 0);\n    }\n\n    return (\n      <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...this.props.itemProperties}>\n        <DateTimePicker\n          value={date}\n          viewMode=\"time\"\n          dateFormat={false}\n          timeFormat=\"HH:mm\"\n          // newDate argument is a Moment object given by react-datetime\n          onChange={newDate => this.updateDate(newDate)}\n          inputProps={{ name: this.props.name }}\n        />\n      </Components.FormItem>\n    );\n  }\n}\n\nTime.propTypes = {\n  input: PropTypes.any,\n  datatype: PropTypes.any,\n  group: PropTypes.any,\n  label: PropTypes.string,\n  name: PropTypes.string,\n  value: PropTypes.any,\n};\n\nTime.contextTypes = {\n  updateCurrentValues: PropTypes.func,\n};\n\nexport default Time;\n\nregisterComponent('FormComponentTime', Time);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/forms/Url.jsx",
    "content": "import React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport { mergeWithComponents, registerComponent } from 'meteor/vulcan:core';\n\nconst UrlComponent = ({ refFunction, inputProperties, itemProperties, formComponents }) => {\n  const Components = mergeWithComponents(formComponents);\n  return (\n  <Components.FormItem path={inputProperties.path} label={inputProperties.label} {...itemProperties}>\n    <Form.Control ref={refFunction} {...inputProperties} type=\"url\" />\n  </Components.FormItem>\n)};\n\nregisterComponent('FormComponentUrl', UrlComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/ui/Alert.jsx",
    "content": "import React from 'react';\nimport Alert from 'react-bootstrap/Alert';\nimport { registerComponent } from 'meteor/vulcan:lib';\n\nconst BootstrapAlert = ({ children, variant = 'danger',  ...rest }) => \n  <Alert variant={variant} {...rest}>{children}</Alert>;\n\nregisterComponent('Alert', BootstrapAlert);"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/ui/Button.jsx",
    "content": "import React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport { registerComponent } from 'meteor/vulcan:lib';\n\nconst BootstrapButton = ({ children, variant, size, iconButton, ...rest }) => \n  <Button variant={variant} size={size} {...rest}>{children}</Button>;\n\nregisterComponent('Button', BootstrapButton);"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/ui/Dropdown.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent } from 'meteor/vulcan:lib';\nimport Dropdown from 'react-bootstrap/Dropdown';\nimport DropdownItem from 'react-bootstrap/DropdownItem';\nimport DropdownButton from 'react-bootstrap/DropdownButton';\nimport { LinkContainer } from 'react-router-bootstrap';\n\n/*\n\nA node contains a menu item, and optionally a list of child items\n\n*/\nconst Node = ({ childrenItems, ...rest }) => {\n  return (\n    <>\n      <Item {...rest} />\n      {childrenItems &&\n        !!childrenItems.length && (\n          <div className=\"menu-node-children\">{childrenItems.map((item, index) => <Item key={index} {...item} />)}</div>\n        )}\n    </>\n  );\n};\n\nNode.propTypes = {\n  childrenItems: PropTypes.array, // an array of dropdown items used as children of the current item\n};\n\n/*\n\nNote: `rest` is used to ensure auto-generated props from parent dropdown\ncomponents are properly passed down to MenuItem\n\n*/\nconst Item = ({ index, to, labelId, label, component, componentProps = {}, itemProps, linkProps, ...rest }) => {\n  let menuComponent;\n\n  if (component) {\n    menuComponent = React.cloneElement(component, componentProps);\n  } else if (labelId) {\n    menuComponent = <Components.FormattedMessage id={labelId} />;\n  } else {\n    menuComponent = <span>{label}</span>;\n  }\n\n  const item = (\n    <DropdownItem eventKey={index} {...itemProps} {...rest}>\n      {menuComponent}\n    </DropdownItem>\n  );\n\n  return to ? <LinkContainer to={to} {...linkProps}>{item}</LinkContainer> : item;\n};\n\nItem.propTypes = {\n  index: PropTypes.number, // index\n  to: PropTypes.any, // a string or object, used to generate the router path for the menu item\n  labelId: PropTypes.string, // an i18n id for the item label\n  label: PropTypes.string, // item label string, used if id is not provided\n  component: PropTypes.object, // a component to use as menu item\n  componentProps: PropTypes.object, // props passed to the component\n  itemProps: PropTypes.object, // props for the <MenuItem/> component\n};\n\nconst BootstrapDropdown = ({ label, labelId, trigger, menuItems, menuContents, variant = 'dropdown', buttonProps, ...dropdownProps }) => {\n  const menuBody = menuContents ? menuContents : menuItems.map((item, index) => {\n    if (item === 'divider') {\n      return <Dropdown.Divider key={index} />;\n    } else {\n      return <Node {...item} key={index} index={index} />;\n    }\n  });\n\n  if (variant === 'flat') {\n    \n    return menuBody;\n\n  } else {\n    if (trigger) {\n      // if a trigger component has been provided, use it\n      return (\n        <Dropdown {...dropdownProps}>\n          <Dropdown.Toggle>{trigger}</Dropdown.Toggle>\n          <Dropdown.Menu>{menuBody}</Dropdown.Menu>\n        </Dropdown>\n      );\n    } else {\n      // else default to DropdownButton\n      return (\n        <DropdownButton {...buttonProps} title={labelId ? <Components.FormattedMessage id={labelId} /> : label} {...dropdownProps}>\n          {menuBody}\n        </DropdownButton>\n      );\n    }\n  }\n};\n\nBootstrapDropdown.propTypes = {\n  labelId: PropTypes.string, // menu title/label i18n string\n  label: PropTypes.string, // menu title/label\n  trigger: PropTypes.object, // component used as menu trigger (the part you click to open the menu)\n  menuContents: PropTypes.object, // a component specifying the menu contents\n  menuItems: PropTypes.array, // an array of menu items, used if menuContents is not provided\n  variant: PropTypes.string, // dropdown (default) or flat\n  buttonProps: PropTypes.object, // props specific to the dropdown button\n};\n\nregisterComponent('Dropdown', BootstrapDropdown);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/ui/Modal.jsx",
    "content": "import { registerComponent } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport Modal from 'react-bootstrap/Modal';\n\nconst BootstrapModal = ({ children, size = 'lg', show = false, onHide, title, showCloseButton = true, header, footer, ...rest }) => {\n\n  let headerComponent;\n  if (header) {\n    headerComponent = <Modal.Header>{header}</Modal.Header>;\n  } else if (title) {\n    headerComponent = <Modal.Header closeButton={showCloseButton}><Modal.Title>{title}</Modal.Title></Modal.Header>;\n  } else {\n    headerComponent = <Modal.Header closeButton={showCloseButton}></Modal.Header>;\n  }\n\n  const footerComponent = footer ? <Modal.Footer>{footer}</Modal.Footer> : null;\n  \n  return (\n    <Modal size={size} show={show} onHide={onHide} {...rest}>\n      {headerComponent}\n      <Modal.Body>\n        {children}\n      </Modal.Body>\n      {footerComponent}\n    </Modal>\n  );\n};\n\nBootstrapModal.propTypes = {\n  size: PropTypes.string,\n  show: PropTypes.bool,\n  showCloseButton: PropTypes.bool,\n  onHide: PropTypes.func,\n  title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),\n  header: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),\n  footer: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),\n};\n\nregisterComponent('Modal', BootstrapModal);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/ui/ModalTrigger.jsx",
    "content": "import { Components, registerComponent } from 'meteor/vulcan:core';\nimport React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\n\nclass ModalTrigger extends PureComponent {\n  constructor() {\n    super();\n    this.state = {\n      modalIsOpen: false,\n    };\n  }\n\n  clickHandler = (e) => {\n    e.preventDefault();\n    if (this.props.onClick) {\n      this.props.onClick();\n    }\n    this.openModal();\n  }\n\n  openModal = () => {\n    this.props.openCallback && this.props.openCallback();\n    this.setState({ modalIsOpen: true });\n  }\n\n  closeModal = () => {\n    this.props.closeCallback && this.props.closeCallback();\n    this.setState({ modalIsOpen: false });\n  }\n\n  render() {\n    const {\n      trigger,\n      component,\n      children,\n      label,\n      size,\n      className,\n      dialogClassName,\n      title,\n      modalProps,\n      header,\n      footer,\n    } = this.props;\n\n    let triggerComponent = trigger || component;\n    triggerComponent = triggerComponent ? (\n      <span onClick={this.clickHandler}>{triggerComponent}</span>\n    ) : (\n      <Components.Button onClick={this.clickHandler}>\n        {label}\n      </Components.Button>\n    );\n    const childrenComponent = React.cloneElement(children, { closeModal: this.closeModal });\n    const headerComponent = header && React.cloneElement(header, { closeModal: this.closeModal });\n    const footerComponent = footer && React.cloneElement(footer, { closeModal: this.closeModal });\n\n    return (\n      <div className=\"modal-trigger\">\n        {triggerComponent}\n        <Components.Modal\n          size={size}\n          className={className}\n          show={this.state.modalIsOpen}\n          onHide={this.closeModal}\n          dialogClassName={dialogClassName}\n          title={title}\n          header={headerComponent}\n          footer={footerComponent}\n          {...modalProps}\n        >\n          {childrenComponent}\n        </Components.Modal>\n      </div>\n    );\n  }\n}\n\nModalTrigger.propTypes = {\n  className: PropTypes.string,\n  label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),\n  component: PropTypes.object, // keep for backwards compatibility\n  trigger: PropTypes.object,\n  size: PropTypes.string,\n  title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),\n  onClick: PropTypes.func,\n};\n\nregisterComponent('ModalTrigger', ModalTrigger);\n\nexport default ModalTrigger;\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/ui/Table.jsx",
    "content": "import React from 'react';\nimport Table from 'react-bootstrap/Table';\nimport { registerComponent } from 'meteor/vulcan:lib';\n\nregisterComponent('Table', Table);"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/ui/TooltipTrigger.jsx",
    "content": "/*\n\nchildren: the content of the tooltip\ntrigger: the component that triggers the tooltip to appear\n\n*/\nimport React from 'react';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport Tooltip from 'react-bootstrap/Tooltip';\nimport OverlayTrigger from 'react-bootstrap/OverlayTrigger';\n\nconst TooltipTrigger = ({ children, trigger, placement = 'top', ...rest }) => {\n  const tooltip = <Tooltip id=\"tooltip\">{children}</Tooltip>;\n\n  return (\n    <OverlayTrigger placement={placement} {...rest} overlay={tooltip}>\n      {trigger}\n    </OverlayTrigger>\n  );\n};\n\nregisterComponent('TooltipTrigger', TooltipTrigger);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/components/ui/VerticalNavigation.jsx",
    "content": "import React from 'react';\nimport Nav from 'react-bootstrap/Nav';\nimport { Link } from 'react-router-dom';\n\nimport { registerComponent } from 'meteor/vulcan:lib';\n\nconst MenuItem = (\n  { name, label, path, onClick, labelToken, LeftComponent, RightComponent },\n  { intl }\n) => {\n  let Wrapper = React.Fragment;\n  if (path) {\n    const LinkToPath = ({ children }) => <Nav.Link as={Link} to={path}>{children}</Nav.Link>;\n    Wrapper = LinkToPath;\n  }\n  return (\n    <Wrapper key={name}>\n      <div\n        //selected={path && router.isActive(path)}\n        onClick={onClick}>\n        {LeftComponent && <LeftComponent />}\n        <span>{label || intl.formatMessage({ id: labelToken })}</span>\n        {RightComponent && <RightComponent />}\n      </div>\n    </Wrapper>\n  );\n};\n\nconst VerticalNavigation = ({links}) => {\n  return (\n    <Nav className=\"flex-column\">\n      { links.map(MenuItem) }\n    </Nav>\n  );\n};\n\nregisterComponent('VerticalNavigation', VerticalNavigation);\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/modules/components.js",
    "content": "import '../components/forms/Checkbox.jsx';\nimport '../components/forms/Checkboxgroup.jsx';\nimport '../components/forms/Date.jsx';\nimport '../components/forms/Date2.jsx';\nimport '../components/forms/Datetime.jsx';\nimport '../components/forms/Default.jsx';\nimport '../components/forms/Password.jsx';\nimport '../components/forms/Email.jsx';\nimport '../components/forms/FormComponentInner.jsx';\nimport '../components/forms/FormControl.jsx'; // note: only used by old accounts package, remove soon?\nimport '../components/forms/FormDescription.jsx';\nimport '../components/forms/FormElement.jsx';\nimport '../components/forms/FormGroupDefault';\nimport '../components/forms/FormItem.jsx';\nimport '../components/forms/FormLabel.jsx';\nimport '../components/forms/FormInputLoading.jsx';\nimport '../components/forms/Number.jsx';\nimport '../components/forms/Radiogroup.jsx';\nimport '../components/forms/Select.jsx';\nimport '../components/forms/SelectMultiple.jsx';\nimport '../components/forms/StaticText.jsx';\nimport '../components/forms/Textarea.jsx';\nimport '../components/forms/Time.jsx';\nimport '../components/forms/Url.jsx';\nimport '../components/forms/Likert.jsx';\nimport '../components/forms/Autocomplete.jsx';\nimport '../components/forms/AutocompleteMultiple.jsx';\n\nimport '../components/ui/Button.jsx';\nimport '../components/ui/Alert.jsx';\nimport '../components/ui/Modal.jsx';\nimport '../components/ui/ModalTrigger.jsx';\nimport '../components/ui/TooltipTrigger.jsx';\nimport '../components/ui/Dropdown.jsx';\nimport '../components/ui/Table.jsx';\nimport '../components/ui/VerticalNavigation.jsx';\n\nimport '../components/backoffice/BackofficeNavbar.jsx';\nimport '../components/backoffice/BackofficeVerticalMenuLayout.jsx';\nimport '../components/backoffice/BackofficePageLayout.jsx';\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/modules/index.js",
    "content": "export * from './components';"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/server/main.js",
    "content": " export * from '../modules/index.js'; \n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/stylesheets/datetime.scss",
    "content": "/*!\n * https://github.com/arqex/react-datetime\n */\n\n.rdt {\n  position: relative;\n}\n.rdtPicker {\n  display: none;\n  position: absolute;\n  width: 250px;\n  padding: 4px;\n  margin-top: 1px;\n  z-index: 99999 !important;\n  background: #fff;\n  box-shadow: 0 1px 3px rgba(0,0,0,.1);\n  border: 1px solid #f9f9f9;\n}\n.rdtOpen .rdtPicker {\n  display: block;\n}\n.rdtStatic .rdtPicker {\n  box-shadow: none;\n  position: static;\n}\n\n.rdtPicker .rdtTimeToggle {\n  text-align: center;\n}\n\n.rdtPicker table {\n  width: 100%;\n  margin: 0;\n}\n.rdtPicker td,\n.rdtPicker th {\n  text-align: center;\n  height: 28px;\n}\n.rdtPicker td {\n  cursor: pointer;\n}\n.rdtPicker td.rdtToday:hover,\n.rdtPicker td.rdtHour:hover,\n.rdtPicker td.rdtMinute:hover,\n.rdtPicker td.rdtSecond:hover,\n.rdtPicker .rdtTimeToggle:hover {\n  background: #eeeeee;\n  cursor: pointer;\n}\n.rdtPicker td.rdtOld,\n.rdtPicker td.rdtNew {\n  color: #999999;\n}\n.rdtPicker td.rdtToday {\n  position: relative;\n}\n.rdtPicker td.rdtToday:before {\n  content: '';\n  display: inline-block;\n  border-left: 7px solid transparent;\n  border-bottom: 7px solid #428bca;\n  border-top-color: rgba(0, 0, 0, 0.2);\n  position: absolute;\n  bottom: 4px;\n  right: 4px;\n}\n.rdtPicker td.rdtActive,\n.rdtPicker td.rdtActive:hover {\n  background-color: #428bca;\n  color: #fff;\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.rdtPicker td.rdtActive.rdtToday:before {\n  border-bottom-color: #fff;\n}\n.rdtPicker td.rdtDisabled,\n.rdtPicker td.rdtDisabled:hover {\n  background: none;\n  color: #999999;\n  cursor: not-allowed;\n}\n\n.rdtPicker td span.rdtOld {\n  color: #999999;\n}\n.rdtPicker td span.rdtDisabled,\n.rdtPicker td span.rdtDisabled:hover {\n  background: none;\n  color: #999999;\n  cursor: not-allowed;\n}\n.rdtPicker th {\n  border-bottom: 1px solid #f9f9f9;\n}\n.rdtPicker .dow {\n  width: 14.2857%;\n  border-bottom: none;\n}\n.rdtPicker th.rdtSwitch {\n  width: 100px;\n}\n.rdtPicker th.rdtNext,\n.rdtPicker th.rdtPrev {\n  font-size: 21px;\n  vertical-align: top;\n}\n\n.rdtPrev span,\n.rdtNext span {\n  display: block;\n  -webkit-touch-callout: none; /* iOS Safari */\n  -webkit-user-select: none;   /* Chrome/Safari/Opera */\n  -khtml-user-select: none;    /* Konqueror */\n  -moz-user-select: none;      /* Firefox */\n  -ms-user-select: none;       /* Internet Explorer/Edge */\n  user-select: none;\n}\n\n.rdtPicker th.rdtDisabled,\n.rdtPicker th.rdtDisabled:hover {\n  background: none;\n  color: #999999;\n  cursor: not-allowed;\n}\n.rdtPicker thead tr:first-child th {\n  cursor: pointer;\n}\n.rdtPicker thead tr:first-child th:hover {\n  background: #eeeeee;\n}\n\n.rdtPicker tfoot{\n  border-top: 1px solid #f9f9f9;\n}\n\n.rdtPicker button {\n  border: none;\n  background: none;\n  cursor: pointer;\n}\n.rdtPicker button:hover {\n  background-color: #eee;\n}\n\n.rdtPicker thead button {\n  width: 100%;\n  height: 100%;\n}\n\ntd.rdtMonth,\ntd.rdtYear {\n  height: 50px;\n  width: 25%;\n  cursor: pointer;\n}\ntd.rdtMonth:hover,\ntd.rdtYear:hover {\n  background: #eee;\n}\n\n.rdtCounters {\n  display: inline-block;\n}\n\n.rdtCounters > div{\n  float: left;\n}\n\n.rdtCounter {\n  height: 100px;\n}\n\n.rdtCounter {\n  width: 40px;\n}\n\n.rdtCounterSeparator {\n  line-height: 100px;\n}\n\n.rdtCounter .rdtBtn {\n  height: 40%;\n  line-height: 40px;\n  cursor: pointer;\n  display: block;\n\n  -webkit-touch-callout: none; /* iOS Safari */\n  -webkit-user-select: none;   /* Chrome/Safari/Opera */\n  -khtml-user-select: none;    /* Konqueror */\n  -moz-user-select: none;      /* Firefox */\n  -ms-user-select: none;       /* Internet Explorer/Edge */\n  user-select: none;\n}\n.rdtCounter .rdtBtn:hover {\n  background: #eee;\n}\n.rdtCounter .rdtCount {\n  height: 20%;\n  font-size: 1.2em;\n}\n\n.rdtMilli {\n  vertical-align: middle;\n  padding-left: 8px;\n  width: 48px;\n}\n\n.rdtMilli input {\n  width: 100%;\n  font-size: 1.2em;\n  margin-top: 37px;\n}\n\n.rdtDayPart {\n  margin-top: 43px;\n}"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/stylesheets/likert.scss",
    "content": "$spacing: 20px;\n\n.likert-scale{\n  display: table;\n  width: 100%;\n}\n.likert-row{\n  // display: grid;\n  // grid-template-columns: 200px auto;\n  display: table-row;\n  &:nth-child(2n) {\n    background: rgba(0,0,0,0.05);\n  }\n}\n.likert-row-heading{\n  display: table-cell;\n  padding: calc($spacing / 4);\n}\n.likert-heading{\n  font-weight: bold;\n}\n.likert-row-contents{\n  display: grid;\n  grid-auto-columns: 1fr;\n  grid-auto-flow: column;\n}\n.likert-row-cell{\n  display: table-cell;\n  text-align: center;\n  padding: calc($spacing / 4);\n  .form-check{\n    display: inline;\n    padding: 0;\n    label{\n      display: none;\n    }\n  }\n  .form-check-input{\n    margin: 0;\n    position: static;\n  }\n}"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/stylesheets/style.scss",
    "content": "$light-grey: #ddd;\n$medium-grey: #bbb;\n$vmargin: 15px;\n$light-border: $light-grey;\n\nselect.form-control{\n  height: 38px;\n}\n\n/* Example Styles for React Tags*/\ndiv.ReactTags__tags {\n  position: relative;\n}\n\n/* Styles for the input */\ndiv.ReactTags__tagInput {\n  width: 200px;\n  border-radius: 2px;\n  display: inline-block;\n}\ndiv.ReactTags__tagInput input, \ndiv.ReactTags__tagInput input:focus {\n  // height: 31px;\n  // margin: 0;\n  // font-size: 12px;\n  // width: 100%;\n  // border: 1px solid #eee;\n\n  display: block;\n  width: 100%;\n  padding: .375rem .75rem;\n  font-size: 1rem;\n  line-height: 1.5;\n  color: #55595c;\n  background-color: #fff;\n  background-image: none;\n  border: 1px solid #ccc;\n  border-radius: .25rem;\n}\n\n/* Styles for selected tags */\ndiv.ReactTags__selected {\n  float: left;\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n}\ndiv.ReactTags__selected span.ReactTags__tag {\n  border: 1px solid #ddd;\n  background: #eee;\n  font-size: 12px;\n  display: inline-block;\n  padding: 5px;\n  margin: 0 5px;\n  // cursor: move;\n  border-radius: 2px;\n}\ndiv.ReactTags__selected a.ReactTags__remove {\n  color: #aaa;\n  margin-left: 5px;\n  cursor: pointer;\n}\n\n/* Styles for suggestions */\ndiv.ReactTags__suggestions {\n  position: absolute;\n  z-index: 10;\n}\ndiv.ReactTags__suggestions ul {\n  list-style-type: none;\n  box-shadow: .05em .01em .5em rgba(0,0,0,.2);\n  background: white;\n  width: 200px;\n  padding: 0;\n}\ndiv.ReactTags__suggestions li {\n  border-bottom: 1px solid #ddd;\n  padding: 5px 10px;\n  margin: 0;\n}\ndiv.ReactTags__suggestions li mark {\n  text-decoration: underline;\n  background: none;\n  font-weight: 600;\n}\ndiv.ReactTags__suggestions ul li.active {\n  background: #b7cfe0;\n  cursor: pointer;\n}\n\ndiv.ReactTags__suggestions mark{\n  padding: 0;\n}\n\n.form-section{\n  .form-section-heading{\n    border-bottom: 1px solid $light-border;\n    padding-bottom: $vmargin;\n    margin-bottom: $vmargin;\n    font-size: 1.2rem;\n    font-weight: bold;\n  }\n}\n.control-label{\n  strong{\n    font-weight: normal;\n  }\n}\n\n.search-form{\n  position: relative;\n  .search-form-reset{\n    position: absolute;\n    top: 6px;\n    right: 5px;\n    color: $light-grey;\n  }\n}\n\n.form-input{\n  position: relative;\n  margin-bottom: 1rem;\n}\n.form-group{\n  margin-bottom: 0;\n}\n.form-control-limit{\n  position: absolute;\n  background: white;\n  padding: 5px;\n  top: 5px;\n  right: 5px;\n  color: $light-grey;\n  font-size: 80%;\n  &.danger{\n    color: #EF1642;\n  }\n}\n\n.form-section-heading{\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  cursor: pointer;\n  padding-bottom: 10px;\n  margin-bottom: 10px;\n  border-bottom: 1px solid $light-grey;\n}\n\n.form-section-heading-title{\n  margin: 0;\n  font-size: 1.25rem;\n}\n\n.form-section-collapsed{\n  display: none;\n}\n\n.form-section-hidden {\n  display: none;\n}\n\n.form-errors{\n  .alert{\n    ul{\n      padding: 0 0 0 20px;\n      margin: 0;\n    }\n  }\n}\n\n.input-error{\n  color: #EF1642;\n  input, textarea, select{\n    border-color: #EF1642;\n  }\n}\n\n.form-input-errors{\n  color: #EF1642;\n  text-align: right;\n  list-style-type: none;\n  padding: 0;\n  margin: 5px 0 0 0;\n  font-size: 0.8rem;\n  li{\n    margin: 0;\n  }\n}\n\n.form-component-select, .form-component-date,  .form-component-date2,  .form-component-datetime, .form-component-time, .form-component-date {\n  .col-sm-9{\n    padding-right: 40px;\n  }\n}\n\n.form-component-clear{\n  cursor: pointer;\n  position: absolute;\n  top: 11px;\n  right: 0px;\n  background: $light-grey;\n  color: #fff;\n  border-radius: 100%;\n  height: 16px;\n  width: 16px;\n  border: 0;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  span{\n    font-size: 8px;\n    display: block;\n    line-height: 1;\n  }\n  &:hover{\n    text-decoration: none;\n    background: $medium-grey;\n    color: #fff;\n  }\n}\n\n.form-nested-item{\n  border: 1px solid $light-grey;\n  padding: 10px;\n  border-radius: 3px;\n  margin-bottom: 10px;\n  display: flex;\n  align-items: center;\n  position: relative;\n  .form-nested-item-deleted-overlay{\n    position: absolute;\n    display: none;\n    opacity: 0.3;\n    z-index: 10000;\n    top: 0;\n    left: 0;\n    bottom: 0;\n    right: 0;\n    height: 100%;\n    width: 100%;\n    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='%23000000' fill-opacity='0.5' fill-rule='evenodd'%3E%3Cpath d='M0 40L40 0H20L0 20M40 40V20L20 40'/%3E%3C/g%3E%3C/svg%3E\");\n  }\n}\n\n.form-nested-item-inner{\n  flex: 1;\n}\n.form-nested-item-remove{\n  margin-left: 10px;\n}\n.form-nested-button{\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  svg{\n    display:block;\n    path{\n      fill: rgba(255,255,255,0.7);\n      stroke: rgba(255,255,255,0.7);\n    }\n  }\n}\n\n.form-component-radiogroup{\n  .col-sm-9{\n    padding-top: calc(.375rem + 1px);\n  }\n}\n\n.avatar {\n  border-radius: 50%;\n  overflow: hidden;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-direction: column;\n  background-color: #EEE;\n  \n  width: 48px;\n  height: 48px;\n  \n  &.size-xsmall {\n    width: 32px;\n    height: 32px;\n  }\n  \n  &.size-small {\n    width: 40px;\n    height: 40px;\n  }\n  \n  &.size-large {\n    width: 56px;\n    height: 56px;\n  }\n  \n  &.size-profile {\n    width: 120px;\n    height: 120px;\n  }\n  \n  &.gutter-bottom {\n    margin-bottom: 8px;\n  }\n  \n  &.gutter-left {\n    margin-left: 8px;\n  }\n  \n  &.gutter-right {\n    margin-right: 8px;\n  }\n  \n  &.gutter-sides {\n    margin-right: 8px;\n    margin-left: 8px;\n  }\n  \n  &.gutter-all {\n    margin: 8px;\n  }\n  \n  & img {\n    width: 100%;\n  }\n}\n\n.form-input-loading{\n  position: relative;\n}\n.form-input-loading-isLoading{\n  pointer-events: none;\n}\n.form-input-loading-loader{\n  background: rgba(0,0,0,.05);\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.form-component-checkbox{\n  .col-sm-9{\n    padding-top: calc(.375rem + 1px);\n  }\n}"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/stylesheets/typeahead-bs4.scss",
    "content": "$box-shadow-dimensions: 0 0 0 0.2rem;\n$placeholder-color: #6c757d;\n\n.rbt {\n  &-input-multi {\n    // Bootstrap 4 focus style\n    &.focus {\n      background-color: #fff;\n      border-color: #80bdff;\n      box-shadow: $box-shadow-dimensions rgba(0, 123, 255, 0.25);\n      color: #495057;\n      outline: 0;\n    }\n\n    &.is-invalid.focus {\n      border-color: #dc3545;\n      box-shadow: $box-shadow-dimensions rgba(220, 53, 69, 0.25);\n    }\n\n    &.is-valid.focus {\n      border-color: #28a745;\n      box-shadow: $box-shadow-dimensions rgba(40, 167, 69, 0.25);\n    }\n\n    input {\n      // Firefox\n      &::-moz-placeholder {\n        color: $placeholder-color;\n        opacity: 1;\n      }\n\n      // Internet Explorer 10+\n      &:-ms-input-placeholder {\n        color: $placeholder-color;\n      }\n\n      // Safari and Chrome\n      &::-webkit-input-placeholder  {\n        color: $placeholder-color;\n      }\n    }\n\n    // Override height in main CSS file :/\n    & .rbt-input-main,\n    &.form-control-lg .rbt-input-main,\n    &.form-control-sm .rbt-input-main {\n      height: auto;\n    }\n  }\n\n  &-token {\n    margin: 1px 3px 2px 0;\n  }\n}\n"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/lib/stylesheets/typeahead.scss",
    "content": "$animation: loader-animation 600ms infinite linear;\n$inset: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n$placeholder-color: #999;\n\n.rbt {\n  &-menu {\n    margin-bottom: 2px; // Spacing for dropup\n\n    & > li a {\n      overflow: hidden;\n      text-overflow: ellipsis;\n\n      &:focus {\n        outline: none;\n      }\n    }\n\n    &-pagination-option {\n      text-align: center;\n    }\n  }\n\n  // Hide IE's native \"clear\" button\n  & .rbt-input-main::-ms-clear {\n    display: none;\n  }\n\n  &-input-multi {\n    cursor: text;\n    overflow: hidden;\n    position: relative;\n    height: auto;\n\n    // Apply Bootstrap focus styles\n    &.focus {\n      box-shadow: $inset, 0 0 8px rgba(102, 175, 233, 0.6);\n      border-color: #66afe9;\n      outline: 0;\n    }\n\n    // BS4 uses the :disabled pseudo-class, which doesn't work with non-inputs.\n    &.form-control[disabled] {\n      background-color: #e9ecef;\n      opacity: 1;\n    }\n\n    // Apply Bootstrap placeholder styles\n    input {\n      // Firefox\n      &::-moz-placeholder {\n        color: $placeholder-color;\n        opacity: 1;\n      }\n\n      // Internet Explorer 10+\n      &:-ms-input-placeholder {\n        color: $placeholder-color;\n      }\n\n      // Safari and Chrome\n      &::-webkit-input-placeholder  {\n        color: $placeholder-color;\n      }\n    }\n\n    .rbt-input-wrapper {\n      align-items: flex-start;\n      display: flex;\n      flex-wrap: wrap;\n      margin-bottom: -4px;\n      margin-top: -1px;\n      overflow: hidden;\n    }\n\n    .rbt-input-main {\n      // Set input height for cross-browser consistency\n      height: 20px;\n      margin: 1px 0 4px;\n    }\n\n    &.input,\n    &.form-control {\n      &-lg {\n        .rbt-input-main {\n          height: 24px;\n        }\n      }\n\n      &-sm {\n        .rbt-input-main {\n          height: 18px;\n        }\n      }\n    }\n  }\n\n  &-close {\n    z-index: 1;\n\n    &-lg {\n      font-size: 24px;\n    }\n  }\n\n  &-token {\n    background-color: #e7f4ff;\n    border: 0;\n    border-radius: 2px;\n    color: #1f8dd6;\n    display: inline-block;\n    line-height: 1em;\n    margin: 0 3px 3px 0;\n    padding: 4px 7px;\n    position: relative;\n\n    &-disabled {\n      background-color: #ddd;\n      color: #888;\n      pointer-events: none;\n    }\n\n    &-removeable {\n      cursor: pointer;\n      padding-right: 21px;\n    }\n\n    &-active {\n      background-color: #1f8dd6;\n      color: #fff;\n      outline: none;\n      text-decoration: none;\n    }\n\n    & &-remove-button {\n      bottom: 0;\n      color: inherit;\n      font-size: inherit;\n      font-weight: normal;\n      opacity: 1;\n      outline: none;\n      padding: 3px 7px;\n      position: absolute;\n      right: 0;\n      text-shadow: none;\n      top: -2px;\n    }\n  }\n\n  &-loader {\n    -moz-animation: $animation;\n    -webkit-animation: $animation;\n    animation: $animation;\n    border: 1px solid #d5d5d5;\n    border-radius: 50%;\n    border-top-color: #1f8dd6;\n    display: block;\n    height: 16px;\n    width: 16px;\n\n    &-lg {\n      height: 20px;\n      width: 20px;\n    }\n  }\n\n  &-aux {\n    align-items: center;\n    display: flex;\n    bottom: 0;\n    justify-content: center;\n    pointer-events: none; /* Don't block clicks on the input */\n    position: absolute;\n    right: 0;\n    top: 0;\n    width: 34px;\n\n    &-lg {\n      width: 46px;\n    }\n\n    & .rbt-close {\n      margin-top: -4px;\n      pointer-events: auto; /* Override pointer-events: none; above */\n    }\n  }\n\n  .has-aux &-input {\n    padding-right: 34px;\n  }\n\n  &-highlight-text {\n    background-color: inherit;\n    color: inherit;\n    font-weight: bold;\n    padding: 0;\n  }\n}\n\n/* Input Groups */\n.input-group > .rbt {\n  flex: 1;\n\n  // Form-controls within input-groups have a higher z-index.\n  & .rbt-input-hint,\n  & .rbt-aux {\n    z-index: 5;\n  }\n\n  &:not(:first-child) .form-control {\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0;\n  }\n\n  &:not(:last-child) .form-control {\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0;\n  }\n}\n\n/* Validation States */\n.has-error .rbt-input-multi.focus {\n  border-color: #843534;\n  box-shadow: $inset, 0 0 6px #ce8483;\n}\n\n.has-warning .rbt-input-multi.focus {\n  border-color: #66512c;\n  box-shadow: $inset, 0 0 6px #c0a16b;\n}\n\n.has-success .rbt-input-multi.focus {\n  border-color: #2b542c;\n  box-shadow: $inset, 0 0 6px #67b168;\n}\n\n@keyframes loader-animation {\n  to {\n    transform: rotate(1turn);\n  }\n}"
  },
  {
    "path": "packages/vulcan-ui-bootstrap/package.js",
    "content": "Package.describe({\n  name: 'vulcan:ui-bootstrap',\n  summary: 'Vulcan Bootstrap UI components.',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:lib@=1.16.9', 'vulcan:scss@4.12.0']);\n\n  api.addFiles(\n    [\n      'lib/stylesheets/style.scss',\n      'lib/stylesheets/datetime.scss',\n      'lib/stylesheets/likert.scss',\n      'lib/stylesheets/typeahead.scss',\n      'lib/stylesheets/typeahead-bs4.scss',\n    ],\n    'client',\n  );\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "packages/vulcan-ui-material/accounts.css",
    "content": ".accounts-ui .form-control {\n  color: rgba(0, 0, 0, 0.87);\n  padding: 8px 0;\n  font-size: 1rem;\n  font-family: \"Roboto\", \"Helvetica\", \"Arial\", sans-serif;\n  border: none;\n  box-shadow: none;\n  border-bottom: 1px solid rgba(0, 0, 0, 0.87);\n  margin-bottom: 1px;\n  border-radius: 0;\n  width: 100%;\n}\n\n.accounts-ui .form-control:focus {\n  border-bottom-width: 2px;\n  margin-bottom: 0;\n}\n\n"
  },
  {
    "path": "packages/vulcan-ui-material/en_US.js",
    "content": "import { addStrings } from 'meteor/vulcan:core';\n\n\naddStrings('en', {\n\n  'search.search': 'Search',\n  'search.clear': 'Clear search',\n\n\n  'modal.close': 'Close',\n\n\n  'forms.phone_help': 'Call number',\n  'forms.email_help': 'Send email',\n  'forms.url_help': 'Visit site',\n\n\n  'load_more.load_more': 'Load more',\n  'load_more.loaded_count': 'Loaded {count} of {totalCount}',\n  //'load_more.loaded_all': '{totalCount, plural, =0 {No items} one {One item} other {# items}}',\n  'load_more.no_items': 'No items',\n  'load_more.one_item': 'One item',\n  'load_more.total_items': '{totalCount} items',\n\n});\n"
  },
  {
    "path": "packages/vulcan-ui-material/forms.css",
    "content": ".form-nested-item {\n  display: flex;\n}\n\n.form-nested-item-inner {\n  flex-grow: 1;\n}\n\n.form-nested-item-remove {\n  padding-top: 8px;\n  margin-left: 8px;\n}\n\n.form-nested-item-remove > button {\n  margin-right: -4px;\n}\n"
  },
  {
    "path": "packages/vulcan-ui-material/fr_FR.js",
    "content": "import { addStrings } from 'meteor/vulcan:core';\n\naddStrings('fr', {\n  'search.search': 'Recherche',\n  'search.clear': 'Effacer la recherche',\n  'modal.close': 'Fermer',\n});\n"
  },
  {
    "path": "packages/vulcan-ui-material/history.md",
    "content": "1.16.0_3 / 2021-02-16\n=====================\n\n * Theme provider now uses Components.ThemeProvider on the server side, similar to client side, in order to enable theming overrides in SSR.\n\n1.16.0_2 / 2020-11-16\n=====================\n\n * Base controls\n   * I have renamed components in `vulcan-ui-material/lib/components/forms/base-controls/` because their names conflict with the style sheet names (used for [Global theme overrides](https://material-ui.com/customization/components/#global-theme-override)) of some core MUI components - for example `MuiInput`.\n   * These components are not registered with `registerComponent`, only exported.\n   * This will be a breaking change for anyone who has built custom components based on these base controls using import - the file and component names have to be updated.\n   * The new names are:\n     * **/forms/helpers/**\n       * MuiFormControl => FormControlLayout\n       * MuiFormHelper => FormHelper\n       * MuiRequiredIndicator => RequiredIndicator\n     * **/forms/base-controls/**\n       * MuiCheckbox => FormCheckbox\n       * MuiCheckboxGroup => FormCheckboxGroup\n       * MuiInput => FormInput\n       * MuiPicker => FormPicker\n       * MuiRadioGroup => FormRadioGroup\n       * MuiSelect => FormSelect\n       * MuiSuggest => FormSuggest\n       * MuiSwitch => FormSwitch\n       * MuiText => FormText\n * The sample **Theme styles** page now displays sample buttons in addition to typography and color palettes\n \n1.16.0_1 / 2020-10-24\n=====================\n\n * MuiSuggest\n   * Fixed how styles are applied for focused, disabled, and error states\n   * Enabled the styling of `menuItem`, `menuItemHighlight`, and `menuItemIcon` classes\n   * Renamed `muiIcon` class to `selectIcon`\n   * Fixed a bug when `disableSelectOnBlur` prop was passed\n   * Tweaked the behavior of `highlightFirstSuggestion()`\n   * Added support for `inputRef` prop\n * MuiInput: Previously the field value was not updated until the user exited the input (`blur` event); now the value is updated as you type, enabling the form submit button sooner\n * Various components: Changed the type of component props to `PropTypes.oneOfType([PropTypes.node, PropTypes.elementType])`\n * Various components: Updated spacing to comply with linting rules\n * ScrollTrigger: Refined functionality\n * TooltipButton: Changed `buttonWrap` display to `inline-flex`\n * Datatable: Added support for the `label` column prop where you can pass text, or a React element\n * Component mixin: Updated list of fields in `cleanProps()`\n * EndAdornment: Tweaked button spacing\n * StartAdornment: Changed icon button to a `TooltipButton`\n * Countries: Added `getRegionCode()` function\n * FormSubmit: Tweaked button spacing\n * ThemeStyles: Fixed minor bugs\n * ModalTrigger: Now the trigger component is rendered using `instantiateComponent()`, the same as component props are rendered elsewhere\n \n1.16.1 / 2020-08-17\n===================\n\n * Updated Material UI to version 4.11.0 and updated related packages to the latest version\n * Fixed minor bugs related to the MUI update\n * MuiInput, Email, Url\n   * The value of `url` and `email` type inputs are scrubbed to make sure they output a valid url; `url`, `email`, and `social` type inputs display an active link\n   * MuiInput: The input now supports an empty label and adjusts the spacing accordingly\n * EndAdornment: refactored the menu indicator  \n * MuiFormControl: new `layout` prop value of 'shrink' turns off the `fullWidth` option for the control  \n * MuiSelect: added clear button to select controls the same as input and suggest controls  \n * MuiSwitch: added support for `addonBefore` and `addonAfter` the same as input and suggest controls\n * Modal\n   * Removed bottom border when `Modal` dialog title is empty\n   * Moved `closeButton` style to `theme.utils`\n   * New `dontWrapDialogContent` prop prevents wrapping the children in a `DialogContent` component\n   * `DialogTrigger`'s content is now lazy rendered\n   * Added deprecation warning: _ModalTrigger’s \"dialogProperties\" prop has been renamed \"dialogProps\"_\n * LoadMore, ScrollTrigger\n   * Added `scroller` prop which defaults to `window`, but can be set to the ref of another element\n   * Refactored for more reliable performance\n * MuiSuggest\n   * Wrapped option icon in ListItemIcon component\n   * Fixed bug: `MuiSuggest` would not accept or display values that don't match an option value, even when `limitToList` was false\n   * Numerous other bug fixes and refactoring \n * TooltipButtonUpgrades\n   * Added new props: `danger` and `cursor`\n   * Added new value for `type` prop: `menu`\n * Datatable\n   * Changed `editComponent` prop type from `func` to `node`\n   * New `SearchInputProps` and `TableProps` props allow sending props to the `SearchInput` and `Table` components\n   * New `wrapComponent` prop allows overriding the scroller that the `Table` is wrapped in by default\n   * New `cellStyle` prop of column definitions accepts a function or object to add style to individual cells\n   * Added default value for `paginationTerms`\n   * New bonus component `DatatableFromArray` is a wrapper for `Datatable` that takes an array of objects and supports pagination\n   \n1.13.2_2 / 2020-01-20\n=====================\n\n * MuiSuggest: Removed `selectedOption` and `inputFormatted` from the component state\n * TooltipIntl: Fixed bug: `titleValues` prop was not implemented\n\n1.13.2_1 / 2019-10-02\n=====================\n\n * ModalTrigger: Moved inner part of `ModalTrigger` to `Modal`\n * MuiSuggest: Modified component to be able to display pre-formatted values, not just simple strings\n * MuiSuggest: Added `disableSelectOnBlur` prop to prevent selecting the highlighted option on blur\n * MuiSuggest: Added `disableMatchParts` prop to prevent highlighting of matched sub-strings\n * TooltipButton: when passing `true` in `loading` prop the button is disabled... unless you pass `false` in `disabled`\n * FormGroupDefault, FormGroupLine, FormGroupNone : Implemented `group.hidden property`\n * LoadMore: Removed dependency on `react-intl`\n * SearchInput: Removed last usage of `TooltipIntl`\n * Datatable: Added scroller in case the table is too wide\n * StaticText: Added missing form control backed by `MuiText` base control\n * StartAdornment: Fixed bug\n * FormControl, FormComponentDate2, FormComponentText: Added missing form controls\n * Button: Added support for 'default' variant prop value\n * Added `shrinkLabel` option to `inputProperties`\n \n1.13.2 / 2019-09-13\n===================\n\n * Forms: Added indicator for required fields\n * Forms: Added support for styling of disabled input\n * LoadMore: Fixed bug that would sometimes display \"NaN items\"\n * StartAdornment: url and email inputs are now adorned with an icon button link - unless you pass `hideLink: true` in inputProperties\n \n1.13.0_1 / 2019-07-23\n=====================\n\n * TooltipButton: Deprecated TooltipIntl and TooltipIconButton in favor of TooltipButton - they will be deleted in Vulcan 1.15.2\n \n1.13.0 / 2019-07-19\n===================\n\n * TooltipIntl: Changed display from 'inline-block' to 'inherit' for more flexibility\n \n1.12.8_17 / 2019-02-02\n======================\n\n * TooltipIntl: Changed display from 'inline-block' to 'inherit' for more flexibility\n * Countries: Added getRegionLabel function\n \n1.12.8_16 / 2019-01-21\n======================\n\n * Countries: Fixed bug in validateRegion\n \n1.12.8_15 / 2019-01-21\n======================\n\n * Countries: Fixed bug in validateRegion\n \n1.12.8_14 / 2019-01-20\n======================\n\n * Countries: Added validateRegion function, which given a region value or label, will return the region value ('NY' or 'New York' => 'NY)\n * The contents of countries is now exported - this may be refactored out of the core vulcan-material-ui as some point\n \n1.12.8_13 / 2019-01-14\n======================\n\n * ModalTrigger: Added boolean dialogOverflow prop for use cases like popups that can go beyond the size of the dialog box\n * MuiSuggest: Fixed bug - The disabled state was not displayed correctly\n * MuiSuggest: Fixed bug - After selecting a suggestion, clicking on the control did not re-open the suggestions menu\n \n1.12.8_12 / 2019-01-12\n======================\n\n * Upgraded to Meteor 1.8.0.2\n \n1.12.8_11 / 2018-12-21\n======================\n\n * SearchInput: Added install autosize-input to readme\n * Datatable: Fixed sorting delay\n * Datatable: Added tableHeadCell class\n * Datatable: Added cellClass column property, which can be a string or a function: column.cellClass({ column, document, currentUser })\n \n1.12.8_10 / 2018-12-09\n======================\n\n * TooltipIntl: Added icon class\n * FormGroupWithLine: Moved caret from the right side to next to the title\n * Changed load_more.loaded_all string\n \n1.12.8_9 / 2018-11-26\n=====================\n\n * Fixed bug that displayed invalid total count at the bottom of data tables\n \n1.12.8_8 / 2018-11-23\n=====================\n\n * Improved the functionality of the LoadMore component\n * The showNoMore property has been deprecated\n * A showCount property has been added (true by default) that shows a count of loaded and total items\n * The load more icon or button is displayed even when infiniteScroll is enabled\n \n1.12.8_7 / 2018-11-10\n=====================\n\n * Fixed bug in Datatable.jsx\n * Updated ReadMe\n \n1.12.8_6 / 2018-11-06\n=====================\n\n * Fixed bug in Datatable.jsx\n * Reduced spacing of form components\n \n1.12.8_5 / 2018-10-31\n=====================\n\n * Fixed bugs in Datatable pagination\n * Set Datatable paginate prop to false by default\n \n1.12.8_4 / 2018-10-31\n=====================\n\n * Removed 'fr_FR.js' from package.js because any french strings loaded activates the french language\n * Fixed delete button and its tooltips positioning in FormSubmit\n * Added pagination to Datatable\n \n1.12.8_2 / 2018-10-29\n=====================\n\n * Fixed localization in \"clear search\" tooltip\n * Added name and aria-haspopup properties to the input component to improve compliance and facilitate UAT\n * Replaced Date, Time and DateTime form controls with native controls as recommended by MUI. \n   The deprecated react-datetime version of the controls are still there as DateRdt, TimeRdt and DateTimeRdt, but they are not registered.\n * Updated readme\n \n1.12.8_1 / 2018-10-22\n=====================\n\n * Made form components compatible with new Form.formComponents property\n \n1.12.8 / 2018-10-19\n===================\n\n * Made improvements to the search box, including keyboard shortcuts (s: focus search; c: clear search)\n * Added support in TooltipIntl for tooltips in popovers\n * Added action prop to ModalTrigger that enables a parent component to call openModal and closeModal\n * Started using MUI tables in Card component\n * Fixed bugs in MuiSuggest component\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/client/main.js",
    "content": "export * from '../modules/index';\nimport './wrapWithMuiTheme';\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/client/wrapWithMuiTheme.jsx",
    "content": "import React from 'react';\nimport { addCallback, Components } from 'meteor/vulcan:core';\n\nfunction wrapWithMuiTheme(app, { apolloClient }) {\n  return <Components.ThemeProvider apolloClient={apolloClient}>{app}</Components.ThemeProvider>;\n}\n\naddCallback('router.client.wrapper', wrapWithMuiTheme);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/accounts/AccountsButton.jsx",
    "content": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport Button from '@material-ui/core/Button';\nimport { replaceComponent, Utils } from 'meteor/vulcan:core';\nimport classNames from 'classnames';\n\n\nexport class AccountsButton extends Component {\n  render() {\n\n    const {\n      label,\n      type,\n      disabled = false,\n      className,\n      onClick\n    } = this.props;\n\n    return (\n      <Button\n        variant={type === 'link' ? 'text' : 'contained'}\n        size={type === 'link' ? 'small' : undefined}\n        color=\"primary\"\n        className={classNames(`button-${Utils.slugify(label)}`, className)}\n        type={type === 'link' ? 'button' : type}\n        disabled={disabled}\n        onClick={onClick}\n        disableRipple={true}\n      >\n        {label}\n      </Button>\n    );\n  }\n}\n\n\nAccountsButton.propTypes = {\n  label: PropTypes.string.isRequired,\n  type: PropTypes.oneOf(['link', 'submit', 'button']),\n  disabled: PropTypes.bool,\n  className: PropTypes.string,\n  onClick: PropTypes.func.isRequired,\n};\n\n\nreplaceComponent('AccountsButton', AccountsButton);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/accounts/AccountsButtons.jsx",
    "content": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, replaceComponent } from 'meteor/vulcan:core';\nimport CardActions from '@material-ui/core/CardActions';\nimport { withStyles } from '@material-ui/core/styles';\nimport classNames from 'classnames';\n\n\nconst styles = theme => ({\n  root: {\n    flexDirection: 'row-reverse',\n    padding: theme.spacing(2),\n    height: 'auto',\n  },\n});\n\n\nexport class AccountsButtons extends Component {\n  render() {\n\n    const {\n      classes,\n      buttons = {},\n      className = 'buttons',\n    } = this.props;\n\n    return (\n      <CardActions className={classNames(classes.root, className)}>\n        {Object.keys(buttons).map((id, i) =>\n          <Components.AccountsButton {...buttons[id]} key={i} />\n        )}\n      </CardActions>\n    );\n  }\n}\n\n\nAccountsButtons.propTypes = {\n  classes: PropTypes.object.isRequired,\n  buttons: PropTypes.object,\n  className: PropTypes.string,\n};\n\n\nAccountsButtons.displayName = 'AccountsButtons';\n\n\nreplaceComponent('AccountsButtons', AccountsButtons, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/accounts/AccountsField.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { replaceComponent } from 'meteor/vulcan:core';\nimport TextField from '@material-ui/core/TextField';\n\n\nconst autocompleteValues = {\n  'username': 'username',\n  'usernameOrEmail': 'email',\n  'email': 'email',\n  'password': 'current-password'\n};\n\n\nexport class AccountsField extends PureComponent {\n  \n  \n  constructor (props) {\n    super(props);\n    this.state = {\n      mount: true\n    };\n  }\n  \n  \n  triggerUpdate () {\n    // Trigger an onChange on initial load, to support browser pre-filled values.\n    const { onChange } = this.props;\n    if (this.input && onChange) {\n      onChange({ target: { value: this.input.value } });\n    }\n  }\n  \n  \n  componentDidMount () {\n    this.triggerUpdate();\n  }\n  \n  \n  componentDidUpdate (prevProps) {\n    // Re-mount component so that we don't expose browser pre-filled passwords if the component was\n    // a password before and now something else.\n    if (prevProps.id !== this.props.id) {\n      this.setState({ mount: false });\n    } else if (!this.state.mount) {\n      this.setState({ mount: true });\n      this.triggerUpdate();\n    }\n  }\n  \n  \n  render () {\n    const {\n      id,\n      hint,\n      label,\n      type = 'text',\n      onChange,\n      required = false,\n      className = 'field',\n      defaultValue = '',\n      autoFocus,\n      messages,\n    } = this.props;\n    let { message } = this.props;\n    const { mount = true } = this.state;\n    \n    if (type === 'notice') {\n      return <div className={className}>{label}</div>;\n    }\n  \n    const autoComplete = autocompleteValues[id];\n    \n    if (messages && messages.find && typeof id === 'string') {\n      const foundMessage = messages.find(element => {\n        if (typeof element.field !== 'string') return false;\n        return id.toLowerCase().indexOf(element.field.toLowerCase()) > -1;\n      });\n      if (foundMessage) {\n        message = foundMessage;\n      }\n    }\n  \n    return (\n      mount &&\n      \n      <div className={className} style={{ marginBottom: '10px' }}>\n        <TextField\n          id={id}\n          type={type}\n          inputRef={ref => { this.input = ref; }}\n          onChange={onChange}\n          placeholder={hint}\n          defaultValue={defaultValue}\n          autoComplete={autoComplete }\n          label={label}\n          autoFocus={autoFocus}\n          required={required}\n          error={!!message}\n          helperText={message && message.message}\n          fullWidth\n        />\n      </div>\n    );\n  }\n}\n\n\nAccountsField.propTypes = {\n  onChange: PropTypes.func,\n};\n\n\nreplaceComponent('AccountsField', AccountsField);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/accounts/AccountsFields.jsx",
    "content": "import React, { Component } from 'react';\nimport { Components, replaceComponent } from 'meteor/vulcan:core';\nimport CardContent from '@material-ui/core/CardContent';\n\n\nexport class AccountsFields extends Component {\n  render () {\n    const {\n      fields = {},\n      className = 'fields',\n      messages,\n    } = this.props;\n    \n    return (\n      <CardContent className={className}>\n        {\n          Object.keys(fields).map((id, i) =>\n            <Components.AccountsField {...fields[id]} messages={messages} autoFocus={i === 0} key={i}/>\n          )\n        }\n      </CardContent>\n    );\n  }\n}\n\n\nreplaceComponent('AccountsFields', AccountsFields);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/accounts/AccountsForm.jsx",
    "content": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\n\n\nconst styles = theme => ({\n  messages: {\n    '& .message': theme.utils.errorMessage\n  },\n});\n\n\nexport class AccountsForm extends Component {\n  \n  \n  componentDidMount () {\n    let form = this.form;\n    if (form) {\n      form.addEventListener('submit', (e) => {\n        e.preventDefault();\n      });\n    }\n  }\n  \n  \n  render () {\n    const {\n      oauthServices,\n      fields,\n      buttons,\n      messages,\n      ready = true,\n      className,\n      classes,\n    } = this.props;\n  \n    return (\n      <form ref={(ref) => this.form = ref}\n            className={classNames(className, 'accounts-ui', { 'ready': ready, })}\n            noValidate\n      >\n        <Components.AccountsFields fields={fields} messages={messages}/>\n        <Components.AccountsButtons buttons={{...buttons}}/>\n        <Components.AccountsPasswordOrService oauthServices={oauthServices}/>\n        <Components.AccountsSocialButtons oauthServices={oauthServices}/>\n        <Components.AccountsFormMessages messages={messages} className={classes.messages}/>\n      </form>\n    );\n  }\n  \n  \n}\n\n\nAccountsForm.propTypes = {\n  oauthServices: PropTypes.object,\n  fields: PropTypes.object.isRequired,\n  buttons: PropTypes.object.isRequired,\n  error: PropTypes.string,\n  ready: PropTypes.bool,\n  classes: PropTypes.object.isRequired,\n};\n\n\nAccountsForm.displayName = 'AccountsForm';\n\n\nregisterComponent('AccountsForm', AccountsForm, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/accounts/AccountsPasswordOrService.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { replaceComponent } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport Typography from '@material-ui/core/Typography';\nimport CardActions from '@material-ui/core/CardActions';\nimport { withStyles } from '@material-ui/core/styles';\nimport classNames from 'classnames';\n\nconst styles = theme => ({\n  root: {\n    flexDirection: 'row-reverse',\n    paddingRight: theme.spacing(2),\n    paddingLeft: theme.spacing(2),\n    height: 'auto',\n  },\n  typography: {\n    marginRight: theme.spacing(1),\n  },\n});\n\nexport function hasPasswordService() {\n  // First look for OAuth services.\n  return !!Package['accounts-password'];\n}\n\nexport class AccountsPasswordOrService extends PureComponent {\n  render() {\n    let { className = 'password-or-service', classes } = this.props;\n    const services = Object.keys(this.props.oauthServices).map(service => {\n      return this.props.oauthServices[service].label;\n    });\n    let labels = services;\n    if (services.length > 2) {\n      labels = [];\n    }\n\n    if (hasPasswordService() && services.length > 0) {\n      return (\n        <CardActions className={classNames(className, classes.root)}>\n          <Typography variant=\"caption\" className={classes.typography} align=\"right\">\n            {`${this.context.intl.formatMessage({ id: 'accounts.or_use' })} ${labels.join(' / ')}`}\n          </Typography>\n        </CardActions>\n      );\n    }\n    return null;\n  }\n}\n\nAccountsPasswordOrService.propTypes = {\n  oauthServices: PropTypes.object,\n};\n\nAccountsPasswordOrService.contextTypes = {\n  intl: intlShape,\n};\n\nreplaceComponent('AccountsPasswordOrService', AccountsPasswordOrService, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/accounts/AccountsSocialButtons.jsx",
    "content": "import React from 'react';\nimport { Components, replaceComponent } from 'meteor/vulcan:core';\nimport CardActions from '@material-ui/core/CardActions';\nimport { withStyles } from '@material-ui/core/styles';\nimport classNames from 'classnames';\n\nconst styles = theme => ({\n  root: {\n    justifyContent: 'flex-end',\n    padding: theme.spacing(2),\n    height: 'auto',\n  },\n});\n\nexport class AccountsSocialButtons extends React.Component {\n  render() {\n    let { oauthServices = {}, className = 'social-buttons', classes } = this.props;\n    return (\n      <CardActions className={classNames(classes.root, className)}>\n        {Object.keys(oauthServices).map((id, i) => {\n          return <Components.AccountsButton {...oauthServices[id]} key={i} />;\n        })}\n      </CardActions>\n    );\n  }\n}\n\nreplaceComponent('AccountsSocialButtons', AccountsSocialButtons, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/backoffice/BackofficeNavbar.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:lib';\nimport AppBar from '@material-ui/core/AppBar';\nimport Toolbar from '@material-ui/core/Toolbar';\nimport Typography from '@material-ui/core/Typography';\nimport IconButton from '@material-ui/core/IconButton';\nimport MenuIcon from 'mdi-material-ui/Menu';\nimport { Link } from 'react-router-dom';\n\nconst BackofficeNavbar = ({ onClick, basePath }) => {\n  // console.log('Icon render', MenuIcon); // @see https://github.com/VulcanJS/Vulcan/issues/2580\n  return (\n  <AppBar position=\"static\">\n    <Toolbar>\n      <IconButton edge=\"start\" color=\"inherit\" aria-label=\"menu\" onClick={onClick}>\n        <MenuIcon />\n      </IconButton>\n\n      <Link to={basePath}>\n        <Typography variant=\"h6\">Backoffice</Typography>\n      </Link>\n    </Toolbar>\n  </AppBar>\n);};\n\nregisterComponent('VulcanBackofficeNavbar', BackofficeNavbar);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/backoffice/BackofficePageLayout.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:lib';\nimport { withStyles } from '@material-ui/core/styles';\n\nconst baseStyles = theme => ({\n  pageLayout: {\n    display: 'flex',\n    flexDirection: 'column',\n    height: '100vh',\n    overflow: 'hidden',\n  },\n});\n\nconst BackofficePageLayout = ({ children, classes }) => {\n  return <div className={classes.pageLayout}>{children}</div>;\n};\n\nregisterComponent('VulcanBackofficePageLayout', BackofficePageLayout, [withStyles, baseStyles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/backoffice/BackofficeVerticalMenuLayout.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:lib';\nimport { withStyles } from '@material-ui/core/styles';\nimport classnames from 'classnames';\n\nconst baseStyles = theme => ({\n  wrapper: {\n    display: 'flex',\n    overflow: 'auto',\n    height: '100%',\n  },\n  side: {\n    top: '0',\n    left: '0',\n    height: '100%',\n    overflowX: 'hidden',\n    backgroundColor: '#f7f7f7',\n    borderRight: '1px solid #ececec',\n    padding: '0',\n    transition: 'all 0.5s ease-out',\n  },\n  sideOpen: {\n    minWidth: '200px',\n    width: '200px',\n    visibility: 'visible',\n  },\n  sideClosed: {\n    minWidth: '0',\n    width: '0',\n    visibility: 'hidden',\n  },\n  main: {\n    flexGrow: '1',\n    overflow: 'auto',\n  },\n  margin: {\n    margin: '16px',\n  },\n});\n\nconst BackofficeVerticalMenuLayout = ({ side, main, open, classes }) => {\n  return (\n    <div className={classes.wrapper}>\n      <aside className={classnames(classes.side, open && classes.sideOpen, !open && classes.sideClosed)}>{side}</aside>\n\n      <main className={classes.main}>\n        <div className={classes.margin}>{main}</div>\n      </main>\n    </div>\n  );\n};\n\nregisterComponent('VulcanBackofficeVerticalMenuLayout', BackofficeVerticalMenuLayout, [withStyles, baseStyles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/bonus/DatatableFromArray.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { Components as C, registerComponent } from 'meteor/vulcan:core';\n\n\nclass DatatableFromArray extends PureComponent {\n  \n  \n  constructor (props) {\n    super(props);\n    \n    this.state = {\n      paginationTerms: {\n        itemsPerPage: props.itemsPerPage || 25,\n        limit: props.itemsPerPage || 25,\n        offset: 0,\n      }\n    };\n  }\n  \n  \n  setPaginationTerms = (paginationTerms) => {\n    this.setState({ paginationTerms });\n  };\n  \n  \n  render () {\n    const { paginate, data } = this.props;\n    const { itemsPerPage, offset } = this.state.paginationTerms;\n    const dataPage = paginate ? data.slice(offset, offset + itemsPerPage) : data;\n    \n    return (\n      <C.Datatable {...this.props}\n                   results={dataPage}\n                   count={dataPage.length}\n                   totalCount={data.length}\n                   showEdit={false}\n                   showNew={false}\n                   paginationTerms={paginate ? this.state.paginationTerms : undefined}\n                   setPaginationTerms={paginate ? this.setPaginationTerms : undefined}\n      />\n    );\n    \n  }\n  \n}\n\nDatatableFromArray.propTypes = {\n  data: PropTypes.array,\n  paginate: PropTypes.bool,\n  itemsPerPage: PropTypes.number,\n};\n\n\nDatatableFromArray.displayName = 'DatatableFromArray';\n\n\nregisterComponent('DatatableFromArray', DatatableFromArray);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/bonus/KeyEventHandler.jsx",
    "content": "import React from 'react';\nimport KeyboardEventHandler from 'react-keyboard-event-handler';\n\n\nconst KeyEventHandler = (props) => <KeyboardEventHandler {...props}/>;\n\n\nKeyEventHandler.displayName = 'KeyEventHandler';\n\n\nexport default KeyEventHandler;\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/bonus/LoadMore.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport Typography from '@material-ui/core/Typography';\nimport Button from '@material-ui/core/Button';\nimport IconButton from '@material-ui/core/IconButton';\nimport ArrowDownIcon from 'mdi-material-ui/ArrowDown';\nimport ScrollTrigger from './ScrollTrigger';\nimport classNames from 'classnames';\n\nconst styles = theme => ({\n  root: {\n    textAlign: 'center',\n    flexBasis: '100%',\n    marginTop: theme.spacing(3),\n  },\n\n  textButton: {\n    marginTop: theme.spacing(2),\n  },\n\n  iconButton: {},\n\n  caption: {\n    paddingTop: theme.spacing(1),\n    paddingBottom: theme.spacing(1),\n  },\n});\n\nconst LoadMore = (\n  {\n    classes,\n    count,\n    totalCount,\n    loadMore,\n    networkStatus,\n    showCount,\n    useTextButton,\n    className,\n    infiniteScroll,\n    scroller,\n  },\n  { intl }\n) => {\n  const isLoadingMore = networkStatus < 7;\n  const loadMoreText = intl.formatMessage({ id: 'load_more.load_more' });\n  const title = `${loadMoreText} (${count}/${totalCount})`;\n  const hasMore = totalCount > count;\n  const countValues = { count, totalCount };\n  const loadMoreId = hasMore\n    ? 'loaded_count'\n    : !totalCount\n    ? 'no_items'\n    : totalCount === 1\n    ? 'one_item'\n    : 'total_items';\n  showCount = isNaN(totalCount) || isNaN(count) ? false : showCount;\n\n  const loadMoreButton = useTextButton ? (\n    <Button className={classes.textButton} onClick={() => loadMore()}>\n      {title}\n    </Button>\n  ) : (\n    <IconButton className={classes.iconButton} onClick={() => loadMore()}>\n      <ArrowDownIcon />\n    </IconButton>\n  );\n\n  return (\n    <div className={classNames('load-more', classes.root, className)}>\n      {showCount && (\n        <Typography variant=\"caption\" className={classes.caption}>\n          <Components.FormattedMessage id={`load_more.${loadMoreId}`} values={countValues} />\n        </Typography>\n      )}\n      {isLoadingMore ? (\n        <Components.Loading />\n      ) : hasMore ? (\n        infiniteScroll ? (\n          <ScrollTrigger scroller={scroller} onTrigger={() => loadMore()}>\n            {loadMoreButton}\n          </ScrollTrigger>\n        ) : (\n          loadMoreButton\n        )\n      ) : null}\n    </div>\n  );\n};\n\nLoadMore.propTypes = {\n  classes: PropTypes.object.isRequired,\n  count: PropTypes.number,\n  totalCount: PropTypes.number,\n  loadMore: PropTypes.func,\n  networkStatus: PropTypes.number,\n  showCount: PropTypes.bool,\n  useTextButton: PropTypes.bool,\n  className: PropTypes.string,\n  infiniteScroll: PropTypes.bool,\n  scroller: PropTypes.object,\n};\n\nLoadMore.defaultProps = {\n  showCount: true,\n};\n\nLoadMore.contextTypes = {\n  intl: intlShape.isRequired,\n};\n\nLoadMore.displayName = 'LoadMore';\n\nregisterComponent('LoadMore', LoadMore, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/bonus/ScrollTrigger.jsx",
    "content": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport _throttle from 'lodash/throttle';\n\nclass ScrollTrigger extends Component {\n\n  constructor (props) {\n    super();\n\n    this.onScroll = _throttle(this.onScroll.bind(this), 100, {\n      leading: true,\n      trailing: true,\n    });\n\n    this.onResize = _throttle(this.onResize.bind(this), 100, {\n      leading: true,\n      trailing: true,\n    });\n\n    this.element = React.createRef();\n    this.passive = this.supportsPassive() ? { passive: true } : false;\n  }\n\n  supportsPassive () {\n    let supportsPassive = false;\n    try {\n      const opts = Object.defineProperty({}, 'passive', {\n        //eslint-disable-next-line getter-return\n        get: function () {\n          supportsPassive = true;\n        }\n      });\n      window.addEventListener('testPassive', null, opts);\n      window.removeEventListener('testPassive', null, opts);\n      //eslint-disable-next-line no-empty\n    } catch (e) {}\n    return supportsPassive;\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    this.checkStatus();\n    return false;\n  }\n\n  componentDidMount () {\n    this.addEventListeners();\n  }\n\n  componentWillUnmount () {\n    this.removeEventListeners();\n  }\n\n  componentDidUpdate (prevProps, prevState, snapshot) {\n    if (prevProps.scroller !== this.props.scroller) {\n      this.removeEventListeners();\n      if (this.props.scroller) {\n        this.addEventListeners();\n      }\n    }\n    this.checkStatus();\n  }\n\n  addEventListeners () {\n    if (this.props.scroller === window) {\n      window.addEventListener('scroll', this.onScroll);\n    } else if (this.props.scroller) {\n      this.props.scroller.addEventListener('scroll', this.onScroll, this.passive);\n    }\n\n    if (window) {\n      window.addEventListener('resize', this.onResize, this.passive);\n    }\n\n    if (this.props.triggerOnLoad) {\n      this.checkStatus();\n    }\n  }\n\n  removeEventListeners () {\n    if (this.props.scroller === window) {\n      window.removeEventListener('scroll', this.onScroll);\n    } else if (this.props.scroller) {\n      this.props.scroller.removeEventListener('scroll', this.onScroll);\n    }\n\n    if (window) {\n      window.removeEventListener('resize', this.onResize);\n    }\n  }\n\n  onResize () {\n    this.checkStatus();\n  }\n\n  onScroll () {\n    this.checkStatus();\n  }\n\n  checkStatus () {\n    const { onTrigger, scroller } = this.props;\n    if (!onTrigger || !scroller || !this.element.current) return;\n\n    const elementRect = this.element.current.getBoundingClientRect();\n    const viewportEnd = this.props.scroller.clientHeight + this.props.preloadHeight;\n    const inViewport = elementRect.top < viewportEnd;\n\n    if (inViewport) {\n      onTrigger(this);\n    }\n  }\n\n  render () {\n    const {\n      children,\n    } = this.props;\n\n    return (\n      <div ref={this.element}>\n        {children}\n      </div>\n    );\n  }\n}\n\n\nScrollTrigger.propTypes = {\n  scroller: PropTypes.object,\n  triggerOnLoad: PropTypes.bool,\n  preloadHeight: PropTypes.number,\n  onTrigger: PropTypes.func,\n};\n\n\nScrollTrigger.defaultProps = {\n  scroller: typeof window !== 'undefined' ? window : null,\n  preloadHeight: 1000,\n  triggerOnLoad: true,\n};\n\n\nexport default ScrollTrigger;\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/bonus/SearchInput.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport SearchIcon from 'mdi-material-ui/Magnify';\nimport ClearIcon from 'mdi-material-ui/CloseCircle';\nimport TextField from '@material-ui/core/TextField';\nimport NoSsr from '@material-ui/core/NoSsr';\nimport classNames from 'classnames';\nimport _debounce from 'lodash/debounce';\n\nconst styles = theme => ({\n  '@global': {\n    'input[type=text]::-ms-clear, input[type=text]::-ms-reveal': {\n      display: 'none',\n      width: 0,\n      height: 0,\n    },\n    'input[type=\"search\"]::-webkit-search-decoration, input[type=\"search\"]::-webkit-search-cancel-button': {\n      display: 'none',\n    },\n    'input[type=\"search\"]::-webkit-search-results-button, input[type=\"search\"]::-webkit-search-results-decoration': {\n      display: 'none',\n    },\n  },\n\n  root: {\n    marginTop: 0,\n  },\n\n  clear: {\n    transition: theme.transitions.create('opacity,transform', {\n      duration: theme.transitions.duration.short,\n    }),\n    opacity: 0.65,\n    width: 36,\n    height: 36,\n    margin: -6,\n    marginLeft: 0,\n    '& svg': {\n      width: 16,\n      height: 16,\n    },\n    flexDirection: 'column',\n  },\n\n  clearDense: {\n    width: 32,\n    height: 32,\n    margin: -4,\n    marginLeft: 0,\n  },\n\n  clearDisabled: {\n    opacity: 0,\n    pointerEvents: 'none',\n  },\n\n  icon: {\n    color: theme.palette.common.lightBlack,\n    marginLeft: theme.spacing(1),\n    marginRight: theme.spacing(1),\n  },\n\n  input: {\n    lineHeight: 1,\n    paddingTop: 2,\n    paddingBottom: 2,\n    marginBottom: 1,\n    /*transition: theme.transitions.create('width', {\n      duration: theme.transitions.duration.shortest,\n    }),*/\n    minWidth: 130,\n  },\n});\n\nclass SearchInput extends PureComponent {\n  constructor(props) {\n    super(props);\n\n    this.state = {\n      value: props.defaultValue || '',\n    };\n\n    this.input = null;\n\n    this.updateQuery = _debounce(this.updateQuery, 500);\n  }\n\n  componentDidMount() {\n    if (!document) return;\n    const element = document.querySelector(`.search-input-${this.props.name} input[type=search]`);\n\n    element._addEventListener = element.addEventListener;\n    element.addEventListener = function(type, listener, useCapture) {\n      if (useCapture === undefined) useCapture = false;\n      this._addEventListener(type, listener, useCapture);\n    };\n\n    element.addEventListener = element._addEventListener;\n  }\n\n  componentWillUnmount() {}\n\n  handleShortcutKeys = (key, event) => {\n    switch (key) {\n      case 's':\n        this.focusInput();\n        event.preventDefault();\n        break;\n      case 'c':\n      case 'esc':\n        this.clearSearch(event, true);\n        event.preventDefault();\n        break;\n    }\n  };\n\n  handleFocus = () => {\n    this.input.select();\n  };\n\n  focusInput = event => {\n    this.input.focus();\n  };\n\n  clearSearch = (event, dontFocus) => {\n    this.setState({ value: '' });\n    this.updateQuery('');\n\n    if (!dontFocus) {\n      this.focusInput();\n    }\n  };\n\n  updateSearch = event => {\n    const value = event.target.value;\n    this.setState({ value: value });\n    this.updateQuery(value);\n  };\n\n  updateQuery = value => {\n    this.props.updateQuery(value);\n  };\n\n  render() {\n    const { classes, className, dense, noShortcuts, name } = this.props;\n\n    const searchIcon = <SearchIcon className={classes.icon} onClick={this.focusInput} />;\n\n    const clearButton = (\n      <Components.TooltipButton\n        titleId=\"search.clear\"\n        icon={<ClearIcon />}\n        onClick={this.clearSearch}\n        classes={{\n          root: classNames(!this.state.value && classes.clearDisabled),\n          button: classNames('clear-button', classes.clear, dense && classes.clearDense),\n        }}\n        disabled={!this.state.value}\n      />\n    );\n\n    return (\n      <React.Fragment>\n        <TextField\n          label=\"Search\"\n          type=\"search\"\n          id={`search-input-${name}`}\n          name={name}\n          title=\"Search\"\n          value={this.state.value}\n          inputRef={input => (this.input = input)}\n          fullWidth\n          className={classNames(\n            'search-input',\n            `search-input-${name}`,\n            classes.root,\n            dense && classes.inputTypeSearch,\n            className,\n            classes.textField\n          )}\n          margin=\"normal\"\n          variant=\"outlined\"\n          onChange={this.updateSearch}\n          onFocus={this.handleFocus}\n          InputProps={{\n            startAdornment: searchIcon,\n            endAdornment: clearButton,\n          }}\n        />\n        <NoSsr>\n          {\n            !noShortcuts &&\n\n            <Components.KeyEventHandler\n              handleKeys={['s', 'c', 'esc']}\n              onKeyEvent={this.handleShortcutKeys}\n            />\n          }\n        </NoSsr>\n      </React.Fragment>\n    );\n  }\n}\n\nSearchInput.propTypes = {\n  classes: PropTypes.object.isRequired,\n  updateQuery: PropTypes.func.isRequired,\n  className: PropTypes.string,\n  dense: PropTypes.bool,\n  noShortcuts: PropTypes.bool,\n  name: PropTypes.string.isRequired,\n};\n\nSearchInput.defaultProps = {\n  name: 'search',\n};\n\nSearchInput.displayName = 'SearchInput';\n\nregisterComponent('SearchInput', SearchInput, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/bonus/TooltipButton.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent, instantiateComponent, Utils } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { withStyles } from '@material-ui/core/styles';\nimport { withTheme } from '@material-ui/core/styles';\nimport Tooltip from '@material-ui/core/Tooltip';\nimport IconButton from '@material-ui/core/IconButton';\nimport Button from '@material-ui/core/Button';\nimport Fab from '@material-ui/core/Fab';\nimport CircularProgress from '@material-ui/core/CircularProgress';\nimport MenuItem from '@material-ui/core/MenuItem';\nimport ListItemIcon from '@material-ui/core/ListItemIcon';\nimport ListItemText from '@material-ui/core/ListItemText';\nimport classNames from 'classnames';\n\n\nconst styles = theme => ({\n\n  root: {\n    display: 'contents',\n  },\n\n  tooltip: {},\n\n  buttonWrap: {\n    position: 'relative',\n    display: 'inline-flex',\n  },\n\n  button: {},\n\n  fab: {},\n\n  menu: {},\n\n  popoverPopper: {\n    zIndex: 1700,\n  },\n\n  popoverTooltip: {\n    zIndex: 1701,\n  },\n\n  iconWrap: {\n    position: 'relative',\n  },\n\n  icon: {\n    width: 24,\n    height: 24,\n  },\n\n  xsmall: {\n    width: 32,\n    height: 32,\n  },\n\n  small: {\n    width: 40,\n    height: 40,\n  },\n\n  medium: {\n    width: 48,\n    height: 48,\n  },\n\n  large: {\n    width: 56,\n    height: 56,\n  },\n\n  dangerButton: {\n    ...theme.utils.dangerButton,\n  },\n\n  progress: {\n    color: theme.palette.secondary.main,\n    position: 'absolute',\n    top: 0,\n    right: 0,\n    bottom: 0,\n    left: 0,\n    //zIndex: 1,\n    pointerEvents: 'none',\n  },\n\n  buttonProgress: {\n    color: theme.palette.secondary.main,\n    position: 'absolute',\n    top: -4,\n    right: -4,\n    bottom: -4,\n    left: -4,\n    //zIndex: 1,\n  },\n\n});\n\nconst TooltipButton = (props, { intl }) => {\n  const {\n    title,\n    titleId,\n    titleValues,\n    label,\n    labelId,\n    placement,\n    icon,\n    loading,\n    disabled,\n    type,\n    size,\n    danger,\n    className,\n    classes,\n    theme,\n    enterDelay,\n    leaveDelay,\n    buttonRef,\n    parent,\n    children,\n    cursor,\n    TooltipProps,\n    ...properties\n  } = props;\n\n  const iconWithClass = instantiateComponent(icon, { className: classNames('icon', classes.icon) });\n  const popperClass = parent === 'popover' && classes.popoverPopper;\n  const tooltipClass = parent === 'popover' && classes.popoverTooltip;\n  const tooltipEnterDelay = typeof enterDelay === 'number' ? enterDelay : theme.utils.tooltipEnterDelay;\n  const tooltipLeaveDelay = typeof leaveDelay === 'number' ? leaveDelay : theme.utils.tooltipLeaveDelay;\n  let titleText = title || (titleId ? intl.formatMessage({ id: titleId }, titleValues) : '');\n  let labelText = label || (labelId ? intl.formatMessage({ id: labelId }, titleValues) : '');\n  if (type === 'button' || type === 'menu') {\n    if (!labelText) labelText = titleText;\n    if (titleText === labelText) titleText = '';\n  }\n  const slug = Utils.slugify(titleId || labelId);\n  const buttonWrapStyle = cursor ? { cursor: cursor } : null;\n\n  return (\n    <span className={classNames('tooltip-button', classes.root, className)}>\n\n      <Tooltip id={`tooltip-${slug}`}\n               title={titleText}\n               placement={placement}\n               arrow\n               enterDelay={tooltipEnterDelay}\n               leaveDelay={tooltipLeaveDelay}\n               classes={{\n                 tooltip: classNames(classes.tooltip, tooltipClass),\n                 popper: popperClass,\n               }}\n               PopperProps={{\n                 ref: (popper) => { if (popper && popper.popper) popper.popper.scheduleUpdate(); },\n               }}\n               {...TooltipProps}\n      >\n        <span className={classes.buttonWrap} style={buttonWrapStyle}>\n          {\n            type === 'menu'\n\n              ?\n\n              <MenuItem className={classNames(classes.menu, slug)}\n                        {...properties}\n                        button={true}\n                        disabled={loading || disabled}\n              >\n                <ListItemIcon>\n                  {icon}\n                </ListItemIcon>\n                <ListItemText primary={labelText}/>\n              </MenuItem>\n\n              :\n\n              type === 'fab' && !!icon\n\n                ?\n\n                <>\n                  <Fab className={classNames(classes.button, classes.fab, danger && classes.dangerButton, slug)}\n                       {...properties}\n                       size={size}\n                       aria-label={title}\n                       ref={buttonRef}\n                       disabled={loading || disabled}\n                  >\n                    {iconWithClass}\n                  </Fab>\n                  {loading && <CircularProgress size=\"auto\" className={classes.progress}/>}\n                </>\n\n                :\n\n                ['button', 'submit'].includes(type)\n\n                  ?\n\n                  <Button className={classNames(classes.button, danger && classes.dangerButton, slug)}\n                          {...properties}\n                          type={type}\n                          size={size}\n                          aria-label={title}\n                          ref={buttonRef}\n                          disabled={loading || disabled}\n                  >\n                    {\n                      iconWithClass &&\n\n                      <span className={classNames('icon-wrap', classes.iconWrap)}>\n                        {iconWithClass}\n                        {loading && <CircularProgress size=\"auto\" className={classes.buttonProgress}/>}\n                      </span>\n                    }\n                    {labelText}\n                  </Button>\n\n                  :\n\n                  !!icon\n\n                    ?\n\n                    <>\n                      <IconButton\n                        className={classNames(classes.button, danger && classes.dangerButton, classes[size], slug)}\n                        {...properties}\n                        aria-label={title}\n                        ref={buttonRef}\n                        disabled={(loading && !(disabled === false)) || disabled}\n                      >\n                        {iconWithClass}\n                      </IconButton>\n                      {loading && <CircularProgress size=\"auto\" className={classes.progress}/>}\n                    </>\n\n                    :\n\n                    children\n          }\n        </span>\n      </Tooltip>\n\n    </span>\n  );\n\n};\n\nTooltipButton.propTypes = {\n  title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),\n  titleId: PropTypes.string,\n  titleValues: PropTypes.object,\n  label: PropTypes.node,\n  labelId: PropTypes.string,\n  type: PropTypes.oneOf(['simple', 'fab', 'button', 'submit', 'icon', 'menu']),\n  size: PropTypes.oneOf(['icon', 'xsmall', 'small', 'medium', 'large']),\n  danger: PropTypes.bool,\n  placement: PropTypes.oneOf(['bottom-end', 'bottom-start', 'bottom',\n    'left-end', 'left-start', 'left', 'right-end', 'right-start', 'right', 'top-end', 'top-start', 'top']),\n  icon: PropTypes.node,\n  loading: PropTypes.bool,\n  disabled: PropTypes.bool,\n  className: PropTypes.string,\n  classes: PropTypes.object,\n  buttonRef: PropTypes.func,\n  theme: PropTypes.object,\n  enterDelay: PropTypes.number,\n  leaveDelay: PropTypes.number,\n  parent: PropTypes.oneOf(['default', 'popover']),\n  children: PropTypes.node,\n  cursor: PropTypes.string,\n  TooltipProps: PropTypes.object,\n  variant: PropTypes.oneOf(['contained', 'outlined', 'text']),\n  color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),\n};\n\nTooltipButton.defaultProps = {\n  placement: 'bottom',\n  parent: 'default',\n  size: 'medium',\n};\n\nTooltipButton.contextTypes = {\n  intl: intlShape.isRequired,\n};\n\nTooltipButton.displayName = 'TooltipButton';\n\nregisterComponent('TooltipButton', TooltipButton, [withStyles, styles], withTheme);\n\nexport default TooltipButton;\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/bonus/TooltipIconButton.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent, Utils } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { withStyles } from '@material-ui/core/styles';\nimport { withTheme } from '@material-ui/core/styles';\nimport Tooltip from '@material-ui/core/Tooltip';\nimport IconButton from '@material-ui/core/IconButton';\nimport Fab from '@material-ui/core/Fab';\nimport classNames from 'classnames';\n\nconst styles = theme => ({\n  root: {},\n  tooltip: {\n    margin: '4px !important',\n  },\n  buttonWrap: {\n    display: 'inline-block',\n  },\n  button: {},\n});\n\nconst TooltipIconButton = (props, { intl }) => {\n  //eslint-disable-next-line no-console\n  console.warn(\n    'WARNING! TooltipIconButton is deprecated in favor of TooltipButton as of vulcan:ui-material 1.13.0_1 and will be deleted in version 1.15.2'\n  );\n\n  const { title, titleId, placement, icon, className, classes, theme, buttonRef, variant, ...properties } = props;\n\n  const titleText = props.title || intl.formatMessage({ id: titleId });\n  const slug = Utils.slugify(titleId);\n\n  return (\n    <Tooltip\n      classes={{ tooltip: classNames('tooltip-icon-button', classes.tooltip, className) }}\n      id={`tooltip-${slug}`}\n      title={titleText}\n      placement={placement}\n      enterDelay={theme.utils.tooltipEnterDelay}>\n      <div className={classes.buttonWrap}>\n        {variant === 'fab' ? (\n          <Fab className={classNames(classes.button, slug)} aria-label={titleText} ref={buttonRef} {...properties}>\n            {icon}\n          </Fab>\n        ) : (\n          <IconButton className={classNames(classes.button, slug)} aria-label={titleText} ref={buttonRef} {...properties}>\n            {icon}\n          </IconButton>\n        )}\n      </div>\n    </Tooltip>\n  );\n};\n\nTooltipIconButton.propTypes = {\n  title: PropTypes.node,\n  titleId: PropTypes.string,\n  placement: PropTypes.string,\n  icon: PropTypes.node.isRequired,\n  className: PropTypes.string,\n  classes: PropTypes.object,\n  buttonRef: PropTypes.func,\n  variant: PropTypes.string,\n  theme: PropTypes.object,\n};\n\nTooltipIconButton.defaultProps = {\n  placement: 'bottom',\n};\n\nTooltipIconButton.contextTypes = {\n  intl: intlShape.isRequired,\n};\n\nTooltipIconButton.displayName = 'TooltipIconButton';\n\nregisterComponent('TooltipIconButton', TooltipIconButton, [withStyles, styles], withTheme);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/bonus/TooltipIntl.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent, Utils } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { withStyles } from '@material-ui/core/styles';\nimport { withTheme } from '@material-ui/core/styles';\nimport Tooltip from '@material-ui/core/Tooltip';\nimport IconButton from '@material-ui/core/IconButton';\nimport Button from '@material-ui/core/Button';\nimport classNames from 'classnames';\nimport Fab from '@material-ui/core/Fab';\n\nconst styles = theme => ({\n  root: {\n    display: 'inherit',\n  },\n\n  tooltip: {\n    margin: '4px !important',\n  },\n\n  buttonWrap: {\n    display: 'inherit',\n  },\n\n  button: {},\n\n  icon: {},\n\n  popoverPopper: {\n    zIndex: 1700,\n  },\n\n  popoverTooltip: {\n    zIndex: 1701,\n  },\n});\n\nconst TooltipIntl = (props, { intl }) => {\n  //eslint-disable-next-line no-console\n  console.warn(\n    'WARNING! TooltipIntl is deprecated in favor of TooltipButton as of vulcan:ui-material 1.13.0_1 and will be deleted in version 1.15.2'\n  );\n\n  const {\n    title,\n    titleId,\n    titleValues,\n    placement,\n    icon,\n    className,\n    classes,\n    theme,\n    enterDelay,\n    leaveDelay,\n    buttonRef,\n    variant,\n    parent,\n    children,\n    ...properties\n  } = props;\n\n  const iconWithClass = icon && React.cloneElement(icon, { className: classes.icon });\n  const popperClass = parent === 'popover' && classes.popoverPopper;\n  const tooltipClass = parent === 'popover' && classes.popoverTooltip;\n  const tooltipEnterDelay = typeof enterDelay === 'number' ? enterDelay : theme.utils.tooltipEnterDelay;\n  const tooltipLeaveDelay = typeof leaveDelay === 'number' ? leaveDelay : theme.utils.tooltipLeaveDelay;\n  const titleText = props.title || intl.formatMessage({ id: titleId });\n  const slug = Utils.slugify(titleId);\n\n  return (\n    <span className={classNames('tooltip-intl', classes.root, className)}>\n      <Tooltip\n        id={`tooltip-${slug}`}\n        title={titleText}\n        placement={placement}\n        enterDelay={tooltipEnterDelay}\n        leaveDelay={tooltipLeaveDelay}\n        classes={{\n          tooltip: classNames(classes.tooltip, tooltipClass),\n          popper: popperClass,\n        }}>\n        <span className={classes.buttonWrap}>\n          {variant === 'fab' && !!icon ? (\n            <Fab className={classNames(classes.button, slug)} aria-label={title} ref={buttonRef} {...properties}>\n              {iconWithClass}\n            </Fab>\n          ) : !!icon ? (\n            <IconButton className={classNames(classes.button, slug)} aria-label={title} ref={buttonRef} {...properties}>\n              {iconWithClass}\n            </IconButton>\n          ) : variant === 'button' ? (\n            <Button className={classNames(classes.button, slug)} aria-label={title} ref={buttonRef} {...properties}>\n              {children}\n            </Button>\n          ) : (\n            children\n          )}\n        </span>\n      </Tooltip>\n    </span>\n  );\n};\n\nTooltipIntl.propTypes = {\n  title: PropTypes.node,\n  titleId: PropTypes.string,\n  titleValues: PropTypes.object,\n  placement: PropTypes.string,\n  icon: PropTypes.node,\n  className: PropTypes.string,\n  classes: PropTypes.object,\n  buttonRef: PropTypes.func,\n  variant: PropTypes.string,\n  theme: PropTypes.object,\n  enterDelay: PropTypes.number,\n  leaveDelay: PropTypes.number,\n  parent: PropTypes.oneOf(['default', 'popover']),\n  children: PropTypes.node,\n};\n\nTooltipIntl.defaultProps = {\n  placement: 'bottom',\n  parent: 'default',\n};\n\nTooltipIntl.contextTypes = {\n  intl: intlShape.isRequired,\n};\n\nTooltipIntl.displayName = 'TooltipIntl';\n\nregisterComponent('TooltipIntl', TooltipIntl, [withStyles, styles], withTheme);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/core/Avatar.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { Link } from 'react-router-dom';\nimport Users, { getProfileUrl } from 'meteor/vulcan:users';\nimport { withStyles } from '@material-ui/core/styles';\nimport MuiAvatar from '@material-ui/core/Avatar';\nimport ButtonBase from '@material-ui/core/ButtonBase';\nimport Tooltip from '@material-ui/core/Tooltip';\nimport AdminIcon from 'mdi-material-ui/Star';\nimport classNames from 'classnames';\n\nconst styles = theme => ({\n  root: {\n    padding: 0,\n    borderRadius: '50%',\n    display: 'inline-block',\n    verticalAlign: 'middle',\n    position: 'relative',\n  },\n\n  avatar: {},\n\n  statusIcon: {\n    position: 'absolute',\n    top: -2,\n    right: -2,\n    width: 16,\n    height: 16,\n    filter: 'drop-shadow(0 0 1px rgba(0, 0, 0, 0.9))',\n  },\n\n  statusIconProfile: {\n    position: 'absolute',\n    top: 0,\n    right: 0,\n    width: 32,\n    height: 32,\n    filter: 'drop-shadow(0 0 2px rgba(0, 0, 0, 0.9))',\n  },\n\n  admin: {\n    color: theme.palette.error.main,\n  },\n\n  host: {\n    color: theme.palette.secondary.main,\n  },\n\n  icon: {\n    width: 24,\n    height: 24,\n  },\n\n  xsmall: {\n    width: 32,\n    height: 32,\n  },\n\n  small: {\n    width: 40,\n    height: 40,\n  },\n\n  medium: {\n    width: 48,\n    height: 48,\n  },\n\n  large: {\n    width: 56,\n    height: 56,\n  },\n\n  profile: {\n    width: 120,\n    height: 120,\n  },\n\n  bottom: {\n    marginBottom: theme.spacing(1),\n  },\n\n  left: {\n    marginLeft: theme.spacing(1),\n  },\n\n  right: {\n    marginRight: theme.spacing(1),\n  },\n\n  sides: {\n    marginRight: theme.spacing(1),\n    marginLeft: theme.spacing(1),\n  },\n\n  all: {\n    margin: theme.spacing(1),\n  },\n\n  none: {},\n});\n\nconst Avatar = ({ classes, className, user, size, gutter, link, buttonRef }, { intl }) => {\n  let avatarUrl = user.avatarUrl || Users.avatar.getUrl(user);\n  if (avatarUrl && avatarUrl.indexOf('gravatar.com') > -1) avatarUrl = null;\n  const statusIconClass = `statusIcon${size === 'profile' ? 'Profile' : ''}`;\n  const userStatus = Users.avatar.getUserStatus(user);\n\n  const statusIcon = userStatus && (\n    <Tooltip title={intl.formatMessage({ id: `users.${userStatus}` })} placement=\"bottom\">\n      <AdminIcon className={classNames(classes[statusIconClass], classes[userStatus])} />\n    </Tooltip>\n  );\n\n  const avatar = (\n    <MuiAvatar\n      alt={Users.getDisplayName(user)}\n      src={avatarUrl}\n      className={classNames('users-avatar', classes[size], classes.avatar)}\n      data-email={Users.getEmail(user)}>\n      {!avatarUrl ? Users.avatar.getInitials(user) : null}\n    </MuiAvatar>\n  );\n\n  //onClick = onClick || function () { RouteTools.go('users.profile', { slug: user.slug }); };\n\n  return link ? (\n    <ButtonBase\n      className={className}\n      classes={{ root: classNames(classes.root, classes[gutter]) }}\n      component={Link}\n      ref={buttonRef}\n      to={getProfileUrl(user)}>\n      {avatar}\n      {statusIcon}\n    </ButtonBase>\n  ) : (\n    <div className={classNames(classes.root, classes[gutter], className)}>\n      {avatar}\n      {statusIcon}\n    </div>\n  );\n};\n\nAvatar.propTypes = {\n  classes: PropTypes.object.isRequired,\n  className: PropTypes.string,\n  user: PropTypes.object.isRequired,\n  size: PropTypes.oneOf(['xsmall', 'small', 'medium', 'large', 'profile']),\n  gutter: PropTypes.oneOf(['bottom', 'left', 'right', 'sides', 'all', 'none']),\n  link: PropTypes.bool,\n  buttonRef: PropTypes.func,\n};\n\nAvatar.defaultProps = {\n  size: 'small',\n  gutter: 'none',\n  link: true,\n};\n\nAvatar.contextTypes = {\n  intl: intlShape.isRequired,\n};\n\nAvatar.displayName = 'Avatar';\n\nregisterComponent('Avatar', Avatar, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/core/Card.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { replaceComponent, Components } from 'meteor/vulcan:core';\nimport moment from 'moment';\nimport { withStyles } from '@material-ui/core/styles';\nimport IconButton from '@material-ui/core/IconButton';\nimport Checkbox from '@material-ui/core/Checkbox';\nimport EditIcon from 'mdi-material-ui/Pencil';\nimport Table from '@material-ui/core/Table';\nimport TableBody from '@material-ui/core/TableBody';\nimport TableRow from '@material-ui/core/TableRow';\nimport TableCell from '@material-ui/core/TableCell';\nimport classNames from 'classnames';\nimport get from 'lodash/get';\nimport Users from 'meteor/vulcan:users';\n\nconst getLabel = (field, fieldName, collection, intl) => {\n  const schema = collection.simpleSchema()._schema;\n  const fieldSchema = schema[fieldName];\n  if (fieldSchema) {\n    return intl.formatMessage(\n      { id: `${collection._name}.${fieldName}`, defaultMessage: fieldSchema.label });\n  } else {\n    return fieldName;\n  }\n};\n\n\nconst getTypeName = (field, fieldName, collection) => {\n  const schema = collection.simpleSchema()._schema;\n  const fieldSchema = schema[fieldName];\n  if (fieldSchema) {\n    const type = fieldSchema.type.singleType;\n    const typeName = typeof type === 'function' ? type.name : type;\n    return typeName;\n  } else {\n    return typeof field;\n  }\n};\n\n\nconst parseImageUrl = value => {\n  const isImage = ['.png', '.jpg', '.gif'].indexOf(value.substr(-4)) !== -1 ||\n    ['.webp', '.jpeg'].indexOf(value.substr(-5)) !== -1;\n  return isImage ?\n    <img style={{ width: '100%', maxWidth: 200 }} src={value} alt={value}/> :\n    <LimitedString string={value}/>;\n};\n\n\nconst LimitedString = ({ string }) =>\n  <div>\n    {string.indexOf(' ') === -1 && string.length > 30 ?\n      <span title={string}>{string.substr(0, 30)}…</span> :\n      <span>{string}</span>\n    }\n  </div>;\n\n\nexport const getFieldValue = (value, typeName, classes={}) => {\n  \n  if (typeof value === 'undefined' || value === null) {\n    return '';\n  }\n  \n  if (Array.isArray(value)) {\n    typeName = 'Array';\n  }\n  \n  if (typeof typeName === 'undefined') {\n    typeName = typeof value;\n  }\n  \n  switch (typeName) {\n    \n    case 'Boolean':\n    case 'boolean':\n      return <Checkbox checked={value} disabled style={{ width: '32px', height: '32px' }}/>;\n    \n    case 'Number':\n    case 'number':\n    case 'SimpleSchema.Integer':\n      return <code>{value.toString()}</code>;\n    \n    case 'Array':\n      return <ol>{value.map(\n        (item, index) => <li key={index}>{getFieldValue(item, typeof item, classes)}</li>)}</ol>;\n    \n    case 'Object':\n    case 'object':\n      return (\n        <Table className=\"table\">\n          <TableBody>\n            {_.map(value, (value, key) =>\n              <TableRow className={classNames(classes.table, 'table')} key={key}>\n                <TableCell className={classNames(classes.tableHeadCell, 'datacard-label')} variant=\"head\">{key}</TableCell>\n                <TableCell className={classNames(classes.tableCell, 'datacard-value')} >{getFieldValue(value, typeof value, classes)}</TableCell>\n              </TableRow>\n            )}\n          </TableBody>\n        </Table>\n      );\n    \n    case 'Date':\n      return moment(new Date(value)).format('dddd, MMMM Do YYYY, h:mm:ss');\n    \n    default:\n      return parseImageUrl(value);\n  }\n};\n\n\nconst CardItem = ({ label, value, typeName, classes }) =>\n  <TableRow className={classes.tableRow}>\n    <TableCell className={classNames(classes.tableHeadCell, 'datacard-label')} variant=\"head\">\n      {label}\n    </TableCell>\n    <TableCell className={classNames(classes.tableCell, 'datacard-value')}>\n      {getFieldValue(value, typeName, classes)}\n    </TableCell>\n  </TableRow>;\n\n\nconst CardEdit = (props, context) => {\n  const classes = props.classes;\n  const editTitle = context.intl.formatMessage({ id: 'cards.edit' });\n  return (\n    <TableRow className={classes.tableRow}>\n      <TableCell className={classes.tableCell} colSpan=\"2\">\n        <Components.ModalTrigger label={editTitle}\n                                 component={<IconButton aria-label={editTitle}>\n                                   <EditIcon/>\n                                 </IconButton>}\n        >\n          <CardEditForm {...props} />\n        </Components.ModalTrigger>\n      </TableCell>\n    </TableRow>\n  );\n};\n\n\nCardEdit.contextTypes = { intl: intlShape };\n\n\nconst CardEditForm = ({ collection, document, closeModal }) =>\n  <Components.SmartForm\n    collection={collection}\n    documentId={document._id}\n    showRemove={true}\n    successCallback={document => {\n      closeModal();\n    }}\n  />;\n\n\nconst styles = theme => ({\n  root: {},\n  table: {\n    maxWidth: '100%'\n  },\n  tableBody: {},\n  tableRow: {},\n  tableCell: {},\n  tableHeadCell: {},\n});\n\n\nconst Card = ({ className, collection, document, currentUser, fields, classes }, { intl }) => {\n  \n  const fieldNames = fields ? fields : _.without(_.keys(document), '__typename');\n  let canUpdate = false;\n\n  // new APIs\n  const permissionCheck = get(collection, 'options.permissions.canUpdate');\n  // openCRUD backwards compatibility\n  const check = get(collection, 'options.mutations.edit.check') || get(collection, 'options.mutations.update.check');\n\n  if (Users.isAdmin(currentUser)) {\n    canUpdate = true;\n  } else if (permissionCheck) {\n    canUpdate = Users.permissionCheck({\n      check: permissionCheck,\n      user: currentUser,\n      context: { Users },\n      operationName: 'update',\n    });\n  } else if (check) {\n    canUpdate = check && check(currentUser, document, { Users });\n  }\n\n  return (\n    <div className={classNames(classes.root, 'datacard', `datacard-${collection._name}`, className)}>\n      <Table className={classNames(classes.table, 'table')} style={{ maxWidth: '100%' }}>\n        <TableBody>\n          {canUpdate ? <CardEdit collection={collection} document={document} classes={classes}/> : null}\n          {fieldNames.map((fieldName, index) =>\n            <CardItem key={index}\n                      value={document[fieldName]}\n                      typeName={getTypeName(document[fieldName], fieldName, collection)}\n                      label={getLabel(document[fieldName], fieldName, collection, intl)}\n                      classes={classes}\n            />\n          )}\n        </TableBody>\n      </Table>\n    </div>\n  );\n};\n\n\nCard.displayName = 'Card';\n\n\nCard.propTypes = {\n  className: PropTypes.string,\n  collection: PropTypes.object,\n  document: PropTypes.object,\n  currentUser: PropTypes.object,\n  fields: PropTypes.array,\n  classes: PropTypes.object.isRequired,\n};\n\n\nCard.contextTypes = {\n  intl: intlShape\n};\n\n\nreplaceComponent('Card', Card, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/core/Datatable.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport {\n  Components,\n  replaceComponent,\n  withCurrentUser,\n  Utils,\n  withMulti,\n  getCollection,\n  instantiateComponent,\n} from 'meteor/vulcan:core';\nimport { compose } from 'meteor/vulcan:lib';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { withStyles } from '@material-ui/core/styles';\nimport Table from '@material-ui/core/Table';\nimport TableBody from '@material-ui/core/TableBody';\nimport TableHead from '@material-ui/core/TableHead';\nimport TableRow from '@material-ui/core/TableRow';\nimport TableCell from '@material-ui/core/TableCell';\nimport TableFooter from '@material-ui/core/TableFooter';\nimport Tooltip from '@material-ui/core/Tooltip';\nimport TableSortLabel from '@material-ui/core/TableSortLabel';\nimport TablePagination from '@material-ui/core/TablePagination';\nimport Toolbar from '@material-ui/core/Toolbar';\nimport Typography from '@material-ui/core/Typography';\nimport { getFieldValue } from './Card';\nimport _assign from 'lodash/assign';\nimport _sortBy from 'lodash/sortBy';\nimport classNames from 'classnames';\n\n/*\n\nDatatable Component\n\n*/\nconst baseStyles = theme => ({\n  root: {\n    position: 'relative',\n    display: 'flex',\n    flexDirection: 'column',\n    alignItems: 'stretch',\n  },\n  header: {\n    display: 'flex',\n    flexDirection: 'row',\n    width: '100%',\n    justifyContent: 'space-between',\n    alignItems: 'center',\n  },\n  scroller: {\n\t\toverflowX: 'auto',\n\t\toverflowY: 'hidden'\n  },\n  searchWrapper: {},\n  addButtonWrapper: {\n    alignItems: 'center',\n  },\n  addButton: {\n    // Floating button won't work with multiple datatables, buttons are superposed\n    // top: '9.5rem',\n    // right: '2rem',\n    // position: 'fixed',\n    // bottom: 'auto',\n  },\n  table: {\n    marginTop: 0,\n  },\n  denseTable: {},\n  denserTable: {},\n  flatTable: {},\n  tableHead: {},\n  tableBody: {},\n  tableFooter: {},\n  tablePagination: {},\n  tableRow: {},\n  tableHeadCell: {},\n  tableCell: {},\n  clickRow: {},\n  editCell: {},\n  editButton: {},\n});\n\nconst delay = (function() {\n  var timer = 0;\n  return function(callback, ms) {\n    clearTimeout(timer);\n    timer = setTimeout(callback, ms);\n  };\n})();\n\nclass Datatable extends PureComponent {\n  constructor(props) {\n    super(props);\n\n    this.updateQuery = this.updateQuery.bind(this);\n\n    this.state = {\n      value: '',\n      query: '',\n      currentSort: {},\n    };\n  }\n\n  toggleSort = column => {\n    let currentSort;\n    if (!this.state.currentSort[column]) {\n      currentSort = { [column]: 1 };\n    } else if (this.state.currentSort[column] === 1) {\n      currentSort = { [column]: -1 };\n    } else {\n      currentSort = {};\n    }\n    this.setState({ currentSort });\n  };\n\n  updateQuery(value) {\n    this.setState({\n      value: value,\n    });\n    delay(() => {\n      this.setState({\n        query: value,\n      });\n    }, 700);\n  }\n\n  render() {\n    if (this.props.data) {\n      return (\n        <Components.DatatableContents\n          columns={this.props.data.length ? Object.keys(this.props.data[0]) : undefined}\n          results={this.props.data}\n          count={this.props.data.length}\n          totalCount={this.props.data.length}\n          showEdit={false}\n          showNew={false}\n          {...this.props}\n        />\n      );\n    } else {\n      const { className, options, showSearch, showNew, classes, TableProps, SearchInputProps } = this.props;\n      const wrapComponent = this.props.wrapComponent || <div className={classes.scroller}/>;\n\n      const collection = this.props.collection || getCollection(this.props.collectionName);\n\n      const listOptions = {\n        collection: collection,\n        ...options,\n      };\n\n      const DatatableWithMulti = compose(withMulti(listOptions))(Components.DatatableContents);\n\n      // add _id to orderBy when we want to sort a column, to avoid breaking the graphql() hoc;\n      // see https://github.com/VulcanJS/Vulcan/issues/2090#issuecomment-433860782\n      // this.state.currentSort !== {} is always false, even when console.log(this.state.currentSort) displays\n      // {}. So we test on the length of keys for this object.\n      const orderBy =\n        Object.keys(this.state.currentSort).length == 0\n          ? {}\n          : { ...this.state.currentSort, _id: -1 };\n\n      return (\n        <div\n          className={classNames(\n            'datatable',\n            `datatable-${collection._name}`,\n            classes.root,\n            className\n          )}>\n          {/* DatatableAbove Component part*/}\n          {(showSearch || showNew) && (\n            <div className={classes.header}>\n              {showSearch && (\n                <div className={classes.searchWrapper}>\n                  <Components.SearchInput\n                    value={this.state.query}\n                    updateQuery={this.updateQuery}\n                    className={classes.search}\n                    labelId={'datatable.search'}\n                    {...SearchInputProps}\n                  />\n                </div>\n              )}\n              {showNew && (\n                <div className={classes.addButtonWrapper}>\n                  <Components.NewButton\n                    collection={collection}\n                    variant=\"fab\"\n                    color=\"primary\"\n                    className={classes.addButton}\n                  />\n                </div>\n              )}\n            </div>\n          )}\n\n          {instantiateComponent(wrapComponent, {\n            children: <DatatableWithMulti\n              {...this.props}\n              collection={collection}\n              terms={{ query: this.state.query, orderBy: orderBy }}\n              currentUser={this.props.currentUser}\n              toggleSort={this.toggleSort}\n              currentSort={this.state.currentSort}\n              {...TableProps}\n            />\n          })}\n\n        </div>\n      );\n    }\n  }\n}\n\nDatatable.propTypes = {\n  title: PropTypes.string,\n  className: PropTypes.string,\n  collection: PropTypes.object,\n  options: PropTypes.object,\n  columns: PropTypes.array,\n  showEdit: PropTypes.bool,\n  editComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),\n  showNew: PropTypes.bool,\n  showSearch: PropTypes.bool,\n  emptyState: PropTypes.node,\n  currentUser: PropTypes.object,\n  classes: PropTypes.object,\n  data: PropTypes.array,\n  footerData: PropTypes.array,\n  dense: PropTypes.string,\n  queryDataRef: PropTypes.func,\n  rowClass: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),\n  handleRowClick: PropTypes.func,\n  intlNamespace: PropTypes.string,\n  toggleSort: PropTypes.func,\n  currentSort: PropTypes.object,\n  paginate: PropTypes.bool,\n  wrapComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),\n  TableProps: PropTypes.object,\n  SearchInputProps: PropTypes.object,\n};\n\nDatatable.defaultProps = {\n  showNew: true,\n  showEdit: true,\n  showSearch: true,\n  paginate: false,\n};\n\nreplaceComponent('Datatable', Datatable, withCurrentUser, [withStyles, baseStyles]);\n\nconst DatatableTitle = ({ title }) => (\n  <Toolbar>\n    <Typography variant=\"h6\" id=\"tableTitle\">\n      {title}\n    </Typography>\n  </Toolbar>\n);\nreplaceComponent('DatatableTitle', DatatableTitle);\n\nconst DatatableContentsInnerLayout = Table;\nreplaceComponent('DatatableContentsInnerLayout', DatatableContentsInnerLayout);\n/*\n\nDatatableContents Component\n\n*/\n\nconst wrapColumns = c => ({ name: c });\n\nconst getColumns = (columns, results, data) => {\n  if (columns) {\n    // convert all columns to objects\n    const convertedColums = columns.map(column =>\n      typeof column === 'object' ? column : { name: column }\n    );\n    const sortedColumns = _sortBy(convertedColums, column => column.order);\n    return sortedColumns;\n  } else if (results && results.length > 0) {\n    // if no columns are provided, default to using keys of first array item\n    return Object.keys(results[0])\n      .filter(k => k !== '__typename')\n      .map(wrapColumns);\n  } else if (data) {\n    // note: withMulti HoC also passes a prop named data, but in this case\n    // data should be the prop passed to the Datatable\n    return Object.keys(data[0]).map(wrapColumns);\n  }\n  return [];\n};\n\nconst datatableContentsStyles = theme =>\n  _assign({}, baseStyles(theme), {\n    table: {\n      marginTop: theme.spacing(3),\n      marginBottom: theme.spacing(3),\n    },\n    denseTable: theme.utils.denseTable,\n    flatTable: theme.utils.flatTable,\n    denserTable: theme.utils.denserTable,\n  });\n\nconst DatatableContents = ({\n  collection,\n  error,\n  columns,\n  results,\n  loading,\n  loadMore,\n  count,\n  totalCount,\n  networkStatus,\n  refetch,\n  showEdit,\n  editComponent,\n  emptyState,\n  currentUser,\n  classes,\n  footerData,\n  dense,\n  queryDataRef,\n  rowClass,\n  handleRowClick,\n  intlNamespace,\n  title,\n  toggleSort,\n  currentSort,\n  paginate,\n  paginationTerms = {\n    itemsPerPage: 25,\n    limit: 25,\n    offset: 0,\n  },\n  setPaginationTerms,\n  TableProps,\n}) => {\n  if (loading) {\n    return <Components.Loading />;\n  } else if (!results || !results.length) {\n    return emptyState || null;\n  }\n\n  if (queryDataRef) queryDataRef(this.props);\n\n  let sortedColumns = getColumns(columns, results);\n\n  const denseClass = dense && classes[dense + 'Table'];\n\n  // Pagination functions\n  const getPage = paginationTerms =>\n    parseInt((paginationTerms.limit - 1) / paginationTerms.itemsPerPage);\n\n  const onChangePage = (event, page) => {\n    setPaginationTerms({\n      itemsPerPage: paginationTerms.itemsPerPage,\n      limit: (page + 1) * paginationTerms.itemsPerPage,\n      offset: page * paginationTerms.itemsPerPage,\n    });\n  };\n\n  const onChangeRowsPerPage = event => {\n    let value = event.target.value;\n    let offset = Math.max(\n      0,\n      parseInt((paginationTerms.limit - paginationTerms.itemsPerPage) / value) * value\n    );\n    let limit = Math.min(offset + value, totalCount);\n    setPaginationTerms({\n      itemsPerPage: value,\n      limit: limit,\n      offset: offset,\n    });\n  };\n\n  return (\n    <React.Fragment>\n      {error && <Components.Alert variant=\"danger\">{error.message}</Components.Alert>}\n      {title && <Components.DatatableTitle title={title} />}\n      <Components.DatatableContentsInnerLayout className={classNames(classes.table, denseClass)} {...TableProps}>\n        {sortedColumns && (\n          <TableHead className={classes.tableHead}>\n            <TableRow className={classes.tableRow}>\n              {_sortBy(sortedColumns, column => column.order).map((column, index) => (\n                <Components.DatatableHeader\n                  key={index}\n                  collection={collection}\n                  intlNamespace={intlNamespace}\n                  column={column}\n                  classes={classes}\n                  toggleSort={toggleSort}\n                  currentSort={currentSort}\n                />\n              ))}\n              {(showEdit || editComponent) && <TableCell className={classes.tableCell} />}\n            </TableRow>\n          </TableHead>\n        )}\n\n        {results && (\n          <TableBody className={classes.tableBody}>\n            {results.map((document, index) => (\n              <Components.DatatableRow\n                collection={collection}\n                columns={sortedColumns}\n                document={document}\n                refetch={refetch}\n                key={index}\n                showEdit={showEdit}\n                editComponent={editComponent}\n                currentUser={currentUser}\n                classes={classes}\n                rowClass={rowClass}\n                handleRowClick={handleRowClick}\n              />\n            ))}\n          </TableBody>\n        )}\n\n        {footerData && (\n          <TableFooter className={classes.tableFooter}>\n            <TableRow className={classes.tableRow}>\n              {_sortBy(columns, column => column.order).map((column, index) => (\n                <TableCell\n                  key={index}\n                  className={classNames(classes.tableCell, column.footerClass)}>\n                  {footerData[index]}\n                </TableCell>\n              ))}\n              {(showEdit || editComponent) && <TableCell className={classes.tableCell} />}\n            </TableRow>\n          </TableFooter>\n        )}\n      </Components.DatatableContentsInnerLayout>\n      {paginate && (\n        <TablePagination\n          className={classes.tablePagination}\n          component=\"div\"\n          count={totalCount}\n          rowsPerPage={paginationTerms.itemsPerPage}\n          page={getPage(paginationTerms)}\n          backIconButtonProps={{\n            'aria-label': 'Previous Page',\n          }}\n          nextIconButtonProps={{\n            'aria-label': 'Next Page',\n          }}\n          onChangePage={onChangePage}\n          onChangeRowsPerPage={onChangeRowsPerPage}\n        />\n      )}\n      {!paginate && loadMore && (\n        <Components.LoadMore\n          className={classes.loadMore}\n          count={count}\n          totalCount={totalCount}\n          loadMore={loadMore}\n          networkStatus={networkStatus}\n        />\n      )}\n    </React.Fragment>\n  );\n};\n\nreplaceComponent('DatatableContents', DatatableContents, [withStyles, datatableContentsStyles]);\n\n/*\n\nDatatableHeader Component\n\n*/\nconst DatatableHeader = (\n  { collection, intlNamespace, column, classes, toggleSort, currentSort },\n  { intl }\n) => {\n  const columnName = typeof column === 'string' ? column : column.name || column.label;\n  let formattedLabel = '';\n\n  if (column.label) {\n    formattedLabel = column.label;\n  } else if (collection) {\n    const schema = collection.simpleSchema()._schema;\n    /*\n    use either:\n\n    1. the column name translation\n    2. the column name label in the schema (if the column name matches a schema field)\n    3. the raw column name.\n    */\n    const defaultMessage = schema[columnName]\n      ? schema[columnName].label\n      : Utils.camelToSpaces(columnName);\n    formattedLabel =\n      (typeof columnName === 'string' &&\n        intl.formatMessage({\n          id: `${collection._name}.${columnName}`,\n          defaultMessage: defaultMessage,\n        })) ||\n      defaultMessage;\n\n    // if sortable is a string, use it as the name of the property to sort by. If it's just `true`, use\n    // column.name\n    const sortPropertyName = typeof column.sortable === 'string' ? column.sortable : column.name;\n\n    if (column.sortable) {\n      return (\n        <Components.DatatableSorter\n          name={sortPropertyName}\n          label={formattedLabel}\n          toggleSort={toggleSort}\n          currentSort={currentSort}\n          sortable={column.sortable}\n        />\n      );\n    }\n  } else if (intlNamespace) {\n    formattedLabel =\n      (typeof columnName === 'string' &&\n        intl.formatMessage({\n          id: `${intlNamespace}.${columnName}`,\n          defaultMessage: columnName,\n        })) ||\n      columnName;\n  } else {\n    formattedLabel = intl.formatMessage({ id: columnName, defaultMessage: columnName });\n  }\n\n  return (\n    <TableCell className={classNames(classes.tableHeadCell, column.headerClass)}>\n      {formattedLabel}\n    </TableCell>\n  );\n};\n\nDatatableHeader.contextTypes = {\n  intl: intlShape,\n};\n\nreplaceComponent('DatatableHeader', DatatableHeader);\n\n/*\n\nDatatableSorter Component\n\n*/\n\nconst DatatableSorter = ({ name, label, toggleSort, currentSort, sortable }) => (\n  <TableCell\n    className=\"datatable-sorter\"\n    sortDirection={!currentSort[name] ? false : currentSort[name] === 1 ? 'asc' : 'desc'}>\n    <Tooltip title=\"Sort\" placement=\"bottom-start\" enterDelay={300}>\n      <TableSortLabel\n        active={!currentSort[name] ? false : true}\n        direction={currentSort[name] === 1 ? 'desc' : 'asc'}\n        onClick={() => toggleSort(name)}>\n        {label}\n      </TableSortLabel>\n    </Tooltip>\n  </TableCell>\n);\n\nreplaceComponent('DatatableSorter', DatatableSorter);\n\n/*\n\nDatatableRow Component\n\n*/\nconst datatableRowStyles = theme =>\n  _assign({}, baseStyles(theme), {\n    clickRow: {\n      cursor: 'pointer',\n    },\n    editCell: {\n      paddingTop: '0 !important',\n      paddingBottom: '0 !important',\n      textAlign: 'right',\n    },\n  });\n\nconst DatatableRow = (\n  {\n    collection,\n    columns,\n    document,\n    refetch,\n    showEdit,\n    editComponent,\n    currentUser,\n    rowClass,\n    handleRowClick,\n    classes,\n  },\n  { intl }\n) => {\n  if (typeof rowClass === 'function') {\n    rowClass = rowClass(document);\n  }\n\n  return (\n    <TableRow\n      className={classNames(\n        'datatable-item',\n        classes.tableRow,\n        rowClass,\n        handleRowClick && classes.clickRow\n      )}\n      onClick={handleRowClick && (event => handleRowClick(event, document))}\n      hover>\n      {_sortBy(columns, column => column.order).map((column, index) => (\n        <Components.DatatableCell\n          key={index}\n          column={column}\n          document={document}\n          currentUser={currentUser}\n          classes={classes}\n        />\n      ))}\n\n      {(showEdit || editComponent) && (\n        <TableCell className={classes.editCell}>\n          {editComponent && instantiateComponent(editComponent, { collection, document, refetch })}\n          {showEdit && (\n            <Components.EditButton\n              collection={collection}\n              document={document}\n              buttonClasses={{ button: classes.editButton }}\n            />\n          )}\n        </TableCell>\n      )}\n    </TableRow>\n  );\n};\n\nreplaceComponent('DatatableRow', DatatableRow, [withStyles, datatableRowStyles]);\n\nDatatableRow.contextTypes = {\n  intl: intlShape,\n};\n\n/*\n\nDatatableCell Component\n\n*/\nconst DatatableCell = ({ column, document, currentUser, classes }) => {\n  const Component =\n    column.component || Components[column.componentName] || Components.DatatableDefaultCell;\n\n  const columnName = typeof column === 'string' ? column : column.name;\n  const className =\n    typeof columnName === 'string' ? `datatable-item-${columnName.toLowerCase()}` : '';\n  const cellClass =\n    typeof column.cellClass === 'function'\n      ? column.cellClass({ column, document, currentUser })\n      : typeof column.cellClass === 'string'\n      ? column.cellClass\n      : null;\n  const cellStyle =\n    typeof column.cellStyle === 'function'\n      ? column.cellStyle({ column, document, currentUser })\n      : typeof column.cellStyle === 'object'\n      ? column.cellStyle\n      : null;\n\n  return (\n    <TableCell className={classNames(classes.tableCell, cellClass, className)} style={cellStyle}>\n      <Component column={column} document={document} currentUser={currentUser} />\n    </TableCell>\n  );\n};\n\nreplaceComponent('DatatableCell', DatatableCell);\n\n/*\n\nDatatableDefaultCell Component\n\n*/\nconst DatatableDefaultCell = ({ column, document }) => (\n  <div>\n    {typeof column === 'string'\n      ? getFieldValue(document[column])\n      : getFieldValue(document[column.name])}\n  </div>\n);\n\nreplaceComponent('DatatableDefaultCell', DatatableDefaultCell);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/core/EditButton.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport EditIcon from 'mdi-material-ui/Pencil';\n\n\nconst EditButton = (\n  {\n    collection,\n    document,\n    color = 'default',\n    variant,\n    triggerClasses,\n    buttonClasses,\n    showRemove,\n    ...props\n  },\n  { intl }\n) => (\n  <Components.ModalTrigger\n    classes={triggerClasses}\n    component={\n      <Components.TooltipButton\n        titleId=\"datatable.edit\"\n        icon={<EditIcon/>}\n        color={color}\n        variant={variant}\n        classes={buttonClasses}\n      />\n    }\n  >\n    <Components.EditForm\n      collection={collection}\n      document={document}\n      showRemove={showRemove}\n      {...props}\n    />\n  </Components.ModalTrigger>\n);\n\n\nEditButton.propTypes = {\n  collection: PropTypes.object.isRequired,\n  document: PropTypes.object,\n  color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),\n  variant: PropTypes.string,\n  triggerClasses: PropTypes.object,\n  buttonClasses: PropTypes.object,\n  showRemove: PropTypes.bool,\n};\n\n\nEditButton.contextTypes = {\n  intl: intlShape\n};\n\n\nEditButton.displayName = 'EditButton';\n\n\nregisterComponent('EditButton', EditButton);\n\n\n/*\n\nEditForm Component\n\n*/\nconst EditForm = (\n  {\n    collection,\n    document,\n    closeModal,\n    options,\n    successCallback,\n    removeSuccessCallback,\n    showRemove,\n    ...props\n  }) => {\n  \n  const success = successCallback\n    ? () => {\n      successCallback();\n      closeModal();\n    }\n    : () => {\n      closeModal();\n    };\n  \n  const remove = removeSuccessCallback\n    ? () => {\n      removeSuccessCallback();\n      closeModal();\n    }\n    : () => {\n      closeModal();\n    };\n  \n  return (\n    <Components.SmartForm\n      {...props}\n      collection={collection}\n      documentId={document && document._id}\n      showRemove={showRemove ? true : showRemove}\n      successCallback={success}\n      removeSuccessCallback={remove}\n    />\n  );\n};\n\nregisterComponent('EditForm', EditForm);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/core/Flash.jsx",
    "content": "import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { Utils, replaceComponent, registerSetting, getSetting } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { makeStyles } from '@material-ui/core/styles';\nimport Snackbar from '@material-ui/core/Snackbar';\nimport Alert from '@material-ui/lab/Alert';\nimport IconButton from '@material-ui/core/IconButton';\nimport CloseIcon from 'mdi-material-ui/Close';\nimport Slide from '@material-ui/core/Slide';\nimport DOMPurify from 'dompurify';\n\n\nregisterSetting('flash.infoHideSeconds', 5, 'Seconds to display flash info messages');\nregisterSetting('flash.errorHideSeconds', 15, 'Seconds to display flash error messages');\n\n\nconst styles = theme => ({\n\n  root: {\n    maxWidth: 600,\n    transition: theme.transitions.create(['opacity'], {\n      duration: theme.transitions.duration.short,\n    }),\n    opacity: theme.opacity.darker,\n    '&:hover': {\n      opacity: 1,\n    },\n    '& code': {\n      fontSize: '0.9rem',\n    },\n  },\n\n  alert: {\n    lineHeight: 1.3,\n  },\n\n  infoAlert: {\n    backgroundColor: theme.palette.grey[800],\n  },\n\n});\n\nconst useStyles = makeStyles(styles);\n\n\nconst Flash = (props, context) => {\n  const [isOpen, setIsOpen] = useState(true);\n  const classes = useStyles(props);\n  const intl = context.intl;\n  const { message, type, _id } = props.message;\n  const infoOrError = ['info', 'success'].includes(type) ? 'info' : 'error';\n  const hideDuration = getSetting(`flash.${infoOrError}HideSeconds`) * 1000;\n\n  const handleClose = (event, reason) => {\n    if (reason === 'clickaway') return;\n\n      setIsOpen(false);\n      setTimeout(() => { props.dismissFlash(props.message._id); }, 500);\n  };\n\n  return (\n    <Snackbar key={message.content}\n              anchorOrigin={{\n                vertical: 'bottom',\n                horizontal: 'left',\n              }}\n              open={isOpen}\n              TransitionComponent={Slide}\n              classes={{ root: classes.root }}\n              autoHideDuration={hideDuration}\n              onClose={handleClose}\n              ContentProps={{\n                'aria-describedby': _id,\n              }}\n              action={[\n                <IconButton key=\"close\"\n                            aria-label={intl.formatMessage({ id: 'global.close' })}\n                            color=\"inherit\"\n                            onClick={handleClose}\n                >\n                  <CloseIcon/>\n                </IconButton>,\n              ]}\n    >\n      <Alert onClose={handleClose}\n             severity={infoOrError}\n             variant=\"filled\"\n             classes={{\n               root: classes.alert,\n               filledInfo: classes.infoAlert,\n             }}\n      >\n        <span id={_id} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(message) }}/>\n      </Alert>\n    </Snackbar>\n  );\n};\n\n\nFlash.propTypes = {\n  message: PropTypes.object.isRequired,\n  dismissFlash: PropTypes.func.isRequired,\n};\n\n\nFlash.contextTypes = {\n  intl: intlShape.isRequired,\n};\n\n\nFlash.displayName = 'Flash';\n\n\nreplaceComponent('Flash', Flash);\nexport default Flash;\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/core/Loading.jsx",
    "content": "import React from 'react';\nimport { replaceComponent } from 'meteor/vulcan:core';\nimport CircularProgress from '@material-ui/core/CircularProgress';\n\nfunction Loading(props) {\n  return <CircularProgress {...props} />;\n}\n\nreplaceComponent('Loading', Loading);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/core/NewButton.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, replaceComponent } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport AddIcon from 'mdi-material-ui/Plus';\n\n\nconst NewButton = ({\n                     className,\n                     collection,\n                     color = 'default',\n                     variant,\n                   }, { intl }) => (\n  \n  <Components.ModalTrigger\n    className={className}\n    component={<Components.TooltipButton titleId=\"datatable.new\"\n                                         icon={<AddIcon/>}\n                                         color={color}\n                                         variant={variant}\n    />}\n  >\n    <Components.EditForm collection={collection}/>\n  </Components.ModalTrigger>\n);\n\n\nNewButton.propTypes = {\n  className: PropTypes.string,\n  collection: PropTypes.object.isRequired,\n  color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),\n  variant: PropTypes.string,\n};\n\n\nNewButton.contextTypes = {\n  intl: intlShape\n};\n\n\nNewButton.displayName = 'NewButton';\n\n\nreplaceComponent('NewButton', NewButton);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/FormComponentInner.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport {\n  Components,\n  registerComponent,\n  instantiateComponent,\n  getHtmlInputProps,\n} from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport classNames from 'classnames';\nimport _omit from 'lodash/omit';\n\nconst styles = theme => ({\n  formInput: {\n    position: 'relative',\n    marginBottom: theme.spacing(3),\n    '&:last-child': {\n      marginBottom: 0,\n    },\n  },\n\n  halfWidthLeft: {\n    display: 'inline-block',\n    width: '48%',\n    verticalAlign: 'top',\n    marginRight: '4%',\n  },\n\n  halfWidthRight: {\n    display: 'inline-block',\n    width: '48%',\n    verticalAlign: 'top',\n  },\n\n  thirdWidthLeft: {\n    display: 'inline-block',\n    width: '31%',\n    verticalAlign: 'top',\n    marginRight: '3.5%',\n  },\n\n  thirdWidthRight: {\n    display: 'inline-block',\n    width: '31%',\n    verticalAlign: 'top',\n  },\n\n  hidden: {\n    display: 'none',\n  },\n});\n\nclass FormComponentInner extends PureComponent {\n  getProperties = () => {\n    return _omit(getHtmlInputProps(this.props), 'classes');\n  };\n\n  render() {\n    const {\n      classes,\n      inputClassName,\n      name,\n      input,\n      hidden,\n      beforeComponent,\n      afterComponent,\n      formInput,\n      intlInput,\n      nestedInput,\n      formComponents,\n    } = this.props;\n\n    const FormComponents = formComponents;\n\n    const inputClass = classNames(\n      classes.formInput,\n      hidden && classes.hidden,\n      inputClassName && classes[inputClassName],\n      `input-${name}`,\n      `form-component-${input || 'default'}`\n    );\n\n    const properties = this.getProperties();\n\n    const FormInput = formInput;\n\n    if (intlInput) {\n      return <Components.FormIntl {...properties} />;\n    } else {\n      return (\n        <div className={inputClass}>\n          {instantiateComponent(beforeComponent, properties)}\n          <FormInput {...properties} />\n          {instantiateComponent(afterComponent, properties)}\n        </div>\n      );\n    }\n  }\n}\n\nFormComponentInner.contextTypes = {\n  intl: intlShape,\n};\n\nFormComponentInner.propTypes = {\n  classes: PropTypes.object.isRequired,\n  inputClassName: PropTypes.string,\n  name: PropTypes.string.isRequired,\n  input: PropTypes.any,\n  beforeComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),\n  afterComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),\n  errors: PropTypes.array.isRequired,\n  help: PropTypes.node,\n  onChange: PropTypes.func,\n  showCharsRemaining: PropTypes.bool.isRequired,\n  charsRemaining: PropTypes.number,\n  charsCount: PropTypes.number,\n  max: PropTypes.oneOfType([PropTypes.number, PropTypes.instanceOf(Date)]),\n  formInput: PropTypes.elementType.isRequired,\n};\n\nFormComponentInner.displayName = 'FormComponentInner';\n\nregisterComponent('FormComponentInner', FormComponentInner, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/FormErrors.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { replaceComponent, Components } from 'meteor/vulcan:core';\n\nimport Snackbar from '@material-ui/core/Snackbar';\nimport { withStyles } from '@material-ui/core/styles';\nimport classNames from 'classnames';\n\nconst styles = theme => ({\n  root: {\n    position: 'relative',\n    boxShadow: 'none',\n    marginBottom: theme.spacing(2),\n  },\n  list: {\n    marginBottom: 0,\n  },\n  error: { '& > div': { backgroundColor: theme.palette.error[500] } },\n  danger: { '& > div': { backgroundColor: theme.palette.error[500] } },\n  warning: { '& > div': { backgroundColor: theme.palette.error[500] } },\n});\n\nconst FormErrors = ({ errors, classes }) => {\n  const messageNode = (\n    <ul className={classes.list}>\n      {errors.map((error, index) => (\n        <li key={index}>\n          <Components.FormError error={error} />\n        </li>\n      ))}\n    </ul>\n  );\n\n  return (\n    <div>\n      {!!errors.length && (\n        <Snackbar\n          open={true}\n          className={classNames('flash-message', classes.root, classes.danger)}\n          message={messageNode}\n        />\n      )}\n    </div>\n  );\n};\n\nreplaceComponent('FormErrors', FormErrors, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/FormGroupDefault.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport Collapse from '@material-ui/core/Collapse';\nimport Paper from '@material-ui/core/Paper';\nimport Typography from '@material-ui/core/Typography';\nimport ExpandLessIcon from 'mdi-material-ui/ChevronUp';\nimport ExpandMoreIcon from 'mdi-material-ui/ChevronDown';\nimport classNames from 'classnames';\n\nconst styles = theme => ({\n  layoutRoot: {\n    minWidth: '320px',\n  },\n\n  headerRoot: {},\n\n  paper: {\n    padding: theme.spacing(3),\n  },\n\n  subtitle1: {\n    display: 'flex',\n    alignItems: 'center',\n    paddingLeft: theme.spacing(0.5),\n    marginTop: theme.spacing(5),\n    marginBottom: theme.spacing(1),\n    color: theme.palette.primary.main,\n  },\n\n  collapsible: {\n    cursor: 'pointer',\n  },\n\n  label: {},\n\n  toggle: {\n    '& svg': {\n      width: 21,\n      height: 21,\n      display: 'block',\n    },\n  },\n\n  container: {\n    paddingLeft: 4,\n    paddingRight: 4,\n    marginLeft: -4,\n    marginRight: -4,\n  },\n\n  entered: {\n    overflow: 'visible',\n  },\n\n  hidden: {\n    display: 'none',\n  },\n});\n\nconst FormGroupHeader = ({ toggle, collapsed, hidden, label, group, classes }) => {\n  const collapsible = (group && group.collapsible) || (group && group.name === 'admin');\n\n  return (\n    <div\n      className={classNames(\n        classes.headerRoot,\n        collapsible && classes.collapsible,\n        hidden && classes.hidden,\n        'form-group-header'\n      )}\n      onClick={collapsible ? toggle : null}>\n      <Typography\n        className={classNames(\n          'form-group-header-title',\n          classes.subtitle1,\n          collapsible && classes.collapsible\n        )}\n        variant=\"subtitle1\"\n      >\n        <div className={classes.label}>{label}</div>\n\n        {collapsible && (\n          <div className={classes.toggle}>\n            {collapsed ? <ExpandMoreIcon /> : <ExpandLessIcon />}\n          </div>\n        )}\n      </Typography>\n    </div>\n  );\n};\n\nFormGroupHeader.propTypes = {\n  toggle: PropTypes.func,\n  collapsed: PropTypes.bool,\n  hidden: PropTypes.bool,\n  label: PropTypes.string.isRequired,\n  group: PropTypes.object,\n  classes: PropTypes.object.isRequired,\n};\n\nregisterComponent('FormGroupHeader', FormGroupHeader, [withStyles, styles]);\n\nconst FormGroupLayout = ({\n  label,\n  anchorName,\n  collapsed,\n  hidden,\n  hasErrors,\n  heading,\n  group,\n  children,\n  classes,\n}) => {\n  const collapsedIn = (!collapsed && !hidden) || hasErrors;\n\n  return (\n    <div className={classNames(classes.layoutRoot, 'form-section', `form-section-${anchorName}`)}>\n      <a name={anchorName} />\n\n      {heading}\n\n      <Collapse\n        classes={{ container: classes.container, entered: classes.entered }}\n        in={collapsedIn}>\n        <Paper className={classes.paper}>{children}</Paper>\n      </Collapse>\n    </div>\n  );\n};\n\nFormGroupLayout.propTypes = {\n  label: PropTypes.string.isRequired,\n  anchorName: PropTypes.string.isRequired,\n  collapsed: PropTypes.bool.isRequired,\n  hidden: PropTypes.bool.isRequired,\n  hasErrors: PropTypes.bool.isRequired,\n  heading: PropTypes.node,\n  group: PropTypes.object.isRequired,\n  children: PropTypes.node.isRequired,\n  classes: PropTypes.object.isRequired,\n};\n\nregisterComponent('FormGroupLayout', FormGroupLayout, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/FormGroupLine.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport Collapse from '@material-ui/core/Collapse';\nimport Typography from '@material-ui/core/Typography';\nimport Divider from '@material-ui/core/Divider';\nimport ExpandLessIcon from 'mdi-material-ui/ChevronUp';\nimport ExpandMoreIcon from 'mdi-material-ui/ChevronDown';\nimport classNames from 'classnames';\n\nconst styles = theme => ({\n  layoutRoot: {\n    minWidth: '320px',\n  },\n\n  headerRoot: {\n    marginTop: theme.spacing(3),\n  },\n\n  divider: {\n    marginLeft: theme.spacing(-3),\n    marginRight: theme.spacing(-3),\n  },\n\n  collapsible: {\n    cursor: 'pointer',\n  },\n\n  label: {},\n\n  subtitle1: {\n    display: 'flex',\n    alignItems: 'center',\n    '& > div': {\n      display: 'flex',\n      alignItems: 'center',\n    },\n    '& > div:first-child': {\n      ...theme.typography.subtitle1,\n    },\n    paddingTop: theme.spacing(1),\n    paddingBottom: theme.spacing(1),\n  },\n\n  toggle: {\n    color: theme.palette.action.active,\n  },\n\n  entered: {\n    overflow: 'visible',\n  },\n});\n\nconst FormGroupHeaderLine = ({ toggle, collapsed, label, group, classes }) => {\n  const collapsible = (group && group.collapsible) || (group && group.name === 'admin');\n\n  return (\n    <div\n      className={classNames(\n        classes.headerRoot,\n        collapsible && classes.collapsible,\n        'form-group-header'\n      )}\n      onClick={collapsible ? toggle : null}>\n      <Divider className={classes.divider} />\n\n      <Typography\n        className={classNames(\n          'form-group-header-title',\n          classes.subtitle1,\n          collapsible && classes.collapsible\n        )}\n        variant=\"subtitle1\"\n        gutterBottom>\n        <div className={classes.label}>{label}</div>\n        {collapsible && (\n          <div className={classes.toggle}>\n            {collapsed ? <ExpandMoreIcon /> : <ExpandLessIcon />}\n          </div>\n        )}\n      </Typography>\n    </div>\n  );\n};\n\nFormGroupHeaderLine.propTypes = {\n  toggle: PropTypes.func,\n  collapsed: PropTypes.bool,\n  label: PropTypes.string.isRequired,\n  group: PropTypes.object,\n  classes: PropTypes.object.isRequired,\n};\n\nFormGroupHeaderLine.displayName = 'FormGroupHeaderLine';\n\nregisterComponent('FormGroupHeaderLine', FormGroupHeaderLine, [withStyles, styles]);\n\nconst FormGroupLayoutLine = ({\n  label,\n  anchorName,\n  collapsed,\n  hidden,\n  hasErrors,\n  heading,\n  group,\n  children,\n  classes,\n}) => {\n  const collapsedIn = (!collapsed && !hidden) || hasErrors;\n\n  return (\n    <div className={classNames(classes.layoutRoot, 'form-section', `form-section-${anchorName}`)}>\n      <a name={anchorName} />\n\n      {heading}\n\n      <Collapse classes={{ entered: classes.entered }} in={collapsedIn}>\n        {children}\n      </Collapse>\n    </div>\n  );\n};\n\nFormGroupLayoutLine.propTypes = {\n  label: PropTypes.string.isRequired,\n  anchorName: PropTypes.string.isRequired,\n  collapsed: PropTypes.bool.isRequired,\n  hidden: PropTypes.bool.isRequired,\n  hasErrors: PropTypes.bool.isRequired,\n  heading: PropTypes.node,\n  group: PropTypes.object.isRequired,\n  children: PropTypes.node.isRequired,\n  classes: PropTypes.object.isRequired,\n};\n\nFormGroupLayoutLine.displayName = 'FormGroupLayoutLine';\n\nregisterComponent('FormGroupLayoutLine', FormGroupLayoutLine, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/FormGroupNone.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport classNames from 'classnames';\n\n\nconst styles = theme => ({\n  \n  root: {\n    minWidth: '320px'\n  },\n  \n  hidden: {\n    display: 'none',\n  }\n\n});\n\n\nconst FormGroupHeaderNone = () => {\n  return null;\n};\n\n\nregisterComponent('FormGroupHeaderNone', FormGroupHeaderNone, [withStyles, styles]);\n\n\nconst FormGroupLayoutNone = ({ label, anchorName, collapsed, hidden, hasErrors, heading, group, children, classes }) => {\n  return (\n    <div className={classNames(classes.root, hidden && classes.hidden,'form-section', `form-section-${anchorName}`)}>\n      \n      <a name={anchorName}/>\n  \n      {children}\n    \n    </div>\n  );\n};\n\n\nFormGroupLayoutNone.propTypes = {\n  label: PropTypes.string.isRequired,\n  anchorName: PropTypes.string.isRequired,\n  collapsed: PropTypes.bool.isRequired,\n  hidden: PropTypes.bool.isRequired,\n  hasErrors: PropTypes.bool.isRequired,\n  heading: PropTypes.node,\n  group: PropTypes.object.isRequired,\n  children: PropTypes.node.isRequired,\n  classes: PropTypes.object.isRequired,\n};\n\n\nregisterComponent('FormGroupLayoutNone', FormGroupLayoutNone, [withStyles, styles]);\n\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/FormNestedArrayLayout.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { instantiateComponent, replaceComponent } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport Typography from '@material-ui/core/Typography';\nimport Fab from '@material-ui/core/Fab';\nimport Grid from '@material-ui/core/Grid';\nimport RemoveIcon from 'mdi-material-ui/Delete';\nimport AddIcon from 'mdi-material-ui/Plus';\n\n\nconst IconRemove = () => <RemoveIcon/>;\nreplaceComponent('IconRemove', IconRemove);\n\n\nconst IconAdd = () => <AddIcon/>;\nreplaceComponent('IconAdd', IconAdd);\n\n\nconst FormNestedArrayLayout = (props, context) => {\n  const {\n    hasErrors,\n    nestedArrayErrors,\n    label,\n    hideLabel,\n    addItem,\n    beforeComponent,\n    afterComponent,\n    formComponents,\n    children,\n  } = props;\n  const { intl } = context;\n  const FormComponents = formComponents;\n  \n  return (\n    <div className=\"form-nested-array-layout\">\n      \n      {instantiateComponent(beforeComponent, props)}\n      \n      {\n        !hideLabel &&\n        \n        <Typography\n          component=\"label\"\n          variant=\"subtitle1\"\n          gutterBottom\n        >\n          {label}\n        </Typography>\n      }\n      \n      {children}\n      \n      {\n        addItem &&\n        \n        <Grid container direction=\"column\" alignItems=\"flex-end\">\n          <Fab \n            color=\"primary\" \n            onClick={addItem} \n            className=\"form-nested-button\" \n            aria-label={intl.formatMessage({ id: 'forms.add_nested_field' }, { label: label })}\n          >\n            <AddIcon/>\n          </Fab>\n        </Grid>\n      }\n      \n      {\n        hasErrors\n          ?\n          <FormComponents.FieldErrors errors={nestedArrayErrors}/>\n          :\n          null\n      }\n      \n      {instantiateComponent(afterComponent, props)}\n    \n    </div>\n  );\n};\n\n\nFormNestedArrayLayout.propTypes = {\n  hasErrors: PropTypes.bool.isRequired,\n  nestedArrayErrors: PropTypes.array,\n  label: PropTypes.node,\n  hideLabel: PropTypes.bool,\n  addItem: PropTypes.func,\n  beforeComponent: PropTypes.node,\n  afterComponent: PropTypes.node,\n  formComponents: PropTypes.object,\n  children: PropTypes.node,\n};\n\nFormNestedArrayLayout.contextTypes = {\n  intl: intlShape,\n};\n\nreplaceComponent({\n  name: 'FormNestedArrayLayout',\n  component: FormNestedArrayLayout,\n});\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/FormNestedDivider.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { replaceComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport Divider from '@material-ui/core/Divider';\nimport classNames from 'classnames';\n\nconst styles = theme => ({\n  divider: {\n    marginTop: theme.spacing(2),\n    marginBottom: theme.spacing(3),\n  },\n});\n\nconst FormNestedDivider = ({ classes, label, addItem }) => (\n  <Divider className={classNames('form-nested-divider', classes.divider)} />\n);\n\nFormNestedDivider.propTypes = {\n  classes: PropTypes.object.isRequired,\n  label: PropTypes.string,\n  addItem: PropTypes.func,\n};\n\nreplaceComponent('FormNestedDivider', FormNestedDivider, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/FormSubmit.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, replaceComponent } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { withStyles } from '@material-ui/core/styles';\nimport Button from '@material-ui/core/Button';\nimport IconButton from '@material-ui/core/IconButton';\nimport DeleteIcon from 'mdi-material-ui/Delete';\nimport Tooltip from '@material-ui/core/Tooltip';\nimport classNames from 'classnames';\n\nconst styles = theme => ({\n  root: {\n    textAlign: 'center',\n    marginTop: theme.spacing(3),\n    marginBottom: theme.spacing(3),\n  },\n  button: {\n    margin: theme.spacing(1),\n  },\n  delete: {\n    float: 'left',\n  },\n  tooltip: {\n    margin: 3,\n  },\n});\n\nconst FormSubmit = (\n  {\n    submitLabel,\n    cancelLabel,\n    cancelCallback,\n    revertLabel,\n    revertCallback,\n    document,\n    deleteDocument,\n    collectionName,\n    classes,\n  },\n  { intl, isChanged, clearForm }\n) => {\n  if (typeof isChanged !== 'function') {\n    isChanged = () => true;\n  }\n\n  return (\n    <div className={classes.root}>\n      {deleteDocument ? (\n        <Tooltip\n          id={`tooltip-delete-${collectionName}`}\n          classes={{ tooltip: classNames('delete-button', classes.tooltip) }}\n          title={intl.formatMessage({ id: 'forms.delete' })}\n          placement=\"bottom\">\n          <IconButton onClick={deleteDocument} className={classes.delete}>\n            <DeleteIcon />\n          </IconButton>\n        </Tooltip>\n      ) : null}\n\n      {cancelCallback ? (\n        <Button\n          variant=\"contained\"\n          className={classNames('cancel-button', classes.button)}\n          onClick={event => {\n            event.preventDefault();\n            cancelCallback(document);\n          }}>\n          {cancelLabel ? cancelLabel : <Components.FormattedMessage id=\"forms.cancel\" />}\n        </Button>\n      ) : null}\n\n      {revertCallback ? (\n        <Button\n          variant=\"contained\"\n          className={classNames('revert-button', classes.button)}\n          disabled={!isChanged()}\n          onClick={event => {\n            event.preventDefault();\n            clearForm({ clearErrors: true, clearCurrentValues: true, clearDeletedValues: true });\n            revertCallback(document);\n          }}>\n          {revertLabel ? revertLabel : <Components.FormattedMessage id=\"forms.revert\" />}\n        </Button>\n      ) : null}\n\n      <Button\n        variant=\"contained\"\n        type=\"submit\"\n        color=\"secondary\"\n        className={classNames('submit-button', classes.button)}\n        disabled={!isChanged()}>\n        {submitLabel ? submitLabel : <Components.FormattedMessage id=\"forms.submit\" />}\n      </Button>\n    </div>\n  );\n};\n\nFormSubmit.propTypes = {\n  submitLabel: PropTypes.node,\n  cancelLabel: PropTypes.node,\n  revertLabel: PropTypes.node,\n  cancelCallback: PropTypes.func,\n  revertCallback: PropTypes.func,\n  document: PropTypes.object,\n  deleteDocument: PropTypes.func,\n  collectionName: PropTypes.string,\n  classes: PropTypes.object,\n};\n\nFormSubmit.contextTypes = {\n  intl: intlShape,\n  isChanged: PropTypes.func,\n  clearForm: PropTypes.func,\n};\n\nreplaceComponent('FormSubmit', FormSubmit, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/EndAdornment.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { instantiateComponent } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { withStyles } from '@material-ui/core/styles';\nimport InputAdornment from '@material-ui/core/InputAdornment';\nimport IconButton from '@material-ui/core/IconButton';\nimport CloseIcon from 'mdi-material-ui/CloseCircle';\nimport MenuDownIcon from 'mdi-material-ui/MenuDown';\nimport classNames from 'classnames';\nimport _omit from 'lodash/omit';\n\n\nexport const styles = theme => ({\n\n  inputAdornment: {\n    whiteSpace: 'nowrap',\n    marginTop: '0 !important',\n    '& > *': {\n      verticalAlign: 'bottom',\n    },\n    '& > svg': {\n      color: theme.palette.common.darkBlack,\n    },\n    '& > * + *': {\n      marginLeft: 8,\n    },\n    height: 'auto',\n  },\n\n  menuIndicator: {\n    padding: 10,\n    marginRight: -40,\n    marginLeft: -16,\n    display: 'flex',\n    alignItems: 'center',\n    justifyContent: 'center',\n    color: theme.palette.common.midBlack,\n    pointerEvents: 'none',\n    transition: theme.transitions.create(['opacity'], {\n      duration: theme.transitions.duration.short,\n    }),\n  },\n\n  clearButton: {\n    opacity: 0,\n    '& svg': {\n      width: 20,\n      height: 20,\n    },\n    marginRight: -12,\n    marginLeft: -4,\n    '&:first-child': {\n      marginLeft: -12,\n    },\n    transition: theme.transitions.create('opacity', {\n      duration: theme.transitions.duration.short,\n    }),\n  },\n\n  urlButton: {\n    width: 40,\n    height: 40,\n    fontSize: 20,\n    marginLeft: -4,\n    marginRight: -4,\n  },\n\n});\n\n\nconst EndAdornment = (props, context) => {\n  const { classes, value, addonAfter, changeValue, showMenuIndicator, hideClear, disabled } = props;\n  const { intl } = context;\n\n  if (!addonAfter && (!changeValue || hideClear || disabled)) return null;\n  const hasValue = !!value || value === 0;\n\n  const clearButton = changeValue && !hideClear && !disabled &&\n    <IconButton className={classNames('clear-button', classes.clearButton, hasValue && 'has-value')}\n                onClick={event => {\n                  event.preventDefault();\n                  changeValue(null);\n                }}\n                onMouseDown={event => {\n                  event.preventDefault();\n                }}\n                tabIndex={-1}\n                aria-label={intl.formatMessage({ id: 'forms.delete_field' })}\n                disabled={!hasValue}\n    >\n      <CloseIcon/>\n    </IconButton>;\n\n  const menuIndicator = showMenuIndicator && !disabled &&\n    <div className={classNames('menu-indicator', classes.menuIndicator, hasValue && 'has-value')}>\n      <MenuDownIcon/>\n    </div>;\n\n  return (\n    <InputAdornment classes={{ root: classes.inputAdornment }} position=\"end\">\n      {instantiateComponent(addonAfter, _omit(props, ['classes']))}\n      {menuIndicator}\n      {clearButton}\n    </InputAdornment>\n  );\n};\n\n\nEndAdornment.propTypes = {\n  classes: PropTypes.object.isRequired,\n  value: PropTypes.any,\n  changeValue: PropTypes.func,\n  showMenuIndicator: PropTypes.bool,\n  hideClear: PropTypes.bool,\n  addonAfter: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),\n};\n\nEndAdornment.contextTypes = {\n  intl: intlShape,\n};\n\nexport default withStyles(styles)(EndAdornment);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/FormCheckbox.jsx",
    "content": "import { Components } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport createReactClass from 'create-react-class';\nimport StartAdornment, { hideStartAdornment } from './StartAdornment';\nimport EndAdornment from './EndAdornment';\nimport ComponentMixin from './mixins/component';\nimport { withStyles } from '@material-ui/core/styles';\nimport FormControlLabel from '@material-ui/core/FormControlLabel';\nimport Checkbox from '@material-ui/core/Checkbox';\nimport FormControlLayout from './FormControlLayout';\nimport FormHelper from './FormHelper';\n\n\nexport const styles = theme => ({\n\n  inputRoot: {\n    height: 50,\n  },\n\n  inputFocused: {},\n\n  inputDisabled: {},\n\n  checkboxRoot: {},\n\n  checkboxDisabled: {},\n\n});\n\n\nconst FormCheckbox = createReactClass({\n\n  mixins: [ComponentMixin],\n\n  getDefaultProps: function () {\n    return {\n      label: '',\n      value: false\n    };\n  },\n\n  changeValue: function (event) {\n    const target = event.target;\n    const value = target.checked;\n\n    this.props.handleChange(value);\n\n    setTimeout(() => {document.activeElement.blur();});\n  },\n\n  render: function () {\n    const startAdornment = hideStartAdornment(this.props) ? null :\n      <StartAdornment {...this.props}\n                      classes={null}\n      />;\n    const endAdornment =\n      <EndAdornment {...this.props}\n                    classes={null}\n      />;\n\n    const element = this.renderElement(startAdornment, endAdornment);\n\n    if (this.props.layout === 'elementOnly') {\n      return element;\n    }\n\n    return (\n      <FormControlLayout {...this.getFormControlProperties()} hideLabel={true} htmlFor={this.getId()}>\n        {element}\n        <FormHelper {...this.getFormHelperProperties()}/>\n      </FormControlLayout>\n    );\n  },\n\n  renderElement: function (startAdornment, endAdornment) {\n    const { classes, disabled, value, label } = this.props;\n\n    return (\n      <>\n        {startAdornment}\n      <FormControlLabel\n        classes={{\n          root: classes.inputRoot,\n          disabled: classes.inputDisabled,\n        }}\n        control={\n          <Checkbox\n            ref={(c) => this.element = c}\n            {...this.cleanSwitchProps(this.cleanProps(this.props))}\n            id={this.getId()}\n            checked={value === true}\n            onChange={this.changeValue}\n            disabled={disabled}\n            classes={{\n              root: classes.checkboxRoot,\n              disabled: classes.checkboxDisabled,\n            }}\n          />\n        }\n        label={<>{label}<Components.RequiredIndicator optional={this.props.optional} value={value}/></>}\n      />\n        {endAdornment}\n      </>\n    );\n  },\n\n});\n\n\nexport default withStyles(styles)(FormCheckbox);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/FormCheckboxGroup.jsx",
    "content": "import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\nimport createReactClass from 'create-react-class';\nimport ComponentMixin from './mixins/component';\nimport { withStyles } from '@material-ui/core/styles';\nimport FormGroup from '@material-ui/core/FormGroup';\nimport FormControlLabel from '@material-ui/core/FormControlLabel';\nimport FormControlLayout from './FormControlLayout';\nimport FormHelper from './FormHelper';\nimport Checkbox from '@material-ui/core/Checkbox';\nimport Switch from '@material-ui/core/Switch';\nimport classNames from 'classnames';\nimport isEmpty from 'lodash/isEmpty';\nimport { Components } from 'meteor/vulcan:core';\nimport without from 'lodash/without';\nimport uniq from 'lodash/uniq';\n\nconst styles = theme => ({\n  group: {\n    marginTop: '8px',\n  },\n  twoColumn: {\n    display: 'block',\n    [theme.breakpoints.down('md')]: {\n      '& > label': {\n        marginRight: theme.spacing(5),\n      },\n    },\n    [theme.breakpoints.up('md')]: {\n      '& > label': {\n        width: '49%',\n      },\n    },\n  },\n  threeColumn: {\n    display: 'block',\n    [theme.breakpoints.down('xs')]: {\n      '& > label': {\n        marginRight: theme.spacing(5),\n      },\n    },\n    [theme.breakpoints.up('xs')]: {\n      '& > label': {\n        width: '49%',\n      },\n    },\n    [theme.breakpoints.up('md')]: {\n      '& > label': {\n        width: '32%',\n      },\n    },\n  },\n});\n\n// this marker is used to identify \"other\" values\nexport const otherMarker = '[other]';\n\n// check if a string is an \"other\" value\nexport const isOtherValue = s => s && typeof s === 'string' && s.substr(0, otherMarker.length) === otherMarker;\n\n// remove the \"other\" marker from a string\nexport const removeOtherMarker = s => s && typeof s === 'string' && s.substr(otherMarker.length);\n\n// add the \"other\" marker to a string\nexport const addOtherMarker = s => `${otherMarker}${s}`;\n\n// return array of values without the \"other\" value\nexport const removeOtherValue = a => {\n  return a.filter(s => !isOtherValue(s));\n};\n\nconst OtherComponent = ({ value: _values, path, updateCurrentValues }) => {\n  const otherValue = removeOtherMarker(_values.find(isOtherValue));\n  // get copy of checkbox group values with \"other\" value removed\n  const withoutOtherValue = removeOtherValue(_values);\n\n  // keep track of whether \"other\" field is shown or not\n  const [showOther, setShowOther] = useState(!!otherValue);\n\n  // keep track of \"other\" field value locally\n  const [textFieldValue, setTextFieldValue] = useState(otherValue);\n\n  // textfield properties\n  const textFieldInputProperties = {\n    value: textFieldValue,\n    onChange: fieldValue => {\n      // first, update local state\n      setTextFieldValue(fieldValue);\n      // then update global form state\n      const newValue = isEmpty(fieldValue) ? withoutOtherValue : [...withoutOtherValue, addOtherMarker(fieldValue)];\n      updateCurrentValues({ [path]: newValue });\n    },\n  };\n\n  const textFieldItemProperties = {layout: 'elementOnly'};\n\n  return (\n    <div className=\"form-option-other\">\n      <FormControlLabel\n        control={\n          <Checkbox\n            inputRef={c => (this[name + '-' + 'other'] = c)}\n            checked={showOther}\n            onChange={event => {\n              const isChecked = event.target.checked;\n              setShowOther(isChecked);\n              if (isChecked) {\n                // if checkbox is checked and textfield has value, update global form state with current textfield value\n                if (textFieldValue) {\n                  updateCurrentValues({ [path]: [...withoutOtherValue, addOtherMarker(textFieldValue)] });\n                }\n              } else {\n                // if checkbox is unchecked, also clear out field value from global form state\n                updateCurrentValues({ [path]: withoutOtherValue });\n              }\n            }}\n            value={'other'}\n          />\n        }\n        label={'Other'}\n      />\n      {showOther && <Components.FormComponentText itemProperties={textFieldItemProperties}\n                                                  value={textFieldInputProperties.value}\n                                                  handleChange={textFieldInputProperties.onChange}/>}\n    </div>\n  );\n};\n\nconst FormCheckboxGroup = createReactClass({\n  mixins: [ComponentMixin],\n\n  propTypes: {\n    classes: PropTypes.object.isRequired,\n    inputProperties: PropTypes.shape({\n      variant: PropTypes.oneOf(['checkbox', 'switch']),\n      name: PropTypes.string,\n      options: PropTypes.array.isRequired,\n      columnClass: PropTypes.oneOf(['twoColumn', 'threeColumn']),\n    }).isRequired,\n  },\n\n  componentDidMount: function () {\n    if (this.props.refFunction) {\n      this.props.refFunction(this);\n    }\n  },\n\n  getDefaultProps: function () {\n    return {\n      label: '',\n      help: null,\n    };\n  },\n\n  validate: function () {\n    if (this.props.onBlur) {\n      this.props.onBlur();\n    }\n    return true;\n  },\n\n  renderElement: function () {\n    const {name, options, disabled: _disabled} = this.props.inputProperties;\n    let {value: _values} = this.props.inputProperties;\n    const {itemProperties, updateCurrentValues, value, path} = this.props;\n\n    // get rid of duplicate values; or any values that are not included in the options provided\n    // (unless they have the \"other\" marker)\n    _values = _values ? uniq(value.filter(v => isOtherValue(v) || options.map(o => o.value).includes(v))) : [];\n    const controls = options.map((checkbox, key) => {\n      let checkboxValue = checkbox.value;\n      let checked = _values.indexOf(checkboxValue) !== -1;\n      let disabled = checkbox.disabled || _disabled;\n      const Component = this.props.variant === 'switch' ? Switch : Checkbox;\n\n      return (\n        <FormControlLabel\n          key={key}\n          control={\n            <Component\n              inputRef={c => (this[name + '-' + checkboxValue] = c)}\n              checked={checked}\n              onChange={event => {\n                const isChecked = event.target.checked;\n                const newValue = isChecked ? [..._values, checkbox.value] : without(_values, checkbox.value);\n                updateCurrentValues({ [path]: newValue });\n              }}\n              value={checkboxValue}\n              disabled={disabled}\n            />\n          }\n          label={checkbox.label}\n        />\n      );\n    });\n\n    const maxLength = options.reduce(\n      (max, option) => (option.label.length > max ? option.label.length : max),\n      0,\n    );\n\n    const columnClass = this.props.inputProperties.columnClass ||\n      (maxLength < 20 ? 'threeColumn' : maxLength < 30 ? 'twoColumn' : '');\n\n    return (\n      <FormGroup className={classNames(this.props.classes.group, this.props.classes[columnClass])}>\n        {controls}\n        {itemProperties.showOther && <OtherComponent value={_values} path={path} updateCurrentValues={updateCurrentValues}/>}\n      </FormGroup>\n    );\n  },\n\n  render: function () {\n    if (this.props.layout === 'elementOnly') {\n      return <div>{this.renderElement()}</div>;\n    }\n\n    return (\n      <FormControlLayout {...this.getFormControlProperties()} fakeLabel={true}>\n        {this.renderElement()}\n        <FormHelper {...this.getFormHelperProperties()} />\n      </FormControlLayout>\n    );\n  },\n});\n\nexport default withStyles(styles)(FormCheckboxGroup);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/FormControlLayout.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport createReactClass from 'create-react-class';\nimport InputLabel from '@material-ui/core/InputLabel';\nimport FormControl from '@material-ui/core/FormControl';\nimport FormLabel from '@material-ui/core/FormLabel';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\n\n\n//noinspection JSUnusedGlobalSymbols\nconst FormControlLayout = createReactClass({\n\n  propTypes: {\n    label: PropTypes.node,\n    children: PropTypes.node,\n    optional: PropTypes.bool,\n    hasErrors: PropTypes.bool,\n    fakeLabel: PropTypes.bool,\n    hideLabel: PropTypes.bool,\n    shrinkLabel: PropTypes.bool,\n    layout: PropTypes.oneOf(['horizontal', 'vertical', 'elementOnly', 'shrink']),\n    htmlFor: PropTypes.string,\n    inputType: PropTypes.string,\n  },\n\n  renderLabel: function () {\n    const { fakeLabel, hideLabel, shrinkLabel, layout, optional, label, value } = this.props;\n\n    if (layout === 'elementOnly' || hideLabel) {\n      return null;\n    }\n\n    if (fakeLabel) {\n      return (\n        <FormLabel className=\"control-label legend\"\n                   component=\"legend\"\n                   data-required={!optional}\n        >\n          {label}<Components.RequiredIndicator optional={optional} value={value}/>\n        </FormLabel>\n      );\n    }\n\n    const shrink = shrinkLabel || ['date', 'time', 'datetime'].includes(this.props.inputType) ? true : undefined;\n\n    return (\n      <InputLabel className=\"control-label\"\n                  data-required={!optional}\n                  htmlFor={this.props.htmlFor}\n                  shrink={shrink}\n      >\n        {label}<Components.RequiredIndicator optional={optional} value={value}/>\n      </InputLabel>\n    );\n  },\n\n  render: function () {\n    const { layout, className, children, hasErrors } = this.props;\n\n    if (layout === 'elementOnly') {\n      return <span>{children}</span>;\n    }\n\n    return (\n      <FormControl component=\"fieldset\" error={hasErrors} fullWidth={layout !== 'shrink'} className={className}>\n        {this.renderLabel()}\n        {children}\n      </FormControl>\n    );\n  }\n\n});\n\n\nexport default FormControlLayout;\n\n\nregisterComponent('FormControl', FormControlLayout);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/FormHelper.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport FormHelperText from '@material-ui/core/FormHelperText';\nimport classNames from 'classnames';\n\n\nexport const styles = theme => ({\n\n  error: {\n    color: theme.palette.error.main,\n  },\n\n  formHelperText: {\n    display: 'flex',\n    '& :first-child': {\n      flexGrow: 1,\n    }\n  },\n  \n  charCount: {\n    whiteSpace: 'nowrap',\n    marginLeft: theme.spacing(1),\n  },\n\n});\n\n\nconst FormHelper = (props) => {\n  const {\n    className,\n    classes,\n    help,\n    errors,\n    hasErrors,\n    showCharsRemaining,\n    charsRemaining,\n    charsCount,\n    max,\n  } = props;\n\n  if (!help && !hasErrors && !showCharsRemaining) {\n    return null;\n  }\n\n  const errorMessage = hasErrors &&\n    <Components.FormError error={errors[0]} />;\n\n  return (\n    <FormHelperText className={classNames(className, classes.formHelperText)} error={hasErrors}>\n\n      <span>\n        {\n          hasErrors ? errorMessage : help\n        }\n      </span>\n\n      {\n        showCharsRemaining &&\n\n        <span className={classNames(classes.charCount, charsRemaining < 0 ? classes.error : null)}>\n          {charsCount} / {max}\n        </span>\n      }\n\n    </FormHelperText>\n  );\n};\n\n\nFormHelper.propTypes = {\n  className: PropTypes.string,\n  classes: PropTypes.object.isRequired,\n  value: PropTypes.any,\n  changeValue: PropTypes.func,\n  addonAfter: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),\n};\n\n\nexport default withStyles(styles)(FormHelper);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/FormInput.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport createReactClass from 'create-react-class';\nimport { withStyles } from '@material-ui/core/styles';\nimport ComponentMixin from './mixins/component';\nimport FormControlLayout from './FormControlLayout';\nimport FormHelper from './FormHelper';\nimport Input from '@material-ui/core/Input';\nimport StartAdornment, {hideStartAdornment} from './StartAdornment';\nimport EndAdornment from './EndAdornment';\nimport _debounce from 'lodash/debounce';\nimport classNames from 'classnames';\n\n\nexport const styles = theme => ({\n\n  root: {},\n\n  inputRoot: {\n    '& .clear-button.has-value': {opacity: 0},\n    '&:hover .clear-button.has-value': {opacity: 0.54},\n  },\n\n  inputFocused: {\n    '& .clear-button.has-value': {opacity: 0.54},\n  },\n\n  inputDisabled: {},\n\n  inputNoLabel: {\n    marginTop: '0 !important',\n  },\n\n  inputInput: {},\n\n  rootMultiline: {},\n\n  inputMultiline: {},\n\n});\n\n\n//noinspection JSUnusedGlobalSymbols\nconst FormInput = createReactClass({\n  element: null,\n\n  mixins: [ComponentMixin],\n\n  displayName: 'FormInput',\n\n  propTypes: {\n    type: PropTypes.oneOf([\n      'color',\n      'date',\n      'datetime',\n      'datetime-local',\n      'email',\n      'hidden',\n      'month',\n      'number',\n      'password',\n      'range',\n      'search',\n      'tel',\n      'text',\n      'time',\n      'url',\n      'social',\n      'week',\n    ]),\n    errors: PropTypes.array,\n    placeholder: PropTypes.string,\n    formatValue: PropTypes.func,\n    scrubValue: PropTypes.func,\n    getUrl: PropTypes.func,\n    hideClear: PropTypes.bool,\n  },\n\n  getDefaultProps: function () {\n    return {\n      type: 'text',\n    };\n  },\n\n  getInitialState: function () {\n    this.handleChangeDebounced = _debounce((value) => {\n      if (!this.props.handleChange) return;\n      if (value !== this.props.value) {\n        this.props.handleChange(value);\n      }\n    }, 500, { leading: true });\n\n    if (this.props.refFunction) {\n      this.props.refFunction(this);\n    }\n\n    return {\n      value: this.props.value,\n    };\n  },\n\n  componentDidUpdate(prevProps, prevState) {\n    if (this.props.value !== prevProps.value) {\n      this.handleChangeDebounced.cancel();\n      this.setState({ value: String(this.props.value) });\n    }\n  },\n\n  handleInputChange: function (event) {\n    let value = event.target.value;\n    this.changeValue(value);\n  },\n\n  changeValue: function (value) {\n    value = String(value);\n    if (this.props.scrubValue) {\n      value = this.props.scrubValue(value, this.props);\n    }\n    this.setState({ value });\n\n    this.handleChangeDebounced(value);\n  },\n\n  render: function () {\n    const startAdornment = hideStartAdornment(this.props) ? null :\n        <StartAdornment {...this.props}\n                        classes={null}\n                        value={this.state.value}\n                        changeValue={this.changeValue}\n        />;\n    const endAdornment =\n        <EndAdornment {...this.props}\n                      classes={null}\n                      value={this.state.value}\n                      changeValue={this.changeValue}\n        />;\n\n    let element = this.renderElement(startAdornment, endAdornment);\n\n    if (this.props.layout === 'elementOnly' || this.props.type === 'hidden') {\n      return element;\n    }\n\n    return (\n        <FormControlLayout {...this.getFormControlProperties()}\n                        htmlFor={this.getId()}>\n          {element}\n          <FormHelper {...this.getFormHelperProperties()}/>\n        </FormControlLayout>\n    );\n  },\n\n  renderElement: function (startAdornment, endAdornment) {\n    const {classes, disabled, autoFocus, formatValue, label, multiline, rows, rowsMax, inputProps} = this.props;\n    const value = formatValue ? formatValue(this.state.value) : this.state.value;\n    const options = this.props.options || {};\n\n    return (\n        <Input\n            ref={c => (this.element = c)}\n            id={this.getId()}\n            value={value || ''}\n            label={label}\n            onChange={this.handleInputChange}\n            disabled={disabled}\n            multiline={multiline}\n            rows={options.rows || rows}\n            rowsMax={options.rowsMax || rowsMax}\n            autoFocus={options.autoFocus || autoFocus}\n            startAdornment={startAdornment}\n            endAdornment={endAdornment}\n            placeholder={this.props.placeholder}\n            classes={{\n              root: classNames(classes.inputRoot, label === null && classes.inputNoLabel),\n              input: classes.inputInput,\n              focused: classes.inputFocused,\n              disabled: classes.inputDisabled,\n              multiline: classes.rootMultiline,\n              inputMultiline: classes.inputMultiline,\n            }}\n            {...inputProps}\n        />\n    );\n  },\n\n\n});\n\n\nexport default withStyles(styles)(FormInput);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/FormPicker.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport createReactClass from 'create-react-class';\nimport { withStyles } from '@material-ui/core/styles';\nimport ComponentMixin from './mixins/component';\nimport FormControlLayout from './FormControlLayout';\nimport FormHelper from './FormHelper';\nimport TextField from '@material-ui/core/TextField';\n\nimport moment from 'moment';\n//import 'moment-timezone';\n\nconst dateFormat = 'YYYY-MM-DD';\n\nexport const styles = theme => ({\n  inputRoot: {\n    'marginTop': '16px',\n    '& .clear-button.has-value': { opacity: 0 },\n    '&:hover .clear-button.has-value': { opacity: 0.54 },\n  },\n  inputFocused: {\n    '& .clear-button.has-value': { opacity: 0.54 }\n  },\n  inputDisabled: {},\n});\n\n//noinspection JSUnusedGlobalSymbols\nconst FormPicker = createReactClass({\n\n  mixins: [ComponentMixin],\n\n  displayName: 'FormPicker',\n\n  propTypes: {\n    type: PropTypes.oneOf([\n      'date',\n      'datetime',\n      'datetime-local',\n    ]),\n    errors: PropTypes.array,\n    placeholder: PropTypes.string,\n    formatValue: PropTypes.func,\n    hideClear: PropTypes.bool,\n  },\n\n  getDefaultProps: function () {\n    return {\n      type: 'date',\n    };\n  },\n\n  handleChange: function (event) {\n    let value = event.target.value;\n    if (this.props.scrubValue) {\n      value = this.props.scrubValue(value, this.props);\n    }\n    this.props.handleChange(value);\n  },\n\n  render: function () {\n    const { classes, disabled, autoFocus } = this.props;\n    const value = moment(this.props.value, dateFormat, true).isValid() ? this.props.value : moment(this.props.value).format(dateFormat);\n\n    const options = this.props.options || {};\n\n    return (\n      <FormControlLayout {...this.getFormControlProperties()} htmlFor={this.getId()}>\n        <TextField\n            ref={c => (this.element = c)}\n            {...this.cleanProps(this.props)}\n            id={this.getId()}\n            value={value}\n            autoFocus={options.autoFocus || autoFocus}\n            onChange={this.handleChange}\n            disabled={disabled}\n            placeholder={this.props.placeholder}\n            classes={{ root: classes.inputRoot }}\n        />\n        <FormHelper {...this.getFormHelperProperties()}/>\n      </FormControlLayout>\n    );\n  }\n});\n\n\nexport default withStyles(styles)(FormPicker);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/FormRadioGroup.jsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport PropTypes from 'prop-types';\nimport createReactClass from 'create-react-class';\nimport ComponentMixin from './mixins/component';\nimport { withStyles } from '@material-ui/core/styles';\nimport FormControlLayout from './FormControlLayout';\nimport FormHelper from './FormHelper';\nimport FormControlLabel from '@material-ui/core/FormControlLabel';\nimport Radio from '@material-ui/core/Radio';\nimport RadioGroup from '@material-ui/core/RadioGroup';\nimport classNames from 'classnames';\nimport _isArray from 'lodash/isArray';\nimport {\n  addOtherMarker,\n  isOtherValue,\n  removeOtherMarker,\n} from './FormCheckboxGroup';\nimport isEmpty from 'lodash/isEmpty';\nimport { Components } from 'meteor/vulcan:core';\n\nconst styles = theme => ({\n  group: {\n    marginTop: '8px',\n  },\n  inline: {\n    flexDirection: 'row',\n    '& > label': {\n      marginRight: theme.spacing(5),\n    },\n  },\n  twoColumn: {\n    display: 'block',\n\n    [theme.breakpoints.down('md')]: {\n      '& > label': {\n        marginRight: theme.spacing(5),\n      },\n    },\n    [theme.breakpoints.up('md')]: {\n      '& > label': {\n        width: '49%',\n      },\n    },\n  },\n  threeColumn: {\n    display: 'block',\n\n    [theme.breakpoints.down('xs')]: {\n      '& > label': {\n        marginRight: theme.spacing(5),\n      },\n    },\n\n    [theme.breakpoints.up('sm')]: {\n      '& > label': {\n        width: '48%',\n      },\n    },\n    [theme.breakpoints.up('md')]: {\n      '& > label': {\n        width: '32%',\n      },\n    },\n  },\n  fiveColumn: {\n    display: 'block',\n    [theme.breakpoints.down('xs')]: {\n      '& > label': {\n        marginRight: theme.spacing(5),\n      },\n    },\n    [theme.breakpoints.up('xs')]: {\n      '& > label': {\n        width: '38%',\n      },\n    },\n    [theme.breakpoints.up('sm')]: {\n      '& > label': {\n        width: '32%',\n      },\n    },\n    [theme.breakpoints.up('md')]: {\n      '& > label': {\n        width: '19%',\n      },\n    },\n  },\n  eightColumn: {\n    display: 'block',\n\n    [theme.breakpoints.down('xs')]: {\n      '& > label': {\n        marginRight: theme.spacing(1),\n      },\n    },\n    [theme.breakpoints.up('xs')]: {\n      '& > label': {\n        width: '49%',\n      },\n    },\n    [theme.breakpoints.up('sm')]: {\n      '& > label': {\n        width: '32%',\n      },\n    },\n    [theme.breakpoints.up('md')]: {\n      '& > label': {\n        width: '12%',\n      },\n    },\n  },\n  tenColumn: {\n    display: 'block',\n\n    [theme.breakpoints.down('xs')]: {\n      '& > label': {\n        marginRight: theme.spacing(1),\n      },\n    },\n    [theme.breakpoints.up('xs')]: {\n      '& > label': {\n        width: '24%',\n      },\n    },\n    [theme.breakpoints.up('sm')]: {\n      '& > label': {\n        width: '14%',\n      },\n    },\n    [theme.breakpoints.up('md')]: {\n      '& > label': {\n        width: '9%',\n      },\n    },\n  },\n  radio: {\n    width: '32px',\n    height: '32px',\n    marginLeft: '8px',\n  },\n  line: {\n    marginBottom: '12px',\n  },\n  label: {\n    marginBottom: '0px',\n  },\n  inputDisabled: {},\n});\n\nconst OtherComponent = ({value, path, updateCurrentValues, classes, key, disabled}) => {\n  const otherValue = removeOtherMarker(value);\n\n  // keep track of whether \"other\" field is shown or not\n  const [showOther, setShowOther] = useState(isOtherValue(value));\n\n  // keep track of \"other\" field value locally\n  const [textFieldValue, setTextFieldValue] = useState(otherValue);\n\n  // whenever value changes (and is not empty), if it's not an \"other\" value\n  // this means another option has been selected and we need to uncheck the \"other\" radio button\n  useEffect(() => {\n    if (value) {\n      setShowOther(isOtherValue(value));\n    }\n  }, [value]);\n\n  // textfield properties\n  const textFieldInputProperties = {\n    name: path,\n    value: textFieldValue,\n    onChange: fieldValue => {\n      // first, update local state\n      setTextFieldValue(fieldValue);\n      // then update global form state\n      const newValue = isEmpty(fieldValue) ? null : addOtherMarker(fieldValue);\n      updateCurrentValues({[path]: newValue});\n    },\n  };\n  const textFieldItemProperties = {layout: 'elementOnly'};\n\n  return (\n    <React.Fragment>\n      <FormControlLabel\n        key={key}\n        value={'[other]'}\n        control={\n          <Radio\n            className={classes.radio}\n            inputRef={c => (this['element-' + 'other'] = c)}\n            checked={showOther}\n            disabled={disabled}\n          />\n        }\n        className={classes.line}\n        classes={{label: classes.label}}\n        label={'Other'}\n      />\n      {showOther && <Components.FormComponentText itemProperties={textFieldItemProperties}\n                                                  value={textFieldInputProperties.value}\n                                                  handleChange={textFieldInputProperties.onChange}/>}\n    </React.Fragment>\n  );\n};\n\nconst FormRadioGroup = createReactClass({\n  mixins: [ComponentMixin],\n\n  propTypes: {\n    type: PropTypes.oneOf(['inline', 'stacked']),\n    inputProperties: PropTypes.shape({\n      name: PropTypes.string.isRequired,\n      options: PropTypes.array.isRequired,\n    }),\n  },\n\n  getDefaultProps: function() {\n    return {\n      type: 'stacked',\n      label: '',\n      help: null,\n      classes: PropTypes.object.isRequired,\n    };\n  },\n\n  changeRadio: function(event) {\n    const value = event.target.value;\n    //this.setValue(value);\n    this.props.handleChange(value);\n  },\n\n  validate: function() {\n    if (this.props.onBlur) {\n      this.props.onBlur();\n    }\n    return true;\n  },\n\n  renderElement: function() {\n    const {options, name, disabled: _disabled} = this.props.inputProperties;\n    const {itemProperties, updateCurrentValues, path} = this.props;\n    let value = this.props.inputProperties.value;\n    if (_isArray(value)) value = value[0];\n    const controls = options.map((radio, key) => {\n      let checked = value === radio.value;\n      let disabled = radio.disabled || _disabled;\n\n      return (\n        <FormControlLabel\n          key={key}\n          value={radio.value}\n          control={\n            <Radio\n              className={this.props.classes.radio}\n              inputRef={c => (this['element-' + key] = c)}\n              checked={checked}\n              disabled={disabled}\n            />\n          }\n          className={this.props.classes.line}\n          classes={{label: this.props.classes.label}}\n          label={radio.label}\n        />\n      );\n    });\n\n    const maxLength = options.reduce(\n      (max, option) => (option.label.length > max ? option.label.length : max),\n      0,\n    );\n\n    const getColumnClass = maxLength => {\n      if (maxLength < 3) {\n        return 'tenColumn';\n      }\n      if (maxLength < 7) {\n        return 'eightColumn';\n      }\n      if (maxLength < 12) {\n        return 'fiveColumn';\n      }\n      if (maxLength < 18) {\n        return 'threeColumn';\n      }\n      if (maxLength < 30) {\n        return 'twoColumn';\n      }\n    };\n\n    let columnClass = getColumnClass(maxLength);\n\n    if (this.props.type === 'inline') columnClass = 'inline';\n\n    return (\n      <RadioGroup\n        aria-label={name}\n        name={name}\n        className={classNames(this.props.classes.group, this.props.classes[columnClass])}\n        value={value}\n        onChange={this.changeRadio}>\n        {controls}\n        {itemProperties.showOther &&\n        <OtherComponent value={value} path={path} updateCurrentValues={updateCurrentValues}\n                        classes={this.props.classes} key={controls.length + 1} disabled={_disabled}/>}\n      </RadioGroup>\n    );\n  },\n\n  render: function() {\n    if (this.props.layout === 'elementOnly') {\n      return <div>{this.renderElement()}</div>;\n    }\n\n    return (\n      <FormControlLayout {...this.getFormControlProperties()} fakeLabel={true}>\n        {this.renderElement()}\n        <FormHelper {...this.getFormHelperProperties()} />\n      </FormControlLayout>\n    );\n  },\n});\n\nexport default withStyles(styles)(FormRadioGroup);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/FormSelect.jsx",
    "content": "import { withStyles } from '@material-ui/core/styles';\nimport React from 'react';\nimport createReactClass from 'create-react-class';\nimport PropTypes from 'prop-types';\nimport ComponentMixin from './mixins/component';\nimport FormControlLayout from './FormControlLayout';\nimport FormHelper from './FormHelper';\nimport Select from '@material-ui/core/Select';\nimport Input from '@material-ui/core/Input';\nimport MenuItem from '@material-ui/core/MenuItem';\nimport MenuList from '@material-ui/core/MenuList';\nimport ListSubheader from '@material-ui/core/ListSubheader';\nimport StartAdornment, { hideStartAdornment } from './StartAdornment';\nimport EndAdornment from './EndAdornment';\nimport _isArray from 'lodash/isArray';\nimport classNames from 'classnames';\nimport { styles } from './FormSuggest';\n\n\nconst FormSelect = createReactClass({\n\n  element: null,\n\n  mixins: [ComponentMixin],\n\n  propTypes: {\n    options: PropTypes.arrayOf(PropTypes.shape({\n      label: PropTypes.string,\n      value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),\n    })),\n    classes: PropTypes.object.isRequired,\n    showMenuIndicator: PropTypes.bool,\n  },\n\n  getDefaultProps: function () {\n    return {\n      showMenuIndicator: true,\n    };\n  },\n\n  getInitialState: function () {\n    return {\n      isOpen: false,\n    };\n  },\n\n  handleOpen: function () {\n    // this doesn't work\n    this.setState({\n      isOpen: true,\n    });\n  },\n\n  handleClose: function () {\n    // this doesn't work\n    this.setState({\n      isOpen: false,\n    });\n  },\n\n  handleChange: function (event) {\n    const target = event.target;\n    let value;\n    if (this.props.multiple && this.props.native) {\n      value = [];\n      for (let i = 0; i < target.length; i++) {\n        const option = target.options[i];\n        if (option.selected) {\n          value.push(option.value);\n        }\n      }\n    } else {\n      value = target.value;\n    }\n    this.changeValue(value);\n  },\n\n  changeValue: function (value) {\n    this.props.handleChange(value);\n  },\n\n  render: function () {\n    if (this.props.layout === 'elementOnly') {\n      return this.renderElement();\n    }\n\n    return (\n      <FormControlLayout{...this.getFormControlProperties()} htmlFor={this.getId()}>\n        {this.renderElement()}\n        <FormHelper {...this.getFormHelperProperties()}/>\n      </FormControlLayout>\n    );\n  },\n\n  renderElement: function () {\n    const renderOption = (item, key) => {\n      //eslint-disable-next-line no-unused-vars\n      const { group, label, ...rest } = item;\n      return this.props.native\n        ?\n        <option key={key} {...rest}>{label}</option>\n        :\n        <MenuItem key={key} {...rest} className={classes.selectItem}>{label}</MenuItem>;\n    };\n\n    const renderGroup = (label, key, nodes) => {\n      return this.props.native\n        ?\n        <optgroup label={label} key={key}>\n          {nodes}\n        </optgroup>\n        :\n        <MenuList subheader={<ListSubheader component=\"div\">{label}</ListSubheader>} key={key}>\n          {nodes}\n        </MenuList>;\n    };\n\n    const { options = [], classes } = this.props;\n\n    let groups = options.filter(function (item) {\n      return item.group;\n    }).map(function (item) {\n      return item.group;\n    });\n    // Get the unique items in group.\n    groups = [...new Set(groups)];\n\n    let optionNodes = [];\n\n    if (groups.length === 0) {\n      optionNodes = options.map(function (item, index) {\n        return renderOption(item, index);\n      });\n    } else {\n      // For items without groups.\n      const itemsWithoutGroup = options.filter(function (item) {\n        return !item.group;\n      });\n\n      itemsWithoutGroup.forEach(function (item, index) {\n        optionNodes.push(renderOption(item, 'no-group-' + index));\n      });\n\n      groups.forEach(function (group, groupIndex) {\n\n        const groupItems = options.filter(function (item) {\n          return item.group === group;\n        });\n\n        const groupOptionNodes = groupItems.map(function (item, index) {\n          return renderOption(item, groupIndex + '-' + index);\n        });\n\n        optionNodes.push(renderGroup(group, groupIndex, groupOptionNodes));\n      });\n    }\n\n    let value = this.props.value;\n    if (!this.props.multiple && _isArray(value)) {\n      value = value.length ? value[0] : '';\n    }\n\n    const startAdornment = hideStartAdornment(this.props) ? null :\n      <StartAdornment {...this.props}\n                      value={value}\n                      classes={null}\n                      changeValue={this.changeValue}\n      />;\n    const endAdornment =\n      <EndAdornment {...this.props}\n                    value={value}\n                    classes={{ inputAdornment: classes.inputAdornment }}\n                    changeValue={this.changeValue}\n      />;\n\n    return (\n      <Select className=\"select\"\n              ref={(c) => this.element = c}\n              {...this.cleanProps(this.props)}\n              value={value}\n              onChange={this.handleChange}\n              onOpen={this.handleOpen}\n              onClose={this.handleClose}\n              disabled={this.props.disabled}\n              input={<Input id={this.getId()}\n                            startAdornment={startAdornment}\n                            endAdornment={endAdornment}\n                            classes={{\n                              root: classes.inputRoot,\n                              focused: classes.inputFocused,\n                              input: classNames(classes.input, !value && classes.inputPlaceholder),\n                            }}\n              />}\n              classes={{ icon: classes.selectIcon }}\n      >\n        {optionNodes}\n      </Select>\n    );\n  }\n});\n\n\nexport default withStyles(styles)(FormSelect);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/FormSuggest.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport createReactClass from 'create-react-class';\nimport ComponentMixin from './mixins/component';\nimport { withStyles } from '@material-ui/core/styles';\nimport Input from '@material-ui/core/Input';\nimport Autosuggest from 'react-autosuggest';\nimport Paper from '@material-ui/core/Paper';\nimport MenuItem from '@material-ui/core/MenuItem';\nimport ListItemIcon from '@material-ui/core/ListItemIcon';\nimport match from 'autosuggest-highlight/match';\nimport parse from 'autosuggest-highlight/parse';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport StartAdornment, { hideStartAdornment } from './StartAdornment';\nimport EndAdornment from './EndAdornment';\nimport FormControlLayout from './FormControlLayout';\nimport FormHelper from './FormHelper';\nimport _isEqual from 'lodash/isEqual';\nimport classNames from 'classnames';\nimport IsolatedScroll from 'react-isolated-scroll';\n\nconst maxSuggestions = 100;\n\n/*{\n  container:                'react-autosuggest__container',\n  containerOpen:            'react-autosuggest__container--open',\n  input:                    'react-autosuggest__input',\n  inputOpen:                'react-autosuggest__input--open',\n  inputFocused:             'react-autosuggest__input--focused',\n  suggestionsContainer:     'react-autosuggest__suggestions-container',\n  suggestionsContainerOpen: 'react-autosuggest__suggestions-container--open',\n  suggestionsList:          'react-autosuggest__suggestions-list',\n  suggestion:               'react-autosuggest__suggestion',\n  suggestionFirst:          'react-autosuggest__suggestion--first',\n  suggestionHighlighted:    'react-autosuggest__suggestion--highlighted',\n  sectionContainer:         'react-autosuggest__section-container',\n  sectionContainerFirst:    'react-autosuggest__section-container--first',\n  sectionTitle:             'react-autosuggest__section-title'\n}*/\nexport const styles = theme => {\n  const light = theme.palette.type === 'light';\n  const bottomLineColor = light ? 'rgba(0, 0, 0, 0.42)' : 'rgba(255, 255, 255, 0.7)';\n\n  return {\n\n    root: {},\n\n    container: {\n      flexGrow: 1,\n      position: 'relative',\n    },\n\n    textField: {\n      width: '100%',\n      'label + div > &': {\n        marginTop: theme.spacing(2),\n      },\n    },\n\n    input: {\n      outline: 0,\n      font: 'inherit',\n      color: 'currentColor',\n      width: '100%',\n      border: '0',\n      margin: '0',\n      padding: '7px 0',\n      display: 'block',\n      boxSizing: 'content-box',\n      background: 'none',\n      verticalAlign: 'middle',\n      '&::-webkit-search-decoration, &::-webkit-search-cancel-button, &::after, &:after': {\n        display: 'none',\n      },\n      '&::-webkit-search-results, &::-webkit-search-results-decoration': { display: 'none' },\n    },\n\n    inputPlaceholder: {\n      color: theme.palette.common.lightBlack,\n    },\n\n    readOnly: {\n      cursor: 'pointer',\n    },\n\n    suggestionsContainer: {\n      display: 'none',\n      position: 'absolute',\n      left: 0,\n      right: 0,\n      zIndex: theme.zIndex.modal,\n      marginBottom: theme.spacing(3),\n      maxHeight: 48 * 8,\n    },\n\n    suggestionsContainerOpen: {\n      display: 'flex',\n    },\n\n    scroller: {\n      flexGrow: 1,\n      overflowY: 'auto',\n    },\n\n    suggestion: {\n      display: 'block',\n    },\n\n    suggestionIcon: {\n      marginRight: theme.spacing(2),\n    },\n\n    current: {\n      backgroundColor: theme.palette.secondary.light,\n    },\n\n    suggestionsList: {\n      margin: 0,\n      padding: 0,\n      listStyleType: 'none',\n    },\n\n    inputRoot: {\n      '&:hover .clear-button.has-value': { opacity: 0.54, pointerEvents: 'initial' },\n      '&:focus .clear-button.has-value': { opacity: 0.54, pointerEvents: 'initial' },\n      '&:hover .menu-indicator.has-value': { opacity: 0 },\n      '&:focus .menu-indicator.has-value': { opacity: 0 },\n    },\n\n    inputFocused: {\n      '& .clear-button.has-value': { opacity: 0.54, pointerEvents: 'initial' },\n      '& .menu-indicator.has-value': { opacity: 0 },\n    },\n\n    inputDisabled: {},\n\n    formatted: {\n      display: 'flex',\n      alignItems: 'center',\n      marginTop: 16,\n      paddingTop: 4,\n      paddingRight: 0,\n      paddingBottom: 4,\n      paddingLeft: 0,\n      fontSize: 17.15,\n      cursor: 'pointer',\n      '&$disabled': {\n        pointerEvents: 'none',\n      },\n    },\n\n    error: {},\n\n    disabled: {},\n\n    focused: {},\n\n    underline: {\n      '&:after': {\n        borderBottom: `2px solid ${theme.palette.primary[light ? 'dark' : 'light']}`,\n        left: 0,\n        bottom: 0,\n        // Doing the other way around crash on IE 11 \"''\" https://github.com/cssinjs/jss/issues/242\n        content: '\"\"',\n        position: 'absolute',\n        right: 0,\n        transform: 'scaleX(0)',\n        transition: theme.transitions.create('transform', {\n          duration: theme.transitions.duration.shorter,\n          easing: theme.transitions.easing.easeOut,\n        }),\n        pointerEvents: 'none', // Transparent to the hover style.\n      },\n      '&:focus:after': {\n        transform: 'scaleX(1)',\n      },\n      '&$error:after': {\n        borderBottomColor: theme.palette.error.main,\n        transform: 'scaleX(1)', // error is always underlined in red\n      },\n      '&:before': {\n        borderBottom: `1px solid ${bottomLineColor}`,\n        left: 0,\n        bottom: 0,\n        // Doing the other way around crash on IE 11 \"''\" https://github.com/cssinjs/jss/issues/242\n        content: '\"\\\\00a0\"',\n        position: 'absolute',\n        right: 0,\n        transition: theme.transitions.create('border-bottom-color', {\n          duration: theme.transitions.duration.shorter,\n        }),\n        pointerEvents: 'none', // Transparent to the hover style.\n      },\n      '&:hover:not($disabled):not($focused):not($error):before': {\n        borderBottom: `2px solid ${theme.palette.text.primary}`,\n        // Reset on touch devices, it doesn't add specificity\n        '@media (hover: none)': {\n          borderBottom: `1px solid ${bottomLineColor}`,\n        },\n      },\n      '&$disabled:before': {\n        borderBottomStyle: 'dotted',\n        '@media print': {\n          borderBottomStyle: 'solid',\n          borderBottomWidth: 'thin',\n        },\n      },\n    },\n\n    formattedNoLabel: {\n      marginTop: 0,\n    },\n\n    selectItem: {\n      paddingTop: 4,\n      paddingBottom: 4,\n      paddingLeft: 9,\n      fontFamily: theme.typography.fontFamily,\n      color: theme.palette.type === 'light' ? 'rgba(0, 0, 0, 0.87)' : theme.palette.common.white,\n      fontSize: theme.typography.pxToRem(16),\n      lineHeight: '1.1875em',\n    },\n\n    selectIcon: {\n      display: 'none',\n    },\n\n    inputAdornment: {\n      pointerEvents: 'none',\n    },\n\n    menuItem: {},\n\n    menuItemHighlight: {},\n\n    menuItemIcon: {},\n\n  };\n};\n\nconst FormSuggest = createReactClass({\n  inputElement: null,\n\n  mixins: [ComponentMixin],\n\n  propTypes: {\n    options: PropTypes.arrayOf(\n      PropTypes.shape({\n        label: PropTypes.string,\n        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),\n        iconComponent: PropTypes.node,\n        formatted: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),\n        onClick: PropTypes.func,\n      }),\n    ),\n    classes: PropTypes.object.isRequired,\n    limitToList: PropTypes.bool,\n    disableText: PropTypes.bool,\n    disableSelectOnBlur: PropTypes.bool,\n    showAllOptions: PropTypes.bool,\n    disableMatchParts: PropTypes.bool,\n    autoComplete: PropTypes.string,\n    autoFocus: PropTypes.bool,\n    showMenuIndicator: PropTypes.bool,\n  },\n\n  getDefaultProps: function() {\n    return {\n      autoComplete: 'off',\n      autoFocus: false,\n      showMenuIndicator: true,\n    };\n  },\n\n  getOptionFormatted: function(option, formattedProps) {\n    if (!option) return;\n    const formatted =\n      option.formatted && typeof option.formatted === 'function'\n        ? option.formatted(formattedProps)\n        : option.formatted;\n    return formatted;\n  },\n\n  getOptionLabel: function(option) {\n    return option && option.label || option && option.value || '';\n  },\n\n  getInitialState: function() {\n    if (this.props.refFunction) {\n      this.props.refFunction(this);\n    }\n\n    const inputValue = this.getInputValue(this.props);\n    return {\n      inputValue,\n      suggestions: [],\n    };\n  },\n\n  UNSAFE_componentWillReceiveProps: function (nextProps) {\n    if (nextProps.value !== this.props.value ||\n      nextProps.options !== this.props.options) {\n      const inputValue = this.getInputValue(nextProps);\n      this.setState({\n        inputValue,\n      });\n    }\n  },\n\n  shouldComponentUpdate: function (nextProps, nextState) {\n    const shouldUpdate = !_isEqual(nextState, this.state) ||\n      nextProps.disabled !== this.props.disabled ||\n      nextProps.help !== this.props.help ||\n      nextProps.charsCount !== this.props.charsCount ||\n      !_isEqual(nextProps.errors, this.props.errors) ||\n      nextProps.options !== this.props.options;\n    return shouldUpdate;\n  },\n\n  getInputValue: function (props) {\n    const selectedOption = this.getSelectedOption(props);\n    const inputValue = selectedOption ?\n      this.getOptionLabel(selectedOption) :\n      props.limitToList ?\n        '' :\n        props.value;\n    return inputValue;\n  },\n\n  getSelectedOption: function(props) {\n    props = props || this.props;\n    const selectedOption = props.options && props.options.find(opt => opt.value === props.value);\n    return selectedOption;\n  },\n\n  handleFocus: function(event) {\n    if (!this.inputElement) return;\n\n    this.inputElement.select();\n  },\n\n  handleBlur: function(event, { highlightedSuggestion: suggestion }) {\n    if (!this.props.disableSelectOnBlur) {\n      const selectedOption = this.getSelectedOption();\n      if (!selectedOption) return;\n\n      this.changeValue(selectedOption);\n      const inputValue = this.getInputValue(this.props);\n      this.setState({\n        inputValue,\n      });\n    }\n  },\n\n  highlightFirstSuggestion: function() {\n    if (this.props.disableText) return false;\n\n    const selectedOption = this.getSelectedOption();\n    if (!selectedOption || !selectedOption.value) return true;\n    return selectedOption.label !== this.state.inputValue;\n  },\n\n  suggestionSelected: function(event, { suggestion }) {\n    event.preventDefault();\n    this.changeValue(suggestion);\n  },\n\n  changeValue: function(suggestion) {\n    if (!suggestion) {\n      suggestion = this.props.limitToList || suggestion === null ?\n        { label: '', value: null } :\n        { label: this.state.inputValue, value: this.state.inputValue };\n    }\n    if (suggestion.onClick) {\n      return;\n    }\n    this.setState({\n      inputValue: this.getOptionLabel(suggestion),\n    });\n    this.props.handleChange(suggestion.value);\n  },\n\n  handleInputChange: function(event) {\n    const value = event.target.value;\n    this.setState({\n      inputValue: value,\n    });\n  },\n\n  handleSuggestionsFetchRequested: function({ value, reason }) {\n    this.setState({\n      suggestions: this.getSuggestions(value),\n    });\n  },\n\n  handleSuggestionsClearRequested: function() {\n    this.setState({\n      suggestions: [],\n    });\n  },\n\n  shouldRenderSuggestions: function(value) {\n    return true;\n  },\n\n  render: function () {\n    const { value, disabled, classes } = this.props;\n    const { inputValue } = this.state;\n    const selectedOption = this.getSelectedOption();\n    const inputFormatted = this.getOptionFormatted(selectedOption, {\n      current: true,\n      disabled,\n    });\n\n    const startAdornment = hideStartAdornment(this.props) ? null :\n      <StartAdornment {...this.props}\n                      value={value}\n                      classes={null}\n      />;\n    const endAdornment =\n      <EndAdornment {...this.props}\n                    value={value}\n                    classes={{ inputAdornment: classes.inputAdornment }}\n                    changeValue={this.changeValue}\n      />;\n\n    const element = this.renderElement(startAdornment, endAdornment);\n\n    if (this.props.layout === 'elementOnly') {\n      return element;\n    }\n\n    return (\n      <FormControlLayout\n        {...this.getFormControlProperties()}\n        shrinkLabel={inputFormatted && inputFormatted !== inputValue}\n        htmlFor={this.getId()}>\n        {element}\n        <FormHelper {...this.getFormHelperProperties()} />\n      </FormControlLayout>\n    );\n  },\n\n  renderElement: function(startAdornment, endAdornment) {\n    const { classes, autoFocus, disableText, placeholder, inputProperties, disabled } = this.props;\n    const { inputValue } = this.state;\n    const selectedOption = this.getSelectedOption();\n    const inputFormatted = this.getOptionFormatted(selectedOption, {\n      current: true,\n      disabled,\n    });\n    return (\n      <Autosuggest\n        theme={{\n          container: classes.container,\n          input: classNames(classes.input, disableText && classes.readOnly),\n          suggestionsContainer: classes.suggestionsContainer,\n          suggestionsContainerOpen: classes.suggestionsContainerOpen,\n          suggestion: classes.suggestion,\n          suggestionsList: classes.suggestionsList,\n        }}\n        highlightFirstSuggestion={this.highlightFirstSuggestion()}\n        renderInputComponent={this.renderInputComponent}\n        suggestions={this.state.suggestions}\n        onSuggestionsFetchRequested={this.handleSuggestionsFetchRequested}\n        onSuggestionsClearRequested={this.handleSuggestionsClearRequested}\n        renderSuggestionsContainer={this.renderSuggestionsContainer}\n        shouldRenderSuggestions={this.shouldRenderSuggestions}\n        focusInputOnSuggestionClick={false}\n        alwaysRenderSuggestions={false}\n        getSuggestionValue={this.getSuggestionValue}\n        renderSuggestion={this.renderSuggestion}\n        onSuggestionSelected={this.suggestionSelected}\n        inputProps={{\n          ...this.cleanProps(inputProperties),\n          autoFocus,\n          classes,\n          onChange: this.handleInputChange,\n          onFocus: this.handleFocus,\n          onBlur: this.handleBlur,\n          value: inputValue,\n          formatted: inputFormatted,\n          placeholder: placeholder,\n          readOnly: disableText,\n          disabled: this.props.disabled,\n          name: this.props.name,\n          'aria-haspopup': 'true',\n          startAdornment,\n          endAdornment,\n        }}\n      />\n    );\n  },\n\n  renderInputComponent: function(inputProps) {\n    const {\n      classes,\n      autoFocus,\n      autoComplete,\n      value,\n      formatted,\n      ref,\n      startAdornment,\n      endAdornment,\n      disabled,\n      errors,\n      ...rest\n    } = inputProps;\n    const { hideLabel, inputRef } = this.props;\n\n    if (formatted && formatted !== value) {\n      return (\n        <div\n          aria-readonly={disabled}\n          {...rest}\n          tabIndex={0}\n          className={classNames(\n            classes.inputRoot,\n            classes.underline,\n            disabled && classes.disabled,\n            errors?.length && classes.error,\n            classes.formatted,\n            hideLabel && classes.formattedNoLabel,\n          )}>\n          {startAdornment}\n          {formatted}\n          {endAdornment}\n        </div>\n      );\n    }\n\n    return (\n      <Input\n        autoFocus={autoFocus}\n        autoComplete={autoComplete}\n        className={classes.textField}\n        classes={{\n          root: classes.inputRoot,\n          underline: classes.underline,\n          focused: classes.inputFocused,\n        }}\n        value={value}\n        inputRef={c => {\n          ref(c);\n          if (inputRef) {\n            inputRef(c);\n          }\n          this.inputElement = c;\n        }}\n        type=\"text\"\n        startAdornment={startAdornment}\n        endAdornment={endAdornment}\n        disabled={disabled}\n        inputProps={{\n          ...rest,\n        }}\n      />\n    );\n  },\n\n  renderSuggestion: function (suggestion, { query, isHighlighted }) {\n    const { classes } = this.props;\n    const formatted = this.getOptionFormatted(suggestion, {\n      disabled: this.props.disabled,\n      selected: isHighlighted,\n    });\n    if (formatted) return formatted;\n\n    const label = suggestion.label || suggestion.value || '';\n    const matches = match(label, query);\n    const parts = parse(label, matches);\n    const primary = this.props.disableMatchParts\n      ?\n      label\n      :\n      parts.map((part, index) => {\n        return part.highlight\n          ?\n          <span key={index} className={classes.menuItemHighlight}>{part.text}</span>\n          :\n          <span key={index}>{part.text}</span>;\n      });\n    const isCurrent = suggestion.value === this.props.value;\n    const className = classNames(classes.menuItem, isCurrent && classes.current);\n    return (\n      <MenuItem selected={isHighlighted}\n                component=\"div\"\n                className={className}\n                onClick={suggestion.onClick}\n                data-value={suggestion.value}\n      >\n        {\n          suggestion.iconComponent &&\n          <ListItemIcon classes={{ root: classes.menuItemIcon }}>{suggestion.iconComponent}</ListItemIcon>\n        }\n        <div>\n          {primary}\n        </div>\n      </MenuItem>\n    );\n  },\n\n  renderSuggestionsContainer: function({ containerProps, children }) {\n    const { classes } = this.props;\n\n    return (\n      <Paper {...containerProps} id={`menu-${this.props.name}`} square>\n        <IsolatedScroll className={classes.scroller}>{children}</IsolatedScroll>\n      </Paper>\n    );\n  },\n\n  getSuggestionValue: function(suggestion) {\n    return suggestion.value;\n  },\n\n  getSuggestions: function(value) {\n    const inputValue = value.trim().toLowerCase();\n    const inputLength = inputValue.length;\n    let count = 0;\n    const inputMatchesSelection = value === this.getOptionLabel(this.getSelectedOption());\n\n    return (this.props.disableText || this.props.showAllOptions) && inputMatchesSelection\n      ? this.props.options.filter(suggestion => {\n        return true;\n      })\n      : inputLength === 0\n        ? this.props.options.filter(suggestion => {\n          count++;\n          return count <= maxSuggestions;\n        })\n        : this.props.options.filter(suggestion => {\n          const label = this.getOptionLabel(suggestion);\n          const keep = count < maxSuggestions && label.toLowerCase().includes(inputValue);\n\n          if (keep) {\n            count++;\n          }\n\n          return keep;\n        });\n  },\n\n});\n\nexport default withStyles(styles)(FormSuggest);\nregisterComponent('FormSuggest', FormSuggest, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/FormSwitch.jsx",
    "content": "import { Components } from 'meteor/vulcan:lib';\nimport React from 'react';\nimport createReactClass from 'create-react-class';\nimport StartAdornment, { hideStartAdornment } from './StartAdornment';\nimport EndAdornment from './EndAdornment';\nimport ComponentMixin from './mixins/component';\nimport { withStyles } from '@material-ui/core/styles';\nimport FormControlLabel from '@material-ui/core/FormControlLabel';\nimport Switch from '@material-ui/core/Switch';\nimport FormControlLayout from './FormControlLayout';\nimport FormHelper from './FormHelper';\n\n\nexport const styles = theme => ({\n\n  inputRoot: {\n    height: 50,\n  },\n\n  inputFocused: {},\n\n  inputDisabled: {},\n\n  switchRoot: {},\n\n  switchDisabled: {},\n\n});\n\n\nconst FormSwitch = createReactClass({\n\n  mixins: [ComponentMixin],\n\n  getDefaultProps: function () {\n    return {\n      label: '',\n      value: false\n    };\n  },\n\n  changeValue: function (event) {\n    const target = event.target;\n    const value = target.checked;\n\n    this.props.handleChange(value);\n\n    setTimeout(() => {document.activeElement.blur();});\n  },\n\n  render: function () {\n    const startAdornment = hideStartAdornment(this.props) ? null :\n      <StartAdornment {...this.props}\n                      classes={null}\n      />;\n    const endAdornment =\n      <EndAdornment {...this.props}\n                    classes={null}\n      />;\n\n    const element = this.renderElement(startAdornment, endAdornment);\n\n    if (this.props.layout === 'elementOnly') {\n      return element;\n    }\n\n    return (\n      <FormControlLayout {...this.getFormControlProperties()} hideLabel={true} htmlFor={this.getId()}>\n        {element}\n        <FormHelper {...this.getFormHelperProperties()}/>\n      </FormControlLayout>\n    );\n  },\n\n  renderElement: function (startAdornment, endAdornment) {\n    const { classes, disabled, value, label } = this.props;\n\n    return (\n      <>\n        {startAdornment}\n        <FormControlLabel\n          classes={{\n            root: classes.inputRoot,\n            disabled: classes.inputDisabled,\n          }}\n          control={\n            <Switch\n              classes={{\n                root: classes.switchRoot,\n                disabled: classes.switchDisabled,\n              }}\n              ref={(c) => this.element = c}\n              {...this.cleanSwitchProps(this.cleanProps(this.props))}\n              id={this.getId()}\n              checked={value === true}\n              onChange={this.changeValue}\n              disabled={disabled}\n            />\n          }\n          label={<>{label}<Components.RequiredIndicator optional={this.props.optional} value={value}/></>}\n        />\n        {endAdornment}\n      </>\n    );\n  },\n\n});\n\n\nexport default withStyles(styles)(FormSwitch);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/FormText.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport createReactClass from 'create-react-class';\nimport { withStyles } from '@material-ui/core/styles';\nimport ComponentMixin from './mixins/component';\nimport FormControlLayout from './FormControlLayout';\nimport FormHelper from './FormHelper';\nimport Typography from '@material-ui/core/Typography';\n\nexport const styles = theme => ({\n  inputRoot: {\n    marginTop: theme.spacing(2),\n    fontSize: '1.0714285714285714rem',\n    borderBottom: '1px solid rgba(0, 0, 0, 0.42)',\n    cursor: 'not-allowed',\n  },\n  inputFocused: {},\n  inputDisabled: {},\n});\n\n//noinspection JSUnusedGlobalSymbols\nconst FormText = createReactClass({\n  element: null,\n\n  mixins: [ComponentMixin],\n\n  displayName: 'FormText',\n\n  propTypes: {},\n\n  getInitialState: function() {\n    if (this.props.refFunction) {\n      this.props.refFunction(this);\n    }\n    return {};\n  },\n\n  parseUrl: function(value) {\n    if (!value) return '';\n    value = value.toString();\n\n    return value.indexOf('http://') > -1 || value.indexOf('https://') > -1 ? (\n      <a href={value} target=\"_blank\" rel=\"noopener noreferrer\">\n        {value}\n      </a>\n    ) : (\n      value\n    );\n  },\n\n  render: function() {\n    const { inputProperties, classes, layout } = this.props;\n    const variant = inputProperties.variant || 'body2';\n    const color = inputProperties.color || 'default';\n\n    let element = (\n      <Typography variant={variant} color={color} className={classes.inputRoot}>\n        {this.parseUrl(inputProperties.value)}\n      </Typography>\n    );\n\n    if (layout === 'elementOnly') {\n      return element;\n    }\n\n    return (\n      <FormControlLayout\n        {...this.getFormControlProperties()}\n        shrinkLabel={true}\n        htmlFor={this.getId()}>\n        {element}\n        <FormHelper {...this.getFormHelperProperties()} />\n      </FormControlLayout>\n    );\n  },\n});\n\nexport default withStyles(styles)(FormText);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/RequiredIndicator.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent } from 'meteor/vulcan:lib';\nimport { withStyles } from '@material-ui/core/styles';\nimport classNames from 'classnames';\n\n\nexport const styles = theme => ({\n\n  root: {\n    marginLeft: 4,\n  },\n\n  missing: {\n    color: theme.palette.error.main,\n  },\n\n});\n\n\nconst RequiredIndicator = (props) => {\n  const { classes, optional, value } = props;\n  const className = classNames('required-indicator', 'optional-symbol', classes.root, !value && classes.missing);\n\n  return optional\n    ?\n    null\n    :\n    <span className={className}>*</span>;\n};\n\n\nRequiredIndicator.propTypes = {\n  classes: PropTypes.object.isRequired,\n  optional: PropTypes.bool,\n  value: PropTypes.any,\n};\n\n\nregisterComponent('RequiredIndicator', RequiredIndicator, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/StartAdornment.jsx",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport { Components as C, instantiateComponent } from 'meteor/vulcan:core';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { withStyles } from '@material-ui/core/styles';\nimport InputAdornment from '@material-ui/core/InputAdornment';\nimport WebIcon from 'mdi-material-ui/Web';\nimport EmailIcon from 'mdi-material-ui/EmailOutline';\nimport { styles } from './EndAdornment';\n\n\nconst linkTypes = ['url', 'email', 'social'];\n\n\nexport const hideStartAdornment = (props) => {\n  const { type, hideLink } = props;\n  return !props.addonBefore && (!linkTypes.includes(type) || hideLink);\n};\n\n\nconst StartAdornment = (props, context) => {\n  const { intl } = context;\n\n  if (hideStartAdornment(props)) return null;\n\n  const { classes, type, scrubValue, getUrl } = props;\n  let value = props.value;\n  if (scrubValue) {\n    value = scrubValue(value, props);\n  }\n  const url = getUrl ? getUrl(value, props) : value;\n  const socialIcon = type === 'social' ? props.addonBefore : undefined;\n  const addonBefore = type === 'social' ? undefined : props.addonBefore;\n  const icon = type === 'email'\n    ?\n    <EmailIcon/>\n    :\n    socialIcon\n      ?\n      instantiateComponent(socialIcon)\n      :\n      <WebIcon/>;\n\n  const urlButton = linkTypes.includes(type) &&\n    <C.TooltipButton classes={{ button: classes.urlButton }}\n                     titleId={`forms.${type}_help`}\n                     type=\"icon\"\n                     icon={icon}\n                     size=\"small\"\n                     href={url}\n                     target=\"_blank\"\n                     disabled={!value}\n                     aria-label={intl.formatMessage({\n                       id: `forms.start_adornment_${type}_icon`,\n                     })}\n    />;\n\n  return (\n    <InputAdornment classes={{ root: classes.inputAdornment }} position=\"start\">\n      {instantiateComponent(addonBefore)}\n      {urlButton}\n    </InputAdornment>\n  );\n};\n\n\nStartAdornment.propTypes = {\n  classes: PropTypes.object.isRequired,\n  value: PropTypes.any,\n  type: PropTypes.string,\n  addonBefore: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),\n};\n\nStartAdornment.contextTypes = {\n  intl: intlShape,\n};\n\nexport default withStyles(styles)(StartAdornment);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/base-controls/mixins/component.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport _omit from 'lodash/omit';\nimport classNames from 'classnames';\n\n\nexport default {\n\n  propTypes: {\n    label: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),\n    hideLabel: PropTypes.bool,\n    layout: PropTypes.string,\n    optional: PropTypes.bool,\n    errors: PropTypes.arrayOf(PropTypes.object),\n    className: PropTypes.string,\n    inputType: PropTypes.string,\n  },\n\n  getFormControlProperties: function () {\n    return {\n      label: this.props.label,\n      hideLabel: this.props.hideLabel,\n      layout: this.props.layout,\n      optional: this.props.optional,\n      value: this.props.value,\n      hasErrors: this.hasErrors(),\n      className: classNames(this.props.className, this.props.classes?.root),\n      inputType: this.props.inputType,\n    };\n  },\n\n  getFormHelperProperties: function () {\n    return {\n      help: this.props.help,\n      errors: this.props.errors,\n      hasErrors: this.hasErrors(),\n      showCharsRemaining: this.props.showCharsRemaining,\n      charsRemaining: this.props.charsRemaining,\n      charsCount: this.props.charsCount,\n      max: this.props.max,\n      className: 'form-helper-text',\n    };\n  },\n\n  hashString: function (string) {\n    let hash = 0;\n    for (let i = 0; i < string.length; i++) {\n      hash = (((hash << 5) - hash) + string.charCodeAt(i)) & 0xFFFFFFFF;\n    }\n    return hash;\n  },\n\n  /**\n   * The ID is used as an attribute on the form control, and is used to allow\n   * associating the label element with the form control.\n   *\n   * If we don't explicitly pass an `id` prop, we generate one based on the\n   * `name`, `label` and `itemIndex` (for nested forms) properties.\n   */\n  getId: function () {\n    const { id, label = '', name, itemIndex = '' } = this.props;\n    if (id) {\n      return id;\n    }\n    const cleanName = name ? name.split('[').join('_').replace(']', '') : '';\n    return [\n      'frc',\n      cleanName,\n      itemIndex,\n      this.hashString(JSON.stringify(label))\n    ].join('-');\n  },\n\n  hasErrors: function () {\n    return !!(this.props.errors && this.props.errors.length);\n  },\n\n  cleanProps: function (props) {\n    const removedFields = [\n      'addItem',\n      'addToDeletedValues',\n      'addonAfter',\n      'addonBefore',\n      'afterComponent',\n      'allowedValues',\n      'arrayField',\n      'arrayFieldSchema',\n      'autoValue',\n      'beforeComponent',\n      'charsCount',\n      'charsRemaining',\n      'className',\n      'classes',\n      'clearField',\n      'clearFieldErrors',\n      'currentUser',\n      'currentValues',\n      'custom',\n      'deletedValues',\n      'description',\n      'document',\n      'errors',\n      'formComponents',\n      'formInput',\n      'formType',\n      'formatValue',\n      'getUrl',\n      'handleChange',\n      'hasErrors',\n      'help',\n      'hideClear',\n      'hideLabel',\n      'hideLink',\n      'inputClassName',\n      'inputProperties',\n      'inputProps',\n      'inputType',\n      'itemDataType',\n      'itemIndex',\n      'itemProperties',\n      'label',\n      'labelId',\n      'layout',\n      'maxCount',\n      'minCount',\n      'mustComplete',\n      'nestedArrayErrors',\n      'nestedSchema',\n      'networkId',\n      'optional',\n      'options',\n      'parentFieldName',\n      'prefilledProps',\n      'regEx',\n      'renderComponent',\n      'scrubValue',\n      'showCharsRemaining',\n      'showMenuIndicator',\n      'submitForm',\n      'throwError',\n      'updateCurrentValues',\n      'validateOnSubmit',\n      'validatePristine',\n      'visibleItemIndex',\n      'itemDatatype',\n      'limitToList',\n      'disableText',\n      'disableSelectOnBlur',\n      'showAllOptions',\n      'disableMatchParts',\n      'autoComplete',\n      'autoFocus',\n      'intlKeys',\n  ];\n\n    return _omit(props, removedFields);\n  },\n\n  cleanSwitchProps: function (props) {\n    const removedFields = [\n      'value',\n      'error',\n      'label',\n    ];\n\n    return _omit(props, removedFields);\n  },\n\n};\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/Checkbox.jsx",
    "content": "import React from 'react';\nimport FormSwitch from '../base-controls/FormSwitch';\nimport FormCheckbox from '../base-controls/FormCheckbox';\nimport { registerComponent } from 'meteor/vulcan:core';\n\nconst CheckboxComponent = ({ variant, refFunction, ...properties }) =>\n  variant === 'checkbox' ?\n    <FormCheckbox {...properties} ref={refFunction} /> :\n    <FormSwitch {...properties} ref={refFunction} />;\n\nregisterComponent('FormComponentCheckbox', CheckboxComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/CheckboxGroup.jsx",
    "content": "import React from 'react';\nimport FormCheckboxGroup from '../base-controls/FormCheckboxGroup';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nconst CheckboxGroupComponent = ({ refFunction, ...properties }) =>\n  <FormCheckboxGroup {...properties} ref={refFunction}/>;\n\n\nregisterComponent('FormComponentCheckboxGroup', CheckboxGroupComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/CountrySelect.jsx",
    "content": "import React from 'react';\nimport FormSuggest from '../base-controls/FormSuggest';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { countries } from './countries';\n\n\nconst CountrySelect = ({ refFunction, ...properties }) =>\n  <FormSuggest {...properties} ref={refFunction} options={countries} limitToList={true}/>;\n\n\nregisterComponent('CountrySelect', CountrySelect);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/Date.jsx",
    "content": "import React from 'react';\nimport FormPicker from '../base-controls/FormPicker';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\n\n\nexport const styles = theme => ({\n\n  '@global': {\n    'input[type=date]::-ms-clear, input[type=date]::-ms-reveal': {\n      display: 'none',\n      width: 0,\n      height: 0,\n    },\n    'input[type=date]::-webkit-search-cancel-button': {\n      display: 'none',\n      '-webkit-appearance': 'none',\n    },\n    'input[type=\"date\"]::-webkit-clear-button': {\n      display: 'none',\n      '-webkit-appearance': 'none',\n    },\n\n    'input[type=\"date\"]::-webkit-inner-spin-button,input[type=\"date\"]::-webkit-outer-spin-button': {\n      '-webkit-appearance': 'none',\n      margin: 0,\n    },\n  },\n\n});\n\nconst DateComponent = ({ refFunction, classes, ...properties }) =>\n  <FormPicker {...properties} {...classes} ref={refFunction}/>;\n\nregisterComponent('FormComponentDate', DateComponent, [withStyles, styles]);\nregisterComponent('FormComponentDate2', DateComponent, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/DateRdt.jsx",
    "content": "// Deprecated react-datetime version\n\nimport React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport DateTimePicker from 'react-datetime';\nimport { registerComponent } from 'meteor/vulcan:core';\n\nclass DateComponent extends PureComponent {\n  \n  constructor(props) {\n    super(props);\n    this.updateDate = this.updateDate.bind(this);\n  }\n\n  // when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)\n  // componentDidMount() {\n  //   if (this.props.value) {\n  //     this.updateDate(this.props.value);\n  //   }\n  // }\n\n  updateDate(date) {\n    this.context.updateCurrentValues({[this.props.path]: date});\n  }\n\n  render() {\n    const { value, label } = this.props.inputProperties;\n\n    const date = value ? (typeof value === 'string' ? new Date(value) : value) : null;\n\n    return (\n      <div className=\"form-group row\">\n        <label className=\"control-label col-sm-3\">{label}</label>\n        <div className=\"col-sm-9\">\n          <DateTimePicker\n            value={date}\n            timeFormat={false}\n            // newDate argument is a Moment object given by react-datetime\n            onChange={newDate => this.updateDate(newDate)}\n            inputProps={this.props.inputProperties}\n          />\n        </div>\n      </div>\n    );\n  }\n}\n\nDateComponent.propTypes = {\n  control: PropTypes.any,\n  datatype: PropTypes.any,\n  group: PropTypes.any,\n  inputProperties: PropTypes.shape({\n    label: PropTypes.string.isRequired,\n    value: PropTypes.any,\n  }),\n};\n\nDateComponent.contextTypes = {\n  updateCurrentValues: PropTypes.func,\n};\n\nexport default DateComponent;\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/DateTime.jsx",
    "content": "import React from 'react';\nimport FormInput from '../base-controls/FormInput';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\n\n\nexport const styles = theme => ({\n\n  '@global': {\n    'input[type=datetime]::-ms-clear, input[type=datetime]::-ms-reveal': {\n      display: 'none',\n      width: 0,\n      height: 0,\n    },\n    'input[type=datetime]::-webkit-search-cancel-button': {\n      display: 'none',\n      '-webkit-appearance': 'none',\n    },\n    'input[type=\"datetime\"]::-webkit-clear-button': {\n      display: 'none',\n      '-webkit-appearance': 'none',\n    },\n\n    'input[type=\"datetime\"]::-webkit-inner-spin-button,input[type=\"datetime\"]::-webkit-outer-spin-button': {\n      '-webkit-appearance': 'none',\n      margin: 0,\n    },\n  },\n\n});\n\n\nconst DateTimeComponent = ({ refFunction, classes, ...properties }) =>\n  <FormInput {...properties} ref={refFunction} type=\"datetime-local\"/>;\n\n\nregisterComponent('FormComponentDateTime', DateTimeComponent, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/DateTimeRdt.jsx",
    "content": "// Deprecated react-datetime version\n\nimport React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport DateTimePicker from 'react-datetime';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nclass DateTimeRdt extends PureComponent {\n  \n  constructor(props) {\n    super(props);\n    this.updateDate = this.updateDate.bind(this);\n  }\n\n  // when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)\n  componentDidMount() {\n    if (this.props.inputProperties.value) {\n      this.updateDate(this.props.inputProperties.value);\n    }\n  }\n\n  updateDate(date) {\n    this.context.updateCurrentValues({[this.props.inputProperties.name]: date});\n  }\n\n  render() {\n\n    const { value, label } = this.props.inputProperties;\n    const date = value ? (typeof value === 'string' ? new Date(value) : value) : null;\n    \n    return (\n      <div className=\"form-group row\">\n        <label className=\"control-label col-sm-3\">{label}</label>\n        <div className=\"col-sm-9\">\n          <DateTimePicker\n            value={date}\n            // newDate argument is a Moment object given by react-datetime\n            onChange={newDate => this.updateDate(newDate._d)}\n            format={'x'}\n            inputProps={this.props.inputProperties}\n          />\n        </div>\n      </div>\n    );\n  }\n}\n\nDateTimeRdt.propTypes = {\n  control: PropTypes.any,\n  datatype: PropTypes.any,\n  group: PropTypes.any,\n  inputProperties: PropTypes.shape({\n    label: PropTypes.string.isRequired,\n    value: PropTypes.any,\n    name: PropTypes.string.isRequired\n  }),\n};\n\nDateTimeRdt.contextTypes = {\n  updateCurrentValues: PropTypes.func,\n};\n\nexport default DateTimeRdt;\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/Default.jsx",
    "content": "import React from 'react';\nimport FormInput from '../base-controls/FormInput';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nconst Default = ({ refFunction, ...properties }) =>\n  <FormInput {...properties} ref={refFunction}/>;\n\n\nregisterComponent('FormComponentDefault', Default);\nregisterComponent('FormComponentText', Default);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/Email.jsx",
    "content": "import React from 'react';\nimport FormInput from '../base-controls/FormInput';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nexport const getUrl = function (value, props) {\n  if (!value) return value;\n  if (typeof value !== 'string') {\n    value = String(value);\n  }\n  if ('mailto:'.startsWith(value)) return 'mailto:';\n  return !value.startsWith('mailto:') ? 'mailto:' + value : value;\n};\n\n\nconst EmailComponent = ({ refFunction, ...properties }) =>\n  <FormInput {...properties} ref={refFunction} type=\"email\" getUrl={getUrl}/>;\n\n\nregisterComponent('FormComponentEmail', EmailComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/Number.jsx",
    "content": "import React from 'react';\nimport FormInput from '../base-controls/FormInput';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nexport const scrubNumberValue = function (value, props) {\n  if (!value) return value;\n  if (typeof value !== 'string') {\n    value = String(value);\n  }\n  // number should only contain digits and periods\n  value = value.replace(/[^0-9.]/g, '');\n  // number should not start with a period\n  if (value.startsWith('.')) {\n    value = `0${value}`;\n  }\n  // number should not contain more than one period\n  const parts = value.split('.');\n  if (parts.length > 1) {\n    parts[0] = `${parts[0]}.`;\n    value = parts.join('');\n  }\n  return value;\n};\n\nconst NumberComponent = ({ refFunction, ...properties }) =>\n  <FormInput {...properties} ref={refFunction} scrubValue={scrubNumberValue} type=\"number\" />;\n\n\nregisterComponent('FormComponentNumber', NumberComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/Password.jsx",
    "content": "import React from 'react';\nimport FormInput from '../base-controls/FormInput';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nconst Password = ({ refFunction, ...properties }) =>\n  <FormInput {...properties} ref={refFunction} type='password'/>;\n\n\nregisterComponent('FormComponentPassword', Password);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/PostalCode.jsx",
    "content": "import React from 'react';\nimport FormInput from '../base-controls/FormInput';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { getCountryInfo } from './RegionSelect';\n\n\nconst PostalCode = ({ classes, refFunction,  ...properties }) => {\n  const currentCountryInfo = getCountryInfo(properties);\n  const postalLabel = currentCountryInfo ? currentCountryInfo.postalLabel : 'Postal code';\n\n  return <FormInput {...properties} ref={refFunction} label={postalLabel}/>;\n};\n\n\nregisterComponent('PostalCode', PostalCode);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/RadioGroup.jsx",
    "content": "import React from 'react';\nimport FormRadioGroup from '../base-controls/FormRadioGroup';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nconst RadioGroupComponent = ({ refFunction, ...properties }) => {\n  return <FormRadioGroup {...properties} ref={refFunction}/>;\n};\n\n\nregisterComponent('FormComponentRadioGroup', RadioGroupComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/RegionSelect.jsx",
    "content": "import React from 'react';\nimport FormSuggest from '../base-controls/FormSuggest';\nimport FormInput from '../base-controls/FormInput';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { countryInfo } from './countries';\nimport _get from 'lodash/get';\n\n\nexport const getCountryInfo = function (formComponentProps) {\n  const addressPath = formComponentProps.path;\n  const countryParts = addressPath.split('.');\n  countryParts[countryParts.length-1] = 'country';\n  const country = _get(formComponentProps.document, countryParts);\n  return country && countryInfo[country];\n};\n\n\nconst RegionSelect = ({ classes, refFunction, ...properties }) => {\n  const currentCountryInfo = getCountryInfo(properties);\n  const options = currentCountryInfo ? currentCountryInfo.regions : null;\n  const regionLabel = currentCountryInfo ? currentCountryInfo.regionLabel : 'Region';\n\n  if (options) {\n    return <FormSuggest {...properties} ref={refFunction} options={options} label={regionLabel}/>;\n  } else {\n    return <FormInput {...properties} ref={refFunction} label={regionLabel}/>;\n  }\n};\n\n\nregisterComponent('RegionSelect', RegionSelect);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/Select.jsx",
    "content": "import React from 'react';\nimport FormSelect from '../base-controls/FormSelect';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nconst SelectComponent = ({ refFunction, ...properties }) => {\n  return <FormSelect {...properties} ref={refFunction}/>;\n};\n\n\nregisterComponent('FormComponentSelect', SelectComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/SelectMultiple.jsx",
    "content": "import React from 'react';\nimport FormSelect from '../base-controls/FormSelect';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nconst SelectMultiple = ({ refFunction, ...properties }) => {\n  return <FormSelect {...properties} multiple={true} ref={refFunction}/>;\n};\n\n\nregisterComponent('FormComponentSelectMultiple', SelectMultiple);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/StaticText.jsx",
    "content": "import React from 'react';\nimport FormText from '../base-controls/FormText';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nconst StaticText = ({ refFunction, ...properties }) =>\n  <FormText {...properties} ref={refFunction}/>;\n\n\nregisterComponent('FormComponentStaticText', StaticText);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/Textarea.jsx",
    "content": "import React from 'react';\nimport FormInput from '../base-controls/FormInput';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nconst TextareaComponent = ({ refFunction, inputProperties, ...properties }) =>\n  <FormInput {...properties}\n            ref={refFunction}\n            multiline={true}\n            inputProperties={inputProperties}\n            rows={inputProperties.rows ? inputProperties.rows : 2}\n            rowsMax={10}\n  />;\n\n\nregisterComponent('FormComponentTextarea', TextareaComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/Time.jsx",
    "content": "import React from 'react';\nimport FormInput from '../base-controls/FormInput';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\n\n\nexport const styles = theme => ({\n\n  '@global': {\n    'input[type=time]::-ms-clear, input[type=time]::-ms-reveal': {\n      display: 'none',\n      width: 0,\n      height: 0,\n    },\n    'input[type=time]::-webkit-search-cancel-button': {\n      display: 'none',\n      '-webkit-appearance': 'none',\n    },\n    'input[type=\"time\"]::-webkit-clear-button': {\n      display: 'none',\n      '-webkit-appearance': 'none',\n    },\n\n    'input[type=\"time\"]::-webkit-inner-spin-button,input[type=\"time\"]::-webkit-outer-spin-button': {\n      '-webkit-appearance': 'none',\n      margin: 0,\n    },\n  },\n\n});\n\n\nconst TimeComponent = ({ refFunction, classes, ...properties }) =>\n  <FormInput {...properties} ref={refFunction} type=\"time\"/>;\n\n\nregisterComponent('FormComponentTime', TimeComponent, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/TimeRdt.jsx",
    "content": "// Deprecated react-datetime version\n\nimport React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport DateTimePicker from 'react-datetime';\nimport { registerComponent } from 'meteor/vulcan:core';\n\nclass TimeRdt extends PureComponent {\n  \n  constructor(props) {\n    super(props);\n    this.updateDate = this.updateDate.bind(this);\n  }\n\n  // when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)\n  // componentDidMount() {\n  //   if (this.props.value) {\n  //     this.context.updateCurrentValues({[this.props.path]: this.props.value});\n  //   }\n  // }\n\n  updateDate(mDate) {\n    // if this is a properly formatted moment date, update time\n    if (typeof mDate === 'object') {\n      this.context.updateCurrentValues({[this.props.path]: mDate.format('HH:mm')});\n    }\n  }\n\n  render() {\n\n    const date = new Date();\n\n    // transform time string into date object to work inside datetimepicker\n    const time = this.props.inputProperties.value;\n    if (time) {\n      date.setHours(parseInt(time.substr(0,2)), parseInt(time.substr(3,5)));\n    } else {\n      date.setHours(0,0);\n    }\n\n    return (\n      <div className=\"form-group row\">\n        <label className=\"control-label col-sm-3\">{this.props.inputProperties.label}</label>\n        <div className=\"col-sm-9\">\n          <DateTimePicker\n            value={date}\n            viewMode=\"time\"\n            dateFormat={false}\n            timeFormat=\"HH:mm\"\n            // newDate argument is a Moment object given by react-datetime\n            onChange={newDate => this.updateDate(newDate)}\n            inputProps={this.props.inputProperties}\n          />\n        </div>\n      </div>\n    );\n  }\n}\n\nTimeRdt.propTypes = {\n  control: PropTypes.any,\n  datatype: PropTypes.any,\n  group: PropTypes.any,\n  inputProperties: PropTypes.shape({\n    label: PropTypes.string.isRequired,\n    name: PropTypes.string.isRequired,\n    value: PropTypes.any,\n  }),\n};\n\nTimeRdt.contextTypes = {\n  updateCurrentValues: PropTypes.func,\n};\n\nexport default TimeRdt;\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/Url.jsx",
    "content": "import React from 'react';\nimport FormInput from '../base-controls/FormInput';\nimport { registerComponent } from 'meteor/vulcan:core';\n\n\nexport const scrubValue = function (value, props) {\n  if (!value) return value;\n  if (typeof value !== 'string') {\n    value = String(value);\n  }\n  value = value.trim();\n  if ('https://'.startsWith(value)) return 'https://';\n  if ('http://'.startsWith(value)) return 'http://';\n  return !value.startsWith('http://') && !value.startsWith('https://') ? 'https://' + value : value;\n};\n\n\nconst UrlComponent = ({ refFunction, ...properties }) =>\n  <FormInput {...properties} ref={refFunction} scrubValue={scrubValue} type=\"url\"/>;\n\n\nregisterComponent('FormComponentUrl', UrlComponent);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/forms/controls/countries.js",
    "content": "export const countries = [\n  { value: 'AF', label: 'Afghanistan' },\n  { value: 'AX', label: 'Åland Islands' },\n  { value: 'AL', label: 'Albania' },\n  { value: 'DZ', label: 'Algeria' },\n  { value: 'AS', label: 'American Samoa' },\n  { value: 'AD', label: 'Andorra' },\n  { value: 'AO', label: 'Angola' },\n  { value: 'AI', label: 'Anguilla' },\n  { value: 'AQ', label: 'Antarctica' },\n  { value: 'AG', label: 'Antigua and Barbuda' },\n  { value: 'AR', label: 'Argentina' },\n  { value: 'AM', label: 'Armenia' },\n  { value: 'AW', label: 'Aruba' },\n  { value: 'AU', label: 'Australia' },\n  { value: 'AT', label: 'Austria' },\n  { value: 'AZ', label: 'Azerbaijan' },\n  { value: 'BS', label: 'Bahamas' },\n  { value: 'BH', label: 'Bahrain' },\n  { value: 'BD', label: 'Bangladesh' },\n  { value: 'BB', label: 'Barbados' },\n  { value: 'BY', label: 'Belarus' },\n  { value: 'BE', label: 'Belgium' },\n  { value: 'BZ', label: 'Belize' },\n  { value: 'BJ', label: 'Benin' },\n  { value: 'BM', label: 'Bermuda' },\n  { value: 'BT', label: 'Bhutan' },\n  { value: 'BO', label: 'Bolivia' },\n  { value: 'BQ', label: 'Bonaire, Sint Eustatius, Saba' },\n  { value: 'BA', label: 'Bosnia and Herzegovina' },\n  { value: 'BW', label: 'Botswana' },\n  { value: 'BV', label: 'Bouvet Island' },\n  { value: 'BR', label: 'Brazil' },\n  { value: 'IO', label: 'British Indian Ocean Territory' },\n  { value: 'BN', label: 'Brunei Darussalam' },\n  { value: 'BG', label: 'Bulgaria' },\n  { value: 'BF', label: 'Burkina Faso' },\n  { value: 'BI', label: 'Burundi' },\n  { value: 'CV', label: 'Cabo Verde' },\n  { value: 'KH', label: 'Cambodia' },\n  { value: 'CM', label: 'Cameroon' },\n  { value: 'CA', label: 'Canada' },\n  { value: 'KY', label: 'Cayman Islands' },\n  { value: 'CF', label: 'Central African Republic' },\n  { value: 'TD', label: 'Chad' },\n  { value: 'CL', label: 'Chile' },\n  { value: 'CN', label: 'China' },\n  { value: 'CX', label: 'Christmas Island' },\n  { value: 'CC', label: 'Cocos (Keeling) Islands' },\n  { value: 'CO', label: 'Colombia' },\n  { value: 'KM', label: 'Comoros' },\n  { value: 'CG', label: 'Congo' },\n  { value: 'CD', label: 'Congo (Democratic Republic of the)' },\n  { value: 'CK', label: 'Cook Islands' },\n  { value: 'CR', label: 'Costa Rica' },\n  { value: 'CI', label: 'Côte d’Ivoire' },\n  { value: 'HR', label: 'Croatia' },\n  { value: 'CU', label: 'Cuba' },\n  { value: 'CW', label: 'Curaçao' },\n  { value: 'CY', label: 'Cyprus' },\n  { value: 'CZ', label: 'Czechia' },\n  { value: 'DK', label: 'Denmark' },\n  { value: 'DJ', label: 'Djibouti' },\n  { value: 'DM', label: 'Dominica' },\n  { value: 'DO', label: 'Dominican Republic' },\n  { value: 'EC', label: 'Ecuador' },\n  { value: 'EG', label: 'Egypt' },\n  { value: 'SV', label: 'El Salvador' },\n  { value: 'GQ', label: 'Equatorial Guinea' },\n  { value: 'ER', label: 'Eritrea' },\n  { value: 'EE', label: 'Estonia' },\n  { value: 'ET', label: 'Ethiopia' },\n  { value: 'FK', label: 'Falkland Islands' },\n  { value: 'FO', label: 'Faroe Islands' },\n  { value: 'FJ', label: 'Fiji' },\n  { value: 'FI', label: 'Finland' },\n  { value: 'FR', label: 'France' },\n  { value: 'GF', label: 'French Guiana' },\n  { value: 'PF', label: 'French Polynesia' },\n  { value: 'TF', label: 'French Southern Territories' },\n  { value: 'GA', label: 'Gabon' },\n  { value: 'GM', label: 'Gambia' },\n  { value: 'GE', label: 'Georgia' },\n  { value: 'DE', label: 'Germany' },\n  { value: 'GH', label: 'Ghana' },\n  { value: 'GI', label: 'Gibraltar' },\n  { value: 'GR', label: 'Greece' },\n  { value: 'GL', label: 'Greenland' },\n  { value: 'GD', label: 'Grenada' },\n  { value: 'GP', label: 'Guadeloupe' },\n  { value: 'GU', label: 'Guam' },\n  { value: 'GT', label: 'Guatemala' },\n  { value: 'GG', label: 'Guernsey' },\n  { value: 'GN', label: 'Guinea' },\n  { value: 'GW', label: 'Guinea-Bissau' },\n  { value: 'GY', label: 'Guyana' },\n  { value: 'HT', label: 'Haiti' },\n  { value: 'HM', label: 'Heard Island, Mcdonald Islands' },\n  { value: 'VA', label: 'Vatican City State' },\n  { value: 'HN', label: 'Honduras' },\n  { value: 'HK', label: 'Hong Kong' },\n  { value: 'HU', label: 'Hungary' },\n  { value: 'IS', label: 'Iceland' },\n  { value: 'IN', label: 'India' },\n  { value: 'ID', label: 'Indonesia' },\n  { value: 'IR', label: 'Iran' },\n  { value: 'IQ', label: 'Iraq' },\n  { value: 'IE', label: 'Ireland' },\n  { value: 'IM', label: 'Isle of Man' },\n  { value: 'IL', label: 'Israel' },\n  { value: 'IT', label: 'Italy' },\n  { value: 'JM', label: 'Jamaica' },\n  { value: 'JP', label: 'Japan' },\n  { value: 'JE', label: 'Jersey' },\n  { value: 'JO', label: 'Jordan' },\n  { value: 'KZ', label: 'Kazakhstan' },\n  { value: 'KE', label: 'Kenya' },\n  { value: 'KI', label: 'Kiribati' },\n  { value: 'KW', label: 'Kuwait' },\n  { value: 'KG', label: 'Kyrgyzstan' },\n  { value: 'LA', label: 'Lao' },\n  { value: 'LV', label: 'Latvia' },\n  { value: 'LB', label: 'Lebanon' },\n  { value: 'LS', label: 'Lesotho' },\n  { value: 'LR', label: 'Liberia' },\n  { value: 'LY', label: 'Libya' },\n  { value: 'LI', label: 'Liechtenstein' },\n  { value: 'LT', label: 'Lithuania' },\n  { value: 'LU', label: 'Luxembourg' },\n  { value: 'MO', label: 'Macao' },\n  { value: 'MK', label: 'Macedonia' },\n  { value: 'MG', label: 'Madagascar' },\n  { value: 'MW', label: 'Malawi' },\n  { value: 'MY', label: 'Malaysia' },\n  { value: 'MV', label: 'Maldives' },\n  { value: 'ML', label: 'Mali' },\n  { value: 'MT', label: 'Malta' },\n  { value: 'MH', label: 'Marshall Islands' },\n  { value: 'MQ', label: 'Martinique' },\n  { value: 'MR', label: 'Mauritania' },\n  { value: 'MU', label: 'Mauritius' },\n  { value: 'YT', label: 'Mayotte' },\n  { value: 'MX', label: 'Mexico' },\n  { value: 'FM', label: 'Micronesia' },\n  { value: 'MD', label: 'Moldova' },\n  { value: 'MC', label: 'Monaco' },\n  { value: 'MN', label: 'Mongolia' },\n  { value: 'ME', label: 'Montenegro' },\n  { value: 'MS', label: 'Montserrat' },\n  { value: 'MA', label: 'Morocco' },\n  { value: 'MZ', label: 'Mozambique' },\n  { value: 'MM', label: 'Myanmar' },\n  { value: 'NA', label: 'Namibia' },\n  { value: 'NR', label: 'Nauru' },\n  { value: 'NP', label: 'Nepal' },\n  { value: 'NL', label: 'Netherlands' },\n  { value: 'NC', label: 'New Caledonia' },\n  { value: 'NZ', label: 'New Zealand' },\n  { value: 'NI', label: 'Nicaragua' },\n  { value: 'NE', label: 'Niger' },\n  { value: 'NG', label: 'Nigeria' },\n  { value: 'NU', label: 'Niue' },\n  { value: 'NF', label: 'Norfolk Island' },\n  { value: 'KP', label: 'North Korea' },\n  { value: 'MP', label: 'Northern Mariana Islands' },\n  { value: 'NO', label: 'Norway' },\n  { value: 'OM', label: 'Oman' },\n  { value: 'PK', label: 'Pakistan' },\n  { value: 'PW', label: 'Palau' },\n  { value: 'PS', label: 'Palestine' },\n  { value: 'PA', label: 'Panama' },\n  { value: 'PG', label: 'Papua New Guinea' },\n  { value: 'PY', label: 'Paraguay' },\n  { value: 'PE', label: 'Peru' },\n  { value: 'PH', label: 'Philippines' },\n  { value: 'PN', label: 'Pitcairn' },\n  { value: 'PL', label: 'Poland' },\n  { value: 'PT', label: 'Portugal' },\n  { value: 'PR', label: 'Puerto Rico' },\n  { value: 'QA', label: 'Qatar' },\n  { value: 'RE', label: 'Réunion' },\n  { value: 'RO', label: 'Romania' },\n  { value: 'RU', label: 'Russian Federation' },\n  { value: 'RW', label: 'Rwanda' },\n  { value: 'BL', label: 'Saint Barthélemy' },\n  { value: 'SH', label: 'Saint Helena, Ascension, Tristan Da Cunha' },\n  { value: 'KN', label: 'Saint Kitts and Nevis' },\n  { value: 'LC', label: 'Saint Lucia' },\n  { value: 'MF', label: 'Saint Martin (French Portion)' },\n  { value: 'PM', label: 'Saint Pierre and Miquelon' },\n  { value: 'VC', label: 'Saint Vincent and the Grenadines' },\n  { value: 'WS', label: 'Samoa' },\n  { value: 'SM', label: 'San Marino' },\n  { value: 'ST', label: 'Sao Tome and Principe' },\n  { value: 'SA', label: 'Saudi Arabia' },\n  { value: 'SN', label: 'Senegal' },\n  { value: 'RS', label: 'Serbia' },\n  { value: 'SC', label: 'Seychelles' },\n  { value: 'SL', label: 'Sierra Leone' },\n  { value: 'SG', label: 'Singapore' },\n  { value: 'SX', label: 'Sint Maarten (Dutch part)' },\n  { value: 'SK', label: 'Slovakia' },\n  { value: 'SI', label: 'Slovenia' },\n  { value: 'SB', label: 'Solomon Islands' },\n  { value: 'SO', label: 'Somalia' },\n  { value: 'ZA', label: 'South Africa' },\n  { value: 'GS', label: 'South Georgia, South Sandwich Islands' },\n  { value: 'KR', label: 'South Korea' },\n  { value: 'SS', label: 'South Sudan' },\n  { value: 'ES', label: 'Spain' },\n  { value: 'LK', label: 'Sri Lanka' },\n  { value: 'SD', label: 'Sudan' },\n  { value: 'SR', label: 'Suriname' },\n  { value: 'SJ', label: 'Svalbard and Jan Mayen' },\n  { value: 'SZ', label: 'Swaziland' },\n  { value: 'SE', label: 'Sweden' },\n  { value: 'CH', label: 'Switzerland' },\n  { value: 'SY', label: 'Syria' },\n  { value: 'TW', label: 'Taiwan' },\n  { value: 'TJ', label: 'Tajikistan' },\n  { value: 'TZ', label: 'Tanzania' },\n  { value: 'TH', label: 'Thailand' },\n  { value: 'TL', label: 'Timor-Leste' },\n  { value: 'TG', label: 'Togo' },\n  { value: 'TK', label: 'Tokelau' },\n  { value: 'TO', label: 'Tonga' },\n  { value: 'TT', label: 'Trinidad and Tobago' },\n  { value: 'TN', label: 'Tunisia' },\n  { value: 'TR', label: 'Turkey' },\n  { value: 'TM', label: 'Turkmenistan' },\n  { value: 'TC', label: 'Turks and Caicos Islands' },\n  { value: 'TV', label: 'Tuvalu' },\n  { value: 'UG', label: 'Uganda' },\n  { value: 'UA', label: 'Ukraine' },\n  { value: 'AE', label: 'United Arab Emirates' },\n  { value: 'GB', label: 'United Kingdom' },\n  { value: 'US', label: 'United States' },\n  { value: 'UM', label: 'United States Minor Outlying Islands' },\n  { value: 'UY', label: 'Uruguay' },\n  { value: 'UZ', label: 'Uzbekistan' },\n  { value: 'VU', label: 'Vanuatu' },\n  { value: 'VE', label: 'Venezuela' },\n  { value: 'VN', label: 'Viet Nam' },\n  { value: 'VG', label: 'Virgin Islands, British' },\n  { value: 'VI', label: 'Virgin Islands, U.S.' },\n  { value: 'WF', label: 'Wallis and Futuna' },\n  { value: 'EH', label: 'Western Sahara' },\n  { value: 'YE', label: 'Yemen' },\n  { value: 'ZM', label: 'Zambia' },\n  { value: 'ZW', label: 'Zimbabwe' },\n];\n\n\nexport const countryInfo = {\n  US: {\n    regionLabel: 'State',\n    postalLabel: 'Zip code',\n    regions: [\n      { value: 'AL', label: 'Alabama' },\n      { value: 'AK', label: 'Alaska' },\n      { value: 'AZ', label: 'Arizona' },\n      { value: 'AR', label: 'Arkansas' },\n      { value: 'CA', label: 'California' },\n      { value: 'CO', label: 'Colorado' },\n      { value: 'CT', label: 'Connecticut' },\n      { value: 'DE', label: 'Delaware' },\n      { value: 'FL', label: 'Florida' },\n      { value: 'GA', label: 'Georgia' },\n      { value: 'HI', label: 'Hawaii' },\n      { value: 'ID', label: 'Idaho' },\n      { value: 'IL', label: 'Illinois' },\n      { value: 'IN', label: 'Indiana' },\n      { value: 'IA', label: 'Iowa' },\n      { value: 'KS', label: 'Kansas' },\n      { value: 'KY', label: 'Kentucky' },\n      { value: 'LA', label: 'Louisiana' },\n      { value: 'ME', label: 'Maine' },\n      { value: 'MD', label: 'Maryland' },\n      { value: 'MA', label: 'Massachusetts' },\n      { value: 'MI', label: 'Michigan' },\n      { value: 'MN', label: 'Minnesota' },\n      { value: 'MS', label: 'Mississippi' },\n      { value: 'MO', label: 'Missouri' },\n      { value: 'MT', label: 'Montana' },\n      { value: 'NE', label: 'Nebraska' },\n      { value: 'NV', label: 'Nevada' },\n      { value: 'NH', label: 'New Hampshire' },\n      { value: 'NJ', label: 'New Jersey' },\n      { value: 'NM', label: 'New Mexico' },\n      { value: 'NY', label: 'New York' },\n      { value: 'NC', label: 'North Carolina' },\n      { value: 'ND', label: 'North Dakota' },\n      { value: 'OH', label: 'Ohio' },\n      { value: 'OK', label: 'Oklahoma' },\n      { value: 'OR', label: 'Oregon' },\n      { value: 'PA', label: 'Pennsylvania' },\n      { value: 'RI', label: 'Rhode Island' },\n      { value: 'SC', label: 'South Carolina' },\n      { value: 'SD', label: 'South Dakota' },\n      { value: 'TN', label: 'Tennessee' },\n      { value: 'TX', label: 'Texas' },\n      { value: 'UT', label: 'Utah' },\n      { value: 'VT', label: 'Vermont' },\n      { value: 'VA', label: 'Virginia' },\n      { value: 'WA', label: 'Washington' },\n      { value: 'WV', label: 'West Virginia' },\n      { value: 'WI', label: 'Wisconsin' },\n      { value: 'WY', label: 'Wyoming' },\n    ],\n  },\n  CA: {\n    regionLabel: 'Province',\n    postalLabel: 'Postal code',\n    regions: [\n      { value: 'AB', label: 'Alberta' },\n      { value: 'BC', label: 'British Columbia' },\n      { value: 'MB', label: 'Manitoba' },\n      { value: 'NB', label: 'New Brunswick' },\n      { value: 'NL', label: 'Newfoundland and Labrador' },\n      { value: 'NS', label: 'Nova Scotia' },\n      { value: 'NT', label: 'Northwest Territories' },\n      { value: 'NU', label: 'Nunavut' },\n      { value: 'ON', label: 'Ontario' },\n      { value: 'PE', label: 'Prince Edward Island' },\n      { value: 'QC', label: 'Quebec' },\n      { value: 'SK', label: 'Saskatchewan' },\n      { value: 'YT', label: 'Yukon' },\n    ],\n  },\n  AU: {\n    regionLabel: 'State',\n    postalLabel: 'Postcode',\n    regions: [\n      { value: 'ACT', label: 'Australian Capital Territory' },\n      { value: 'NSW', label: 'New South Wales' },\n      { value: 'NT', label: 'Northern Territory' },\n      { value: 'QLD', label: 'Queensland' },\n      { value: 'SA', label: 'South Australia' },\n      { value: 'TAS', label: 'Tasmania' },\n      { value: 'VIC', label: 'Victoria' },\n      { value: 'WA', label: 'Western Australia' },\n    ],\n  },\n  UK: {\n    regionLabel: 'County',\n    postalLabel: 'Postcode',\n  },\n};\n\n\nexport const getCountryLabel = (countryValue) => {\n  const country = countries.find(country => country.value === countryValue);\n  return country ? country.label : '';\n};\n\n\nexport const getCountryCode = (countryName) => {\n  const country = countries.find(country => country.label === countryName);\n  return country ? country.value : '';\n};\n\n\nexport const getCountryContinent = (countryValue) => {\n  const country = countries.find(country => country.value === countryValue);\n  return country ? country.continent : '';\n};\n\n\nexport const getRegionLabel = (countryValue, regionValue) => {\n  if (!countryInfo[countryValue] || !countryInfo[countryValue].regions) {\n    return regionValue;\n  }\n\n  const regions = countryInfo[countryValue].regions;\n\n  let region = regions.find(nextRegion => nextRegion.value === regionValue);\n\n  if (region) {\n    return region.label;\n  } else {\n    return regionValue;\n  }\n};\n\n\n// Given a region value or label, returns the region value (QC or Quebec => QC)\n// or false if the regionValue is invalid\nexport const validateRegion = (countryValue, regionValue) => {\n  if (!countryInfo[countryValue] || !countryInfo[countryValue].regions) {\n    return regionValue;\n  }\n\n  const regions = countryInfo[countryValue].regions;\n\n  let region = regions.find(nextRegion => nextRegion.value === regionValue);\n\n  if (region) {\n    return regionValue;\n  }\n\n  region = regions.find(nextRegion => nextRegion.label === regionValue);\n\n  if (region) {\n    return region.value;\n  } else {\n    return false;\n  }\n};\n\n\nexport const getRegionCode = (countryValue, regionValue) => {\n  const regionCode = validateRegion(countryValue, regionValue);\n  return regionCode || regionValue;\n};\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/index.js",
    "content": "import './accounts/AccountsButton';\nimport './accounts/AccountsButtons';\nimport './accounts/AccountsField';\nimport './accounts/AccountsFields';\nimport './accounts/AccountsForm';\nimport './accounts/AccountsPasswordOrService';\nimport './accounts/AccountsSocialButtons';\n\nimport './bonus/DatatableFromArray';\nimport './bonus/LoadMore';\nimport './bonus/SearchInput';\nimport './bonus/TooltipButton';\nimport './bonus/TooltipIconButton';\nimport './bonus/TooltipIntl';\n\nimport './core/Avatar';\nimport './core/Card';\nimport './core/Datatable';\nimport './core/EditButton';\nimport './core/Flash';\nimport './core/Loading';\nimport './core/NewButton';\n\nimport './forms/base-controls/RequiredIndicator';\nimport './forms/base-controls/FormControlLayout';\n\nimport './forms/FormComponentInner';\nimport './forms/FormErrors';\nimport './forms/FormGroupDefault';\nimport './forms/FormGroupLine';\nimport './forms/FormGroupNone';\nimport './forms/FormNestedArrayLayout';\nimport './forms/FormNestedDivider';\nimport './forms/FormSubmit';\n\nimport './forms/controls/Checkbox';\nimport './forms/controls/CheckboxGroup';\nimport './forms/controls/CountrySelect';\nimport './forms/controls/Date';\nimport './forms/controls/DateRdt';\nimport './forms/controls/DateTime';\nimport './forms/controls/DateTimeRdt';\nimport './forms/controls/Default';\nimport './forms/controls/Password';\nexport * from './forms/controls/Email';\nimport './forms/controls/Number';\nimport './forms/controls/PostalCode';\nimport './forms/controls/RadioGroup';\nimport './forms/controls/RegionSelect';\nimport './forms/controls/Select';\nimport './forms/controls/SelectMultiple';\nimport './forms/controls/StaticText';\nimport './forms/controls/Textarea';\nimport './forms/controls/Time';\nimport './forms/controls/TimeRdt';\nexport * from './forms/controls/Url';\n\nimport './theme/ThemeStyles';\nimport './theme/ThemeProvider';\n\nimport './ui/Alert';\nimport './ui/Button';\nimport './ui/Modal';\nimport './ui/ModalTrigger';\nimport './ui/Table';\nimport './ui/VerticalNavigation';\n\nimport './upload/UploadImage';\nimport './upload/UploadInner';\n\nimport './backoffice/BackofficeNavbar';\nimport './backoffice/BackofficePageLayout';\nimport './backoffice/BackofficeVerticalMenuLayout';\n\nexport * from './forms/controls/countries';\n\nimport { dynamicLoader, registerComponent } from 'meteor/vulcan:lib';\n\nregisterComponent('KeyEventHandler', dynamicLoader(() => import('./bonus/KeyEventHandler'), true));\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/theme/JssCleanup.jsx",
    "content": "import React, { PureComponent } from 'react';\n\n\nclass JssCleanup extends PureComponent {\n\n\n  // Remove the server-side injected CSS.\n  componentDidMount() {\n    if (!document || !document.getElementById) return;\n    \n    const jssStyles = document.getElementById('jss-server-side');\n    if (jssStyles && jssStyles.parentNode) {\n//      jssStyles.parentNode.removeChild(jssStyles);\n    }\n  }\n  \n  render() {\n    return this.props.children;\n  }\n}\n\n\nexport default JssCleanup;\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/theme/ThemeProvider.jsx",
    "content": "import React from 'react';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { getCurrentTheme } from '../../modules/';\nimport { ThemeProvider } from '@material-ui/core/styles';\nimport JssCleanup from './JssCleanup';\n\nconst AppThemeProvider = ({ children }) => {\n  const theme = getCurrentTheme();\n  return (\n    <ThemeProvider theme={theme}>\n      <JssCleanup>{children}</JssCleanup>\n    </ThemeProvider>\n  );\n};\n\nregisterComponent('ThemeProvider', AppThemeProvider);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/theme/ThemeStyles.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { withTheme } from '@material-ui/core/styles';\nimport { withStyles } from '@material-ui/core/styles';\nimport Typography from '@material-ui/core/Typography';\nimport Button from '@material-ui/core/Button';\nimport Grid from '@material-ui/core/Grid';\nimport Paper from '@material-ui/core/Paper';\nimport Divider from '@material-ui/core/Divider';\nimport { getContrastRatio } from '@material-ui/core/styles';\nimport classNames from 'classnames';\n\n\nconst describeTypography = (theme, className) => {\n  const typography = className ? theme.typography[className] : theme.typography;\n  const fontFamily = typography.fontFamily.split(',')[0];\n  const fontSize = `${typography.fontSize}${typeof typography.fontSize === 'number' ? 'px' : ''}`;\n  return `${fontFamily} ${typography.fontWeight} ${fontSize}`;\n};\n\nfunction getColorBlock(theme, classes, colorName, colorValue, colorTitle) {\n  const bgColor = theme.palette[colorName][colorValue] || theme.palette.common.midBlack;\n  if (typeof bgColor !== 'string' || !bgColor.match(/^(#|rgb|rgba|hsl|hsla)/)) return null;\n\n  let fgColor = theme.palette.common.black;\n  if (getContrastRatio(bgColor, fgColor) < 7) {\n    fgColor = theme.palette.common.white;\n  }\n\n  let blockTitle;\n  if (colorTitle) {\n    blockTitle = <div className={classes.name}>{colorName}</div>;\n  }\n\n  let rowStyle = {\n    backgroundColor: bgColor,\n    color: fgColor,\n    listStyle: 'none',\n    padding: 15,\n  };\n\n  return (\n    <div key={colorName + colorValue}>\n      {\n        colorValue.toString().match(/^(A100|light|contrastText)$/) &&\n        <div className={classes.blockSpace}/>\n      }\n      <li style={rowStyle} key={colorValue}>\n        {blockTitle}\n        <div className={classes.colorContainer}>\n          <span>{colorValue}</span>\n          <span className={classes.colorValue}>{bgColor.toUpperCase()}</span>\n        </div>\n      </li>\n    </div>\n  );\n}\n\nfunction getColorGroup(options) {\n  const { theme, classes, color } = options;\n  if (typeof theme.palette[color] !== 'object') return null;\n\n  const cssColor = color.replace(' ', '').replace(color.charAt(0), color.charAt(0).toLowerCase());\n  let colorsList = [];\n  colorsList =\n    Object.keys(theme.palette[cssColor]).map(mainValue => getColorBlock(theme, classes, cssColor, mainValue));\n\n  return (\n    <ul className={classes.colorGroup} key={cssColor}>\n      {getColorBlock(theme, classes, cssColor, 500, true)}\n      <div className={classes.blockSpace}/>\n      {colorsList}\n    </ul>\n  );\n}\n\nconst styles = theme => ({\n  root: {\n    '& button + button': {\n      marginLeft: theme.spacing(2),\n    }\n  },\n  paper: {\n    padding: theme.spacing(3),\n    marginTop: theme.spacing(3),\n    marginBottom: theme.spacing(3),\n  },\n  name: {\n    marginBottom: 60,\n  },\n  blockSpace: {\n    height: 4,\n    backgroundColor: theme.palette.background.default,\n  },\n  divider: {\n    marginTop: theme.spacing(2),\n    marginBottom: theme.spacing(2),\n  },\n  colorContainer: {\n    display: 'flex',\n    justifyContent: 'space-between',\n    alignItems: 'center',\n  },\n  colorGroup: {\n    backgroundColor: '#888',\n    padding: 0,\n    margin: theme.spacing(0, 2, 2, 0),\n    flexGrow: 1,\n    [theme.breakpoints.up('sm')]: {\n      flexGrow: 0,\n    },\n  },\n  colorValue: {\n    ...theme.typography.caption,\n    color: 'inherit',\n  },\n});\n\nconst specialPalettes = ['common', 'text', 'action', 'grey'];\n\nconst latin =\n  'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur justo quam, ' +\n  'pellentesque ultrices ex a, aliquet porttitor ante. Donec tellus arcu, viverra ut lorem id, ' +\n  'ultrices ultricies enim. Donec enim metus, sollicitudin id lobortis id, iaculis ut arcu. ' +\n  'Maecenas sollicitudin congue nisi. Donec convallis, ipsum ac ultricies dignissim, orci ex ' +\n  'efficitur lectus, ac lacinia risus nunc at diam. Nam gravida bibendum lectus. Donec ' +\n  'scelerisque sem nec urna vestibulum vehicula.';\n\nconst ThemeStyles = ({ theme, classes }) => {\n  return (\n    <Grid container className={classNames('theme-styles', classes.root)}>\n\n      <Grid item xs={6}>\n        <Typography variant=\"h1\">h1: {describeTypography(theme, 'h1')}</Typography>\n        <Typography variant=\"h2\">h2: {describeTypography(theme, 'h2')}</Typography>\n        <Typography variant=\"h3\">h3: {describeTypography(theme, 'h3')}</Typography>\n        <Typography variant=\"h4\">h4: {describeTypography(theme, 'h4')}</Typography>\n      </Grid>\n\n      <Grid item xs={12}>\n        <Paper className={classes.paper}>\n\n          <Grid container>\n\n            <Grid item xs={12} lg={6}>\n              <Typography variant=\"h5\" gutterBottom>\n                h5: {describeTypography(theme, 'h5')}\n              </Typography>\n              <Typography variant=\"h6\" gutterBottom>\n                h6: {describeTypography(theme, 'h6')}\n              </Typography>\n              <Typography variant=\"subtitle1\" gutterBottom>\n                Subtitle1: {describeTypography(theme, 'subtitle1')}\n              </Typography>\n              <Typography variant=\"subtitle2\" gutterBottom>\n                Subtitle2: {describeTypography(theme, 'subtitle2')}\n              </Typography>\n            </Grid>\n\n            <Grid item xs={12} lg={6}>\n              <p>\n                <Button variant=\"contained\">Default</Button>\n                <Button variant=\"contained\" color=\"primary\">Primary</Button>\n                <Button variant=\"contained\" color=\"secondary\">Secondary</Button>\n                <Button variant=\"contained\" disabled>Disabled</Button>\n              </p>\n\n              <p>\n                <Button>Default</Button>\n                <Button color=\"primary\">Primary</Button>\n                <Button color=\"secondary\">Secondary</Button>\n                <Button disabled>Disabled</Button>\n              </p>\n\n              <p>\n                <Button variant=\"outlined\">Default</Button>\n                <Button variant=\"outlined\" color=\"primary\">Primary</Button>\n                <Button variant=\"outlined\" color=\"secondary\">Secondary</Button>\n                <Button variant=\"outlined\" disabled>Disabled</Button>\n              </p>\n            </Grid>\n\n          </Grid>\n\n          <Divider className={classes.divider}/>\n\n          <Typography variant=\"body1\" gutterBottom>\n            Body 1: {describeTypography(theme, 'body1')} - {latin}\n          </Typography>\n          <Typography variant=\"body1\" gutterBottom>\n            {latin}\n          </Typography>\n\n          <Divider className={classes.divider}/>\n\n          <Typography variant=\"body2\" gutterBottom>\n            Body 2: {describeTypography(theme, 'body2')} - {latin}\n          </Typography>\n          <Typography variant=\"body2\" gutterBottom>\n            {latin}\n          </Typography>\n\n          <Divider className={classes.divider}/>\n\n          <Typography variant=\"button\" gutterBottom>\n            Button - {describeTypography(theme)}\n          </Typography>\n\n          <Divider className={classes.divider}/>\n\n          <Typography variant=\"caption\" gutterBottom>\n            Caption: {describeTypography(theme, 'caption')}\n          </Typography>\n\n          <Divider className={classes.divider}/>\n\n          <Typography variant=\"overline\" gutterBottom>\n            Overline: {describeTypography(theme, 'overline')}\n          </Typography>\n\n          <Divider className={classes.divider}/>\n\n          <Typography gutterBottom>\n            Base: {describeTypography(theme)} - {latin}\n          </Typography>\n\n        </Paper>\n      </Grid>\n\n\n      {\n        Object.keys(theme.palette).map(color => {\n          if (specialPalettes.includes(color)) return null;\n\n          const colorGroup = getColorGroup({ theme, classes, color });\n          if (!colorGroup) return null;\n\n          return (\n            <Grid item xs={12} sm={6} md={3} key={color}>\n              {colorGroup}\n            </Grid>\n          );\n        })\n      }\n\n      {\n        Object.keys(theme.palette).map(color => {\n          if (!specialPalettes.includes(color)) return null;\n\n          const colorGroup = getColorGroup({ theme, classes, color });\n          if (!colorGroup) return null;\n\n          return (\n            <Grid item xs={12} sm={6} md={3} key={color}>\n              {colorGroup}\n            </Grid>\n          );\n        })\n      }\n\n    </Grid>\n  );\n};\n\nThemeStyles.propTypes = {\n  theme: PropTypes.object.isRequired,\n  classes: PropTypes.object.isRequired,\n};\n\nregisterComponent('ThemeStyles', ThemeStyles, withTheme, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/ui/Alert.jsx",
    "content": "/**\n * @Author: Apollinaire Lecocq <apollinaire>\n * @Date:   09-01-19\n * @Last modified by:   apollinaire\n * @Last modified time: 10-01-19\n */\nimport React from 'react';\nimport { withStyles } from '@material-ui/core/styles';\nimport { registerComponent } from 'meteor/vulcan:core';\n\nimport Card from '@material-ui/core/Card';\nimport CardContent from '@material-ui/core/CardContent';\n\nconst AlertStyle = theme => ({\n  error: {\n    color: theme.palette.error.main,\n    backgroundColor: theme.palette.error[100],\n    fontFamily: theme.typography.fontFamily,\n  },\n  other: {\n    fontFamily: theme.typography.fontFamily,\n  },\n});\n\nconst Alert = ({ children, variant, classes, ...rest }) => (\n  <Card className={variant === 'danger' ? classes.error : classes.other}>\n    <CardContent>{children}</CardContent>\n  </Card>\n);\n\nregisterComponent({ name: 'Alert', component: Alert, hocs: [[withStyles, AlertStyle]] });\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/ui/Button.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport MuiFab from '@material-ui/core/Fab';\nimport MuiButton from '@material-ui/core/Button';\nimport MuiIconButton from '@material-ui/core/IconButton';\nimport { withTheme } from '@material-ui/core/styles';\n\nconst styles = theme => ({\n  success: {\n    '&:hover': {\n      backgroundColor: theme.palette.success.main,\n      color: theme.palette.success.contrastText,\n    },\n  },\n\n  outline_success: {\n    '&:hover': {\n      borderColor: theme.palette.success.main,\n      color: theme.palette.success.main,\n    },\n  },\n\n  warning: {\n    '&:hover': {\n      backgroundColor: theme.palette.warning.main,\n      color: theme.palette.warning.contrastText,\n    },\n  },\n\n  outline_warning: {\n    '&:hover': {\n      borderColor: theme.palette.warning.main,\n      color: theme.palette.warning.main,\n    },\n  },\n\n  danger: {\n    '&:hover': {\n      backgroundColor: theme.palette.error.main,\n      color: theme.palette.error.contrastText,\n    },\n  },\n\n  outline_danger: {\n    '&:hover': {\n      borderColor: theme.palette.error.main,\n      color: theme.palette.error.main,\n    },\n  },\n\n  info: {\n    '&:hover': {\n      backgroundColor: theme.palette.info.main,\n      color: theme.palette.info.contrastText,\n    },\n  },\n\n  outline_info: {\n    '&:hover': {\n      borderColor: theme.palette.info.main,\n      color: theme.palette.info.main,\n    },\n  },\n\n  light: {},\n\n  outline_light: {},\n\n  dark: {\n    backgroundColor: theme.palette.common.black,\n    color: theme.palette.common.white,\n  },\n\n  outline_dark: {\n    borderColor: theme.palette.common.black,\n    color: theme.palette.common.black,\n  },\n});\n\nconst Button = ({ children, variant, size, iconButton, classes, theme, ...rest }) => {\n  const varParts = variant && variant.split('-');\n  const outline = varParts && varParts.length > 1 ? varParts[0] : null;\n  variant =\n    varParts && varParts.length > 1\n      ? varParts[1]\n      : varParts && varParts.length > 0\n      ? varParts[0]\n      : null;\n  let color;\n\n  switch (variant) {\n    case 'primary':\n      color = 'primary';\n      break;\n    case 'secondary':\n      color = 'secondary';\n      break;\n    case 'inherit':\n      color = 'inherit';\n      break;\n    default:\n      color = 'default';\n      break;\n  }\n\n  // switch between Fab or Button\n  const ButtonComponent = ['fab', 'extendedFab'].includes(variant) ? MuiFab : MuiButton;\n  variant = variant === 'extendedFab' ? 'extended' : variant;\n\n  const root = ['success', 'warning', 'danger', 'info', 'light', 'dark'].includes(variant)\n    ? classes[outline ? outline + '_' + variant : variant]\n    : null;\n\n  variant =\n    outline === 'outline' ? 'outlined' : variant && variant !== 'link' ? 'contained' : 'text';\n\n  switch (size) {\n    case 'sm':\n      size = 'small';\n      break;\n    case 'md':\n      size = 'medium';\n      break;\n    case 'lg':\n      size = 'large';\n      break;\n    default:\n      size = undefined;\n      break;\n  }\n\n  if (iconButton) {\n    return (\n      <MuiIconButton color={color} variant={variant} size={size} classes={{ root }} {...rest}>\n        {children}\n      </MuiIconButton>\n    );\n  }\n\n  return (\n    <ButtonComponent color={color} variant={variant} size={size} classes={{ root }} {...rest}>\n      {children}\n    </ButtonComponent>\n  );\n};\n\nButton.displayName = 'Button';\n\nButton.propTypes = {\n  variant: PropTypes.oneOf([\n    'default',\n    'primary',\n    'secondary',\n    'success',\n    'warning',\n    'danger',\n    'info',\n    'light',\n    'dark',\n    'link',\n    'outline-primary',\n    'outline-secondary',\n    'outline-success',\n    'outline-warning',\n    'outline-danger',\n    'outline-info',\n    'outline-light',\n    'outline-dark',\n    'inherit',\n    'fab',\n    'extendedFab',\n  ]),\n  size: PropTypes.oneOf(['sm', 'md', 'lg']),\n  iconButton: PropTypes.bool,\n  className: PropTypes.string,\n  classes: PropTypes.object.isRequired,\n  theme: PropTypes.object.isRequired,\n};\n\nregisterComponent('Button', Button, [withStyles, styles], withTheme);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/ui/Modal.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { registerComponent, Components } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport Dialog from '@material-ui/core/Dialog';\nimport DialogTitle from '@material-ui/core/DialogTitle';\nimport DialogContent from '@material-ui/core/DialogContent';\nimport Close from 'mdi-material-ui/Close';\nimport classNames from 'classnames';\n\nconst styles = theme => ({\n  dialog: {},\n\n  dialogPaper: {},\n\n  dialogTitle: {},\n\n  dialogTitleEmpty: {\n    padding: 0,\n    height: theme.spacing(3),\n    borderBottomStyle: 'none',\n  },\n\n  dialogContent: {},\n\n  dialogOverflow: {\n    overflowY: 'visible',\n  },\n\n  closeButton: theme.utils.closeButton,\n\n});\n\nconst Modal = props => {\n  const {\n    children,\n    className,\n    show = false,\n    onHide,\n    title,\n    showCloseButton = true,\n    dontWrapDialogContent,\n    dialogOverflow,\n    dialogProps,\n    classes,\n    ...rest\n  } = props;\n\n  const overflowClass = dialogOverflow && classes.dialogOverflow;\n\n  return (\n    <Dialog\n      className={className}\n      open={show}\n      onClose={onHide}\n      onClick={event => {\n        event.stopPropagation();\n      }}\n      fullWidth={true}\n      classes={{ paper: classNames(classes.dialogPaper, overflowClass) }}\n      {...dialogProps}\n    >\n      <DialogTitle className={title ? classes.dialogTitle : classes.dialogTitleEmpty}>\n        {title}\n\n        {\n          showCloseButton &&\n\n          <Components.TooltipButton\n            className={classes.closeButton}\n            icon={<Close/>}\n            titleId=\"global.close\"\n            onClick={onHide}\n            aria-label=\"Close\"\n          />\n        }\n      </DialogTitle>\n\n      {\n        dontWrapDialogContent\n          ?\n          children\n          :\n          <DialogContent className={classNames(classes.dialogContent, overflowClass)}>\n            {children}\n          </DialogContent>\n      }\n    </Dialog>\n  );\n};\n\nModal.propTypes = {\n  children: PropTypes.node,\n  className: PropTypes.string,\n  show: PropTypes.bool,\n  onHide: PropTypes.func,\n  title: PropTypes.node,\n  showCloseButton: PropTypes.bool,\n  dontWrapDialogContent: PropTypes.bool,\n  dialogOverflow: PropTypes.bool,\n  dialogProps: PropTypes.object,\n  classes: PropTypes.object,\n};\n\nregisterComponent('Modal', Modal, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/ui/ModalTrigger.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { intlShape } from 'meteor/vulcan:i18n';\nimport { Components, registerComponent, deprecate, instantiateComponent } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport Button from '@material-ui/core/Button';\nimport classNames from 'classnames';\nimport _omit from 'lodash/omit';\n\n\nconst styles = theme => ({\n  root: {},\n  button: {},\n  anchor: {},\n  dialog: {},\n  dialogPaper: {},\n  dialogTitle: {},\n  dialogTitleEmpty: {},\n  dialogContent: {},\n  dialogOverflow: {},\n  closeButton: {},\n});\n\nclass ModalTrigger extends PureComponent {\n\n  constructor(props) {\n    super(props);\n\n    this.state = { modalIsOpen: false };\n  }\n\n  componentDidMount() {\n    if (this.props.action) {\n      this.props.action({\n        openModal: this.openModal,\n        closeModal: this.closeModal,\n      });\n    }\n  }\n\n  openModal = event => {\n    if (event) {\n      event.preventDefault();\n      event.stopPropagation();\n    }\n    this.setState({ modalIsOpen: true });\n    if (this.props.openStateChanged) {\n      this.props.openStateChanged(true);\n    }\n  };\n\n  closeModal = event => {\n    if (event) {\n      event.stopPropagation();\n    }\n    this.setState({ modalIsOpen: false });\n    if (this.props.openStateChanged) {\n      this.props.openStateChanged(false);\n    }\n  };\n\n  render() {\n    const {\n      className,\n      dialogClassName,\n      dialogOverflow,\n      showCloseButton,\n      dontWrapDialogContent,\n      dialogProperties, // deprecated\n      dialogProps,\n      labelId,\n      component,\n      trigger,\n      titleId,\n      type,\n      children,\n      contentComponent,\n      contentProps,\n      classes,\n    } = this.props;\n\n    if (dialogProperties) {\n      deprecate('1.15.2', 'ModalTrigger’s \"dialogProperties\" prop has been renamed \"dialogProps\"');\n    }\n\n    const intl = this.context.intl;\n\n    const label = labelId ? intl.formatMessage({ id: labelId }) : this.props.label;\n    const title = titleId ? intl.formatMessage({ id: titleId }) : this.props.title;\n\n    const triggerComponent =\n      (component || trigger)\n        ?\n        instantiateComponent(component || trigger, {\n          onClick: this.openModal,\n          className: classNames('modal-trigger', classes.root, className),\n        })\n        :\n        type === 'button'\n          ?\n          <Button\n            className={classNames('modal-trigger', classes.root, classes.button, className)}\n            variant=\"contained\"\n            onClick={this.openModal}>\n            {label}\n          </Button>\n          :\n          <a className={classNames('modal-trigger', classes.root, classes.anchor, className)} href=\"#\"\n             onClick={this.openModal}>\n            {label}\n          </a>;\n\n    return (\n      <>\n        {triggerComponent}\n        <Components.Modal\n          className={dialogClassName}\n          show={this.state.modalIsOpen}\n          onHide={this.closeModal}\n          title={title}\n          dialogOverflow={dialogOverflow}\n          showCloseButton={showCloseButton}\n          dontWrapDialogContent={dontWrapDialogContent}\n          classes={_omit(classes, ['root', 'button', 'anchor'])}\n          dialogProps={{ ...dialogProperties, ...dialogProps }}\n        >\n          {\n            !this.state.modalIsOpen\n              ?\n              null\n              :\n              contentComponent\n                ?\n                instantiateComponent(contentComponent, contentProps)\n                :\n                children\n          }\n        </Components.Modal>\n      </>\n    );\n  }\n\n}\n\nModalTrigger.propTypes = {\n  /**\n   * Callback fired when the component mounts.\n   * This is useful when you want to trigger an action programmatically.\n   * It supports `openModal()` and `closeModal()`.\n   *\n   * @param {object} actions This object contains all possible actions\n   * that can be triggered programmatically.\n   */\n  action: PropTypes.func,\n  className: PropTypes.string,\n  dialogClassName: PropTypes.string,\n  dialogOverflow: PropTypes.bool,\n  showCloseButton: PropTypes.bool,\n  dontWrapDialogContent: PropTypes.bool,\n  dialogProperties: PropTypes.object, // deprecated — use dialogProps\n  dialogProps: PropTypes.object,\n  label: PropTypes.string,\n  labelId: PropTypes.string,\n  component: PropTypes.object,\n  trigger: PropTypes.object,\n  title: PropTypes.node,\n  titleId: PropTypes.string,\n  type: PropTypes.oneOf(['link', 'button']),\n  openStateChanged: PropTypes.func,\n  children: PropTypes.node,\n  contentComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),\n  contentProps: PropTypes.object,\n  classes: PropTypes.object,\n};\n\nModalTrigger.contextTypes = {\n  intl: intlShape,\n};\n\nregisterComponent('ModalTrigger', ModalTrigger, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/ui/Table.jsx",
    "content": "import React from 'react';\nimport Table from '@material-ui/core/Table';\nimport { registerComponent } from 'meteor/vulcan:lib';\n\nregisterComponent('Table', Table);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/ui/VerticalNavigation.jsx",
    "content": "import React from 'react';\nimport Link from '@material-ui/core/Link';\nimport { Link as RLink } from 'react-router-dom';\nimport List from '@material-ui/core/List';\nimport ListItem from '@material-ui/core/ListItem';\n\nimport { registerComponent } from 'meteor/vulcan:lib';\n\nconst MenuItem = ({ name, label, path, onClick, labelToken, LeftComponent, RightComponent }, { intl }) => {\n  let Wrapper = React.Fragment;\n  if (path) {\n    const LinkToPath = ({ children }) => (\n      <Link component={RLink} to={path}>\n        {children}\n      </Link>\n    );\n    Wrapper = LinkToPath;\n  }\n  return (\n    <Wrapper key={name}>\n      <ListItem button>\n        <div\n          //selected={path && router.isActive(path)}\n          onClick={onClick}>\n          {LeftComponent && <LeftComponent />}\n          <span>{label || intl.formatMessage({ id: labelToken })}</span>\n          {RightComponent && <RightComponent />}\n        </div>\n      </ListItem>\n    </Wrapper>\n  );\n};\n\nconst VerticalNavigation = ({ links }) => {\n  return <List component=\"nav\">{links.map(MenuItem)}</List>;\n};\n\nregisterComponent('VerticalNavigation', VerticalNavigation);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/upload/UploadImage.jsx",
    "content": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent } from 'meteor/vulcan:lib';\nimport { withStyles } from '@material-ui/core/styles';\nimport IconButton from '@material-ui/core/IconButton';\nimport DeleteIcon from 'mdi-material-ui/Delete';\nimport classNames from 'classnames';\n\n/**\n * Used by UploadInner to display a single image\n */\nconst styles = theme => ({\n  uploadImage: {\n    textAlign: 'center',\n    marginBottom: theme.spacing(-1),\n    marginLeft: theme.spacing(0.5),\n    marginRight: theme.spacing(0.5),\n  },\n\n  uploadImageContents: {\n    position: 'relative',\n  },\n\n  uploadImageImg: {\n    display: 'block',\n    maxWidth: 150,\n    maxHeight: 150,\n  },\n\n  uploadLoading: {\n    position: 'absolute',\n    top: 0,\n    bottom: 0,\n    left: 0,\n    right: 0,\n    background: 'rgba(255,255,255,0.8)',\n    display: 'flex',\n    justifyContent: 'center',\n    alignItems: 'center',\n    span: {\n      display: 'block',\n      fontSize: '1.5rem',\n    },\n  },\n\n  deleteButton: {},\n});\n\nclass UploadImage extends PureComponent {\n  constructor(props) {\n    super(props);\n    this.handleClear = this.handleClear.bind(this);\n  }\n\n  handleClear(event) {\n    event.preventDefault();\n    this.props.clearImage(this.props.index);\n  }\n\n  // Get the URL of an image or the first in an array of images\n  getImageUrl(imageOrImageArray) {\n    // if image is actually an array of formats, use first format\n    const image = Array.isArray(imageOrImageArray) ? imageOrImageArray[0] : imageOrImageArray;\n\n    // if image is an object, return secure_url; else return image itself\n    return typeof image === 'string' ? image : image.secure_url;\n  }\n\n  render() {\n    const { loading, error, image, style, classes } = this.props;\n\n    return (\n      <div className={classes.uploadImage}>\n        <div className={classes.uploadImageContents}>\n          <img className={classes.uploadImageImg} src={this.getImageUrl(image)} style={style} />\n          {loading && (\n            <div className={classes.uploadLoading}>\n              <Components.Loading />\n            </div>\n          )}\n        </div>\n\n        <IconButton className={classes.deleteButton} onClick={this.handleClear}>\n          <DeleteIcon />\n        </IconButton>\n      </div>\n    );\n  }\n}\n\nUploadImage.propTypes = {\n  clearImage: PropTypes.func.isRequired,\n  index: PropTypes.number.isRequired,\n  image: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]),\n  loading: PropTypes.bool,\n  error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),\n  style: PropTypes.object,\n  classes: PropTypes.object.isRequired,\n};\n\nUploadImage.displayName = 'UploadImageMui';\n\nregisterComponent('UploadImage', UploadImage, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/components/upload/UploadInner.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent, getComponent } from 'meteor/vulcan:lib';\nimport Dropzone from 'react-dropzone';\nimport { withStyles } from '@material-ui/core/styles';\nimport ComponentMixin from 'meteor/vulcan:ui-material/lib/components/forms/base-controls/mixins/component';\nimport FormControlLayout from 'meteor/vulcan:ui-material/lib/components/forms/base-controls/FormControlLayout';\nimport FormHelper from 'meteor/vulcan:ui-material/lib/components/forms/base-controls/FormHelper';\nimport classNames from 'classnames';\n\n/*\n\nMaterial UI GUI for Cloudinary Image Upload component\n\n*/\n\nconst styles = theme => ({\n  root: {},\n\n  label: {},\n\n  uploadField: {\n    marginTop: theme.spacing(1),\n  },\n\n  dropzoneBase: {\n    borderWidth: 3,\n    borderStyle: 'dashed',\n    borderColor: theme.palette.background[900],\n    backgroundColor: theme.palette.background[100],\n    color: theme.palette.common.lightBlack,\n    padding: '30px 60px',\n    transition: 'all 0.5s',\n    cursor: 'pointer',\n    position: 'relative',\n    display: 'flex',\n    alignItems: 'center',\n    justifyContent: 'center',\n    '&[aria-disabled=\"false\"]:hover': {\n      color: theme.palette.common.midBlack,\n      borderColor: theme.palette.background['A200'],\n    },\n  },\n\n  dropzoneActive: {\n    borderStyle: 'solid',\n    borderColor: theme.palette.status.info,\n  },\n\n  dropzoneReject: {\n    borderStyle: 'solid',\n    borderColor: theme.palette.status.danger,\n  },\n\n  uploadState: {},\n\n  uploadImages: {\n    border: `1px solid ${theme.palette.background[500]}`,\n    backgroundColor: theme.palette.background[100],\n    display: 'flex',\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n    justifyContent: 'center',\n    paddingTop: theme.spacing(1),\n    paddingRight: theme.spacing(0.5),\n    paddingBottom: theme.spacing(1),\n    paddingLeft: theme.spacing(0.5),\n  },\n});\n\nconst UploadInner = props => {\n  const {\n    uploading,\n    images,\n    disabled,\n    maxCount,\n    label,\n    help,\n    options,\n    enableMultiple,\n    onDrop,\n    isDeleted,\n    clearImage,\n    classes,\n  } = props;\n\n  const UploadImage = getComponent(options.uploadImageComponentName || 'UploadImage');\n\n  return (\n    <FormControlLayout component=\"fieldset\" fullWidth={true} className={classes.root}>\n      <label component=\"legend\" className={classes.label}>\n        {label}\n      </label>\n      {help && <FormHelper>{help}</FormHelper>}\n      <div className={classes.uploadField}>\n        {disabled && !enableMultiple ? null : (\n          <Dropzone\n            style={options.dropzoneStyle}\n            multiple={enableMultiple}\n            onDrop={onDrop}\n            accept=\"image/*\"\n            className={classes.dropzoneBase}\n            activeClassName={classes.dropzoneActive}\n            rejectClassName={classes.dropzoneReject}\n            disabled={disabled}>\n            <div>\n              <Components.FormattedMessage\n                id={`upload.${disabled ? 'maxReached' : 'prompt'}`}\n                values={{ maxCount }}\n              />\n            </div>\n            {uploading && (\n              <div className=\"upload-uploading\">\n                <span>\n                  <Components.FormattedMessage id={'upload.uploading'} />\n                </span>\n              </div>\n            )}\n          </Dropzone>\n        )}\n\n        {!!images.length && (\n          <div className={classes.uploadState}>\n            <div className={classes.uploadImages}>\n              {images.map(\n                (image, index) =>\n                  !isDeleted(index) && (\n                    <UploadImage\n                      clearImage={clearImage}\n                      key={index}\n                      index={index}\n                      image={image}\n                      loading={image.loading}\n                      preview={image.preview}\n                      error={image.error}\n                      style={options.imageStyle}\n                    />\n                  )\n              )}\n            </div>\n          </div>\n        )}\n      </div>\n    </FormControlLayout>\n  );\n};\n\nUploadInner.propTypes = {\n  uploading: PropTypes.bool,\n  images: PropTypes.array.isRequired,\n  disabled: PropTypes.bool,\n  maxCount: PropTypes.number.isRequired,\n  label: PropTypes.string,\n  help: PropTypes.string,\n  options: PropTypes.object.isRequired,\n  enableMultiple: PropTypes.bool,\n  onDrop: PropTypes.func.isRequired,\n  isDeleted: PropTypes.func.isRequired,\n  clearImage: PropTypes.func.isRequired,\n  classes: PropTypes.object.isRequired,\n};\n\nUploadInner.displayName = 'UploadInnerMui';\n\nregisterComponent('UploadInner', UploadInner, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/example/Header.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport AppBar from '@material-ui/core/AppBar';\nimport Toolbar from '@material-ui/core/Toolbar';\nimport IconButton from '@material-ui/core/IconButton';\nimport Typography from '@material-ui/core/Typography';\nimport MenuIcon from 'mdi-material-ui/Menu';\nimport ChevronLeftIcon from 'mdi-material-ui/ChevronLeft';\nimport { withStyles } from '@material-ui/core/styles';\nimport { getSetting, registerComponent } from 'meteor/vulcan:core';\nimport classNames from 'classnames';\n\nconst drawerWidth = 240;\nconst topBarHeight = 100;\n\nconst styles = theme => ({\n  appBar: {\n    position: 'absolute',\n    transition: theme.transitions.create(['margin', 'width'], {\n      easing: theme.transitions.easing.sharp,\n      duration: theme.transitions.duration.leavingScreen,\n    }),\n  },\n  appBarShift: {\n    marginLeft: drawerWidth,\n    width: `calc(100% - ${drawerWidth}px)`,\n    transition: theme.transitions.create(['margin', 'width'], {\n      easing: theme.transitions.easing.easeOut,\n      duration: theme.transitions.duration.enteringScreen,\n    }),\n  },\n  toolbar: {\n    height: `${topBarHeight}px`,\n    minHeight: `${topBarHeight}px`,\n  },\n  headerMid: {\n    flexGrow: 1,\n    display: 'flex',\n    alignItems: 'center',\n    '& h1': {\n      margin: '0 24px 0 0',\n      fontSize: '18px',\n      lineHeight: 1,\n    },\n  },\n  menuButton: {\n    marginRight: theme.spacing(3),\n  },\n});\n\nconst Header = (props, context) => {\n  const classes = props.classes;\n  const isSideNavOpen = props.isSideNavOpen;\n  const toggleSideNav = props.toggleSideNav;\n\n  const siteTitle = getSetting('title', 'My App');\n\n  return (\n    <AppBar className={classNames(classes.appBar, isSideNavOpen && classes.appBarShift)}>\n      <Toolbar className={classes.toolbar}>\n        <IconButton\n          aria-label=\"open drawer\"\n          onClick={e => toggleSideNav()}\n          className={classNames(classes.menuButton)}\n          color=\"inherit\">\n          {isSideNavOpen ? <ChevronLeftIcon /> : <MenuIcon />}\n        </IconButton>\n\n        <div className={classNames(classes.headerMid)}>\n          <Typography variant=\"h6\" color=\"inherit\" className=\"tagline\">\n            {siteTitle}\n          </Typography>\n        </div>\n      </Toolbar>\n    </AppBar>\n  );\n};\n\nHeader.propTypes = {\n  classes: PropTypes.object.isRequired,\n  isSideNavOpen: PropTypes.bool,\n  toggleSideNav: PropTypes.func,\n};\n\nHeader.displayName = 'Header';\n\nregisterComponent('Header', Header, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/example/Layout.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport Drawer from '@material-ui/core/Drawer';\nimport AppBar from '@material-ui/core/AppBar';\nimport Toolbar from '@material-ui/core/Toolbar';\nimport { Components, replaceComponent, Utils } from 'meteor/vulcan:core';\nimport { withStyles } from '@material-ui/core/styles';\nimport classNames from 'classnames';\n\nconst drawerWidth = 240;\nconst topBarHeight = 100;\n\nconst styles = theme => {\n  const contentPadding = theme.spacing(8);\n\n  return {\n    '@global': {\n      html: {\n        background: theme.palette.background.default,\n        WebkitFontSmoothing: 'antialiased',\n        MozOsxFontSmoothing: 'grayscale',\n        overflow: 'hidden',\n      },\n      body: {\n        margin: 0,\n      },\n    },\n    root: {\n      width: '100%',\n      zIndex: 1,\n      overflow: 'hidden',\n    },\n    appFrame: {\n      position: 'relative',\n      display: 'flex',\n      height: '100vh',\n      alignItems: 'stretch',\n    },\n    drawerPaper: {\n      position: 'relative',\n      width: drawerWidth,\n      backgroundColor: theme.palette.background[200],\n    },\n    drawerHeader: {\n      height: `${topBarHeight}px !important`,\n      minHeight: `${topBarHeight}px !important`,\n      position: 'relative !important',\n    },\n    content: {\n      padding: contentPadding,\n      width: '100%',\n      marginLeft: -drawerWidth,\n      flexGrow: 1,\n      backgroundColor: theme.palette.background.default,\n      color: theme.palette.text.primary,\n      transition: theme.transitions.create('margin', {\n        easing: theme.transitions.easing.sharp,\n        duration: theme.transitions.duration.leavingScreen,\n      }),\n      height: `calc(100% - ${topBarHeight}px - ${contentPadding * 2}px)`,\n      marginTop: topBarHeight,\n      overflowY: 'scroll',\n    },\n    mainShift: {\n      marginLeft: 0,\n      transition: theme.transitions.create('margin', {\n        easing: theme.transitions.easing.easeOut,\n        duration: theme.transitions.duration.enteringScreen,\n      }),\n    },\n  };\n};\n\nclass Layout extends React.Component {\n  state = {\n    isOpen: { sideNav: true },\n  };\n\n  toggle = (item, openOrClose) => {\n    const newState = { isOpen: {} };\n    newState.isOpen[item] =\n      typeof openOrClose === 'string' ? openOrClose === 'open' : !this.state.isOpen[item];\n    this.setState(newState);\n  };\n\n  render = () => {\n    const routeName = Utils.slugify(this.props.currentRoute.name);\n    const classes = this.props.classes;\n    const isOpen = this.state.isOpen;\n\n    return (\n      <div className={classNames(classes.root, 'wrapper', `wrapper-${routeName}`)}>\n        <div className={classes.appFrame}>\n          <Components.Header\n            isSideNavOpen={isOpen.sideNav}\n            toggleSideNav={openOrClose => this.toggle('sideNav', openOrClose)}\n          />\n\n          <Drawer\n            variant=\"persistent\"\n            classes={{ paper: classes.drawerPaper }}\n            open={isOpen.sideNav}>\n            <AppBar className={classes.drawerHeader} elevation={4} square={true}>\n              <Toolbar></Toolbar>\n            </AppBar>\n            <Components.SideNavigation />\n          </Drawer>\n\n          <main className={classNames(classes.content, isOpen.sideNav && classes.mainShift)}>\n            {this.props.children}\n          </main>\n\n          <Components.FlashMessages />\n        </div>\n      </div>\n    );\n  };\n}\n\nLayout.propTypes = {\n  classes: PropTypes.object.isRequired,\n  children: PropTypes.node,\n};\n\nLayout.displayName = 'Layout';\n\nreplaceComponent('Layout', Layout, [withStyles, styles]);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/example/SideNavigation.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';\nimport { withRouter } from 'react-router';\nimport List from '@material-ui/core/List';\nimport ListItem from '@material-ui/core/ListItem';\nimport ListItemIcon from '@material-ui/core/ListItemIcon';\nimport ListItemText from '@material-ui/core/ListItemText';\nimport Divider from '@material-ui/core/Divider';\nimport Collapse from '@material-ui/core/Collapse';\nimport ExpandLessIcon from 'mdi-material-ui/ChevronUp';\nimport ExpandMoreIcon from 'mdi-material-ui/ChevronDown';\nimport LockIcon from 'mdi-material-ui/Lock';\nimport UsersIcon from 'mdi-material-ui/AccountMultiple';\nimport ThemeIcon from 'mdi-material-ui/Palette';\nimport HomeIcon from 'mdi-material-ui/Home';\nimport { withStyles } from '@material-ui/core/styles';\nimport Users from 'meteor/vulcan:users';\n\nconst styles = theme => ({\n  root: {},\n  nested: {\n    paddingLeft: theme.spacing(4),\n  },\n});\n\nclass SideNavigation extends React.Component {\n  state = {\n    isOpen: { admin: false },\n  };\n\n  toggle = item => {\n    const newState = { isOpen: {} };\n    newState.isOpen[item] = !this.state.isOpen[item];\n    this.setState(newState);\n  };\n\n  render() {\n    const { currentUser, classes, history } = this.props;\n    const isOpen = this.state.isOpen;\n\n    return (\n      <div className={classes.root}>\n        <List>\n          <ListItem\n            button\n            onClick={() => {\n              history.push('/');\n            }}>\n            <ListItemIcon>\n              <HomeIcon />\n            </ListItemIcon>\n            <ListItemText inset primary=\"Home\" />\n          </ListItem>\n        </List>\n\n        {Users.isAdmin(currentUser) && (\n          <div>\n            <Divider />\n            <List>\n              <ListItem button onClick={e => this.toggle('admin')}>\n                <ListItemIcon>\n                  <LockIcon />\n                </ListItemIcon>\n                <ListItemText primary=\"Admin\" />\n                {isOpen.admin ? <ExpandLessIcon /> : <ExpandMoreIcon />}\n              </ListItem>\n              <Collapse in={isOpen.admin} transitionduration=\"auto\" unmountOnExit>\n                <ListItem\n                  button\n                  className={classes.nested}\n                  onClick={() => {\n                    history.push('/admin');\n                  }}>\n                  <ListItemIcon>\n                    <UsersIcon />\n                  </ListItemIcon>\n                  <ListItemText inset primary=\"Users\" />\n                </ListItem>\n                <ListItem\n                  button\n                  className={classes.nested}\n                  onClick={() => {\n                    history.push('/theme');\n                  }}>\n                  <ListItemIcon>\n                    <ThemeIcon />\n                  </ListItemIcon>\n                  <ListItemText inset primary=\"Theme\" />\n                </ListItem>\n              </Collapse>\n            </List>\n          </div>\n        )}\n      </div>\n    );\n  }\n}\n\nSideNavigation.propTypes = {\n  classes: PropTypes.object.isRequired,\n  currentUser: PropTypes.object,\n};\n\nSideNavigation.displayName = 'SideNavigation';\n\nregisterComponent(\n  'SideNavigation',\n  SideNavigation,\n  [withStyles, styles],\n  withCurrentUser,\n  withRouter\n);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/modules/components.js",
    "content": "export * from '../components';"
  },
  {
    "path": "packages/vulcan-ui-material/lib/modules/index.js",
    "content": "export * from './components';\nexport * from './themes';\nimport JssCleanup from '../components/theme/JssCleanup';\nexport { JssCleanup };\nimport './sampleTheme';\nimport './routes';\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/modules/routes.js",
    "content": "import { addRoute } from 'meteor/vulcan:core';\n\n//Only create route on dev mode, not production.\nif (Meteor.isDevelopment) {\n  addRoute({\n    name: 'theme',\n    path: '/theme',\n    componentName: 'ThemeStyles',\n  });\n}\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/modules/sampleTheme.js",
    "content": "import { registerTheme } from './themes';\nimport { indigo as primary } from '@material-ui/core/colors';\nimport { deepPurple as secondary } from '@material-ui/core/colors';\nimport { red as error } from '@material-ui/core/colors';\nimport { blue as info } from '@material-ui/core/colors';\nimport { green as success } from '@material-ui/core/colors';\nimport { orange as warning } from '@material-ui/core/colors';\n\n\n/** @ignore */\n\n/**\n *\n * Sample theme to get you out of the gate quickly\n *\n * For a complete list of configuration variables see:\n * https://material-ui.com/customization/themes/\n *\n */\n\n\nconst theme = {\n\n  palette: {\n\n    primary: {\n      light: primary[200],\n      main: primary[500],\n      dark: primary[800],\n      contrastText: '#fff',\n      ...primary\n    },\n\n    secondary: {\n      light: secondary[200],\n      main: secondary[500],\n      dark: secondary[800],\n      contrastText: '#fff',\n      ...secondary\n    },\n\n    error: {\n      light: error[200],\n      main: error[500],\n      dark: error[800],\n      contrastText: '#fff',\n      ...error\n    },\n\n    warning: {\n      light: warning[100],\n      main: warning[500],\n      dark: warning[900],\n      contrastText: '#fff',\n      ...warning\n    },\n\n    success: {\n      light: success[100],\n      main: success[500],\n      dark: success[900],\n      contrastText: '#fff',\n      ...success\n    },\n\n    info: {\n      light: info[100],\n      main: info[500],\n      dark: info[900],\n      contrastText: '#fff',\n      ...info\n    },\n\n  },\n\n  utils: {\n\n    tooltipEnterDelay: 700,\n\n    errorMessage: {\n      textAlign: 'center',\n      backgroundColor: error[500],\n      color: 'white',\n      borderRadius: '4px',\n      fontWeight: 'bold',\n    },\n\n    denseTable: {\n      '& > thead > tr > th, & > tbody > tr > td': {\n        padding: '4px 16px 4px 16px',\n      },\n      '& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {\n        paddingRight: '16px',\n      },\n    },\n\n    flatTable: {\n      '& > thead > tr > th, & > tbody > tr > td': {\n        padding: '4px 16px 4px 16px',\n        whiteSpace: 'nowrap',\n      },\n      '& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {\n        paddingRight: '16px',\n      },\n    },\n\n    denserTable: {\n      '& > thead > tr, & > tbody > tr': {\n        height: '40px',\n      },\n      '& > thead > tr > th, & > tbody > tr > td': {\n        padding: '4px 16px 4px 16px',\n        whiteSpace: 'nowrap',\n      },\n      '& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {\n        paddingRight: '16px',\n      },\n    },\n\n    testPaper: {\n      backgroundColor: warning[50],\n    },\n\n    closeButton: {\n      display: 'block !important',\n      position: 'absolute',\n      right: 8,\n      top: 8,\n    },\n\n  },\n\n  overrides: {\n    MuiButton: {\n      root: {\n        lineHeight: 1,\n        padding: '4px 16px',\n        minHeight: 40,\n      },\n      text: {\n        padding: '4px 16px',\n      },\n      outlined: {\n        padding: '4px 16px',\n      },\n      sizeSmall: {\n        padding: '4px 8px',\n        minHeight: 32,\n      },\n      sizeLarge: {\n        padding: '4px 24px',\n        minHeight: 48,\n      },\n      label: {\n        flexDirection: 'inherit',\n        '& > svg': {\n          marginRight: '8px',\n        },\n        '& > span.icon-wrap': {\n          marginRight: '8px',\n          fontSize: 0,\n        },\n      },\n    },\n  },\n\n};\n\n\nregisterTheme('Sample', theme);\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/modules/themes.js",
    "content": "/** @module vulcan-material-ui */\n\nimport { createTheme } from '@material-ui/core/styles';\nimport { registerSetting, getSetting } from 'meteor/vulcan:core';\n\n\nregisterSetting('muiTheme', 'Sample', 'Material UI theme used by erikdakota:vulcan-material-ui');\n\n\nexport const ThemesTable = {}; // storage for info about themes\n\n\n/**\n * Register a theme with a name\n *\n * @param {String} name The name of the theme to register\n * @param {Object} theme The theme object - see defaultTheme.js\n *\n */\nexport const registerTheme = (name, theme) => {\n  const themeInfo = {\n    name,\n    theme,\n  };\n\n  ThemesTable[name] = themeInfo;\n};\n\n\n/**\n * Get a theme registered with registerTheme()\n *\n * @param {String} name The name of the theme to get\n * \n * @returns {Object} A theme object\n */\nexport const getTheme = (name) => {\n  const themeInfo = ThemesTable[name];\n  if (!themeInfo) return null;\n  themeInfo.theme.typography = { ...themeInfo.theme.typography };\n  return createTheme(themeInfo.theme);\n};\n\n/**\n * Get the raw theme object registered with registerTheme()\n *\n * @param {String} name The name of the theme to get\n * \n * @returns {Object} The object passed to registerTheme\n */\n\nexport const getRawTheme = (name) => {\n  const themeInfo = ThemesTable[name];\n  if (!themeInfo) return null;\n  return themeInfo.theme;\n};\n\n/**\n * Get the theme specified in the 'muiTheme' setting\n * \n * @returns {Object}\n */\nexport const getCurrentTheme = () => {\n  const themeName = getSetting('muiTheme', 'Sample');\n  const theme = getTheme(themeName);\n  return theme;\n};\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/server/main.js",
    "content": "export * from '../modules/index';\nimport './wrapWithMuiTheme';\n"
  },
  {
    "path": "packages/vulcan-ui-material/lib/server/wrapWithMuiTheme.jsx",
    "content": "import React from 'react';\nimport { addCallback, Components } from 'meteor/vulcan:core';\nimport { ServerStyleSheets } from '@material-ui/core/styles';\n\nfunction wrapWithMuiTheme(app, { context, apolloClient }) {\n  // will spawn a StylesProvider automatically during render\n  // replaces the manual setup of JSSProvider\n  // @see https://github.com/mui-org/material-ui/blob/master/packages/material-ui-styles/src/ServerStyleSheets/ServerStyleSheets.js\n  const sheets = new ServerStyleSheets({ disableGeneration: true });\n  context.sheetsRegistry = sheets;\n\n  return sheets.collect(\n    <Components.ThemeProvider apolloClient={apolloClient} context={context}>\n      {app}\n    </Components.ThemeProvider>\n  );\n}\n\nfunction wrapWithMuiStyleGenerator(app, { context, apolloClient }) {\n  const sheets = new ServerStyleSheets();\n  context.sheetsRegistry = sheets;\n\n  // NOTE: The sheets.collect API does not allow to pass a seed\n  // do we still need to force a specific seed?\n  // if yes reenable this code and create the StylesProvider manually as we\n  // used to do for JSSProvider\n  //const generateClassName = createGenerateClassName({ seed: '' });\n\n  return sheets.collect(\n    <Components.ThemeProvider apolloClient={apolloClient} context={context}>\n      {app}\n    </Components.ThemeProvider>\n  );\n}\n\nfunction injectJss(sink, { context }) {\n  const sheets = context.sheetsRegistry.toString();\n  sink.appendToHead(`<style id=\"jss-server-side\">${sheets}</style>`);\n  return sink;\n}\n\n// only run during Apollo's data collection, will provide the theme but won't generate the styles\naddCallback('router.server.dataWrapper', wrapWithMuiTheme);\n// only run during actual rendering, will both provide the theme and generate the styles\naddCallback('router.server.renderWrapper', wrapWithMuiStyleGenerator);\n\n// inject the <style> tag after rendering\naddCallback('router.server.postRender', injectJss);\n"
  },
  {
    "path": "packages/vulcan-ui-material/package.js",
    "content": "Package.describe({\n  name: 'vulcan:ui-material',\n  version: '1.16.9',\n  summary: 'Replacement for Vulcan (http://vulcanjs.org/) components using material-ui',\n  documentation: 'README.md',\n});\n\nPackage.onUse(function(api) {\n  api.versionsFrom('METEOR@1.6');\n\n  api.use(['vulcan:core@=1.16.9', 'vulcan:accounts@=1.16.9', 'vulcan:forms@=1.16.9']);\n\n  api.addFiles(['accounts.css', 'forms.css', 'en_US.js', 'fr_FR.js'], ['client', 'server']);\n\n  api.mainModule('lib/client/main.js', 'client');\n  api.mainModule('lib/server/main.js', 'server');\n});\n"
  },
  {
    "path": "packages/vulcan-ui-material/readme.md",
    "content": "# vulcan:ui-material 1.16.2\n\nPackage initially created by [Erik Dakoda](https://github.com/ErikDakoda) ([`erikdakoda:vulcan-material-ui`](https://github.com/ErikDakoda/vulcan-material-ui))\n\nReplacement for [Vulcan](http://vulcanjs.org/) components using [Material-UI](https://material-ui.com/). \n\nThere are some nice bonus features like a CountrySelect with autocomplete and theming.\n\nAll components in vulcan:ui-bootstrap, vulcan:forms and vulcan:accounts have been implemented.\n\n\n## Installation\n\nTo add vulcan-material-ui to an existing Vulcan project, run the following in the console:\n\n``` sh\nmeteor add vulcan:ui-material\n\nmeteor npm install --save @material-ui/core@4.11.0\nmeteor npm install --save @material-ui/icons@4.9.1\nmeteor npm install --save @material-ui/styles@4.10.0\nmeteor npm install --save react-jss@10.4.0\nmeteor npm install --save mdi-material-ui@6.19.0\nmeteor npm install --save react-autosuggest@10.0.2\nmeteor npm install --save autosuggest-highlight@3.1.1\nmeteor npm install --save react-isolated-scroll@0.1.1\nmeteor npm install --save react-keyboard-event-handler@1.5.4\nmeteor npm install --save dompurify\n```\n\nThis package does not depend on `vulcan:ui-boostrap`, so you can remove it.\n\nTo activate the example layout copy the three components to your project and import them:\n\n``` javascript\nimport './example/Header';\nimport './example/Layout';\nimport './example/SideNavigation';\n```\n\n## Theming\n\nFor an example theme see `modules/sampleTheme.js`. For a complete list of values you can customize, \nsee the [MUI Default Theme](https://material-ui-next.com/customization/default-theme/). \n\nRegister your theme in the Vulcan environment by giving it a name: `registerTheme('MyTheme', theme);`. \nYou can have multiple themes registered and you can specify which one to use in your settings file using the `muiTheme` **public** setting.\n\nIn addition to the Material UI spec, I use a `utils` section in my themes where I place global variables for reusable styles. \nFor example the sample theme contains \n\n```\nconst theme = {\n  \n  . . .\n  \n  utils: {\n    \n    tooltipEnterDelay: 700,\n    \n    errorMessage: {\n      textAlign: 'center',\n      backgroundColor: red[500],\n      color: 'white',\n      borderRadius: '4px',\n    },\n    \n    . . .\n    \n    // additional utils definitions go here\n    \n  },\n  \n};\n```\n\nYou can use tooltipEnterDelay (or any other variable you define in utils) anywhere you include the withTheme HOC. See `/components/bonus/TooltipButton.jsx` for an example.\n\nYou can use errorMessage (or any other style fragment you define in utils) anywhere you include the withStyles HOC. See `/components/accounts/Form.jsx` for an example.\n\nMaterial-ui uses context to provide the theme to all its components. By default, the entire app is wrapped with the following component to provide the current theme to the components underneath:\n\n```jsx\nconst AppThemeProvider = ({ children }) => {\n  const theme = getCurrentTheme();\n  return (\n    <ThemeProvider theme={theme}>\n      <JssCleanup>{children}</JssCleanup>\n    </ThemeProvider>\n  );\n};\n```\n\nThe theme provider is a component registered under `ThemeProvider` so you can replace it as you want with `replaceComponent` from the `vulcan:core` package.\n\nIts props are `apolloClient` (client & server) and `context` (server only, context similar to the graphql requests).\n\n### Example: dynamic theme\n\nLets say your user schema has a field called \"theme\" that is a key to one of your registered themes. You can replace the `ThemeProvider` component to give you a dynamic theme as follows:\n\n```jsx\nimport { ThemeProvider } from '@material-ui/core/styles';\nimport { JssCleanup } from \"meteor/vulcan:ui-material\";\nimport { replaceComponent } from \"meteor/vulcan:core\";\nimport { useQuery } from \"@apollo/client\";\n\nconst CustomThemeProvider = ({ children, apolloClient }) => {\n  const { currentUser } = useQuery(currentUserThemeQuery, { client: apolloClient });\n  const themeKey = currentUser?.theme || defaultThemeKey; // fallback in case of loading or error\n  const theme = getTheme(themeKey);\n  return (\n    <ThemeProvider theme={theme}>\n      <JssCleanup>{children}</JssCleanup>\n    </ThemeProvider>\n  );\n};\n\nreplaceComponent(\"ThemeProvider\", CustomThemeProvider);\n```\n\nThis way the theme depends on the cache  and can update dynamically with the user setting. Note the option `client` given to `useQuery` because the ThemeProvider is higher in the react tree than the ApolloClientProvider, so we have to pass the Apollo client manually.\n\n## Server Side Rendering (SSR)\n\nMaterial UI and Vulcan support SSR, but this is a complex beast with pitfalls. Sometimes you will see a warning like this:\n\n`Warning: Prop className did not match. Server: \"MuiChip-label-131\" Client: \"MuiChip-label-130\"`\n\nSometimes the React rendered on the server and the client don't match exactly and this causes a problem with [JSS](https://material-ui-next.com/customization/css-in-js/#jss). This is a complicated issue that has multiple causes and I will be working on solving each of the issues causing this over time.\n\nYour pages should still render correctly, but there may be a blink and redraw when the first page after SSR loads in the browser.\n\nIn your own code, make sure that your components will render the same on the server and the client. This means not referring to client-side object such as `window`, `document` or `jQuery`. If you have a misbehaving component, try wrapping it in a [NoSsr](https://material-ui.com/components/no-ssr/) component.\n\n## Form Controls\n\nYou can pass a couple of extra options to inputs from the `form` property of your schema:\n\n```\n  userKey: {\n    type: String,\n    label: 'User key',\n    description: 'The user’s key',\n    optional: true,\n    hidden: function ({ document }) {\n      return !document.platformId || !document.usePlatformApp;\n    },\n    inputProperties: {\n      autoFocus: true,                 // focus this input when the form loads\n      addonBefore: <KeyIcon/>,         // adorn the start of the input\n      addonAfter: <KeyIcon/>,          // adorn the end of the input\n      inputClassName: 'halfWidthLeft', // use 'halfWidthLeft' or 'halfWidthRight'\n                                       //   to display two controls side by side\n      hideLabel: true,                 // hide the label\n      shrinkLabel: true,               // shrink the label even if the field is empty\n      hideLink: true,                  // url and email inputs are are adorned with\n                                       // an icon button link - unless you hide them\n      rows: 10,                        // for textareas you can specify the rows\n      variant: 'switch',               // for checkboxgroups you can use either \n                                       //   'checkbox' (default) or 'switch'\n      columnClass: 'twoColumn'         // for checkboxgroups you can set columnClass to\n                                       //   'twoColumn' or 'threeColumn'; if you don't specify\n                                       //   we will guess based on the length of the labels\n      inputProps: { step: 'any' }      // Attributes applied to the input element, for\n                                       //   ex pass the step attr to a number input\n    },\n    group: platformGroup,\n    canRead: ['members'],\n    canCreate: ['members'],\n    canUpdate: ['members'],\n  },\n```\n\n## Form Groups\n\nYou can pass a couple of extra options to form groups as well:\n\n``` javascript\n  const platformGroup = {\n    name: 'shops.platform',\n    order: 4,\n    beforeComponent: 'ShopsPlatformTitle',  // component to put at the top of the form group\n    afterComponent:  'ShopsConnectButtons', // component to put at the bottom of the form group\n    hidden: function ({ document }) {       // hide the group based on the value of a field\n      return !document.listing;\n    },\n  };\n```\n\n\n## DataTable\n\nYou can pass the DataTable component an `editComponent` property in addition to or instead of `showEdit`. Here is a simplified example:\n\n``` jsx\nconst AgendaJobActions = ({ collection, document }) => {\n  const scheduleAgent = () => {\n    Meteor.call('scheduleAgent', document.agentId);\n  };\n  \n  return (\n    <Components.TooltipIconButton \n      titleId=\"executions.execute_now\"\n      icon={<ExecutionsIcon/>}\n      onClick={scheduleAgent}\n    />;\n};\n\nAgendaJobActionsInner.propTypes = {\n  collecion: PropTypes.object.isRequired,\n  document: PropTypes.object.isRequired,\n};\n\n<Components.Datatable\n  editComponent={AgendaJobActions}\n  collection={AgendaJobs}\n  //...\n/>\n```\n\nYou can also control the spacing of the table cells using the `dense` property. Valid values are:\n\n| Value     | Description  |\n| --------- | ------------ |\n| `dense`   | right cell padding of 16px instead of 56px |\n| `flat`    | right cell padding of 16px and nowrap |\n| `denser`  | right cell padding of 16px, nowrap, and row height of 40px instead of 56px |\n\nYou can also use other string values, as long as you define a `utils` entry named the same + `Table`, for example `myCustomTable`. Check out the sample theme for examples.\n\n\n## DatatableFromArray\n\nThis is a wrapper component for Datatable that enables pagination even if you pass the data as an array instead of a query.\n\n``` jsx\n<Components.DatatableFromArray\n  columns={[\n             { name: 'message', component: TextCell },\n             { name: 'type', component: TypeCell },\n             { name: 'properties', component: JsonCell, cellClass: classes.widerCell },\n           ]}\n  data={errorArray}\n  paginate={true}\n  itemsPerPage={10}\n  intlNamespace=\"shops.reports\"\n/>\n```\n\n\n## CountrySelect\n\nOne of the bonus components is **CountrySelect**, a country autosuggest. For an example, see **CountrySelect**.\n\n```\n  country: {\n    type: String,\n    label: 'Country',\n    input: 'CountrySelect',\n    canRead: ['guests'],\n    canCreate: ['members'],\n    canUpdate: ['members'],\n  },\n```\n\nCountries are stored as their 2-letter country codes. I have included a helper function for displaying the country name:\n\n``` jsx\nimport Typography from '@material-ui/core/Typography';\nimport { getCountryLabel } from 'meteor/erikdakoda:vulcan-material-ui';\n\n<Typography variant=\"subtitle1\">\n  {getCountryLabel(supplier.country)}\n</Typography>\n```\n\n## FormSuggest\n\n**FormSuggest** is a base control that you can use to build custom autosuggest controls. Refer to **CountrySelect** for an example.\n\nYou can use the following additional props:\n\n| Property              | Type   | Description  |\n| --------------------- | ------ | ------------ |\n| `limitToList`         | bool   | Don't allow values that are not in the options |\n| `disableText`         | bool   | Don't allow editing of the text |\n| `disableSelectOnBlur` | bool   | When you blur (tab or click away) the suggest, the highlighted option is selected... unless this is true |\n| `showAllOptions`      | bool   | When typing show all options, not just matching ones |\n| `disableMatchParts`   | bool   | Prevent highlighting of matched sub-strings |\n| `autoComplete`        | string | Autocomplete is turned off by default |\n\nIn addition, the `options` that you pass to any select control have additional properties supported by **FormSuggest**:\n\n| Property              | Type   | Description  |\n| --------------------- | ------ | ------------ |\n| `label`         | string           | The option's text label (standard) |\n| `value`         | string or number | The options's value (standard) |\n| `iconComponent` | node             | An icon to put to the left of the label (optional) |\n| `formatted`     | node or func     | Instead of just an icon, you can pass a component for each options for (optional) |\n| `onClick`       | func             | Instead of selecting the option, your onClick handler can do something else, like open an modal to edit the options (optional) |\n\n\n## TooltipButton\n\n**TooltipButton** is an easy way to add icons, icon buttons, buttons, and static elements with a tooltip. It takes intl string IDs for easy localization.\n\n| Property      | Type    | Description  |\n| ------------- | ------- | ------------ |\n| `title`       | node    | Tooltip title as a string or a node |\n| `titleId`     | string  | Tooltip title as an intl string ID |\n| `titleValues` | object  | Values for the intl string |\n| `label`       | node    | Button label as a string or node |\n| `labelId`     | string  | Button label as an intl string ID |\n| `type`        | enum    | `simple`, `fab`, `button`, `submit`, `icon`, `menu` |\n| `size`        | enum    | `icon`, `xsmall`, `small`, `medium`, `large` |\n| `danger`      | bool    | When `true`, the button is highlighted in red on hover |\n| `variant`     | enum    | `text`, `outlined`, `contained` |\n| `placement`   | enum    | Tooltip placement: `bottom-end`, `bottom-start`, `bottom`, `left-end`, `left-start`, `left`, `right-end`, `right-start`, `right`, `top-end`, `top-start`, `top` |\n| `icon`        | node    | Icon element or component name |\n| `loading`     | bool    | When `true`, a loading spinner will be displayed and the button will be disabled |\n| `disabled`    | bool    | When `true`, the button will be disabled, when `false` it will be enabled even when loading is `true` |\n| `className`   | string  | Class name for the element root |\n| `classes`     | object  | Classes to override the defaults |\n| `buttonRef`   | func    | Function for grabbing the a reference to the button |\n| `enterDelay`  | number  | Tooltip enter delay |\n| `leaveDelay`  | number  | Tooltip leave delay |\n| `parent`      | enum    | Set parent to `popover` if the button's parent is a popover to increase the z-index of the tooltip |\n| `children`    | node    | You can optionally render arbitrary content (instead of a button) |\n| `cursor`      | string  | CSS `cursor` property for the button |\n\nSee the Storybook example by running the script `storybook-material`.\n"
  },
  {
    "path": "packages/vulcan-users/README.md",
    "content": "Vulcan users package, used internally. "
  },
  {
    "path": "packages/vulcan-users/TESTS.md",
    "content": "### Creating An Account"
  },
  {
    "path": "packages/vulcan-users/lib/client/main.js",
    "content": "export * from '../modules/index.js';\nexport {default} from '../modules/index.js';\n"
  },
  {
    "path": "packages/vulcan-users/lib/modules/avatar.js",
    "content": "import { getSetting } from 'meteor/vulcan:lib';\nimport Users from './collection.js';\nimport md5 from 'crypto-js/md5';\n\nUsers.avatar = {\n\n  /**\n   * `cleantString` remove starting and trailing whitespaces\n   * and lowercase the input\n   * @param  {String} string input string that may contain leading and trailing\n   * whitespaces and uppercase letters\n   * @return {String}        output cleaned string\n   */\n  cleanString: function (string) {\n    return string.trim().toLowerCase();\n  },\n\n  /**\n   * `isHash` check if a string match the MD5 form :\n   * 32 chars string containing letters from `a` to `f`\n   * and digits from `0` to `9`\n   * @param  {String}  string that might be a hash\n   * @return {Boolean}\n   */\n  isHash: function (string) {\n    var self = this;\n    return /^[a-f0-9]{32}$/i.test(self.cleanString(string));\n  },\n\n  /**\n   * `hash` takes an input and run it through `CryptoJS.MD5`\n   * @see https://atmospherejs.com/jparker/crypto-md5\n   * @param  {String} string input string\n   * @return {String}        md5 hash of the input\n   */\n  hash: function (string) {\n    var self = this;\n    // eslint-disable-next-line babel/new-cap\n    return md5(self.cleanString(string)).toString();\n  },\n\n  /**\n   * `imageUrl` will provide the url for the avatar, given an email or a hash\n   * and a set of options to be passed to the gravatar API\n   * @see https://en.gravatar.com/site/implement/images/\n   * @param  {String} emailOrHash email or pregenerated MD5 hash to query\n   * gravatar with.\n   * @param  {Object} options     options to be passed to gravatar in the query\n   * string. The `secure` will be used to determine which base url to use.\n   * @return {String}             complete url to the avatar\n   */\n  imageUrl: function (emailOrHash, options) {\n    var self = this;\n    options = options || {};\n\n    // Want HTTPS ?\n    var url = options.secure\n    ? 'https://secure.gravatar.com/avatar/'\n    : 'http://www.gravatar.com/avatar/';\n    delete options.secure;\n\n    // Is it an MD5 already ?\n    url += self.isHash(emailOrHash)\n    ? emailOrHash\n    : self.hash(emailOrHash);\n\n    // Have any options to pass ?\n    var params = _.map(options, function (val, key) {\n      return key + '=' + encodeURIComponent(val);\n    }).join('&');\n\n    return (params.length > 0)\n    ? url + '?' + params\n    : url;\n  },\n\n  // Default functionality. You can override these options by calling\n  // Users.avatar.setOptions (do not set this.options directly)\n\n  options: {\n\n    // Determines the type of fallback to use when no image can be found via\n    // linked services (Gravatar included):\n    //   'default image' (the default option, which will show either the image\n    //   specified by defaultImageUrl, the package's default image, or a Gravatar\n    //   default image)\n    //     OR\n    //   'initials' (show the user's initials).\n    fallbackType: '',\n\n    // Default avatar image's URL. It can be a relative path\n    // (relative to website's base URL, e.g. 'images/defaultthis.png').\n    defaultImageUrl: 'http://www.gravatar.com/avatar/?d=identicon',\n\n    // This property name will be used to fetch an avatar url from the user's profile\n    // (e.g. 'avatar'). If this property is set and a property of that name exists\n    // on the user's profile (e.g. user.profile.avatar) that property will be used\n    // as the avatar url.\n    customImageProperty: '',\n\n    // Gravatar default option to use (overrides default image URL)\n    // Options are available at:\n    // https://secure.gravatar.com/site/implement/images/#default-image\n    gravatarDefault: '',\n\n    // This property is used to prefix the CSS classes of the DOM elements.\n    // If no value is set, then the default CSS class assigned to all DOM elements are prefixed with 'avatar' as default.\n    // If a value is set to, 'foo' for example, the resulting CSS classes are prefixed with 'foo'.\n    cssClassPrefix: '',\n\n    // This property defines the various image sizes available\n    imageSizes: {\n      'large': 80,\n      'small': 30,\n      'extra-small': 20\n    },\n\n    // Default background color when displaying the initials.\n    // Can also be set to a function to map an user object to a background color.\n    backgroundColor: '#aaa',\n\n    // Default text color when displaying the initials.\n    // Can also be set to a function to map an user object to a text color.\n    textColor: '#fff',\n\n    // Generate the required CSS and include it in the head of your application.\n    // Setting this to false will exclude the generated CSS and leave the\n    // avatar unstyled by the package.\n    generateCSS: true\n  },\n\n  // Sets the Avatar options. You must use this setter function rather than assigning directly to\n  // this.options, otherwise the stylesheet won't be generated.\n\n  setOptions: function(options) {\n    this.options = _.extend(this.options, options);\n  },\n\n  // Returns the cssClassPrefix property from options\n  getCssClassPrefix: function () {\n    return this.options.cssClassPrefix ? this.options.cssClassPrefix : 'avatar';\n  },\n\n  // Returns a background color for initials\n  getBackgroundColor: function (user) {\n    if (_.isString(this.options.backgroundColor))\n      return this.options.backgroundColor;\n    else if (_.isFunction(this.options.backgroundColor))\n      return this.options.backgroundColor(user);\n  },\n\n  // Returns a text color for initials\n  getTextColor: function (user) {\n    if (_.isString(this.options.textColor))\n      return this.options.textColor;\n    else if (_.isFunction(this.options.textColor))\n      return this.options.textColor(user);\n  },\n\n  // Get the initials of the user\n  getInitials: function (user) {\n\n    let initials = '';\n    let name = '';\n    let parts = [];\n\n    if (user && user.profile && user.profile.firstName) {\n      initials = user.profile.firstName.charAt(0).toUpperCase();\n\n      if (user.profile.lastName) {\n        initials += user.profile.lastName.charAt(0).toUpperCase();\n      }\n      else if (user.profile.familyName) {\n        initials += user.profile.familyName.charAt(0).toUpperCase();\n      }\n      else if (user.profile.secondName) {\n        initials += user.profile.secondName.charAt(0).toUpperCase();\n      }\n    }\n    else {\n      if (user && user.profile && user.profile.name) {\n        name = user.profile.name;\n      }\n      else if (user && user.displayName) {\n        name = user.displayName;\n      }\n      else if (user && user.username) {\n        name = user.username;\n      }\n\n      parts = name.split(' ');\n      // Limit getInitials to first and last initial to avoid problems with\n      // very long multi-part names (e.g. 'Jose Manuel Garcia Galvez')\n      initials = _.first(parts).charAt(0).toUpperCase();\n      if (parts.length > 1) {\n        initials += _.last(parts).charAt(0).toUpperCase();\n      }\n    }\n\n    return initials;\n  },\n  \n  getUserStatus: function (user) {\n    const hostCompanyId = getSetting('hostCompany.companyId');\n  \n    if (Users.isAdmin(user)) {\n      return 'admin';\n    } else if (!!user.companyId && !!hostCompanyId && user.companyId === hostCompanyId) {\n      return 'host';\n    }\n  },\n\n  // Get the url of the user's avatar\n  // XXX: this.getUrl is a reactive function only when no user argument is specified.\n  getUrl: function (user) {\n\n    // Default to the currently logged in user, unless otherwise specified.\n    if (!user) return null;\n\n    var url = '';\n    var defaultUrl, svc;\n\n    if (user) {\n      svc = this.getService(user);\n      if (svc === 'twitter') {\n        // use larger image (200x200 is smallest custom option)\n        url = user.services.twitter.profile_image_url_https.replace('_normal.', '_200x200.');\n      }\n      else if (svc === 'facebook') {\n        // use larger image (~200x200)\n        url = 'https://graph.facebook.com/' + user.services.facebook.id + '/picture/?type=large';\n      }\n      else if (svc === 'google') {\n        url = user.services.google.picture;\n      }\n      else if (svc === 'github') {\n        url = 'https://avatars.githubusercontent.com/' + user.services.github.username + '?s=200';\n      }\n      else if (svc === 'instagram') {\n        url = user.services.instagram.profile_picture;\n      }\n      else if (svc === 'linkedin') {\n        url = user.services.linkedin.pictureUrl;\n      }\n      else if (svc === 'custom') {\n        url = this.getCustomUrl(user);\n      }\n      else if (svc === 'none') {\n        defaultUrl = this.options.defaultImageUrl;\n        // If it's a relative path (no '//' anywhere), complete the URL\n        if (defaultUrl.indexOf('//') === -1) {\n          // remove starting slash if it exists\n          if (defaultUrl.charAt(0) === '/') defaultUrl = defaultUrl.substr(1);\n          // Then add the relative path to the server's base URL\n          defaultUrl = Meteor.absoluteUrl(defaultUrl);\n        }\n        url = this.getGravatarUrl(user, defaultUrl);\n      }\n    }\n\n    return url;\n  },\n\n  getService: function (user) {\n    var services = user && user.services || {};\n    if (this.getCustomUrl(user)) { return 'custom'; }\n    var service = _.find([['twitter', 'profile_image_url_https'], ['facebook', 'id'], ['google', 'picture'], ['github', 'username'], ['instagram', 'profile_picture'], ['linkedin', 'pictureUrl']], function(s) { return !!services[s[0]] && s[1].length && !!services[s[0]][s[1]]; });\n    if(!service)\n      return 'none';\n    else\n      return service[0];\n  },\n\n  computeUrl: function(prop, user) {\n    if (typeof prop === 'function') {\n      prop = prop.call(user);\n    }\n    if (prop && typeof prop === 'string') {\n      return prop;\n    }\n  },\n\n  getDescendantProp: function (obj, desc) {\n    var arr = desc.split('.');\n    while(arr.length && (obj = obj[arr.shift()]));\n    return obj;\n  },\n\n  getCustomUrl: function (user) {\n\n    var customProp = user && this.options.customImageProperty;\n    if (typeof customProp === 'function') {\n      return this.computeUrl(customProp, user);\n    } else if (customProp) {\n      return this.computeUrl(this.getDescendantProp(user, customProp), user);\n    }\n  },\n\n  getGravatarUrl: function (user, defaultUrl) {\n    var gravatarDefault;\n    var validGravatars = ['404', 'mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank'];\n\n    // Initials are shown when Gravatar returns 404.\n    if (this.options.fallbackType !== 'initials') {\n      var valid = _.contains(validGravatars, this.options.gravatarDefault);\n      gravatarDefault = valid ? this.options.gravatarDefault : defaultUrl;\n    }\n    else {\n      gravatarDefault = '404';\n    }\n\n    var emailOrHash = this.getUserEmail(user) || Users.getEmailHash(user);\n    // var secure = true;\n    var options = {\n      // NOTE: Gravatar's default option requires a publicly accessible URL,\n      // so it won't work when your app is running on localhost and you're\n      // using an image with either the standard default image URL or a custom\n      // defaultImageUrl that is a relative path (e.g. 'images/defaultthis.png').\n      size: 200, // use 200x200 like twitter and facebook above (might be useful later)\n      default: gravatarDefault,\n      secure: true\n    };\n    return emailOrHash ? this.imageUrl(emailOrHash, options) : null;\n\n  },\n\n  // Get the user's email address\n  getUserEmail: function (user) {\n    var emails = _.pluck(user.emails, 'address');\n    return emails[0] || null;\n  },\n\n  // Returns the size class to use for an avatar\n  getSizeClass: function(context) {\n    // Defaults are 'large', 'small', 'extra-small', but user can add new ones\n    return this.options.imageSizes[context.size] ? this.getCssClassPrefix() + '-' + context.size : '';\n  },\n\n  // Returns the shape class for an avatar\n  getShapeClass: function (context) {\n    var valid = ['rounded', 'circle'];\n    return _.contains(valid, context.shape) ? this.getCssClassPrefix() + '-' + context.shape : '';\n  },\n\n  // Returns the custom class(es) for an avatar\n  getCustomClasses: function (context) {\n    return context.class ? context.class : '';\n  },\n\n  // Returns the initials text for an avatar\n  getInitialsText: function(user, context) {\n    return context.initials || this.getInitials(user);\n  }\n\n};\n\n// This will be replaced if the user calls setOptions in their own code\nUsers.avatar.setOptions({});\n\nexport const avatar = Users.avatar;\n"
  },
  {
    "path": "packages/vulcan-users/lib/modules/collection.js",
    "content": "import schema from './schema.js';\nimport { createCollection, getDefaultResolvers, getDefaultMutations } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core isn't loaded yet\n\n// check if user is mutating their own user document\nconst isCurrentUser = ({ user, document }) => (user && document && user._id === document._id);\n\n/**\n * @summary Vulcan Users namespace\n * @namespace Users\n */\nexport const Users = createCollection({\n\n  collection: Meteor.users,\n\n  collectionName: 'Users',\n\n  typeName: 'User',\n\n  schema,\n\n  resolvers: getDefaultResolvers({ typeName: 'User' }),\n\n  mutations: getDefaultMutations({ typeName: 'User' }),\n\n  description: 'A user object',\n\n  permissions: {\n    canRead: ['guests'],\n    canCreate: ['admins'], // non-admins have to create new users by signing up \n    canUpdate: isCurrentUser,\n    canDelete: isCurrentUser\n  }\n\n});\n\nexport default Users;\n"
  },
  {
    "path": "packages/vulcan-users/lib/modules/fragments.js",
    "content": "import { registerFragment } from 'meteor/vulcan:lib';\n\n// ------------------------------ Vote ------------------------------ //\n\n// note: fragment used by default on the UsersProfile fragment\nregisterFragment(`\n  fragment UsersCurrent on User {\n    _id\n    username\n    createdAt\n    isAdmin\n    displayName\n    email\n    emailHash\n    slug\n    groups\n    services\n    avatarUrl\n    pageUrl\n    locale\n  }\n`);\n"
  },
  {
    "path": "packages/vulcan-users/lib/modules/helpers.js",
    "content": "import { Utils } from 'meteor/vulcan:lib';\nimport Users from './collection.js';\nimport moment from 'moment';\nimport _ from 'underscore';\n\n////////////////////\n//  User Getters  //\n////////////////////\n\n/**\n * @summary Get a user\n * @param {String} userOrUserId\n */\nUsers.getUser = function(userOrUserId) {\n  if (typeof userOrUserId === 'undefined') {\n    if (!Meteor.user()) {\n      throw new Error();\n    } else {\n      return Meteor.user();\n    }\n  } else if (typeof userOrUserId === 'string') {\n    return Users.findOne(userOrUserId);\n  } else {\n    return userOrUserId;\n  }\n};\n\n/**\n * @summary Get a user's username (unique, no special characters or spaces)\n * @param {Object} user\n */\nUsers.getUserName = function(user) {\n  try {\n    if (user.username) return user.username;\n    if (user && user.services && user.services.twitter && user.services.twitter.screenName)\n      return user.services.twitter.screenName;\n  } catch (error) {\n    console.log(error); // eslint-disable-line\n    return null;\n  }\n};\nUsers.getUserNameById = function(userId) {\n  return Users.getUserName(Users.findOne(userId));\n};\n\n/**\n * @summary Get a user's display name (not unique, can take special characters and spaces)\n * @param {Object} user\n */\nUsers.getDisplayName = function(user) {\n  if (!user) {\n    return '';\n  } else {\n    return user.displayName ? user.displayName : Users.getUserName(user);\n  }\n};\nUsers.getDisplayNameById = function(userId) {\n  return Users.getDisplayName(Users.findOne(userId));\n};\nexport const getDisplayName = Users.getDisplayName;\n\n/**\n * @summary Get a user's profile URL\n * @param {Object} user (note: we only actually need either the _id or slug properties)\n * @param {Boolean} isAbsolute\n */\nUsers.getProfileUrl = function(user, isAbsolute) {\n  if (typeof user === 'undefined') {\n    return '';\n  }\n  isAbsolute = typeof isAbsolute === 'undefined' ? false : isAbsolute; // default to false\n  var prefix = isAbsolute ? Utils.getSiteUrl().slice(0, -1) : '';\n  if (user.slug) {\n    return `${prefix}/users/${user.slug}`;\n  } else {\n    return '';\n  }\n};\nexport const getProfileUrl = Users.getProfileUrl;\n\n/**\n * @summary Get a user's account edit URL\n * @param {Object} user (note: we only actually need either the _id or slug properties)\n * @param {Boolean} isAbsolute\n */\nUsers.getEditUrl = function(user, isAbsolute) {\n  return `${Users.getProfileUrl(user, isAbsolute)}/edit`;\n};\n\n/**\n * @summary Get a user's Twitter name\n * @param {Object} user\n */\nUsers.getTwitterName = function(user) {\n  // return twitter name provided by user, or else the one used for twitter login\n  if (typeof user !== 'undefined') {\n    if (user.twitterUsername) {\n      return user.twitterUsername;\n    } else if (Utils.checkNested(user, 'services', 'twitter', 'screenName')) {\n      return user.services.twitter.screenName;\n    }\n  }\n  return null;\n};\nUsers.getTwitterNameById = function(userId) {\n  return Users.getTwitterName(Users.findOne(userId));\n};\n\n/**\n * @summary Get a user's GitHub name\n * @param {Object} user\n */\nUsers.getGitHubName = function(user) {\n  // return twitter name provided by user, or else the one used for twitter login\n  if (Utils.checkNested(user, 'profile', 'github')) {\n    return user.profile.github;\n  } else if (Utils.checkNested(user, 'services', 'github', 'screenName')) {\n    // TODO: double-check this with GitHub login\n    return user.services.github.screenName;\n  }\n  return null;\n};\nUsers.getGitHubNameById = function(userId) {\n  return Users.getGitHubName(Users.findOne(userId));\n};\n\n/**\n * @summary Get a user's email\n * @param {Object} user\n */\nUsers.getEmail = function(user) {\n  if (user.email) {\n    return user.email;\n  } else {\n    return null;\n  }\n};\nUsers.getEmailById = function(userId) {\n  return Users.getEmail(Users.findOne(userId));\n};\n\n/**\n * @summary Get a user's email hash\n * @param {Object} user\n */\nUsers.getEmailHash = function(user) {\n  return user.emailHash;\n};\nUsers.getEmailHashById = function(userId) {\n  return Users.getEmailHash(Users.findOne(userId));\n};\n\n/**\n * @summary Get a user setting\n * @param {Object} user\n * @param {String} settingName\n * @param {Object} defaultValue\n */\nUsers.getSetting = function(user = null, settingName, defaultValue = null) {\n  if (user) {\n    const settingValue = Users.getProperty(user, settingName);\n    return typeof settingValue === 'undefined' ? defaultValue : settingValue;\n  } else {\n    return defaultValue;\n  }\n};\n\n////////////////////\n//  User Checks   //\n////////////////////\n\n/**\n * @summary Check if the user has completed their profile.\n * @param {Object} user\n */\nUsers.hasCompletedProfile = function(user) {\n  if (!user) return false;\n\n  return _.every(Users.getRequiredFields(), function(fieldName) {\n    return !!Utils.getNestedProperty(user, fieldName);\n  });\n};\n\n///////////////////\n// Other Helpers //\n///////////////////\n\nUsers.findLast = function(user, collection) {\n  return collection.findOne({ userId: user._id }, { sort: { createdAt: -1 } });\n};\n\nUsers.timeSinceLast = function(user, collection) {\n  var now = new Date().getTime();\n  var last = this.findLast(user, collection);\n  if (!last) return 999; // if this is the user's first post or comment ever, stop here\n  return Math.abs(Math.floor((now - last.createdAt) / 1000));\n};\n\nUsers.numberOfItemsInPast24Hours = function(user, collection) {\n  var mNow = moment();\n  var items = collection.find({\n    userId: user._id,\n    createdAt: {\n      $gte: mNow.subtract(24, 'hours').toDate(),\n    },\n  });\n  return items.count();\n};\n\nUsers.getProperty = function(object, property) {\n  // recursive function to get nested properties\n  var array = property.split('.');\n  if (array.length > 1) {\n    var parent = array.shift();\n    // if our property is not at this level, call function again one level deeper if we can go deeper, else return undefined\n    return typeof object[parent] === 'undefined'\n      ? undefined\n      : this.getProperty(object[parent], array.join('.'));\n  } else {\n    // else return property\n    return object[array[0]];\n  }\n};\n\n// Users.setSetting = (user, settingName, value) => {\n//   // all users settings should begin with the prexi : user.setting namespace, so add \"\" if needed\n//   var field = settingName.slice(0,2) === \"\" ? settingName : \"\" + settingName;\n\n//   var modifier = {$set: {}};\n//   modifier.$set[field] = value;\n\n//   Users.update(user._id, modifier);\n// }\n\n////////////////////\n// More Helpers   //\n////////////////////\n\n// helpers that don't take a user as argument\n\n/**\n * @summary @method Users.getRequiredFields\n * Get a list of all fields required for a profile to be complete.\n */\nUsers.getRequiredFields = function() {\n  var schema = Users.simpleSchema()._schema;\n  var fields = _.filter(_.keys(schema), function(fieldName) {\n    var field = schema[fieldName];\n    return !!field.mustComplete;\n  });\n  return fields;\n};\n\n// Users.adminUsers = function (options) {\n//   return this.find({isAdmin : true}, options).fetch();\n// };\n\n// Users.getCurrentUserEmail = function () {\n//   return Meteor.user() ? Users.getEmail(Meteor.user()) : '';\n// };\n\nUsers.findByEmail = function(email) {\n  return Users.findOne({ email: email });\n};\n"
  },
  {
    "path": "packages/vulcan-users/lib/modules/index.js",
    "content": "import Users from './collection.js';\n\nexport * from './avatar.js';\nexport * from './fragments.js';\nexport * from './helpers.js';\nexport * from './permissions.js';\nexport * from './views.js';\n\nexport default Users;\n"
  },
  {
    "path": "packages/vulcan-users/lib/modules/mutations.js",
    "content": "// import { createMutator, updateMutator, deleteMutator, Utils, Connectors, registerCallback } from 'meteor/vulcan:lib';\n// import Users from './collection'; // TODO: circular dependency?\n// import isEmpty from 'lodash/isEmpty';\n\n// const performCheck = (mutation, user, document) => {\n//   if (!mutation.check(user, document))\n//     throw new Error(\n//       Utils.encodeIntlError({ id: 'app.mutation_not_allowed', value: `\"${mutation.name}\" on _id \"${document._id}\"` })\n//     );\n// };\n\n// const createMutation = {\n//   name: 'createUser',\n\n//   check(user, document) {\n//     if (!user) return false;\n//     // OpenCRUD backwards compatibility\n//     return Users.canDo(user, ['user.create', 'users.new']);\n//   },\n\n//   mutation(root, { data }, context) {\n//     const { Users, currentUser } = context;\n//     performCheck(this, currentUser, data);\n\n//     return createMutator({\n//       collection: Users,\n//       data,\n//       currentUser,\n//       validate: true,\n//       context,\n//     });\n//   },\n// };\n\n// const updateMutation = {\n//   name: 'updateUser',\n\n//   check(user, document) {\n//     if (!user || !document) return false;\n//     // OpenCRUD backwards compatibility\n//     return Users.owns(user, document) ? Users.canDo(user, ['user.update.own', 'users.edit.own']) : Users.canDo(user, ['user.update.all', 'users.edit.all']);\n//   },\n\n//   async mutation(root, { selector, data }, context) {\n//     const { Users, currentUser } = context;\n\n//     const document = await Connectors.get(Users, selector);\n\n//     if (!document) {\n//       throw new Error(`Could not find document to update for selector: ${JSON.stringify(selector)}`);\n//     }\n\n//     performCheck(this, currentUser, document);\n\n//     return updateMutator({\n//       collection: Users,\n//       selector,\n//       data,\n//       currentUser,\n//       validate: true,\n//       context,\n//     });\n//   },\n// };\n\n// const deleteMutation = {\n//   name: 'deleteUser',\n\n//   check(user, document) {\n//     if (!user || !document) return false;\n//     // OpenCRUD backwards compatibility\n//     return Users.owns(user, document) ? Users.canDo(user, ['user.delete.own', 'users.remove.own']) : Users.canDo(user, ['user.delete.all', 'users.remove.all']);\n//   },\n\n//   async mutation(root, { selector }, context) {\n\n//     const { Users, currentUser } = context;\n//     if (isEmpty(selector) || (!selector._id && !selector.documentId && !selector.slug)) {\n//       throw new Error('Selector cannot be empty');\n//     }\n//     const document = await Connectors.get(Users, selector);\n\n//     if (!document) {\n//       throw new Error(`Could not find document to delete for selector: ${JSON.stringify(selector)}`);\n//     }\n\n//     performCheck(this, currentUser, document);\n\n//     return deleteMutator({\n//       collection: Users,\n//       selector: { _id: document._id },\n//       currentUser,\n//       validate: true,\n//       context,\n//     });\n//   },\n// };\n// const mutations = {\n//   create: createMutation,\n//   update: updateMutation,\n//   delete: deleteMutation,\n\n//   // OpenCRUD backwards compatibility\n\n//   new: createMutation,\n//   edit: updateMutation,\n//   remove: deleteMutation,\n// };\n\n// export default mutations;\n\n// registerCallback({\n//   name: 'user.create.validate',\n//   iterator: { document: 'the document being inserted' },\n//   properties: [\n//     { document: 'The document being inserted' },\n//     { currentUser: 'The current user' },\n//     { validationErrors: 'An object that can be used to accumulate validation errors' },\n//   ],\n//   runs: 'sync',\n//   returns: 'document',\n//   description: 'Validate a document before insertion (can be skipped when inserting directly on server).',\n// });\n// registerCallback({\n//   name: 'user.create.before',\n//   iterator: { document: 'the document being inserted' },\n//   properties: [\n//     { document: 'The document being inserted' },\n//     { currentUser: 'The current user' },\n//     { validationErrors: 'An object that can be used to accumulate validation errors' },\n//   ],\n//   runs: 'sync',\n//   returns: 'document',\n//   description: 'Perform operations on a new document before it\\'s inserted in the database.',\n// });\n// registerCallback({\n//   name: 'user.create.after',\n//   iterator: { document: 'the document after being inserted in the database' },\n//   properties: [\n//     { currentUser: 'The current user' },\n//   ],\n//   runs: 'sync',\n//   returns: 'document',\n//   description: 'Perform operations on a new document after it\\'s inserted in the database but *before* the mutation returns it.',\n// });\n// registerCallback({\n//   name: 'user.create.async',\n//   iterator: { document: 'The document being inserted' },\n//   properties: [\n//     { data: 'The document being inserted' }, //for backward compatibility\n//     { collection: 'The Users collection' }\n//   ],\n//   runs: 'async',\n//   returns: null,\n//   description: 'Perform operations on a new user after it\\'s inserted in the database asynchronously.',\n// });\n// registerCallback({\n//   name: 'user.update.validate',\n//   iterator: { data: 'The client data' },\n//   properties: [\n//     { document: 'The document being updated' },\n//     { currentUser: 'The current user.' },\n//     { validationErrors: 'an object that can be used to accumulate validation errors.' },\n//   ],\n//   runs: 'sync',\n//   description: 'Validate a document before update (can be skipped when updating directly on server).'\n// });\n// registerCallback({\n//   name: 'user.update.before',\n//   iterator: { data: 'The client data' },\n//   properties: [\n//     { document: 'The document being edited' },\n//     { currentUser: 'The current user' },\n//     { newDocument: 'A preview of the future document' },\n//   ],\n//   runs: 'sync',\n//   description: 'Perform operations on a document before it\\'s updated on the database.',\n// });\n// registerCallback({\n//   name: 'user.update.after',\n//   iterator: { newdocument: 'The document after the update' },\n//   properties: [\n//     { document: 'The document before the update' },\n//     { currentUser: 'The current user' },\n//   ],\n//   runs: 'sync',\n//   description: 'Perform operations on a document after it\\'s updated in the database but *before* the mutation returns it.'\n// });\n// registerCallback({\n//   name: 'user.update.async',\n//   properties: [\n//     { newDocument: 'The document after the update' },\n//     { document: 'The document before the update' },\n//     { currentUser: 'The current user' },\n//     { collection: 'The Users collection' },\n//   ],\n//   runs: 'async',\n//   description: 'Perform operations on a document after it\\'s updated in the database asynchronously.'\n// });\n// registerCallback({\n//   name: 'user.delete.validate',\n//   iterator: { document: 'The document being deleted' },\n//   properties: [\n//     { currentUser: 'The current user' },\n//   ],\n//   runs: 'sync',\n//   description: 'Validate a document before deletion (can be skipped when deleting directly on the server)'\n// });\n// registerCallback({\n//   name: 'user.delete.before',\n//   iterator: { document: 'The document being deleted' },\n//   properties: [\n//     { currentUser: 'The current user' },\n//   ],\n//   runs: 'sync',\n//   description: 'Perform operations on a document before it\\'s deleted from the database',\n// });\n// registerCallback({\n//   name: 'user.delete.async',\n//   properties: [\n//     { document: 'The document being deleted' },\n//     { currentUser: 'The current user' },\n//     { collection: 'The Users collection' },\n//   ],\n//   runs: 'async',\n//   description: 'Perform operations on a document after it\\'s deleted from the database asynchronously.'\n// });\n"
  },
  {
    "path": "packages/vulcan-users/lib/modules/permissions.js",
    "content": "import Users from './collection.js';\nimport intersection from 'lodash/intersection';\nimport compact from 'lodash/compact';\nimport map from 'lodash/map';\nimport difference from 'lodash/difference';\nimport get from 'lodash/get';\nimport unset from 'lodash/unset';\nimport cloneDeep from 'lodash/cloneDeep';\nimport { getCollection, forEachDocumentField, Utils, deprecate } from 'meteor/vulcan:lib';\n\n/**\n * @summary Users.groups object\n */\nUsers.groups = {};\n\n/**\n * @summary Group class\n */\nclass Group {\n  constructor() {\n    this.actions = [];\n  }\n\n  can(actions) {\n    actions = Array.isArray(actions) ? actions : [actions];\n    this.actions = this.actions.concat(actions.map(a => a.toLowerCase()));\n  }\n\n  cannot(actions) {\n    actions = Array.isArray(actions) ? actions : [actions];\n    this.actions = _.difference(\n      this.actions,\n      actions.map(a => a.toLowerCase())\n    );\n  }\n}\n\n////////////////////\n// Helpers        //\n////////////////////\n\n/**\n * @summary create a new group\n * @param {String} groupName\n */\nUsers.createGroup = groupName => {\n  Users.groups[groupName] = new Group();\n};\n\n/**\n * @summary get a list of a user's groups\n * @param {Object} user\n */\nUsers.getGroups = (user, document) => {\n  let userGroups = ['guests'];\n\n  if (user) {\n    userGroups.push('members');\n\n    if (document && Users.owns(user, document)) {\n      userGroups.push('owners');\n    }\n\n    if (user.groups) {\n      // custom groups\n      userGroups = userGroups.concat(user.groups);\n    }\n\n    if (Users.isAdmin(user)) {\n      // admin\n      userGroups.push('admins');\n    }\n  }\n\n  return userGroups;\n};\n\n/**\n * @summary get a list of all the actions a user can perform\n * @param {Object} user\n */\nUsers.getActions = user => {\n  let userGroups = Users.getGroups(user);\n  if (!userGroups.includes('guests')) {\n    // always give everybody permission for guests actions, too\n    userGroups.push('guests');\n  }\n  let groupActions = userGroups.map(groupName => {\n    // note: make sure groupName corresponds to an actual group\n    const group = Users.groups[groupName];\n    return group && group.actions;\n  });\n  return _.unique(_.flatten(groupActions));\n};\n\n/**\n * @summary check if a user is a member of a group\n * @param {Array} user\n * @param {String} group or array of groups\n */\nUsers.isMemberOf = (user, groupOrGroups, document) => {\n  const groups = Array.isArray(groupOrGroups) ? groupOrGroups : [groupOrGroups];\n  return intersection(Users.getGroups(user, document), groups).length > 0;\n};\n\n/**\n * @summary check if a user can perform at least one of the specified actions\n * @param {Object} user\n * @param {String/Array} action or actions\n */\nUsers.canDo = (user, actionOrActions) => {\n  const authorizedActions = Users.getActions(user);\n  const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];\n  return Users.isAdmin(user) || intersection(authorizedActions, actions).length > 0;\n};\n\n// DEPRECATED\n// TODO: remove this\n/**\n * @summary Check if a user can edit a document\n * @param {Object} user - The user performing the action\n * @param {Object} document - The document being edited\n */\n// Users.canEdit = function (user, document) {\n\n//   user = (typeof user === 'undefined') ? Meteor.user() : user;\n\n//   // note(apollo): use of `__typename` given by react-apollo\n//   //const collectionName = document.getCollectionName();\n//   const collectionName = document.__typename ? Utils.getCollectionNameFromTypename(document.__typename) : document.getCollectionName();\n\n//   if (!user || !document) {\n//     return false;\n//   }\n\n//   if (document.hasOwnProperty('isDeleted') && document.isDeleted) return false;\n\n//   if (Users.owns(user, document)) {\n//     // if this is user's document, check if user can edit own documents\n//     return Users.canDo(user, `${collectionName}.edit.own`);\n//   } else {\n//     // if this is not user's document, check if they can edit all documents\n//     return Users.canDo(user, `${collectionName}.edit.all`);\n//   }\n\n// };\n\n/**\n * @summary Check if a user owns a document\n * @param {Object|string} userOrUserId - The user or their userId\n * @param {Object} document - The document to check (post, comment, user object, etc.)\n */\nUsers.owns = function(user, document) {\n  try {\n    if (!!document.userId) {\n      // case 1: use userId to check\n      return user._id === document.userId;\n    } else if (document.user) {\n      // case 2: use user._id to check\n      return user._id === document.user._id;\n    }else {\n      // case 3: document is a user, use _id to check\n      return user._id === document._id;\n    }\n  } catch (e) {\n    return false; // user not logged in\n  }\n};\n\n/**\n * @summary Check if a user is an admin\n * @param {Object|string} userOrUserId - The user or their userId\n */\nUsers.isAdmin = function(userOrUserId) {\n  try {\n    var user = Users.getUser(userOrUserId);\n    return !!user && !!user.isAdmin;\n  } catch (e) {\n    return false; // user not logged in\n  }\n};\nUsers.isAdminById = Users.isAdmin;\n\nexport const isAdmin = Users.isAdmin;\n\n/**\n * @summary Check if a user can view a field\n * @param {Object} user - The user performing the action\n * @param {Object} field - The schema of the requested field\n * @param {Object} field - The full document of the collection\n * @returns {Boolean} - true if the user can read the field, false if not\n */\nUsers.canReadField = function(user, field, document) {\n  const canRead = field.canRead || field.viewableBy; //OpenCRUD backwards compatibility\n  if (canRead) {\n    if (typeof canRead === 'function') {\n      // if canRead is a function, execute it with user and document passed. it must return a boolean\n      return canRead(user, document);\n    } else if (typeof canRead === 'string') {\n      // if canRead is just a string, we assume it's the name of a group and pass it to isMemberOf\n      return canRead === 'guests' || Users.isMemberOf(user, canRead, document);\n    } else if (Array.isArray(canRead) && canRead.length > 0) {\n      // if canRead is an array, we do a recursion on every item and return true if one of the items return true\n      return canRead.some(group => Users.canReadField(user, { canRead: group }, document));\n    }\n  }\n  return false;\n};\n\n/**\n * @summary Get a list of fields viewable by a user\n * @param {Object} user - The user performing the action\n * @param {Object} collection - The collection\n * @param {Object} document - Optionally, get a list for a specific document\n */\nUsers.getViewableFields = function(user, collection, document) {\n  deprecate(\n    '1.13.4',\n    'getViewableFields is deprecated. Use Users.getReadableProjection to get a Mongo projection, or Users.getReadableFields if you need an array of field.'\n  );\n  return Users.getReadableProjection(user, collection, document);\n};\n\nUsers.getReadableFields = function(user, collection, document) {\n  return compact(\n    map(collection.simpleSchema()._schema, (field, fieldName) => {\n      if (fieldName.indexOf('.$') > -1) return null;\n      return Users.canReadField(user, field, document) ? fieldName : null;\n    })\n  );\n};\n\nUsers.getReadableProjection = function(user, collection, document) {\n  return Utils.arrayToFields(Users.getReadableFields(user, collection, document));\n};\n\n/**\n * Check if field canRead include a permission that needs to be checked against the actual document and not just from the user\n */\nUsers.isDocumentBasedPermissionField = field => {\n  const canRead = field.canRead || field.viewableBy; //OpenCRUD backwards compatibility\n  if (canRead) {\n    if (typeof canRead === 'function') {\n      return true;\n    } else if (canRead === 'owners') {\n      return true;\n    } else if (Array.isArray(canRead) && canRead.length > 0) {\n      // recursive call on if canRead is an array\n      return canRead.some(group => Users.isDocumentBasedPermissionField({ canRead: group }));\n    }\n  }\n  return false;\n};\n\n/**\n * Retrieve fields that needs the document to be already fetched to be checked, and not just the user\n * => owners permissions, custom permissions etc.\n */\nUsers.getDocumentBasedPermissionFieldNames = function(collection) {\n  const schema = collection.simpleSchema()._schema;\n  const documentBasedFieldNames = Object.keys(schema).filter(fieldName => {\n    if (fieldName.indexOf('.$') > -1) return false; // ignore arrays\n    const field = schema[fieldName];\n    if (Users.isDocumentBasedPermissionField(field)) return true;\n    return false;\n  });\n  return documentBasedFieldNames;\n};\n\n// collection helper\nUsers.helpers({\n  getViewableFields(collection, document) {\n    return Users.getViewableFields(this, collection, document);\n  },\n});\n\n/**\n * @summary Check if a user can access a list of fields\n * @param {Object} user - The user performing the action\n * @param {Object} collection - The collection\n * @param {Object} fields - The list of fields\n */\nUsers.checkFields = (user, collection, fields) => {\n  const viewableFields = Users.getReadableFields(user, collection);\n  // Initial case without document => we ignore fields that need the document to be checked\n  const ambiguousFields = Users.getDocumentBasedPermissionFieldNames(collection); // these fields need to wait for the document to be present before being checked\n  const fieldsToTest = difference(fields, ambiguousFields); // we only check non-ambiguous fields (canRead: [\"guests\", \"admins\"] for instance)\n  const diff = difference(fieldsToTest, viewableFields);\n\n  if (diff.length) {\n    throw new Error(\n      `You don't have permission to filter collection ${collection.options.collectionName} by the following fields: ${diff.join(\n        ', '\n      )}. Field is not readable or do not exist.`\n    );\n  }\n  return true;\n};\n\n/**\n * Check if user was allowed to filter this document based on some fields\n * @param {Object} user - The user performing the action\n * @param {Object} collection - The collection\n * @param {Object} fields - The list of filtered fields\n * @param {Object} document - The retrieved document\n */\nUsers.canFilterDocument = (user, collection, fields, document) => {\n  const viewableFields = Users.getReadableFields(user, collection, document);\n  const diff = difference(fields, viewableFields);\n  return !diff.length; // if length is > 0, it means that this document wasn't filterable by user in the first place, based on provided filter, we must remove it\n};\n\nconst restrictDocument = (document, schema, currentUser) => {\n  let restrictedDocument = cloneDeep(document);\n  forEachDocumentField(document, schema, ({ fieldName, fieldSchema, currentPath, isNested }) => {\n    if (isNested && (!fieldSchema || !fieldSchema.canRead)) return; // ignore nested fields without permissions\n    if (!fieldSchema || !Users.canReadField(currentUser, fieldSchema, document)) {\n      unset(restrictedDocument, `${currentPath}${fieldName}`);\n    }\n  });\n  return restrictedDocument;\n};\n/**\n * @summary For a given document or list of documents, keep only fields viewable by current user\n * @param {Object} user - The user performing the action\n * @param {Object} collection - The collection\n * @param {Object} document - The document being returned by the resolver\n */\nUsers.restrictViewableFields = function(user, collection, docOrDocs) {\n  if (!docOrDocs) return {};\n  const schema = collection.simpleSchema()._schema;\n  const restrictDoc = document => restrictDocument(document, schema, user);\n\n  return Array.isArray(docOrDocs) ? docOrDocs.map(restrictDoc) : restrictDoc(docOrDocs);\n};\n\n/**\n * @summary For a given of documents, keep only documents and fields viewable by current user (new APIs)\n * @param {Object} user - The user performing the action\n * @param {Object} collection - The collection\n * @param {Object} document - The document being returned by the resolver\n */\nUsers.restrictDocuments = function({ user, collection, documents }) {\n  const check = get(collection, 'options.permissions.canRead');\n  let readableDocuments = documents;\n  if (check) {\n    readableDocuments = documents.filter(document => Users.canRead({ collection, document, user }));\n  }\n  const restrictedDocuments = Users.restrictViewableFields(user, collection, readableDocuments);\n  return restrictedDocuments;\n};\n\n/**\n * @summary Check if a user can submit a field\n * @param {Object} user - The user performing the action\n * @param {Object} field - The field being edited or inserted\n */\nUsers.canCreateField = function(user, field) {\n  const canCreate = field.canCreate || field.insertableBy; //OpenCRUD backwards compatibility\n  if (canCreate) {\n    if (typeof canCreate === 'function') {\n      // if canCreate is a function, execute it with user and document passed. it must return a boolean\n      return canCreate(user);\n    } else if (typeof canCreate === 'string') {\n      // if canCreate is just a string, we assume it's the name of a group and pass it to isMemberOf\n      // note: if canCreate is 'guests' then anybody can create it\n      return canCreate === 'guests' || Users.isMemberOf(user, canCreate);\n    } else if (Array.isArray(canCreate) && canCreate.length > 0) {\n      // if canCreate is an array, we do a recursion on every item and return true if one of the items return true\n      return canCreate.some(group => Users.canCreateField(user, { canCreate: group }));\n    }\n  }\n  return false;\n};\n\n/** @function\n * Check if a user can edit a field\n * @param {Object} user - The user performing the action\n * @param {Object} field - The field being edited or inserted\n * @param {Object} document - The document being edited or inserted\n */\nUsers.canUpdateField = function(user, field, document) {\n  const canUpdate = field.canUpdate || field.editableBy; //OpenCRUD backwards compatibility\n\n  if (canUpdate) {\n    if (typeof canUpdate === 'function') {\n      // if canUpdate is a function, execute it with user and document passed. it must return a boolean\n      return canUpdate(user, document);\n    } else if (typeof canUpdate === 'string') {\n      // if canUpdate is just a string, we assume it's the name of a group and pass it to isMemberOf\n      // note: if canUpdate is 'guests' then anybody can create it\n      return canUpdate === 'guests' || Users.isMemberOf(user, canUpdate, document);\n    } else if (Array.isArray(canUpdate) && canUpdate.length > 0) {\n      // if canUpdate is an array, we look at every item and return true if one of the items return true\n      return canUpdate.some(group => Users.canUpdateField(user, { canUpdate: group }, document));\n    }\n  }\n  return false;\n};\n\n/** @function\n * Check if a user passes a permission check (new API)\n * @param {Object} check - The permission check being tested\n * @param {Object} user - The user performing the action\n * @param {Object} document - The document being edited or inserted\n */\n// TODO: functions should take priority over admins status?\nUsers.permissionCheck = options => {\n  const { check, user, document } = options;\n  if (Users.isAdmin(user)) {\n    // admins always pass all permission checks\n    return true;\n  } else if (typeof check === 'function') {\n    return check(options);\n  } else if (Array.isArray(check)) {\n    return Users.isMemberOf(user, check, document);\n  }\n};\n\nUsers.canRead = options => {\n  const { collectionName, collection = getCollection(collectionName) } = options;\n  const check = get(collection, 'options.permissions.canRead');\n  if (!check) {\n    // eslint-disable-next-line no-console\n    console.warn(\n      `Users.canRead() was called but no [canRead] permission was defined for collection [${collection.options.collectionName}]`\n    );\n  }\n  return check && Users.permissionCheck({ ...options, check, operationName: 'read' });\n};\n\nUsers.canCreate = options => {\n  const { collectionName, collection = getCollection(collectionName) } = options;\n  const check = get(collection, 'options.permissions.canCreate');\n  if (!check) {\n    // eslint-disable-next-line no-console\n    console.warn(\n      `Users.canCreate() was called but no [canCreate] permission was defined for collection [${collection.options.collectionName}]`\n    );\n  }\n  return check && Users.permissionCheck({ ...options, check, operationName: 'create' });\n};\n\nUsers.canUpdate = options => {\n  const { collectionName, collection = getCollection(collectionName) } = options;\n  const check = get(collection, 'options.permissions.canUpdate');\n  if (!check) {\n    // eslint-disable-next-line no-console\n    console.warn(\n      `Users.canUpdate() was called but no [canUpdate] permission was defined for collection [${collection.options.collectionName}]`\n    );\n  }\n  return check && Users.permissionCheck({ ...options, check, operationName: 'update' });\n};\n\nUsers.canDelete = options => {\n  const { collectionName, collection = getCollection(collectionName) } = options;\n  const check = get(collection, 'options.permissions.canDelete');\n  if (!check) {\n    // eslint-disable-next-line no-console\n    console.warn(\n      `Users.canDelete() was called but no [canDelete] permission was defined for collection [${collection.options.collectionName}]`\n    );\n  }\n  return check && Users.permissionCheck({ ...options, check, operationName: 'delete' });\n};\n\n////////////////////\n// Initialize     //\n////////////////////\n\n/**\n * @summary initialize the 3 out-of-the-box groups\n */\nUsers.createGroup('guests'); // non-logged-in users\nUsers.createGroup('members'); // regular users\n\nconst membersActions = [\n  'user.create',\n  'user.update.own',\n  // OpenCRUD backwards compatibility\n  'users.new',\n  'users.edit.own',\n  'users.remove.own',\n];\nUsers.groups.members.can(membersActions);\n\nUsers.createGroup('admins'); // admin users\n\nconst adminActions = [\n  'user.create',\n  'user.update.all',\n  'user.delete.all',\n  'setting.update',\n  // OpenCRUD backwards compatibility\n  'users.new',\n  'users.edit.all',\n  'users.remove.all',\n  'settings.edit',\n];\nUsers.groups.admins.can(adminActions);\n"
  },
  {
    "path": "packages/vulcan-users/lib/modules/schema.js",
    "content": "import SimpleSchema from 'simpl-schema';\nimport { Utils, getCollection, Connectors, Locales, getType, addTypeAndResolvers } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core isn't loaded yet\nimport isEmpty from 'lodash/isEmpty';\n\n///////////////////////////////////////\n// Order for the Schema is as follows. Change as you see fit:\n// 00.\n// 10. Display Name\n// 20. Email\n// 30. Bio\n// 40. Slug\n// 50. Website\n// 60. Twitter username\n// 70.\n// 80.\n// 90.\n// 100.\n// Anything else..\n///////////////////////////////////////\n\nconst createDisplayName = user => {\n  const profileName = Utils.getNestedProperty(user, 'profile.name');\n  const twitterName = Utils.getNestedProperty(user, 'services.twitter.screenName');\n  const linkedinFirstName = Utils.getNestedProperty(user, 'services.linkedin.firstName');\n  if (profileName) return profileName;\n  if (twitterName) return twitterName;\n  if (linkedinFirstName)\n    return `${linkedinFirstName} ${Utils.getNestedProperty(user, 'services.linkedin.lastName')}`;\n  if (user.username) return user.username;\n  if (user.email) return user.email.slice(0, user.email.indexOf('@'));\n  return undefined;\n};\n\nconst adminGroup = {\n  name: 'admin',\n  order: 100,\n};\n\nconst ownsOrIsAdmin = (user, document) => {\n  return getCollection('Users').owns(user, document) || getCollection('Users').isAdmin(user);\n};\n\nconst UserEmail = {\n  address: {\n    type: String,\n    regEx: SimpleSchema.RegEx.Email,\n    optional: true,\n    canRead: ['admin']\n  },\n  verified: {\n    type: Boolean,\n    optional: true,\n    canRead: ['admin']\n  },\n};\n// addTypeAndResolvers({ typeName: 'UserEmail', schema: new SimpleSchema(UserEmail), description: 'The known emails of the user' });\n\n/**\n * @summary Users schema\n * @type {Object}\n */\nconst schema = {\n  _id: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n  },\n  username: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n    canUpdate: ['admins'],\n    canCreate: ['members'],\n    onCreate: ({ document: user }) => {\n      if (\n        !user.username &&\n        user.services &&\n        user.services.twitter &&\n        user.services.twitter.screenName\n      ) {\n        return user.services.twitter.screenName;\n      }\n    },\n    searchable: true,\n  },\n  emails: {\n    type: Array,\n    optional: true,\n    canRead: ['admins'],\n    onCreate: ({ document }) => {\n      // simulate Accounts behaviour\n      if (!document.emails && document.email) return [{ address: document.email }];\n    }\n  },\n  'emails.$': {\n    type: Object,\n    //...getType('UserEmail'), // nested-schemas experiment\n    optional: true,\n  },\n  createdAt: {\n    type: Date,\n    optional: true,\n    canRead: ['admins'],\n    onCreate: () => {\n      return new Date();\n    },\n  },\n  isAdmin: {\n    type: Boolean,\n    label: 'Admin',\n    input: 'checkbox',\n    optional: true,\n    canCreate: ['admins'],\n    canUpdate: ['admins'],\n    canRead: ['guests'],\n    group: adminGroup,\n  },\n\n  locale: {\n    type: String,\n    label: 'Preferred Language',\n    optional: true,\n    input: 'select',\n    canCreate: ['members'],\n    canUpdate: ownsOrIsAdmin,\n    canRead: ['guests'],\n    options: () => Locales.map(({ id, label }) => ({ value: id, label })),\n    hidden: () => Locales.length <= 1,\n  },\n\n  profile: {\n    type: Object,\n    optional: true,\n    blackbox: true,\n    hidden: true,\n    canCreate: ['members'],\n  },\n  // // telescope-specific data, kept for backward compatibility and migration purposes\n  // telescope: {\n  //   type: Object,\n  //   blackbox: true,\n  //   optional: true,\n  // },\n  services: {\n    type: Object,\n    optional: true,\n    blackbox: true,\n    canRead: ownsOrIsAdmin,\n  },\n  /**\n    The name displayed throughout the app. Can contain spaces and special characters, doesn't need to be unique\n  */\n  displayName: {\n    type: String,\n    optional: true,\n    input: 'text',\n    canCreate: ['members'],\n    canUpdate: ownsOrIsAdmin,\n    canRead: ['guests'],\n    order: 10,\n    onCreate: ({ document: user }) => {\n      return user.displayName && !isEmpty(user.displayName) ? user.displayName : createDisplayName(user);\n    },\n    searchable: true,\n  },\n  /**\n    The user's email. Modifiable.\n  */\n  email: {\n    type: String,\n    optional: true,\n    regEx: SimpleSchema.RegEx.Email,\n    mustComplete: true,\n    input: 'text',\n    canCreate: ['members'],\n    canUpdate: ownsOrIsAdmin,\n    canRead: ownsOrIsAdmin,\n    order: 20,\n    onCreate: ({ document: user }) => {\n      // look in a few places for the user email\n      const meteorEmails = Utils.getNestedProperty(user, 'services.meteor-developer.emails');\n      const facebookEmail = Utils.getNestedProperty(user, 'services.facebook.email');\n      const githubEmail = Utils.getNestedProperty(user, 'services.github.email');\n      const googleEmail = Utils.getNestedProperty(user, 'services.google.email');\n      const linkedinEmail = Utils.getNestedProperty(user, 'services.linkedin.emailAddress');\n\n      if (meteorEmails) return _.findWhere(meteorEmails, { primary: true }).address;\n      if (facebookEmail) return facebookEmail;\n      if (githubEmail) return githubEmail;\n      if (googleEmail) return googleEmail;\n      if (linkedinEmail) return linkedinEmail;\n      return undefined;\n    },\n    searchable: true,\n    // unique: true // note: find a way to fix duplicate accounts before enabling this\n  },\n  /**\n    A hash of the email, used for Gravatar // TODO: change this when email changes\n  */\n  emailHash: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n    onCreate: ({ document: user }) => {\n      if (user.email) {\n        return getCollection('Users').avatar.hash(user.email);\n      }\n    },\n  },\n  avatarUrl: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n    onCreate: ({ document: user }) => {\n      const twitterAvatar = Utils.getNestedProperty(\n        user,\n        'services.twitter.profile_image_url_https'\n      );\n      const facebookId = Utils.getNestedProperty(user, 'services.facebook.id');\n\n      if (twitterAvatar) return twitterAvatar;\n      if (facebookId) return `https://graph.facebook.com/${facebookId}/picture?type=large`;\n      return undefined;\n    },\n    resolveAs: {\n      fieldName: 'avatarUrl',\n      type: 'String',\n      resolver: async (user, args, { Users }) => {\n        if (_.isEmpty(user)) return null;\n\n        if (user.avatarUrl) {\n          return user.avatarUrl;\n        } else {\n          // user has already been cleaned up by Users.restrictViewableFields, so we\n          // reload the full user object from the cache to access user.services\n          const fullUser = await Users.loader.load(user._id);\n          return Users.avatar.getUrl(fullUser);\n        }\n      },\n    },\n  },\n  /**\n    The user's profile URL slug // TODO: change this when displayName changes\n  */\n  slug: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n    order: 40,\n    onCreate: ({ document: user }) => {\n      // create a basic slug from display name and then modify it if this slugs already exists;\n      const displayName = createDisplayName(user);\n      const basicSlug = Utils.slugify(displayName);\n      //if the basic slug is falsy, use the user id instead to avoid empty slugs\n      return basicSlug ? Utils.getUnusedSlugByCollectionName('Users', basicSlug) : user._id;\n    },\n  },\n  /**\n  The user's Twitter username\n*/\n  // twitterUsername: {\n  //   type: String,\n  //   optional: true,\n  //   input: 'text',\n  //   canCreate: ['members'],\n  //   canUpdate: ['members'],\n  //   canRead: ['guests'],\n  //   order: 60,\n  //   resolveAs: {\n  //     type: 'String',\n  //     resolver: async (user, args, { Users }) => {\n  //       return Users.getTwitterName(await Connectors.get(Users, user._id));\n  //     },\n  //   },\n  //   onCreate: ({ document: user }) => {\n  //     if (user.services && user.services.twitter && user.services.twitter.screenName) {\n  //       return user.services.twitter.screenName;\n  //     }\n  //   },\n  // },\n  /**\n    Groups\n  */\n  groups: {\n    type: Array,\n    optional: true,\n    input: 'checkboxgroup',\n    canCreate: ['admins'],\n    canUpdate: ['admins'],\n    canRead: ['guests'],\n    group: adminGroup,\n    form: {\n      options: function () {\n        const groups = _.without(\n          _.keys(getCollection('Users').groups),\n          'guests',\n          'members',\n          'owners',\n          'admins'\n        );\n        return groups.map(group => {\n          return { value: group, label: group };\n        });\n      },\n    },\n  },\n  'groups.$': {\n    type: String,\n    optional: true,\n  },\n\n  // GraphQL only fields\n\n  pageUrl: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n    resolveAs: {\n      type: 'String',\n      resolver: (user, args, { Users }) => {\n        return Users.getProfileUrl(user, true);\n      },\n    },\n  },\n\n  pagePath: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n    resolveAs: {\n      type: 'String',\n      resolver: (user, args, { Users }) => {\n        return Users.getProfileUrl(user, false);\n      },\n    },\n  },\n\n  editUrl: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n    resolveAs: {\n      type: 'String',\n      resolver: (user, args, { Users }) => {\n        return Users.getEditUrl(user, true);\n      },\n    },\n  },\n};\n\nexport default schema;\n"
  },
  {
    "path": "packages/vulcan-users/lib/modules/views.js",
    "content": "import Users from './collection.js';\n\nUsers.addView('usersAdmin', terms => ({\n  options: {\n    sort: {createdAt: -1}\n  }\n}));"
  },
  {
    "path": "packages/vulcan-users/lib/server/AuthPassword.js",
    "content": "import { Accounts } from 'meteor/accounts-base';\nimport SimpleSchema from 'simpl-schema';\n\n/**\n * Validate user selector used during login (either email or username but could be any Mongo selector)\n */\nexport const userSelectorSchema = new SimpleSchema({\n  email: {\n    type: String,\n    regEx: SimpleSchema.RegEx.Email,\n    optional: true,\n  },\n  username: {\n    type: String,\n    optional: true,\n  },\n});\nuserSelectorSchema.addDocValidator(userSelector => {\n  if (!userSelector) return [{ name: 'userSelector', type: 'MUST_HAVE_USER_SELECTOR' }];\n  const { email, username } = userSelector;\n  if (!(email || username)) return [{ name: 'userSelector', type: 'MUST_HAVE_USERNAME_OR_EMAIL' }];\n  return [];\n});\n\n/**\n * How Meteor auth works:\n *\n * - accounts-password will register a login handler with all auth logic it needs:\n * https://github.com/meteor/meteor/blob/devel/packages/accounts-password/password_server.js#L350\n * - accounts-base will define a \"login\" method that runs all handlers on data: \n * https://github.com/meteor/meteor/blob/d612d7546fa446cd574b51c0ea7953253f5e4bb7/packages/accounts-base/accounts_server.js#L499\n * - we obtain an object {userId, token}, user is now logged in\n * - token must be stored in Meteor.login_token localstorage\n * - it must be passed in the \"meteor_login_token\" cookie in the header or through the authorization header\n * - currentUser is injected into context for each request based on the token packages/vulcan-lib/lib/server/apollo-server/context.js\n *\n// Acount js method names (inspired by meteor)\n//  createUser?: Maybe<Scalars['ID']>,\n//  verifyEmail?: Maybe<Scalars['Boolean']>,\n//  resetPassword?: Maybe<LoginResult>,\n//  sendVerificationEmail?: Maybe<Scalars['Boolean']>,\n//  sendResetPasswordEmail?: Maybe<Scalars['Boolean']>,\n//  addEmail?: Maybe<Scalars['Boolean']>,\n//  changePassword?: Maybe<Scalars['Boolean']>,\n//  twoFactorSet?: Maybe<Scalars['Boolean']>,\n//  twoFactorUnset?: Maybe<Scalars['Boolean']>,\n//  impersonate?: Maybe<ImpersonateReturn>,\n//  refreshTokens?: Maybe<LoginResult>,\n//  verifyAuthentication?: Maybe<Scalars['Boolean']>,\n */\n\n// Client-side: LoginFormInner signIn method\n// @see https://stackoverflow.com/questions/34085553/login-user-account-on-server-side/40305131#40305131\n\n/**\n * Authentication handler\n *\n * This code reproduces meteor/accounts-password authentication system\n *\n * NOTE: we CAN'T reuse Meteor login method because it expects a DDP connection to be defined\n * const res = Meteor.call('login', { user: usernameOrEmail, password }); // WON'T WORK this.connection is not defined\n * @see https://github.com/meteor/meteor/issues/10937\n *\n * Inspiration\n * @see https://github.com/stubailo/meteor-rest/blob/master/packages/rest-accounts-password/rest-login.js\n *\n * NOTE: this won't work with DDP. If you want to use usual DDP features (eg you develop an actual Meteor client),\n * use the usual Meteor connection pattern instead (see Meteor doc)\n *\n * @param {*} usernameOrEmail\n * @param {*} password\n */\nexport const authenticateWithPassword = async (email, password) => {\n  // We reproduce the usual password auth strategy\n  // Code taken from the loginHandler defined by accounts-password\n  // @see https://github.com/meteor/meteor/blob/devel/packages/accounts-password/password_server.js#L282\n  // To be done in the resolver\n  //check(options, {\n  //  user: userQueryValidator,\n  //  password: passwordValidator\n  //});\n  const user = Accounts.findUserByEmail(email);\n  if (!user) {\n    // TODO: handle errors\n    throw new Error('User not found');\n  }\n  if (!user.services || !user.services.password || !(user.services.password.bcrypt || user.services.password.srp)) {\n    throw new Error('User has no password set');\n  }\n  const passwordCheck = Accounts._checkPassword(user, password);\n  if (!passwordCheck) throw new Error('Password check failed');\n  if (passwordCheck.error) throw new Error(passwordCheck.error);\n  if (!passwordCheck.userId) throw new Error('No password check userId');\n  const { userId } = passwordCheck;\n\n  // TODO: meteor has some logic to check login validation (prevent brute force attacks for example)\n  // @see https://github.com/meteor/meteor/blob/d612d7546fa446cd574b51c0ea7953253f5e4bb7/packages/accounts-base/accounts_server.js#L303\n  //const { userId } = passwordCheck\n  //checkedUser = Users.findOne(userId);\n  //const attempt = {\n  //  type: passwordCheck.type || \"unknown\",\n  //  allowed: true,\n  //  methodName: 'password',\n  //  //methodArguments: Array.from(methodArgs)\n  //};\n  // _validateLogin may mutate `attempt` by adding an error and changing allowed\n  // to false, but that's the only change it can make (and the user's callbacks\n  // only get a clone of `attempt`).\n  //this._validateLogin(methodInvocation.connection, attempt);\n  //if (!attempt.allowed) {\n  //  throw attempt.error;\n  //}\n\n  // generate and store the token in the db\n  // code taken from _loginUser\n  // @see https://github.com/meteor/meteor/blob/d612d7546fa446cd574b51c0ea7953253f5e4bb7/packages/accounts-base/accounts_server.js#L267\n  const stampedLoginToken = Accounts._generateStampedLoginToken();\n  Accounts._insertLoginToken(userId, stampedLoginToken);\n  return {\n    userId,\n    token: stampedLoginToken.token,\n  };\n};\n\n/**\n * Invalidate user's auth token\n *\n * StampedToken = { when, token }\n * Method example:\n * @see https://github.com/meteor/meteor/blob/devel/packages/accounts-base/accounts_server.js#L523\n * Token destruction:\n * @see https://github.com/meteor/meteor/blob/devel/packages/accounts-base/accounts_server.js#L486\n */\nexport const logout = async userId => {\n  // Note: we don't use this function directly because it is meant to keep some old style connection active\n  // this means storing the token and it's creation, Meteor does this on the server, but we don't allow state in the server anymore\n  // we could store it in the client but this is tedious...\n  //\n  // We have a more aggressive strategy in Vulcan and simply delete the list of tokens\n  //\n  //const hashedToken = Accounts._hashStampedToken(stampedToken);\n  //await Accounts.destroyToken(userId, hasedToken);\n  await Accounts.users.update(userId, {\n    $set: {\n      'services.resume.loginTokens': [],\n    },\n  });\n  return { userId };\n};\n\nexport const signup = async (email, password) => {\n  if (Accounts._options.forbidClientAccountCreation) {\n    throw new Error('Signups forbidden');\n  }\n  const options = { email, password };\n  // Create user. result contains id and token.\n  const userId = await Accounts.createUser(options);\n  // safety belt. createUser is supposed to throw on error. send 500 error\n  // instead of sending a verification email with empty userid.\n  if (!userId) throw new Error('createUser failed to insert new user');\n\n  // If `Accounts._options.sendVerificationEmail` is set, register\n  // a token to verify the user's primary email, and send it to\n  // that address.\n  if (options.email && Accounts._options.sendVerificationEmail) Accounts.sendVerificationEmail(userId, options.email);\n  // client gets logged in as the new user afterwards.\n  return { userId: userId };\n};\n\n/**\n *  Set password for a connected user\n * /!\\ WILL LOGOUT AND LOG IN AGAIN THE USER in order to invalidate previous token\n */\nexport const setPassword = async (userId, newPassword) => {\n  await Accounts.setPassword(userId, newPassword);\n  // logout active connexions\n  console.log('logging out');\n  await logout(userId);\n  // relog in and return new token\n  const user = await Meteor.users.findOne({ _id: userId });\n  if (!(user.emails && user.emails.length)) {\n    throw new Error(\"User email not found, couldn't authenticate after password change\");\n  }\n  const email = user.emails[0].address;\n  console.log('email', email);\n  return await authenticateWithPassword(email, newPassword);\n};\n\nexport const sendResetPasswordEmail = async email => {\n  const user = await Accounts.findUserByEmail(email);\n  if (!user) {\n    throw new Error('User not found');\n  }\n  const userId = user._id;\n  await Accounts.sendResetPasswordEmail(userId);\n  return true;\n};\n\n// Utility for plucking addresses from emails\nconst pluckAddresses = (emails = []) => emails.map(email => email.address);\n/**\n * For non connected user, need to parse the token\n * https://github.com/meteor/meteor/blob/devel/packages/accounts-password/password_server.js#L804\n * @param {*} userId\n * @param {*} newPassword\n */\nexport const resetPassword = async (token, newPassword) => {\n  //check(token, String);\n  //check(newPassword, passwordValidator);\n  // Find the user with this token\n  const user = await Meteor.users.findOne(\n    { 'services.password.reset.token': token },\n    {\n      fields: {\n        services: 1,\n        emails: 1,\n      },\n    }\n  );\n  if (!user) {\n    throw new Error('Token expired or invalid');\n  }\n  const userId = user._id;\n  // check the token validity\n  const { when, reason, email } = user.services.password.reset;\n  let tokenLifetimeMs = Accounts._getPasswordResetTokenLifetimeMs();\n  if (reason === 'enroll') {\n    tokenLifetimeMs = Accounts._getPasswordEnrollTokenLifetimeMs();\n  }\n  const currentTimeMs = Date.now();\n  if (currentTimeMs - when > tokenLifetimeMs) throw new Error('Token expired');\n  if (!pluckAddresses(user.emails).includes(email))\n    return {\n      userId,\n      error: new Error('Token has invalid email address'),\n    };\n\n  // Prepare rollback in case of failure\n  // => this.connection is not defined\n  // const oldToken = Accounts._getLoginToken(this.connection.id);\n  // Accounts._setLoginToken(user._id, this.connection, null);\n  // const resetToOldToken = () => Accounts._setLoginToken(user._id, this.connection, oldToken);\n\n  // Meteor had a more complex logic here for some reason\n  // we just reuse the setPassword logic\n  //const hashed = hashPassword(newPassword);\n  // NOTE: We're about to invalidate tokens on the user, who we might be\n  // logged in as. Make sure to avoid logging ourselves out if this\n  // happens. But also make sure not to leave the connection in a state\n  // of having a bad token set if things fail.\n  await setPassword(userId, newPassword);\n\n  // Update the user record by:\n  // - Changing the password to the new one => done by setPassword\n  // - Forgetting about the reset token that was just used\n  // - Verifying their email, since they got the password reset via email.\n  try {\n    const affectedRecords = await Meteor.users.update(\n      {\n        _id: userId,\n        'emails.address': email,\n        'services.password.reset.token': token,\n      },\n      //{$set: {'services.password.bcrypt': hashed,\n      //        'emails.$.verified': true},\n      {\n        $unset: { 'services.password.reset': 1, 'services.password.srp': 1 },\n      }\n    );\n    if (affectedRecords !== 1) {\n      return {\n        userId: userId,\n        error: new Meteor.Error('Invalid email'),\n      };\n    }\n  } catch (err) {\n    // resetToOldToken();\n    throw err;\n  }\n\n  // Replace all valid login tokens with new ones (changing\n  // password should invalidate existing sessions).\n  Accounts._clearAllLoginTokens(userId);\n\n  return { userId: userId };\n};\n\nexport const sendVerificationEmail = async email => {\n  const user = await Accounts.findUserByEmail(email);\n  if (!user) {\n    throw new Error('User not found');\n  }\n  const userId = user._id;\n  await Accounts.sendVerificationEmail(userId);\n  return true;\n};\n\n/**\n * https://github.com/meteor/meteor/blob/devel/packages/accounts-password/password_server.js#L917\n * @param {*} email\n */\nexport const verifyEmail = async token => {\n  const user = await Meteor.users.findOne(\n    { 'services.email.verificationTokens.token': token },\n    {\n      fields: {\n        services: 1,\n        emails: 1,\n      },\n    }\n  );\n  if (!user) throw new Error('Verify email link expired or invalid');\n  const userId = user._id;\n\n  // check validity\n  const tokenRecord = user.services.email.verificationTokens.find(t => t.token == token);\n  if (!tokenRecord)\n    return {\n      userId: userId,\n      error: new Error('Verify email link expired'),\n    };\n\n  // find user based on token email\n  const emailsRecord = user.emails.find(e => e.address == tokenRecord.address);\n  if (!emailsRecord) {\n    return {\n      userId: userId,\n      error: new Error('Verify email link is for unknown address'),\n    };\n  }\n\n  // By including the address in the query, we can use 'emails.$' in the\n  // modifier to get a reference to the specific object in the emails\n  // array. See\n  // http://www.mongodb.org/display/DOCS/Updating/#Updating-The%24positionaloperator)\n  // http://www.mongodb.org/display/DOCS/Updating#Updating-%24pull\n  await Meteor.users.update(\n    { _id: user._id, 'emails.address': tokenRecord.address },\n    { $set: { 'emails.$.verified': true }, $pull: { 'services.email.verificationTokens': { address: tokenRecord.address } } }\n  );\n\n  return { userId: userId };\n};\n"
  },
  {
    "path": "packages/vulcan-users/lib/server/callbacks.js",
    "content": "  import Users from '../modules/collection.js';\n  import marked from 'marked';\n  import { addCallback, Utils, runCallbacksAsync } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core isn't loaded yet\n\n  //////////////////////////////////////////////////////\n  // Callbacks                                        //\n  //////////////////////////////////////////////////////\n\n  function hasCompletedProfile (user) {\n    return Users.hasCompletedProfile(user);\n  }\n  addCallback('users.profileCompleted.sync', hasCompletedProfile);\n\n  // remove this to get rid of dependency on vulcan:email\n\n  // function usersNewAdminUserCreationNotification (user) {\n  //   // send notifications to admins\n  //   const admins = Users.adminUsers();\n  //   admins.forEach(function(admin) {\n  //     if (Users.getSetting(admin, \"notifications_users\", false)) {\n  //       const emailProperties = Users.getNotificationProperties(user);\n  //       const html = VulcanEmail.getTemplate('newUser')(emailProperties);\n  //       VulcanEmail.send(Users.getEmail(admin), `New user account: ${emailProperties.displayName}`, VulcanEmail.buildTemplate(html));\n  //     }\n  //   });\n  //   return user;\n  // }\n  // addCallback(\"users.new.sync\", usersNewAdminUserCreationNotification);\n\n  export function usersMakeAdmin (user) {\n    // if this is not a dummy account, and is the first user ever, make them an admin\n    // TODO: should use await Connectors.count() instead, but cannot await inside Accounts.onCreateUser. Fix later. \n    if (typeof user.isAdmin === 'undefined') {\n      const realUsersCount = Users.find({'isDummy': {$ne: true}}).count();\n      user.isAdmin = !user.isDummy && realUsersCount === 0;\n    }\n    return user;\n  }\n  addCallback('users.new.sync', usersMakeAdmin);\n\n  function usersEditGenerateHtmlBio (modifier) {\n    if (modifier.$set && 'bio' in modifier.$set) {\n      modifier.$set.htmlBio = Utils.sanitize(marked(modifier.$set.bio));\n    }\n    return modifier;\n  }\n  addCallback('users.edit.sync', usersEditGenerateHtmlBio);\n\n  function usersEditCheckEmail (modifier, user) {\n    // if email is being modified, update user.emails too\n    if (modifier.$set && modifier.$set.email) {\n\n      const newEmail = modifier.$set.email;\n\n      // check for existing emails and throw error if necessary\n      const userWithSameEmail = Users.findByEmail(newEmail);\n      if (userWithSameEmail && userWithSameEmail._id !== user._id) {\n        throw new Error(Utils.encodeIntlError({id:'users.email_already_taken', value: newEmail}));\n      }\n\n      // if user.emails exists, change it too\n      if (!!user.emails) {\n        user.emails[0].address = newEmail;\n        user.emails[0].verified = false;\n        modifier.$set.emails = user.emails;\n      }\n\n      // update email hash\n      modifier.$set.emailHash = Users.avatar.hash(newEmail);\n\n    }\n    return modifier;\n  }\n  addCallback('users.edit.sync', usersEditCheckEmail);\n\n  // when a user is edited, check if their profile is now complete\n  function usersCheckCompletion (newUser, oldUser) {\n    if (!Users.hasCompletedProfile(oldUser) && Users.hasCompletedProfile(newUser)) {\n      runCallbacksAsync('users.profileCompleted.async', newUser);\n    }\n  }\n  addCallback('users.edit.async', usersCheckCompletion);\n\n"
  },
  {
    "path": "packages/vulcan-users/lib/server/create_user.js",
    "content": "import Users from '../modules/index.js';\nimport { newMutation } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core isn't loaded yet\n\nconst createUser = user => {\n  \n  // if user has an email, copy it over to emails array\n  if(user.email) {\n    user.emails = [{address: user.email, verified: false}];\n  }\n\n  user.services = {};\n\n  newMutation({\n    collection: Users, \n    document: user,\n    validate: false\n  });\n};\n\nexport default createUser;"
  },
  {
    "path": "packages/vulcan-users/lib/server/graphql_context.js",
    "content": "import { addToGraphQLContext } from 'meteor/vulcan:lib';\nimport Users from '../modules/index.js';\n\naddToGraphQLContext({ getViewableFields: Users.getViewableFields });\n"
  },
  {
    "path": "packages/vulcan-users/lib/server/main.js",
    "content": "import './on_create_user.js';\nimport './urls.js';\nimport './graphql_context.js';\nimport './callbacks.js';\nimport './queries.js';\nimport './mutations.js';\n\nexport { default as createUser } from './create_user.js';\nexport * from '../modules/index.js';\nexport { default } from '../modules/index.js';\n"
  },
  {
    "path": "packages/vulcan-users/lib/server/mutations.js",
    "content": "/**\n * Mutation for the user to manage its own account.\n *\n * Note: we don't have impersonating features yet, nor user enrollment or user email validation.\n */\nimport { addGraphQLResolvers, addGraphQLMutation, addGraphQLSchema } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core isn't loaded yet\nimport {\n  authenticateWithPassword,\n  logout,\n  setPassword,\n  sendResetPasswordEmail,\n  resetPassword,\n  sendVerificationEmail,\n  verifyEmail,\n  signup,\n} from './AuthPassword';\n\naddGraphQLSchema(`\n  input AuthPasswordInput {\n    email: String\n    password: String\n  }\n  type AuthResult {\n    token: String\n    userId: String\n  }\n  type LogoutResult {\n    userId: String\n  }\n\n  input SignupInput {\n    email: String\n    password: String\n  }\n  type SignupResult {\n    userId: String\n  }\n\n  input SetPasswordInput {\n    newPassword: String\n  }\n\n  #type SetPasswordResult # Will auth user again so we reuse AuthResult atm\n\n  input ResetPasswordInput {\n    token: String\n    newPassword: String\n  }\n  type ResetPasswordResult {\n    userId: String\n  }\n\n  input VerifyEmailInput {\n    token: String\n  }\n  type VerifyEmailResult {\n    userId: String\n  }\n\n  input AuthEmailInput {\n    email: String\n  }\n`);\naddGraphQLMutation('authenticateWithPassword(input: AuthPasswordInput): AuthResult');\naddGraphQLMutation('logout: LogoutResult');\naddGraphQLMutation('signup(input: SignupInput): SignupResult');\naddGraphQLMutation('setPassword(input: SetPasswordInput): AuthResult');\naddGraphQLMutation('sendResetPasswordEmail(input: AuthEmailInput): Boolean');\naddGraphQLMutation('resetPassword(input: ResetPasswordInput): ResetPasswordResult');\naddGraphQLMutation('sendVerificationEmail(input: AuthEmailInput): Boolean');\naddGraphQLMutation('verifyEmail(input: VerifyEmailInput): VerifyEmailResult');\n\nconst specificResolvers = {\n  Mutation: {\n    async authenticateWithPassword(root, args, context) {\n      if (context && context.userId) {\n        throw new Error('User already logged in');\n      }\n      const { input } = args;\n      if (!input) {\n        throw new Error('Empty input');\n      }\n      const { email, password } = input;\n      if (!(password && typeof password === 'string')) {\n        throw new Error('Invalid password');\n      }\n      if (!email) {\n        throw new Error('Invalid email');\n      }\n      const authResult = await authenticateWithPassword(email, password);\n      // set an HTTP-only cookie so the user is authenticated\n      const { /*userId,*/ token } = authResult;\n      const tokenCookie = {\n        path: '/',\n        httpOnly: true,\n        secure: process.env.NODE_ENV === 'development' ? false : true,\n        // expires: //\n        //sameSite: ''\n      };\n      context.req.res.cookie('meteor_login_token', token, tokenCookie);\n      return authResult;\n    },\n    async logout(root, args, context) {\n      if (!(context && context.userId)) {\n        throw new Error('User already logged out');\n      }\n      const { userId } = context;\n      context.req.res.clearCookie('meteor_login_token');\n      return await logout(userId);\n    },\n    async signup(root, args, context) {\n      if (context && context.userId) {\n        throw new Error('User already logged in');\n      }\n      const { input } = args;\n      const { email, password } = input;\n      return await signup(email, password);\n    },\n    async setPassword(root, args, context) {\n      if (!(context && context.userId)) {\n        throw new Error('User not logged in');\n      }\n      const { userId } = context;\n      const { input } = args;\n      const { newPassword } = input;\n      if (!newPassword) {\n        throw new Error('Empty password');\n      }\n      return await setPassword(userId, newPassword);\n    },\n    async sendResetPasswordEmail(root, args, context) {\n      if (context && context.userId) {\n        throw new Error('User already logged in');\n      }\n      const { input } = args;\n      const { email } = input;\n      if (!email) {\n        throw new Error('Empty email');\n      }\n      return await sendResetPasswordEmail(email);\n    },\n    async resetPassword(root, args, context) {\n      if (context && context.userId) {\n        throw new Error('User already logged in');\n      }\n      const { input } = args;\n      const { token, newPassword } = input;\n      if (!newPassword) {\n        throw new Error('Empty password');\n      }\n      if (!token) {\n        throw new Error('Empty token');\n      }\n      return await resetPassword(token, newPassword);\n    },\n    async sendVerificationEmail(root, args, context) {\n      if (context && context.userId) {\n        throw new Error('User already logged in');\n      }\n      const { input } = args;\n      const { email } = input;\n      if (!email) {\n        throw new Error('Empty email');\n      }\n      return await sendVerificationEmail(email);\n    },\n    async verifyEmail(root, args, context) {\n      if (context && context.userId) {\n        throw new Error('User already logged in');\n      }\n      const { input } = args;\n      const { token } = input;\n      if (!token) {\n        throw new Error('Empty token');\n      }\n      return await verifyEmail(token);\n    },\n  },\n};\n\naddGraphQLResolvers(specificResolvers);\n"
  },
  {
    "path": "packages/vulcan-users/lib/server/on_create_user.js",
    "content": "import Users from '../modules/index.js';\nimport { runCallbacks, runCallbacksAsync, Utils, debug, debugGroup, debugGroupEnd, throwError } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core isn't loaded yet\nimport clone from 'lodash/clone';\nimport get from 'lodash/get';\n\n// TODO: the following should use async/await, but async/await doesn't seem to work with Accounts.onCreateUser\nfunction onCreateUserCallback(options, user) {\n  debug('');\n  debugGroup('--------------- start \\x1b[35m onCreateUser ---------------');\n  debug(`Options: ${JSON.stringify(options)}`);\n  debug(`User: ${JSON.stringify(user)}`);\n\n  const properties = { data: user, document: user, user, collection: Users };\n\n  const schema = Users.simpleSchema()._schema;\n\n  delete options.password; // we don't need to store the password digest\n  delete options.username; // username is already in user object\n\n  options = runCallbacks({ name: 'user.create.validate.before', iterator: options });\n  // OpenCRUD backwards compatibility\n  options = runCallbacks('users.new.validate.before', options);\n\n  // validate options since they can't be trusted\n  // omit username since we deleted it above\n  Users.simpleSchema()\n    .omit('username')\n    .validate(options);\n\n  // check that the current user has permission to insert each option field\n  _.keys(options).forEach(fieldName => {\n    var field = schema[fieldName];\n    if (!field || !Users.canCreateField(user, field)) {\n      throw new Error(Utils.encodeIntlError({ id: 'app.disallowed_property_detected', value: fieldName }));\n    }\n  });\n\n  // extend user with options\n  user = Object.assign(user, options);\n\n  let validationErrors = [];\n  // new callback API (Oct 2019)\n  // validationErrors = await runCallbacks({\n  //   name: 'user.create.validate',\n  //   callbacks: get(Users, 'options.callbacks.create.validate', []),\n  //   iterator: validationErrors,\n  //   properties,\n  // });\n  // if (validationErrors.length) {\n  //   console.log(validationErrors); // eslint-disable-line no-console\n  //   throwError({ id: 'app.validation_error', data: { break: true, errors: validationErrors } });\n  // }\n\n  // run validation callbacks\n  user = runCallbacks({ name: 'user.create.validate', iterator: user, properties: {} });\n  // OpenCRUD backwards compatibility\n  user = runCallbacks('users.new.validate', user);\n\n  // run onCreate step\n  for (let fieldName of Object.keys(schema)) {\n    let autoValue;\n    if (schema[fieldName].onCreate) {\n      const document = clone(user);\n      // eslint-disable-next-line no-await-in-loop\n      autoValue = schema[fieldName].onCreate({ document });\n    } else if (schema[fieldName].onInsert) {\n      // OpenCRUD backwards compatibility\n      // eslint-disable-next-line no-await-in-loop\n      autoValue = schema[fieldName].onInsert(clone(user));\n    }\n    if (typeof autoValue !== 'undefined') {\n      user[fieldName] = autoValue;\n    }\n  }\n\n  // new callback API (Oct 2019)\n  // user = await runCallbacks({\n  //   name: 'user.create.before',\n  //   callbacks: get(Users, 'options.callbacks.create.after', []),\n  //   iterator: user,\n  //   properties,\n  // });\n  user = runCallbacks({ name: 'user.create.before', iterator: user, properties: {} });\n  user = runCallbacks('users.new.sync', user);\n\n  // new callback API (Oct 2019)\n  // await runCallbacksAsync({\n  //   name: 'user.create.async',\n  //   callbacks: get(Users, 'options.callbacks.create.async', []),\n  //   properties,\n  // });\n\n  runCallbacksAsync({ name: 'user.create.async', properties });\n  // OpenCRUD backwards compatibility\n  runCallbacksAsync('users.new.async', user);\n\n  // check if all required fields have been filled in. If so, run profile completion callbacks\n  if (Users.hasCompletedProfile(user)) {\n    runCallbacksAsync('user.profileCompleted.async', user);\n    // OpenCRUD backwards compatibility\n    runCallbacksAsync('users.profileCompleted.async', user);\n  }\n\n  debug(`Modified User: ${JSON.stringify(user)}`);\n  debugGroupEnd();\n  debug('--------------- end \\x1b[35m onCreateUser ---------------');\n  debug('');\n\n  return user;\n}\n\nMeteor.startup(async () => {\n  if (typeof Accounts !== 'undefined') {\n    Accounts.onCreateUser(onCreateUserCallback);\n  }\n});\n"
  },
  {
    "path": "packages/vulcan-users/lib/server/queries.js",
    "content": "import { addGraphQLResolvers, Connectors, addGraphQLQuery, addGraphQLSchema } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core isn't loaded yet\n\naddGraphQLQuery('currentUser: User');\n\nconst specificResolvers = {\n  Query: {\n    async currentUser(root, args, context) {\n      let user = null;\n      if (context && context.userId) {\n        user = await Connectors.get(context.Users, context.userId);\n\n        if (user.services) {\n          Object.keys(user.services).forEach(key => {\n            user.services[key] = {};\n          });\n        }\n      }\n      return user;\n    },\n  },\n};\n\naddGraphQLResolvers(specificResolvers);"
  },
  {
    "path": "packages/vulcan-users/lib/server/urls.js",
    "content": "Meteor.startup(() => {\n  if (typeof Accounts !== 'undefined') {\n    Accounts.urls.resetPassword = token => Meteor.absoluteUrl(`reset-password/${token}`);\n    Accounts.urls.enrollAccount = token => Meteor.absoluteUrl(`enroll-account/${token}`);\n    Accounts.urls.verifyEmail = token => Meteor.absoluteUrl(`verify-email/${token}`);\n  }\n});\n"
  },
  {
    "path": "packages/vulcan-users/package.js",
    "content": "Package.describe({\n  name: 'vulcan:users',\n  summary: 'Vulcan permissions.',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:lib@=1.16.9']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\nPackage.onTest(function(api) {\n  api.use('vulcan:users');\n  api.use(['ecmascript', 'meteortesting:mocha', 'hwillson:stub-collections']);\n  api.mainModule('./test/server/index.js', 'server');\n  //api.mainModule('./test/index.js');\n});\n"
  },
  {
    "path": "packages/vulcan-users/test/index.js",
    "content": "import './permissions.test';"
  },
  {
    "path": "packages/vulcan-users/test/permissions.test.js",
    "content": "import Users from '../lib/modules/collection';\nimport '../lib/modules/permissions';\nconst test = it;\nimport expect from 'expect';\nimport { createDummyCollection } from 'meteor/vulcan:test';\nimport SimpleSchema from 'simpl-schema';\n\ndescribe('vulcan:users/permissions', () => {\n  const Dummies = createDummyCollection({\n    schema: {\n      guestField: {\n        type: String,\n        canRead: ['guests'],\n      },\n      adminField: {\n        type: String,\n        canRead: ['admins'],\n      },\n      ownerField: {\n        type: String,\n        canRead: ['owners'], // => need a document to be passed in order to check if the field is readable/filterable\n      },\n      customField: {\n        type: String,\n        canRead: [document => document && document.canRead], // => need a document to be passed in order to check if the field is readable/filterable\n      },\n    },\n  });\n  test('getViewableFields as projection (legacy)', () => {\n    const fields = Users.getViewableFields(null, Dummies);\n    expect(fields).toEqual({\n      guestField: true,\n    });\n  });\n  test('getReadableFields', () => {\n    const fields = Users.getReadableFields(null, Dummies);\n    expect(fields).toEqual(['guestField']);\n  });\n  test('getReadableFields with document-based permissions excludes ambiguous fields', () => {\n    const fields = Users.getReadableFields(null, Dummies);\n    expect(fields).not.toContain('ownerField');\n    expect(fields).not.toContain('customField');\n  });\n  test('getReadableProjection', () => {\n    const fields = Users.getReadableProjection(null, Dummies);\n    expect(fields).toEqual({ guestField: true });\n  });\n\n  describe('fields allowed for filtering', () => {\n    test('get fields that needs to be checked against the document to be tested', () => {\n      const documentBasedPermissionFields = Users.getDocumentBasedPermissionFieldNames(Dummies);\n      expect(documentBasedPermissionFields).toContain('ownerField');\n      expect(documentBasedPermissionFields).toContain('customField');\n    });\n    test('checkFields throw on wrong permission', () => {\n      expect(() => Users.checkFields(null, Dummies, ['adminField'])).toThrow();\n      expect(Users.checkFields(null, Dummies, ['guestField'])).toBe(true);\n    });\n    test('checkFields with document-based permissions do not throw for ambigous fields (those field need the document to be checked)', () => {\n      const res = Users.checkFields(null, Dummies, ['guestField', 'ownerField', 'customField']);\n      expect(res).toEqual(true);\n    });\n  });\n\n  test('restrictViewableFields', () => {\n    const fields = Users.restrictViewableFields(null, Dummies, { adminField: 'foo', guestField: 'bar' });\n    expect(fields).toEqual({ guestField: 'bar' });\n  });\n\n  describe('nested fields', () => {\n    test('remove unreadable field of nested object', () => {\n      const Dummies = createDummyCollection({\n        schema: {\n          nested: {\n            canRead: ['guests'],\n            type: new SimpleSchema({\n              ok: {\n                type: String,\n                canRead: ['guests'],\n              },\n              nok: {\n                type: String,\n                canRead: ['members'],\n              },\n            }),\n          },\n        },\n      });\n      const fields = Users.restrictViewableFields(null, Dummies, { nested: { ok: 'foo', nok: 'bar' } });\n      expect(fields).toEqual({ nested: { ok: 'foo' } });\n    });\n    test('remove unreadable field of array of nested objects', () => {\n      const Dummies = createDummyCollection({\n        schema: {\n          array: {\n            type: Array,\n            canRead: ['guests'],\n          },\n          'array.$': {\n            canRead: ['guests'],\n            type: new SimpleSchema({\n              ok: {\n                type: String,\n                canRead: ['guests'],\n              },\n              nok: {\n                type: String,\n                canRead: ['members'],\n              },\n            }),\n          },\n        },\n      });\n      const fields = Users.restrictViewableFields(null, Dummies, { array: [{ ok: 'foo', nok: 'bar' }] });\n      expect(fields).toEqual({ array: [{ ok: 'foo' }] });\n    });\n    test('ignore fields without read permission (parent permissions are used)', () => {\n      const Dummies = createDummyCollection({\n        schema: {\n          nested: {\n            canRead: ['guests'],\n            type: new SimpleSchema({\n              ok: {\n                type: String,\n              },\n              nok: {\n                type: String,\n                canRead: ['members'],\n              },\n            }),\n          },\n        },\n      });\n      const fields = Users.restrictViewableFields(null, Dummies, { nested: { ok: 'foo', nok: 'bar' } });\n      expect(fields).toEqual({ nested: { ok: 'foo' } });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-users/test/server/callback.test.js",
    "content": "import { usersMakeAdmin } from '../../lib/server/callbacks';\nimport Users from '../../lib/modules/collection';\nimport StubCollections from 'meteor/hwillson:stub-collections';\nimport expect from 'expect';\n\ndescribe('vulcan:users/callbacks', function () {\n  beforeEach(function () {\n    StubCollections.stub(Users);\n  });\n  afterEach(function () {\n    StubCollections.restore();\n  });\n  describe('usersMakeAdmin', function () {\n    it('makes the first user an admin', function () {\n      const user = usersMakeAdmin({ email: 'foo@bar.bar', password: 'password' });\n      expect(user.isAdmin).toBe(true);\n    });\n    it('ignores dummy users', function () {\n      const user = usersMakeAdmin({ isDummy: true, email: 'foo@bar.bar', password: 'password' });\n      expect(user.isAdmin).toBe(false);\n    });\n    it('does not make 2nd user admin', function () {\n      const user1 = usersMakeAdmin({ email: '11@11.fr', password: 'password' });\n      Users.insert(user1);\n      const user2 = usersMakeAdmin({ email: '22@22.fr', password: 'password' });\n      expect(user1.isAdmin).toBe(true);\n      expect(user2.isAdmin).toBe(false);\n    });\n    it('does not override isAdmin prop if passed', function () {\n      const userNonAdmin = usersMakeAdmin({ isAdmin: false, email: 'foo@bar.bar', password: 'password' });\n      expect(userNonAdmin.isAdmin).toBe(false);\n      const userAdmin = usersMakeAdmin({ isAdmin: true, email: 'foo@bar.bar', password: 'password' });\n      expect(userAdmin.isAdmin).toBe(true);\n    });\n  });\n});"
  },
  {
    "path": "packages/vulcan-users/test/server/index.js",
    "content": "import './callback.test';\nimport './mutation.test';\nimport '../index';"
  },
  {
    "path": "packages/vulcan-users/test/server/mutation.test.js",
    "content": "import expect from 'expect';\nimport Users from '../../lib/modules/collection';\nimport StubCollections from 'meteor/hwillson:stub-collections';\n\nconst test = it;\n\nconst { create: createUser } = Users.options.mutations;\n\n//const fooUser = { _id: 'foobar' };\n/*\nconst context = {\n    // TODO: move this into vulcan:tests\n    Users: {\n        findOne: () => fooUser,\n        remove: () => 1,\n        options: {\n            collectionName: 'Users',\n            typeName: 'User'\n        },\n        simpleSchema: () => new SimpleSchema({ _id: { type: String, canRead: ['admins'] } }),\n        restrictViewableFields: (currentUser, collection, doc) => doc\n    },\n    currentUser: {\n        isAdmin: true,\n        groups: ['admins']\n    }\n};\n*/\ndescribe('vulcan:users/mutations', () => {\n  beforeEach(function() {\n    StubCollections.stub(Users);\n  });\n  afterEach(function() {\n    StubCollections.restore();\n  });\n\n  describe('create user', () => {\n    const context = {\n      Users,\n      currentUser: { isAdmin: true },\n    };\n    test('respect email if provided', async () => {\n      const email = 'foobar@foo.com';\n      const user = { email };\n      const { data: userDocument } = await createUser.mutation(null, { data: user }, context);\n      expect(userDocument.email).toEqual(email);\n    });\n    test('add email to emails array (for Meteor legacy compatibility)', async () => {\n      const email = 'foobar@foo.com';\n      const user = { email };\n      const { data: userDocument } = await createUser.mutation(null, { data: user }, context);\n      expect(userDocument.emails).toBeDefined();\n      expect(userDocument.emails[0]).toEqual({ address: email });\n    });\n  });\n  describe('auth process', () => {\n    test.skip('sign up user', () => {\n      const query = `\n            mutation signup($input: SignupInput) {signup(input:$input) {userId}}\n            `;\n      const variables = {\n        input: {\n          email: 'test@test.test',\n          password: 'testtest',\n        },\n      };\n      // Manual test: OK\n      // TODO: test the mutation, should return the new userId\n    });\n    test.skip('authenticate with password', () => {\n      const query = `\n            mutation auth($input: AuthPasswordInput) {authenticateWithPassword(input:$input){token\nuserId}}\n            `;\n      const variables = {\n        input: {\n          email: 'test@test.test',\n          password: 'testtest',\n        },\n      };\n      // Manual test: OK\n      // TODO: test the mutation, should return userId and token\n      // token needs to be passed as a cookie for further requests\n      // In GraphQL Playground headers: {\"Authorization\": \"XXX\" } // Cookie seems to have no effect, otherwise the value would be {\"Cookie\":\"meteor_login_token=XXX\"}\n    });\n    test.skip('logout', () => {\n      const query = `mutation logout { logout { userId }}`;\n      // Manual test: OK\n      // TODO: test the mutation\n    });\n    test.skip('set password', () => {\n      const query = `\n            mutation setPassword($input: SetPasswordInput) {setPassword(input: $input){token\nuserId}}\n            `;\n      const variables = { input: { newPassword: 'foobar' } };\n      // Manual test: OK\n      // TODO: test the mutation. Should set the user own password, and return a new auth token (previous token is invalid now)\n    });\n    test.skip('sendResetPasswordEmail', () => {\n      const query = `\n            mutation sendResetPWEMail($input: AuthEmailInput) { sendResetPasswordEmail(input: $input) }\n            `;\n      const variables = {\n        input: { email: 'test@test.test' },\n      };\n      // TODO: test the mutation. It will trigger an email that returns a token.\n      // This token may be used in the reset password mutation\n    });\n    test.skip('resetPassword', () => {\n      const query = `\n            mutation resetPassword ($input:ResetPasswordInput) {resetPassword(input: $input) {userId}}\n            `;\n      // If the email is:\n      // I20200416-18:49:58.433(2)? To reset your password, simply click the link below.\n      // I20200416-18:49:58.433(2)?\n      // I20200416-18:49:58.433(2)? http://localhost:3000/reset-password/X_xBD5todL-3Yca_yuM0veej_2dqZqN91--tcb=\n      // I20200416-18:49:58.433(2)? bel6C\n      // I20200416-18:49:58.433(2)?\n      //\n      // The reset pw token is (remove the = symbol! and concatenate 2 lines)\n      // X_xBD5todL-3Yca_yuM0veej_2dqZqN91--tcbbel6C\n      const variables = { input: { token: 'X_xBD5todL-3Yca_yuM0veej_2dqZqN91--tcbbel6C', newPassword: 'helloworld' } };\n    });\n    test.skip('send verification email', () => {\n      const query = `\n            mutation sendVerifEmail($input: AuthEmailInput ){sendVerificationEmail(input:$input)}`;\n      const variables = { input: { email: 'test@test.test' } };\n      // TODO: test the mutation, should send an email with a verification token\n      // YeG89-Ln14_Sew0dwSkl_QiWn7D9HOzOAhdKAnkqUJW\n    });\n    test.skip('verify email', () => {\n      const query = ``;\n      const variables = {};\n      // TODO: test the mutation\n    });\n  });\n});\n"
  },
  {
    "path": "packages/vulcan-voting/README.md",
    "content": "Vulcan scoring package, used internally. "
  },
  {
    "path": "packages/vulcan-voting/lib/client/fragment_matcher.js",
    "content": "import { VoteableCollections } from '../modules/make_voteable.js';\nimport { addToFragmentMatcher, addCallback } from 'meteor/vulcan:core';\n\nfunction AddVoteableFragmentMatcher() {\n  addToFragmentMatcher({\n    kind: 'UNION',\n    name: 'Voteable',\n    possibleTypes: VoteableCollections.map(collection => ({ name: collection.options.typeName })),\n  });\n  return {};\n}\naddCallback('apolloclient.init.before', AddVoteableFragmentMatcher);\n"
  },
  {
    "path": "packages/vulcan-voting/lib/client/main.js",
    "content": "import './fragment_matcher.js';\n\nexport * from '../modules/index.js';"
  },
  {
    "path": "packages/vulcan-voting/lib/containers/withVote.js",
    "content": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport { graphql } from '@apollo/client/react/hoc';\nimport gql from 'graphql-tag';\nimport { performVoteClient } from '../modules/vote.js';\nimport { VoteableCollections } from '../modules/make_voteable.js';\n\nexport const withVote = component => {\n\n  return graphql(gql`\n    mutation vote($documentId: String, $voteType: String, $collectionName: String, $voteId: String) {\n      vote(documentId: $documentId, voteType: $voteType, collectionName: $collectionName, voteId: $voteId) {\n        ${VoteableCollections.map(collection => `\n          ... on ${collection.typeName} {\n            __typename\n            _id\n            currentUserVotes{\n              _id\n              voteType\n              power\n            }\n            baseScore\n            score\n          }\n        `).join('\\n')}\n      }\n    }\n  `, {\n    props: ({ownProps, mutate}) => ({\n      vote: ({document, voteType, collection, currentUser, voteId = Random.id()}) => {\n\n        const newDocument = performVoteClient({collection, document, user: currentUser, voteType, voteId});\n\n        return mutate({ \n          variables: {\n            documentId: document._id, \n            voteType,\n            collectionName: collection.options.collectionName,\n            voteId,\n          },\n          optimisticResponse: {\n            __typename: 'Mutation',\n            vote: newDocument,\n          }\n        });\n      }\n    }),\n  })(component);\n};"
  },
  {
    "path": "packages/vulcan-voting/lib/modules/custom_fields.js",
    "content": "import { Connectors } from 'meteor/vulcan:core'; // import from vulcan:lib because vulcan:core isn't loaded yet\nimport Users from 'meteor/vulcan:users';\nimport Votes from './votes/collection.js';\n\nUsers.addField([\n  /**\n    An array containing votes\n  */\n  {\n    fieldName: 'votes',\n    fieldSchema: {\n      type: Array,\n      optional: true,\n      canRead: Users.owns,\n      resolveAs: {\n        type: '[Vote]',\n        arguments: 'collectionName: String',\n        resolver: async (user, args, context) => {\n          const selector = {userId: user._id};\n          if (args.collectionName) {\n            selector.collectionName = args.collectionName;\n          }\n          const votes = await Connectors.find(Votes, selector);\n          return votes;\n        }\n      },\n    }\n  },\n  {\n    fieldName: 'votes.$',\n    fieldSchema: {\n      type: Object,\n      optional: true\n    }\n  },\n]);\n\n\n"
  },
  {
    "path": "packages/vulcan-voting/lib/modules/fragments.js",
    "content": "import { registerFragment } from 'meteor/vulcan:core';\n\nregisterFragment(`\n  fragment VoteFragment on Vote {\n    _id\n    voteType\n    power\n  }\n`);"
  },
  {
    "path": "packages/vulcan-voting/lib/modules/helpers.js",
    "content": "\n/**\n * @summary Check if a user has upvoted a document\n * @param {Object} user\n * @param {Object} document\n * @returns {Boolean}\n */\nconst hasUpvoted = (user, document) => {\n  // note(apollo): check upvoters depending if the document is queried by mongo directly or fetched by an apollo resolver\n  return user && document.upvoters && !!document.upvoters.find(u => u && (typeof u === 'string' ? u === user._id : u._id === user._id));\n};\n\n/**\n * @summary Check if a user has downvoted a document\n * @param {Object} user\n * @param {Object} document\n * @returns {Boolean}\n */\nconst hasDownvoted = (user, document) => {\n  // note(apollo): check downvoters depending if the document is queried by mongo directly or fetched by an apollo resolver\n  return user && document.downvoters && !!document.downvoters.find(u => u && (typeof u === 'string' ? u === user._id : u._id === user._id));\n};\n\nexport { hasUpvoted, hasDownvoted };"
  },
  {
    "path": "packages/vulcan-voting/lib/modules/index.js",
    "content": "export { default as Votes } from './votes/collection.js';\nexport * from './make_voteable.js';\nexport { withVote } from '../containers/withVote.js';\nexport * from './helpers.js';\nexport * from './vote.js';\n\nimport './custom_fields.js';\nimport './fragments.js';\n"
  },
  {
    "path": "packages/vulcan-voting/lib/modules/make_voteable.js",
    "content": "import { Connectors } from 'meteor/vulcan:core'; // import from vulcan:lib because vulcan:core isn't loaded yet\n\nexport const VoteableCollections = [];\n\nexport const makeVoteable = collection => {\n\n  VoteableCollections.push(collection);\n\n  collection.addField([\n    /**\n      The current user's votes on the document, if they exists\n    */\n    {\n      fieldName: 'currentUserVotes',\n      fieldSchema: {\n        type: Array,\n        optional: true,\n        canRead: ['guests'],\n        resolveAs: {\n          type: '[Vote]',\n          resolver: async (document, args, { Users, Votes, currentUser }) => {\n            if (!currentUser) return [];\n            const votes = await Connectors.find(Votes, {userId: currentUser._id, documentId: document._id});\n            if (!votes.length) return [];\n            return votes;\n            // return Users.restrictViewableFields(currentUser, Votes, votes);\n          },\n\n        }\n      }\n    },\n    {\n      fieldName: 'currentUserVotes.$',\n      fieldSchema: {\n        type: Object,\n        optional: true,\n      }\n    },\n    /**\n      All votes on the document\n    */\n    {\n      fieldName: 'allVotes',\n      fieldSchema: {\n        type: Array,\n        optional: true,\n        canRead: ['guests'],\n        resolveAs: {\n          type: '[Vote]',\n          resolver: async (document, args, { Users, Votes, currentUser }) => {\n            const votes = await Connectors.find(Votes, { documentId: document._id });\n            if (!votes.length) return [];\n            return votes;\n            // return Users.restrictViewableFields(currentUser, Votes, votes);\n          },\n\n        }\n      }\n    },\n    {\n      fieldName: 'allVotes.$',\n      fieldSchema: {\n        type: Object,\n        optional: true,\n      }\n    },\n    /**\n      An array containing the `_id`s of the document's upvoters\n    */\n    {\n      fieldName: 'voters',\n      fieldSchema: {\n        type: Array,\n        optional: true,\n        canRead: ['guests'],\n        resolveAs: {\n          type: '[User]',\n          resolver: async (document, args, { currentUser, Users }) => {\n            // eslint-disable-next-line no-undef\n            const votes = await Connectors.find(Votes, {itemId: document._id});\n            const votersIds = _.pluck(votes, 'userId');\n            // eslint-disable-next-line no-undef\n            const voters = await Connectors.find(Users, {_id: {$in: votersIds}});\n            return voters;\n            // if (!document.upvoters) return [];\n            // const upvoters = await Users.loader.loadMany(document.upvoters);\n            // return Users.restrictViewableFields(currentUser, Users, upvoters);\n          },\n        },\n      }\n    },\n    {\n      fieldName: 'voters.$',\n      fieldSchema: {\n        type: String,\n        optional: true\n      }\n    },\n    /**\n      The document's base score (not factoring in the document's age)\n    */\n    {\n      fieldName: 'baseScore',\n      fieldSchema: {\n        type: Number,\n        optional: true,\n        defaultValue: 0,\n        canRead: ['guests'],\n        onInsert: document => {\n          // default to 0 if empty\n          return document.baseScore || 0;\n        }\n      }\n    },\n    /**\n      The document's current score (factoring in age)\n    */\n    {\n      fieldName: 'score',\n      fieldSchema: {\n        type: Number,\n        optional: true,\n        defaultValue: 0,\n        canRead: ['guests'],\n        onInsert: document => {\n          // default to 0 if empty\n          return document.score || 0;\n        }\n      }\n    },\n    /**\n      Whether the document is inactive. Inactive documents see their score recalculated less often\n    */\n    { \n      fieldName: 'inactive',\n      fieldSchema: {\n        type: Boolean,\n        optional: true,\n        onInsert: () => false\n      }\n    },\n\n  ]);\n};"
  },
  {
    "path": "packages/vulcan-voting/lib/modules/scoring.js",
    "content": "export const recalculateScore = item => {\n\n  // Age Check\n\n  if (item.postedAt) {\n\n    const postedAt = item.postedAt.valueOf();\n    const now = new Date().getTime();\n    const age = now - postedAt;\n    const ageInHours = age / (60 * 60 * 1000);\n\n    // time decay factor\n    const f = 1.3;\n\n    // use baseScore if defined, if not just use 0\n    const baseScore = item.baseScore || 0;\n\n    // HN algorithm\n    const newScore = Math.round((baseScore / Math.pow(ageInHours + 2, f))*1000000)/1000000;\n\n    return newScore;\n\n  } else {\n\n    return item.baseScore;\n  \n  }\n\n};\n"
  },
  {
    "path": "packages/vulcan-voting/lib/modules/vote.js",
    "content": "import { Connectors, debug, debugGroup, debugGroupEnd, throwError /* runCallbacksAsync, runCallbacks, addCallback */ } from 'meteor/vulcan:core';\nimport Votes from './votes/collection.js';\nimport Users from 'meteor/vulcan:users';\nimport { recalculateScore } from './scoring.js';\n\n/*\n\nDefine voting operations\n\n*/\nconst voteTypes = {};\n\n/*\n\nAdd new vote types\n\n*/\nexport const addVoteType = (voteType, voteTypeOptions) => {\n  voteTypes[voteType] = voteTypeOptions;\n};\n\naddVoteType('upvote', {power: 1, exclusive: true});\naddVoteType('downvote', {power: -1, exclusive: true});\n\n/*\n\nTest if a user has voted on the client\n\n*/\nexport const hasVotedClient = ({ document, voteType }) => {\n  const userVotes = document.currentUserVotes;\n  if (voteType) {\n    return _.where(userVotes, { voteType }).length;\n  } else {\n    return userVotes && userVotes.length;\n  }\n};\n\n/*\n\nCalculate total power of all a user's votes on a document\n\n*/\nconst calculateTotalPower = votes => _.pluck(votes, 'power').reduce((a, b) => a + b, 0);\n\n/*\n\nTest if a user has voted on the server\n\n*/\nconst hasVotedServer = async ({ document, voteType, user }) => {\n  const vote = await Connectors.get(Votes, {documentId: document._id, userId: user._id, voteType});\n  return vote;\n};\n\n/*\n\nAdd a vote of a specific type on the client\n\n*/\nconst addVoteClient = ({ document, collection, voteType, user, voteId }) => {\n\n  const newDocument = {\n    ...document,\n    baseScore: document.baseScore || 0,\n    __typename: collection.options.typeName,\n    currentUserVotes: document.currentUserVotes || [],\n  };\n\n  // create new vote and add it to currentUserVotes array\n  const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });\n  newDocument.currentUserVotes = [...newDocument.currentUserVotes, vote];\n\n  // increment baseScore\n  newDocument.baseScore += vote.power;\n  newDocument.score = recalculateScore(newDocument);\n\n  return newDocument;\n};\n\n/*\n\nAdd a vote of a specific type on the server\n\n*/\nconst addVoteServer = async (voteOptions) => {\n\n  const { document, collection, voteType, user, voteId, updateDocument } = voteOptions;\n  const newDocument = _.clone(document);\n\n  // create vote and insert it\n  const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });\n  delete vote.__typename;\n  await Connectors.create(Votes, vote);\n\n  // initialize baseScore to vote power if not defined yet\n  newDocument.baseScore = document.baseScore ? document.baseScore + vote.power : vote.power;\n  newDocument.score = recalculateScore(newDocument);\n\n  if (updateDocument) {\n    // update document score & set item as active\n    await Connectors.update(collection, {_id: document._id}, {$set: {inactive: false, baseScore: newDocument.baseScore, score: newDocument.score}});\n  }\n\n  return newDocument;\n};\n\n/*\n\nCancel votes of a specific type on a given document (client)\n\n*/\nconst cancelVoteClient = ({ document, voteType }) => {\n  const vote = _.findWhere(document.currentUserVotes, { voteType });\n  const newDocument = _.clone(document);\n  if (vote) {\n    // subtract vote scores\n    newDocument.baseScore -= vote.power;\n    newDocument.score = recalculateScore(newDocument);\n\n    const newVotes = _.reject(document.currentUserVotes, vote => vote.voteType === voteType);\n\n    // clear out vote of this type\n    newDocument.currentUserVotes = newVotes;\n\n  }\n  return newDocument;\n};\n\n/*\n\nClear *all* votes for a given document and user (client)\n\n*/\nconst clearVotesClient = ({ document }) => {\n  const newDocument = _.clone(document);\n  newDocument.baseScore -= calculateTotalPower(document.currentUserVotes);\n  newDocument.score = recalculateScore(newDocument);\n  newDocument.currentUserVotes = [];\n  return newDocument;\n};\n\n/*\n\nClear all votes for a given document and user (server)\n\n*/\nconst clearVotesServer = async ({ document, user, collection, updateDocument }) => {\n  const newDocument = _.clone(document);\n  const votes = await Connectors.find(Votes, { documentId: document._id, userId: user._id});\n  if (votes.length) {\n    await Connectors.delete(Votes, {documentId: document._id, userId: user._id});\n    if (updateDocument) {\n      await Connectors.update(collection, {_id: document._id}, {$inc: {baseScore: -calculateTotalPower(votes) }});\n    }\n    newDocument.baseScore -= calculateTotalPower(votes);\n    newDocument.score = recalculateScore(newDocument);\n  }\n  return newDocument;\n};\n\n/*\n\nCancel votes of a specific type on a given document (server)\n\n*/\nconst cancelVoteServer = async (existingVote, { document, voteType, collection, user, updateDocument }) => {\n\n  const newDocument = _.clone(document);\n\n  const vote = existingVote;\n\n  // remove vote object\n  await Connectors.delete(Votes, {_id: vote._id});\n\n  if (updateDocument) {\n    // update document score\n    await Connectors.update(collection, {_id: document._id}, {$inc: {baseScore: -vote.power }});\n  }\n\n  newDocument.baseScore -= vote.power;\n  newDocument.score = recalculateScore(newDocument);\n\n  return newDocument;\n};\n\n/*\n\nDetermine a user's voting power for a given operation.\nIf power is a function, call it on user\n\n*/\nconst getVotePower = ({ user, voteType, document }) => {\n  const power = voteTypes[voteType] && voteTypes[voteType].power || 1;\n  return typeof power === 'function' ? power(user, document) : power;\n};\n\n/*\n\nCreate new vote object\n\n*/\nconst createVote = ({ document, collectionName, voteType, user, voteId }) => {\n\n  const vote = {\n    documentId: document._id,\n    collectionName,\n    userId: user._id,\n    voteType: voteType,\n    power: getVotePower({user, voteType, document}),\n    votedAt: new Date(),\n    __typename: 'Vote'\n  };\n\n  // when creating a vote from the server, voteId can sometimes be undefined\n  if (voteId) vote._id = voteId;\n\n  return vote;\n\n};\n\n/*\n\nOptimistic response for votes\n\n*/\nexport const performVoteClient = ({ document, collection, voteType = 'upvote', user, voteId }) => {\n\n  const collectionName = collection.options.collectionName;\n  let returnedDocument;\n\n  // console.log('// voteOptimisticResponse')\n  // console.log('collectionName: ', collectionName)\n  // console.log('document:', document)\n  // console.log('voteType:', voteType)\n\n  // make sure item and user are defined\n  if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {\n    throw new Error(`Cannot perform operation '${collectionName.toLowerCase()}.${voteType}'`);\n  }\n\n  const voteOptions = {document, collection, voteType, user, voteId};\n\n  if (hasVotedClient({document, voteType})) {\n\n    // console.log('action: cancel')\n    returnedDocument = cancelVoteClient(voteOptions);\n    // returnedDocument = runCallbacks(`votes.cancel.client`, returnedDocument, collection, user);\n\n  } else {\n\n    // console.log('action: vote')\n\n    if (voteTypes[voteType].exclusive) {\n      clearVotesClient({document, collection, voteType, user, voteId});\n    }\n\n    returnedDocument = addVoteClient(voteOptions);\n    // returnedDocument = runCallbacks(`votes.${voteType}.client`, returnedDocument, collection, user);\n\n  }\n\n  // console.log('returnedDocument:', returnedDocument)\n\n  return returnedDocument;\n};\n\n/*\n\nServer-side database operation\n\n### updateDocument \nif set to true, this will perform its own database updates. If false, will only\nreturn an updated document without performing any database operations on it. \n\n*/\nexport const performVoteServer = async ({ documentId, document, voteType = 'upvote', collection, voteId, user, updateDocument = true }) => {\n\n  const collectionName = collection.options.collectionName;\n  document = document || await Connectors.get(collection, documentId);\n\n  debug('');\n  debugGroup('--------------- start \\x1b[35mperformVoteServer\\x1b[0m  ---------------');\n  debug('collectionName: ', collectionName);\n  debug('document: ', document);\n  debug('voteType: ', voteType);\n\n  const voteOptions = {document, collection, voteType, user, voteId, updateDocument};\n\n  if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {\n    throwError({ id: 'voting.no_permission' });\n  }\n\n  const existingVote = await hasVotedServer({document, voteType, user});\n\n  if (existingVote) {\n\n    // console.log('action: cancel')\n\n    // runCallbacks(`votes.cancel.sync`, document, collection, user);\n    document = await cancelVoteServer(existingVote, voteOptions);\n    // runCallbacksAsync(`votes.cancel.async`, vote, document, collection, user);\n\n  } else {\n\n    // console.log('action: vote')\n\n    if (voteTypes[voteType].exclusive) {\n      document = await clearVotesServer(voteOptions);\n    }\n\n    // runCallbacks(`votes.${voteType}.sync`, document, collection, user);\n    document = await addVoteServer(voteOptions);\n    // runCallbacksAsync(`votes.${voteType}.async`, vote, document, collection, user);\n\n  }\n\n  debug('document after vote: ', document);\n  debugGroupEnd();\n  debug('--------------- end \\x1b[35m performVoteServer\\x1b[0m ---------------');\n  debug('');\n\n  // const newDocument = collection.findOne(documentId);\n  document.__typename = collection.options.typeName;\n  return document;\n\n};\n"
  },
  {
    "path": "packages/vulcan-voting/lib/modules/votes/collection.js",
    "content": "import { createCollection, /* getDefaultResolvers, getDefaultMutations */ } from 'meteor/vulcan:core';\nimport schema from './schema.js';\n\nconst Votes = createCollection({\n\n  collectionName: 'Votes',\n\n  typeName: 'Vote',\n\n  schema,\n  \n  resolvers: null,\n\n  mutations: null,\n\n});\n\n\nexport default Votes;\n"
  },
  {
    "path": "packages/vulcan-voting/lib/modules/votes/schema.js",
    "content": "const schema = {\n\n  _id: {\n    type: String,\n    canRead: ['guests'],\n  },\n\n  /**\n    The id of the document that was voted on\n  */\n  documentId: {\n    type: String,\n    canRead: ['guests'],\n  },\n\n  /**\n    The name of the collection the document belongs to\n  */\n  collectionName: {\n    type: String,\n    canRead: ['guests'],\n  },\n\n  /**\n    The id of the user that voted\n  */\n  userId: {\n    type: String,\n    canRead: ['guests'],\n  },\n\n  /**\n    An optional vote type (for Facebook-style reactions)\n  */\n  voteType: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n  },\n\n  /**\n    The vote power (e.g. 1 = upvote, -1 = downvote, or any other value)\n  */\n  power: {\n    type: Number,\n    optional: true,\n    canRead: ['guests'],\n  },\n  \n  /**\n    The vote timestamp\n  */\n  votedAt: {\n    type: Date,\n    optional: true,\n    canRead: ['guests'],\n  }\n\n};\n\nexport default schema;"
  },
  {
    "path": "packages/vulcan-voting/lib/server/callbacks.js",
    "content": "// import { addCallback, Utils } from 'meteor/vulcan:core';\n// import Users from 'meteor/vulcan:users';\n// import { getVotePower } from '../modules/vote.js';\n// import { updateScore } from './scoring.js';\n\n// ----------------------------- vote.async ------------------------------- //\n\n/**\n * @summary Update an item's (post or comment) score\n * @param {object} item - The item being operated on\n * @param {object} user - The user doing the operation\n * @param {object} collection - The collection the item belongs to\n * @param {string} operation - The operation being performed\n */\n// function updateItemScore(item, user, collection, operation, context) {\n//   updateScore({collection: collection, item: item, forceUpdate: true});\n// }\n\n// addCallback(\"upvote.async\", updateItemScore);\n// addCallback(\"downvote.async\", updateItemScore);\n// addCallback(\"cancelUpvote.async\", updateItemScore);\n// addCallback(\"cancelDownvote.async\", updateItemScore);\n\n\n\n/**\n * @summary Update the profile of the user doing the operation\n * @param {object} item - The item being operated on\n * @param {object} user - The user doing the operation\n * @param {object} collection - The collection the item belongs to\n * @param {string} operation - The operation being performed\n */\n// function updateUser(item, user, collection, operation, context) {\n  \n//   // uncomment for debug\n//   // console.log(item);\n//   // console.log(user);\n//   // console.log(collection._name);\n//   // console.log(operation);\n\n//   const update = {};\n//   const votePower = getVotePower(user);\n//   const vote = {\n//     itemId: item._id,\n//     votedAt: new Date(),\n//     power: votePower\n//   };\n  \n//   const collectionName = Utils.capitalize(collection._name);\n\n//   switch (operation) {\n//     case \"upvote\":\n//       update.$addToSet = {[`upvoted${collectionName}`]: vote};\n//       break;\n//     case \"downvote\":\n//       update.$addToSet = {[`downvoted${collectionName}`]: vote};\n//       break;\n//     case \"cancelUpvote\":\n//       update.$pull = {[`upvoted${collectionName}`]: {itemId: item._id}};\n//       break;\n//     case \"cancelDownvote\":\n//       update.$pull = {[`downvoted${collectionName}`]: {itemId: item._id}};\n//       break;\n//   }\n\n//   Users.update({_id: user._id}, update);\n\n// }\n\n// addCallback(\"upvote.async\", updateUser);\n// addCallback(\"downvote.async\", updateUser);\n// addCallback(\"cancelUpvote.async\", updateUser);\n// addCallback(\"cancelDownvote.async\", updateUser);\n\n/**\n * @summary Update the karma of the item's owner\n * @param {object} item - The item being operated on\n * @param {object} user - The user doing the operation\n * @param {object} collection - The collection the item belongs to\n * @param {string} operation - The operation being performed\n */\n// function updateKarma(item, user, collection, operation, context) {\n\n//   const votePower = getVotePower(user);\n//   const karmaAmount = (operation === \"upvote\" || operation === \"cancelDownvote\") ? votePower : -votePower;\n\n//   // only update karma is the operation isn't done by the item's author\n//   if (item.userId !== user._id) {\n//     Users.update({_id: item.userId}, {$inc: {\"karma\": karmaAmount}});\n//   }\n\n// }\n\n// addCallback(\"upvote.async\", updateKarma);\n// addCallback(\"downvote.async\", updateKarma);\n// addCallback(\"cancelUpvote.async\", updateKarma);\n// addCallback(\"cancelDownvote.async\", updateKarma);\n"
  },
  {
    "path": "packages/vulcan-voting/lib/server/cron.js",
    "content": "import { getSetting, registerSetting, debug } from 'meteor/vulcan:core';\nimport { /*updateScore,*/ batchUpdateScore } from './scoring.js';\nimport { VoteableCollections } from '../modules/make_voteable.js';\n\nregisterSetting('voting.scoreUpdateInterval', 60, 'How often to update scores, in seconds');\n\n// TODO use a node cron or at least synced-cron\nMeteor.startup(function () {\n  \n  const scoreInterval = parseInt(getSetting('voting.scoreUpdateInterval'));\n\n  if (scoreInterval > 0) {\n\n    VoteableCollections.forEach(collection => {\n\n      // active items get updated every N seconds\n      Meteor.setInterval(async function () {\n\n        // let updatedDocuments = 0;\n\n        // console.log('tick ('+scoreInterval+')');\n        // collection.find({'inactive': {$ne : true}}).forEach(document => {\n        //   updatedDocuments += updateScore({collection, item: document});\n        // });\n\n        const updatedDocuments = await batchUpdateScore(collection, false, false);\n\n        debug(`[vulcan:voting] Updated scores for ${updatedDocuments} active documents in collection ${collection.options.collectionName}`);\n\n      }, scoreInterval * 1000);\n\n      // inactive items get updated every hour\n      Meteor.setInterval(async function () {\n\n\n        // let updatedDocuments = 0;\n        //\n        // collection.find({'inactive': true}).forEach(document => {\n        //   updatedDocuments += updateScore({collection, item: document});\n        // });\n\n        const updatedDocuments = await batchUpdateScore(collection, true, false);\n\n        debug(`[vulcan:voting] Updated scores for ${updatedDocuments} inactive documents in collection ${collection.options.collectionName}`);\n\n      }, 3600 * 1000);\n\n    });\n  }\n});\n\n"
  },
  {
    "path": "packages/vulcan-voting/lib/server/graphql.js",
    "content": "import { addCallback, addGraphQLSchema, addGraphQLResolvers, addGraphQLMutation } from 'meteor/vulcan:core';\nimport { performVoteServer } from '../modules/vote.js';\nimport { VoteableCollections } from '../modules/make_voteable.js';\n\nfunction CreateVoteableUnionType() {\n  const voteableSchema = VoteableCollections.length ? `union Voteable = ${VoteableCollections.map(collection => collection.typeName).join(' | ')}` : '';\n  addGraphQLSchema(voteableSchema);\n  return {};\n}\naddCallback('graphql.init.before', CreateVoteableUnionType);\n\nconst resolverMap = {\n  Voteable: {\n    __resolveType(obj, context, info){\n      return obj.__typename;\n    },\n  },\n};\n\naddGraphQLResolvers(resolverMap);\n\naddGraphQLMutation('vote(documentId: String, voteType: String, collectionName: String, voteId: String) : Voteable');\n\nconst voteResolver = {\n  Mutation: {\n    async vote(root, {documentId, voteType, collectionName, voteId}, context) {\n      \n      const { currentUser } = context;\n      const collection = context[collectionName];\n\n      const document = await performVoteServer({documentId, voteType, collection, voteId, user: currentUser});\n      return document;\n\n    },\n  },\n};\n\naddGraphQLResolvers(voteResolver);\n"
  },
  {
    "path": "packages/vulcan-voting/lib/server/indexes.js",
    "content": "import Votes from '../modules/votes/collection.js';\n\nVotes._ensureIndex({ 'userId': 1, 'documentId': 1 });\n"
  },
  {
    "path": "packages/vulcan-voting/lib/server/main.js",
    "content": "import './graphql.js';\nimport './cron.js';\nimport './scoring.js';\nimport './indexes.js';\n\nexport * from '../modules/index.js';\n"
  },
  {
    "path": "packages/vulcan-voting/lib/server/scoring.js",
    "content": "import { Connectors } from 'meteor/vulcan:core';\nimport { recalculateScore } from '../modules/scoring.js';\n\n/*\n\nUpdate a document's score if necessary.\n\nReturns how many documents have been updated (1 or 0).\n\n*/\nexport const updateScore = async ({collection, item, forceUpdate}) => {\n\n  // Age Check\n  const postedAt = item && item.postedAt && item.postedAt.valueOf();\n  const now = new Date().getTime();\n  const age = now - postedAt;\n  const ageInHours = age / (60 * 60 * 1000);\n\n  // If for some reason item doesn't have a \"postedAt\" property, abort\n  // Or, if post has been scheduled in the future, don't update its score\n  if (postedAt || postedAt > now)\n    return 0;\n\n\n\n  // For performance reasons, the database is only updated if the difference between the old score and the new score\n  // is meaningful enough. To find out, we calculate the \"power\" of a single vote after n days.\n  // We assume that after n days, a single vote will not be powerful enough to affect posts' ranking order.\n  // Note: sites whose posts regularly get a lot of votes can afford to use a lower n.\n\n  // n =  number of days after which a single vote will not have a big enough effect to trigger a score update\n  //      and posts can become inactive\n  const n = 30;\n  // x = score increase amount of a single vote after n days (for n=100, x=0.000040295)\n  const x = 1/Math.pow(n*24+2,1.3);\n\n  // HN algorithm\n  const newScore = recalculateScore(item);\n\n  // Note: before the first time updateScore runs on a new item, its score will be at 0\n  const scoreDiff = Math.abs(item.score || 0 - newScore);\n\n  // console.log('// now: ', now)\n  // console.log('// age: ', age)\n  // console.log('// ageInHours: ', ageInHours)\n  // console.log('// baseScore: ', baseScore)\n  // console.log('// item.score: ', item.score)\n  // console.log('// newScore: ', newScore)\n  // console.log('// scoreDiff: ', scoreDiff)\n  // console.log('// x: ', x)\n\n  // only update database if difference is larger than x to avoid unnecessary updates\n  if (forceUpdate || scoreDiff > x) {\n    await Connectors.update(collection, item._id, {$set: {score: newScore, inactive: false}});\n    return 1;\n  } else if(ageInHours > n*24) {\n    // only set a post as inactive if it's older than n days\n    await Connectors.update(collection, item._id, {$set: {inactive: true}});\n  }\n  return 0;\n};\n\nexport const batchUpdateScore = async (collection, inactive = false, forceUpdate = false) => {\n  // n =  number of days after which a single vote will not have a big enough effect to trigger a score update\n  //      and posts can become inactive\n  const n = 30;\n  // x = score increase amount of a single vote after n days (for n=100, x=0.000040295)\n  const x = 1/Math.pow(n*24+2,1.3);\n  // time decay factor\n  const f = 1.3;\n  const itemsPromise = collection.rawCollection().aggregate([\n    {\n      $match: {\n        $and: [\n          {postedAt: {$exists: true}},\n          {postedAt: {$lte: new Date()}},\n          {inactive: inactive ? true : {$ne: true}}\n        ]\n      }\n    },\n    {\n      $project: {\n        postedAt: 1,\n        baseScore: 1,\n        score: 1,\n        newScore: {\n          $divide: [\n            '$baseScore',\n              {\n                $pow: [\n                  {\n                    $add: [\n                      {\n                        $divide: [\n                          {\n                            $subtract: [new Date(), '$postedAt'] // Age in miliseconds\n                          },\n                          60 * 60 * 1000\n                        ]\n                      }, // Age in hours\n                      2\n                    ]\n                  },\n                  f\n                ]\n              }\n            ]\n        }\n      }\n    },\n    {\n      $project: {\n        postedAt: 1,\n        baseScore: 1,\n        score: 1,\n        newScore: 1,\n        scoreDiffSignificant: {\n          $gt: [\n            {$abs: {$subtract: ['$score', '$newScore']}},\n            x\n          ]\n        },\n        oldEnough: { // Only set a post as inactive if it's older than n days\n          $gt: [\n            {$divide: [\n              {\n                $subtract: [new Date(), '$postedAt'] // Difference in miliseconds\n              },\n              60 * 60 * 1000 //Difference in hours\n            ]},\n            n*24]\n        }\n      }\n    },\n  ]);\n\n  const items = await itemsPromise;\n  const itemsArray = await items.toArray();\n  let updatedDocumentsCounter = 0;\n  const itemUpdates = _.compact(itemsArray.map(i => {\n    if (forceUpdate || i.scoreDiffSignificant) {\n      updatedDocumentsCounter++;\n      return {\n        updateOne: {\n          filter: {_id: i._id},\n          update: {$set: {score: i.newScore, inactive: false}},\n          upsert: false,\n        }\n      };\n    } else if (i.oldEnough) {\n      // only set a post as inactive if it's older than n days\n      return {\n        updateOne: {\n          filter: {_id: i._id},\n          update: {$set: {inactive: true}},\n          upsert: false,\n        }\n      };\n    }\n  }));\n  if (itemUpdates && itemUpdates.length) {await collection.rawCollection().bulkWrite(itemUpdates, {ordered: false});}\n  return updatedDocumentsCounter;\n};\n"
  },
  {
    "path": "packages/vulcan-voting/package.js",
    "content": "Package.describe({\n  name: 'vulcan:voting',\n  summary: 'Vulcan scoring package.',\n  version: '1.16.9',\n  git: 'https://github.com/VulcanJS/Vulcan.git',\n});\n\nPackage.onUse(function(api) {\n  api.use(['vulcan:scss@4.12.0', 'vulcan:core@=1.16.9', 'vulcan:i18n@=1.16.9'], ['client', 'server']);\n\n  api.mainModule('lib/server/main.js', 'server');\n  api.mainModule('lib/client/main.js', 'client');\n});\n"
  },
  {
    "path": "sample_settings.json",
    "content": "{\n  \"public\": {\n\n    \"title\": \"Your site title\",\n    \"tagline\":\"Your site tagline\",\n\n    \"logoUrl\": \"http://placekitten.com/250/80\",\n    \"logoHeight\": \"80\",\n    \"logoWidth\": \"250\",\n    \"faviconUrl\": \"/favicon.ico\",\n\n    \"language\": \"en\",\n    \"locale\": \"en\",\n\n    \"twitterAccount\": \"foo\",\n    \"facebookPage\": \"http://facebook.com/foo\",\n\n    \"googleAnalytics\": {\n      \"apiKey\": \"foo123\"\n    },\n\n    \"backoffice\": {\n      \"enabled\": true\n    },\n\n    \"apolloSsr\": {\n      \"disable\": false\n    }\n  },\n\n  \"defaultEmail\": \"hello@world.com\",\n  \"mailUrl\": \"smtp://username%40yourdomain.mailgun.org:yourpassword123@smtp.mailgun.org:587/\",\n\n  \"oAuth\": {\n    \"twitter\": {\n      \"consumerKey\": \"foo\",\n      \"secret\": \"bar\"\n    },\n    \"facebook\": {\n      \"appId\": \"foo\",\n      \"secret\": \"bar\"\n    }\n  },\n  \"apolloServer\": {\n    \"corsWhitelist\": [],\n    \"corsEnableAll\": false\n  }\n}\n"
  },
  {
    "path": "stories/MUI/forms/formBaseControls.stories.js",
    "content": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport { action } from '@storybook/addon-actions';\n// import { linkTo } from '@storybook/addon-links';\nimport { Components } from 'meteor/vulcan:core';\nimport 'meteor/vulcan:forms';\nimport { withKnobs, boolean } from '@storybook/addon-knobs';\n\nconst vulcanFormsBaseControls = storiesOf('MUI/Forms/BaseControls', module);\nvulcanFormsBaseControls.addDecorator(withKnobs);\n\nconst options = [\n  {\n    value: 'Afghan',\n    label: 'Afghan',\n  },\n  {\n    value: 'Albanais',\n    label: 'Albanais',\n  },\n  {\n    value: 'Algérien',\n    label: 'Algérien',\n  },\n  {\n    value: 'Allemand',\n    label: 'Allemand',\n  },\n  {\n    value: 'Belge',\n    label: 'Belge',\n  },\n  {\n    value: 'Beninois',\n    label: 'Beninois',\n  },\n  {\n    value: 'Bosniaque',\n    label: 'Bosniaque',\n  },\n  {\n    value: 'Botswanais',\n    label: 'Botswanais',\n  },\n];\n\nvulcanFormsBaseControls\n  .add('Form base-controls MuiSuggest', () => <Components.MuiSuggest options={options} />)\n  .add('Form base-controls MuiRequiredIndicator', () => {\n    const optional = boolean('optional', false);\n    return <Components.RequiredIndicator optional={optional} />;\n  });\n"
  },
  {
    "path": "stories/MUI/ui-material.stories.js",
    "content": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport { withKnobs, text, boolean, select, number, object } from '@storybook/addon-knobs';\nimport { wrapInParent } from '../helpers';\nimport { Components } from 'meteor/vulcan:core';\nimport { lorem } from '../helpers';\n\n/*\n\nMaterial UI Bonus Components Stories\n\n*/\n\n/*\n\nTooltipButton\n\n*/\n\nconst tooltipButton = storiesOf('MUI/Bonus/TooltipButton', module);\ntooltipButton.addDecorator(withKnobs);\n\ntooltipButton\n  .add('Default', () => {\n    const doWrap = boolean('Wrap in parent', false);\n    const title = text('title', 'Click button to continue');\n    const titleId = text('titleId', null);\n    const titleValues = object('titleValues', {});\n    const label = text('label', 'Click me');\n    const labelId = text('labelId', null);\n    const type = select('type', [null, 'simple', 'fab', 'button', 'submit', 'icon'], 'button');\n    const size = select('size', [null, 'icon', 'xsmall', 'small', 'medium', 'large']);\n    const variant = select('variant', [null, 'text', 'outlined', 'contained'], 'contained');\n    const color = select('color', [null, 'default', 'inherit', 'primary', 'secondary'], 'primary');\n    const placement = select(\n      'placement',\n      [\n        null,\n        'bottom-end',\n        'bottom-start',\n        'bottom',\n        'left-end',\n        'left-start',\n        'left',\n        'right-end',\n        'right-start',\n        'right',\n        'top-end',\n        'top-start',\n        'top',\n      ],\n      null\n    );\n    const icon = text('icon', 'MaterialIcon1');\n    const loading = boolean('loading', null);\n    const disabled = boolean('disabled', null);\n    const enterDelay = number('enterDelay', null);\n    const leaveDelay = number('leaveDelay', null);\n    const parent = select('parent', [null, 'default', 'popover'], null);\n\n    const button = (\n      <Components.TooltipButton\n        title={title}\n        titleId={titleId}\n        titleValues={titleValues}\n        label={label}\n        labelId={labelId}\n        type={type}\n        size={size}\n        variant={variant}\n        color={color}\n        placement={placement}\n        icon={icon}\n        loading={loading}\n        disabled={disabled}\n        enterDelay={enterDelay}\n        leaveDelay={leaveDelay}\n        parent={parent}\n      />\n    );\n\n    return wrapInParent(button, doWrap);\n  })\n\n  .add('Icon', () => {\n    const doWrap = boolean('Wrap in parent', false);\n\n    const button = <Components.TooltipButton title=\"Click button to continue\" type=\"icon\" icon=\"MaterialIcon1\" />;\n\n    return wrapInParent(button, doWrap);\n  })\n\n  .add('Fab', () => {\n    const doWrap = boolean('Wrap in parent', false);\n\n    const button = <Components.TooltipButton title=\"Click button to continue\" type=\"fab\" icon=\"MaterialIcon1\" color=\"secondary\" />;\n\n    return wrapInParent(button, doWrap);\n  })\n\n  .add('Button', () => {\n    const doWrap = boolean('Wrap in parent', false);\n\n    const button = <Components.TooltipButton title=\"Click me\" type=\"button\" />;\n\n    return wrapInParent(button, doWrap);\n  })\n\n  .add('Other', () => {\n    const doWrap = boolean('Wrap in parent', false);\n\n    const button = <Components.TooltipButton title=\"Explanation of static text\">Hover over static text</Components.TooltipButton>;\n\n    return wrapInParent(button, doWrap);\n  });\n\nconst modalProps = {\n  title: 'My Modal',\n  header: 'My Header',\n  footer: 'My Footer',\n};\nstoriesOf('MUI/Modaltrigger').add('ModalTrigger/Button type', () => (\n  <Components.ModalTrigger modalProps={modalProps} type=\"button\" label=\"Click Me\">\n    <div>{lorem}</div>\n  </Components.ModalTrigger>\n));\n"
  },
  {
    "path": "stories/card.stories.js",
    "content": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport { dataSampleCollection } from './dataSample/dummyCollection';\n\nimport { Components } from 'meteor/vulcan:core';\n\n/*\n\nCard\n\n*/\n\nconst card = storiesOf('Core/Card', module);\n\nconst cardProps = {\n  collection: dataSampleCollection,\n  document: {\n    title: 'My title',\n    url: 'https://vulcanjs.org',\n    image: 'https://cl.ly/6906b7446a73/Screen%20Shot%202019-02-25%20at%2010.19.47.png',\n    isTrue: false,\n    answerToLife: 42,\n    myObject: { foo: 12, bar: 'baz' },\n    now: new Date(),\n    // component: (\n    //   <label>\n    //     <input type=\"checkbox\" /> My Checkbox\n    //   </label>\n    // ),\n    array: ['foo', 'bar'],\n  },\n};\n\ncard.add('Default', () => <Components.Card {...cardProps} />);\n"
  },
  {
    "path": "stories/dataSample/dummyCollection.js",
    "content": "import { peopleSchema } from './schema';\nimport { createDummyCollection } from 'meteor/vulcan:test';\n\nexport const dataSampleCollection = createDummyCollection({ schema: peopleSchema });\n"
  },
  {
    "path": "stories/dataSample/schema.js",
    "content": "import { Component } from 'react';\n\nexport const peopleSchema = {\n  _id: {\n    type: String,\n    canRead: ['guests'],\n    label: 'id',\n    optional: true,\n  },\n  createdAt: {\n    type: Date,\n    optional: true,\n    canRead: ['guests'],\n    onCreate: () => new Date(),\n  },\n  lastModification: {\n    type: Date,\n    optional: true,\n    canRead: ['guests'],\n    onCreate: () => new Date(),\n    onUpdate: () => new Date(),\n  },\n  userId: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n    canCreate: ['guests'],\n  },\n  title: {\n    type: String,\n    optional: false,\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n    canCreate: ['guests'],\n  },\n  url: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n    canCreate: ['guests'],\n    input: 'url',\n    placeholder: 'https://',\n  },\n  image: {\n    type: String,\n    optional: true,\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n    canCreate: ['guests'],\n    input: 'url',\n  },\n  isTrue: {\n    type: Boolean,\n    optional: true,\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n    canCreate: ['guests'],\n  },\n  answerToLife: {\n    type: Number,\n    optional: true,\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n    canCreate: ['guests'],\n  },\n  myObject: {\n    type: Object,\n    optional: true,\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n    canCreate: ['guests'],\n  },\n  now: {\n    type: Date,\n    optional: true,\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n    canCreate: ['guests'],\n  },\n  array: {\n    type: Array,\n    optional: true,\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n    canCreate: ['guests'],\n  },\n  'array.$': {\n    type: String,\n  },\n  component: {\n    type: Component,\n    optional: true,\n    canRead: ['guests'],\n    canUpdate: ['guests'],\n    canCreate: ['guests'],\n  },\n};\n"
  },
  {
    "path": "stories/datatable.stories.js",
    "content": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport { Components } from 'meteor/vulcan:core';\nimport 'meteor/vulcan:core';\n\nimport { withKnobs, text, boolean, select, number, object } from '@storybook/addon-knobs';\nimport { action } from '@storybook/addon-actions';\n\n\nconst DatatableContentsStories = storiesOf('Core/Datatable/DatatableContents', module);\nDatatableContentsStories.addDecorator(withKnobs);\n\n\nconst defaultProps = {\n    DatatableContents: {\n        title: 'foobar',\n        results: [{ 'foo': 'bar', 'answer': 42 }, { 'foo': 'bar1', 'answer': 4 }],\n        columns: [{\n            label: 'foo',\n            name: 'foo',\n        }, {\n            label: 'answer',\n            name: 'answer',\n        }],\n        Components: Components,\n    },\n    DatatableContentsWithoutColumns: {\n        title: 'foobar',\n        results: [{ 'foo': 'bar', 'answer': 42 }, { 'foo': 'bar1', 'answer': 4 }],\n        Components: Components,\n    },\n    DatatableAbove: {\n        showSearch: true,\n        showNew: true,\n        canInsert: true,\n        Components: Components\n\n    }\n};\n\nDatatableContentsStories\n    .add('DatatableContents - interactive', () => {\n        const title = text('title', 'My datatable');\n        const results = [{ 'foo': 'bar', 'answer': 42 }];\n        const columns = [{\n            label: 'Foo',\n            name: 'foo',\n            order: 1\n        }, {\n            label: 'answer',\n            name: 'answer',\n            order: 2\n        }];\n        return (\n            <Components.DatatableContents\n                {...defaultProps.DatatableContents}\n                title={title}\n                results={object('results', results)}\n                columns={object('columns', columns)}\n\n            />\n        );\n    })\n    .add('DatatableContents - loading', () => (\n        <Components.DatatableContents\n          {...defaultProps.DatatableContents}\n          loading={true}\n        />\n    ))\n    .add('DatatableContents - error', () => (\n        <Components.DatatableContents\n            {...defaultProps.DatatableContents}\n            error={{ message: 'foo' }}\n        />\n    ))\n\n    // NOT WORKING => supposed to be checking dans DatatableContents   \n    // if no columns are provided, default to using keys of first array item\n    // if (!columns) {\n    //   columns = Object.keys(results[0]).filter(k => k !== '__typename');\n    // }\n\n    .add('DatatableContents - withoutColumns', () => (\n        <Components.DatatableContents\n            {...defaultProps.DatatableContentsWithoutColumns}\n        />\n    ))\n\n    //NOT PERFECT => The Edit part in the DatatableContentsHeadLayout doesn't appear\n    .add('DatatableContents - showEdit', () => (\n        <Components.DatatableContents\n            {...defaultProps.DatatableContents}\n            showEdit={true}\n        />\n    ))\n    // DOES NOT WORK => PR to do on the ShowNew Props \n    .add('DatatableContents - showNew', () => (\n        <Components.DatatableContents\n            {...defaultProps.DatatableContents}\n            showNew={true}\n        />\n    ))\n    // DOES NOT WORK \n    .add('DatatableContents - Display DatableEmpty', () => {\n        return (\n            < Components.DatatableContents\n                {...defaultProps.DatatableContents}\n                results={[]}\n            />\n        );\n    })\n\n    .add('DatableEmpty ', () => (\n        < Components.DatatableEmpty />\n    ))\n\n    // TO FINISH => TRYING TO MAKE APPEAR THE DATATABLE LOADMORE BUTTON \n    .add(' DatatableContents - DatatableLoadMoreButton ', () => {\n        return (<div>\n            <Components.DatatableContents\n                {...defaultProps.DatatableContents}\n            />\n            <p>{defaultProps.DatatableContents.results.length}</p></div >\n        );\n    });\n\n\nconst DatatableAboveStories = storiesOf('Core/Datatable/DatatableAbove', module);\nDatatableAboveStories.addDecorator(withKnobs);\n\nDatatableAboveStories\n    // NOT PERFECT 1/ The value props is not there & 2/ The placeholder props is a string while in the component it's a \n    // placeholder={`${intl.formatMessage({\n    //    id: 'datatable.search',\n    //    defaultMessage: 'Search',\n    //  })}…`}\n    .add('DatatableAboveSearchInput - interactive ', () => (\n        <Components.DatatableAboveSearchInput\n            Components={Components}\n            placeholder={text('placeholder', 'placeholder')}\n            type={text('text', 'text')}\n            onChange={action('onChange')}\n\n        />\n    ))\n\n    .add('NewButton ', () => (\n        <Components.NewButton\n        />\n    ))\n\n\n    .add('DatatableAbove ', () => (\n        <Components.DatatableAbove\n            {...defaultProps.DatatableAbove}>\n        </Components.DatatableAbove>\n\n    ))\n\n    .add('DatatableAbove - Interactive ', () => {\n        const showSearch = boolean('showSearch', true);\n        const showNew = boolean('showNew', true);\n        const canInsert = boolean('canInsert', true);\n        return (\n            <Components.DatatableAbove\n                showSearch={showSearch}\n                showNew={showNew}\n                canInsert={canInsert}\n                Components={Components}\n            >\n            </Components.DatatableAbove>\n\n        );\n    })\n\n\n   \n"
  },
  {
    "path": "stories/form/form.stories.js",
    "content": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport { action } from '@storybook/addon-actions';\n// import { linkTo } from '@storybook/addon-links';\nimport { Components } from 'meteor/vulcan:core';\nimport 'meteor/vulcan:forms';\nimport { withKnobs, boolean, text } from '@storybook/addon-knobs';\n\nconst vulcanForms = storiesOf('Core/Forms/CoreComponents', module);\nvulcanForms.addDecorator(withKnobs);\n\nvulcanForms.add('FormComponentInner with Date component', () => {\n  const name = text('Name', 'myForm');\n  const beforeComponent = text('beforeComponent', 'beforeComponent');\n  const afterComponent = text('afterComponent', 'afterComponent');\n  const help = text('help', 'help');\n  const showCharsRemaining = boolean('showCharsRemaining', false);\n  return (\n    <Components.FormComponentInner\n      name={name}\n      errors={[]}\n      onChange={action('call onChange function')}\n      showCharsRemaining={showCharsRemaining}\n      formInput={Components.FormComponentDate}\n      beforeComponent={<div>{beforeComponent}</div>}\n      afterComponent={<div>{afterComponent}</div>}\n      help={<div>{help}</div>}\n    />\n  );\n});\n\nvulcanForms\n  .add('FormError - message', () => (\n    <Components.FormError\n      error={{\n        message: 'An error message',\n      }}\n    />\n  ))\n  .add('FormError intl - no properties', () => (\n    <Components.FormError\n      error={{\n        id: 'intl-id',\n      }}\n    />\n  ))\n  .add('FormError intl', () => (\n    <Components.FormError\n      error={{\n        id: 'intl-id',\n        properties: {\n          name: 'address.street',\n        },\n      }}\n    />\n  ));\n\nvulcanForms\n  .add('FormGroupHeader', () => {\n    const label = text('Header label', 'myLabel');\n    const collapsed = boolean('collapsed', false);\n    const hidden = boolean('hidden', false);\n    const hasErrors = boolean('hasErrors', false);\n    const textInside = text('Text inside', 'My text inside');\n    const groupName = text('Group name', 'admin');\n    const collapsible = boolean('collapsible', true);\n    const group = { name: [groupName], collapsible: collapsible };\n    return (\n      <div>\n        <Components.FormGroupHeader label={label} group={group} />\n        <Components.FormGroupLayout\n          label=\"labelInner\"\n          anchorName=\"anchorNameInner\"\n          collapsed={collapsed}\n          hidden={hidden}\n          hasErrors={hasErrors}\n          group={group}>\n          {textInside}\n        </Components.FormGroupLayout>\n      </div>\n    );\n  })\n  .add('FormGroupLine', () => {\n    const label = text('Header label', 'myLabel');\n    const collapsed = boolean('collapsed', false);\n    const hidden = boolean('hidden', false);\n    const hasErrors = boolean('hasErrors', false);\n    const textInside = text('Text inside', 'My text inside');\n    const groupName = text('Group name', 'admin');\n    const collapsible = boolean('collapsible', true);\n    const group = { name: [groupName], collapsible: collapsible };\n    return (\n      <div>\n        <Components.FormGroupHeaderLine label={label} group={group} />\n        <Components.FormGroupLayoutLine\n          label=\"labelInner\"\n          anchorName=\"anchorNameInner\"\n          collapsed={collapsed}\n          hidden={hidden}\n          hasErrors={hasErrors}\n          group={group}>\n          {textInside}\n        </Components.FormGroupLayoutLine>\n      </div>\n    );\n  })\n  .add('FormGroupNone', () => {\n    const collapsed = boolean('collapsed', false);\n    const hidden = boolean('hidden', false);\n    const hasErrors = boolean('hasErrors', false);\n    const textInside = text('Text inside', 'My text inside');\n    const groupName = text('Group name', 'admin');\n    const collapsible = boolean('collapsible', true);\n    const group = { name: [groupName], collapsible: collapsible };\n    return (\n      <div>\n        <Components.FormGroupHeaderNone />\n        <Components.FormGroupLayoutNone\n          label=\"labelInner\"\n          anchorName=\"anchorNameInner\"\n          collapsed={collapsed}\n          hidden={hidden}\n          hasErrors={hasErrors}\n          group={group}>\n          {textInside}\n        </Components.FormGroupLayoutNone>\n      </div>\n    );\n  });\n\nvulcanForms.add('FormNestedArrayLayout - nested item before/after components', () => (\n  <Components.FormNestedArray\n    value={[{ name: 'Jane' }, { name: 'DELETED' }, { name: 'John' }]}\n    deletedValues={['peoples.1']}\n    path=\"peoples\"\n    formComponents={{\n      ...Components,\n      FormNestedItem: () => (\n        <div>\n          <input />\n        </div>\n      ),\n    }}\n    errors={[]}\n    updateCurrentValues={action('updateCurrentValues')}\n    arrayField={{\n      beforeComponent: props => (\n        <div>\n          BEFORE {props.itemIndex} {props.visibleItemIndex}\n        </div>\n      ),\n      afterComponent: props => <div>AFTER</div>,\n    }}\n  />\n));\n\nvulcanForms.add('FormSubmit', () => {\n  const submitLabel = text('Submit Label', 'Submit Label');\n  return <Components.FormSubmit submitLabel={submitLabel} />;\n});\n"
  },
  {
    "path": "stories/form/formControls.stories.js",
    "content": "import React from 'react';\nimport merge from 'lodash/merge';\nimport { storiesOf } from '@storybook/react';\n\nimport { Components } from 'meteor/vulcan:core';\n\n/*\n\nForm Components Stories\n\n*/\nconst options = [\n  {\n    label: 'Option 1',\n    value: 'opt1',\n  },\n  {\n    label: 'Option 2',\n    value: 'opt2',\n  },\n  {\n    label: 'Option 3',\n    value: 'opt3',\n  },\n];\n\nconst defaultFormProps = {\n  inputProperties: {\n    value: 'hello world',\n    onChange: () => { },\n  },\n};\n\nconst formComponents = [\n  { name: 'FormComponentCheckbox' },\n  {\n    name: 'FormComponentCheckboxGroup',\n    props: {\n      inputProperties: {\n        value: ['opt1', 'opt3'],\n        options,\n      },\n    },\n  },\n  {\n    name: 'FormComponentDate',\n    props: {\n      inputProperties: {\n        value: new Date(),\n      },\n    },\n  },\n  {\n    name: 'FormComponentDate2',\n    props: {\n      inputProperties: {\n        value: new Date(),\n      },\n    },\n  },\n  {\n    name: 'FormComponentDateTime',\n    props: {\n      inputProperties: {\n        value: new Date(),\n      },\n    },\n  },\n  { name: 'FormComponentDefault' },\n  { name: 'FormComponentText' },\n  { name: 'FormComponentPassword' },\n  {\n    name: 'FormComponentEmail',\n    props: {\n      inputProperties: {\n        value: 'hello@vulcanjs.org',\n      },\n    },\n  },\n  {\n    name: 'FormComponentNumber',\n    props: {\n      inputProperties: {\n        value: 42,\n      },\n    },\n  },\n  {\n    name: 'FormComponentRadioGroup',\n    props: {\n      inputProperties: {\n        value: 'opt2',\n        options,\n      },\n    },\n  },\n  {\n    name: 'FormComponentSelect',\n    props: {\n      value: 'opt2',\n      options,\n    },\n  },\n  {\n    name: 'FormComponentSelectMultiple',\n    props: {\n      value: ['opt2'],\n      options,\n    },\n  },\n  { name: 'FormComponentStaticText' },\n  { name: 'FormComponentTextarea' },\n  { name: 'FormComponentTime' },\n  { name: 'FormComponentUrl', props: { value: 'http://vulcanjs.org/' } },\n  { name: 'FormControl' },\n  { name: 'FormElement' },\n  { name: 'FormItem' },\n];\n\n/*\n  \n  To get form input props, merge:\n  \n  1. default props common to all inputs\n  2. specific props for current input\n  3. dynamic props generated from name\n  4. props specific to the current story\n  \n  */\nconst getFormProps = (componentName, storyProps) => {\n  const component = formComponents.find(c => c.name === componentName);\n  const componentLabel = componentName.replace('FormComponent', '');\n  const dynamicProps = {\n    inputProperties: {\n      label: `${componentLabel} Input`,\n    },\n  };\n  return merge({}, defaultFormProps, component.props, dynamicProps, storyProps);\n};\n\nformComponents.forEach(item => {\n  const { name } = item;\n  const componentLabel = name.replace('FormComponent', '');\n  const storyName = `Core/Forms/Controls/${componentLabel}`;\n  storiesOf(storyName, module)\n    .add('Horizontal Layout', () => {\n      const Component = Components[name];\n      if (!Component) return `Component ${name} not found. Are you importing an UI package?`;\n      <Component {...getFormProps(name)} />;\n    })\n    .add('Input Only', () => {\n      const Component = Components[name];\n      if (!Component) return `Component ${name} not found. Are you importing an UI package?`;\n      return (\n\n        <Component\n          {...getFormProps(name, {\n            itemProperties: { layout: 'inputOnly' },\n          })}\n        />\n      );\n    });\n});\n"
  },
  {
    "path": "stories/form/upload.stories.js",
    "content": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport { action } from '@storybook/addon-actions';\nimport { Components } from 'meteor/vulcan:core';\nimport 'meteor/vulcan:forms-upload/lib/modules.js';\nimport PropTypes from 'prop-types';\n\n// @see https://reactjs.org/docs/legacy-context.html\n// needed for legacy components\nconst makeLegacyContextProvider = (value, shape) => {\n    class LegacyContextProvider extends React.Component {\n        getChildContext() {\n            return value;\n        }\n        render() {\n            return <div>{this.props.children}</div>;\n        }\n    }\n    LegacyContextProvider.childContextTypes = shape;\n    return LegacyContextProvider;\n};\n\nconst contextDecorator = story => {\n    const Provider = makeLegacyContextProvider({\n        addToSubmitForm: action('addToSubmitForm'),\n    }, {\n        addToSubmitForm: PropTypes.func\n    });\n    return (\n        <Provider>\n            {story()}\n        </Provider>\n    );\n};\n\nstoriesOf('Core/FormUpload', module)\n    .addDecorator(contextDecorator)\n    .add('UploadInput', () => {\n        const Provider = makeLegacyContextProvider({\n            addToSubmitForm: action('addToSubmitForm'),\n        }, {\n            addToSubmitForm: PropTypes.func\n        });\n        return (\n            <Provider>\n                <Components.Upload />\n            </Provider >\n        );\n    });\n\n\n"
  },
  {
    "path": "stories/helpers.js",
    "content": "import React from 'react';\nimport SvgIcon from '@material-ui/core/SvgIcon';\nimport { registerComponent } from 'meteor/vulcan:core';\n\nexport function capitalize (string) {\n  return string.replace(/\\-/, ' ').split(' ').map(word => {\n    return word.charAt(0).toUpperCase() + word.slice(1);\n  }).join(' ');\n}\n\nexport function wrapInParent (component, doWrap) {\n  return doWrap\n    ?\n    <span style={{ backgroundColor: '#DDDDDD', padding: 48, display: 'inline-block' }}>\n        <span style={{ backgroundColor: '#FFFFFF', display: 'inline-block' }}>\n          {component}\n        </span>\n      </span>\n    :\n    component;\n}\n\nexport const lorem = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do\neiusmod tempor incididunt ut labore et dolore magna aliqua.`;\n\nexport const MaterialIcon1 = (props) => {\n  return (\n    <SvgIcon {...props}>\n      <path d=\"M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z\" />\n    </SvgIcon>\n  );\n};\n\nregisterComponent('MaterialIcon1', MaterialIcon1);\n"
  },
  {
    "path": "stories/modal.stories.js",
    "content": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport { lorem } from './helpers';\n\nimport { Components } from 'meteor/vulcan:core';\n/*\n\nModal & ModalTrigger\n\n*/\nconst modalProps = {\n  title: 'My Modal',\n  header: 'My Header',\n  footer: 'My Footer',\n};\n\nstoriesOf('UI/Modal', module).add('Default', () => (\n  <Components.Modal {...modalProps} show={true}>\n    {lorem}\n  </Components.Modal>\n));\n\nstoriesOf('UI/ModalTrigger', module)\n  .add('trigger', () => (\n    <Components.ModalTrigger modalProps={modalProps} trigger={<a href=\"#\">Click Me</a>}>\n      <div>{lorem}</div>\n    </Components.ModalTrigger>\n  ))\n  .add('component', () => (\n    <Components.ModalTrigger modalProps={modalProps} component={<a href=\"#\">Click Me</a>}>\n      <div>{lorem}</div>\n    </Components.ModalTrigger>\n  ))\n  .add('label', () => (\n    <Components.ModalTrigger modalProps={modalProps} label=\"Click me\">\n      <div>{lorem}</div>\n    </Components.ModalTrigger>\n  ));\n"
  },
  {
    "path": "stories/ref.stories.js",
    "content": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport { Components, registerComponent } from 'meteor/vulcan:core';\n\nconst ChildComponent = React.forwardRef((props, ref) => {\n  return (\n    <input\n      type='text'\n      ref={ref} />\n  )\n});\n\nregisterComponent({ name: 'ChildComponent', component: ChildComponent });\n\n\nconst FatherComponent = props => {\n  const textInput = React.createRef();\n  const focusTextInput = () => textInput.current.focus();\n  return (\n      <div>\n        <Components.ChildComponent ref={textInput} />\n        <input\n          type='button'\n          value='Focus the text input in ChildComponent'\n          onClick={focusTextInput}\n        />\n      </div>\n  );\n};\n\nregisterComponent({ name: 'FatherComponent', component: FatherComponent });\n\nconst refs = storiesOf('Example/refs', module);\n\nrefs.add('Default', () => {\n  return <Components.FatherComponent />;\n});\n"
  },
  {
    "path": "stories/vulcan.stories.js",
    "content": "import React from 'react';\nimport merge from 'lodash/merge';\nimport { storiesOf } from '@storybook/react';\nimport { withKnobs, text, boolean, select } from '@storybook/addon-knobs';\nimport { capitalize, wrapInParent, lorem } from './helpers';\n// import { action } from '@storybook/addon-actions';\n// import { linkTo } from '@storybook/addon-links';\n\nimport { Components } from 'meteor/vulcan:core';\n\n/*\n\nUI Components Stories\n\n*/\n\n/*\n\nAlert\n\n*/\nconst alerts = ['primary', 'secondary', 'success', 'danger', 'warning'];\n\nconst alert = storiesOf('UI/Alert', module);\n\nalerts.forEach(variant => alert.add(capitalize(variant), () => <Components.Alert variant={variant}>{lorem}</Components.Alert>));\n\n/*\n\nAvatar\n\n*/\nconst avatar = storiesOf('UI/Avatar', module);\navatar.addDecorator(withKnobs);\n\navatar.add('Default', () => {\n  const doWrap = boolean('Wrap in parent', false);\n  const displayName = text('user.displayName', 'John Smith');\n  const avatarUrl = text('user.avatarUrl', 'https://res.cloudinary.com/picoum/image/upload/v1525703248/KeyBee/Erik_2008_Large_ylfrmh.jpg');\n  const isAdmin = boolean('user.isAdmin', false);\n  const size = select('size', [null, 'xsmall', 'small', 'medium', 'large', 'profile']);\n  const gutter = select('gutter', [null, 'bottom', 'left', 'right', 'sides', 'all', 'none']);\n  const link = boolean('link', true);\n\n  const avatar = <Components.Avatar user={{ displayName, avatarUrl, isAdmin }} size={size} gutter={gutter} link={link} />;\n\n  return wrapInParent(avatar, doWrap);\n});\n\n/*\n\nButton\n\n*/\nconst button = storiesOf('UI/Button', module);\nbutton.addDecorator(withKnobs);\n\nbutton.add('Default', () => {\n  const doWrap = boolean('Wrap in parent', false);\n  const children = text('children', 'Click Me');\n  const variant = select('variant', [\n    null,\n    'primary',\n    'secondary',\n    'success',\n    'warning',\n    'danger',\n    'info',\n    'light',\n    'dark',\n    'link',\n    'outline-primary',\n    'outline-secondary',\n    'outline-success',\n    'outline-warning',\n    'outline-danger',\n    'outline-info',\n    'outline-light',\n    'outline-dark',\n    'inherit',\n  ]);\n  const size = select('size', [null, 'sm', 'md', 'lg']);\n  const iconButton = boolean('iconButton', false);\n  const disabled = boolean('disabled', false);\n\n  const button = (\n    <Components.Button variant={variant} size={size} disabled={disabled} iconButton={iconButton}>\n      {iconButton ? <Components.IconAdd /> : children}\n    </Components.Button>\n  );\n\n  return wrapInParent(button, doWrap);\n});\n\n/*\n\nDropdown\n\n*/\nconst dropdownProps = {\n  label: 'My Dropdown',\n  menuItems: [{ label: 'Item 1' }, { label: 'Item 2' }, { label: 'Item 3' }, { label: 'Item 4' }],\n};\n\nstoriesOf('UI/Dropdown', module)\n  .add('Default', () => <Components.Dropdown {...dropdownProps} />)\n  .add('Custom Menu Items', () => (\n    <Components.Dropdown\n      {...merge(dropdownProps, {\n        menuItems: [\n          {\n            component: (\n              <label>\n                <input type=\"checkbox\" />\n                My Checkbox\n              </label>\n            ),\n          },\n        ],\n      })}\n    />\n  ))\n  .add('Custom Content', () => (\n    <Components.Dropdown\n      {...merge(dropdownProps, {\n        menuContents: <div style={{ padding: 20 }}>{lorem}</div>,\n      })}\n    />\n  ))\n  .add('Custom Trigger', () => <Components.Dropdown {...merge(dropdownProps, { trigger: <a href=\"#\">Click Me</a> })} />);\n"
  }
]