diff --git a/.bithoundrc b/.bithoundrc index 1ba637ed768..291cc454954 100644 --- a/.bithoundrc +++ b/.bithoundrc @@ -59,7 +59,14 @@ "mute": [ "wdio-mocha-framework", "griddle-react", - "nodemailer" + "nodemailer", + "twilio", + "react-addons-create-fragment", + "react-addons-pure-render-mixin", + "react-addons-test-utils", + "react-dom", + "react", + "transliteration" ], "unused-ignores": [ "jquery", diff --git a/.meteor/packages b/.meteor/packages index 559d518aadc..1f5ce282dcf 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -11,14 +11,14 @@ meteor-base@1.0.4 # Packages every Meteor app needs to have mobile-experience@1.0.4 # Packages for a great mobile UX blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers. -ecmascript@0.7.2 # Enable ECMAScript2015+ syntax in app code +ecmascript@0.7.3 # Enable ECMAScript2015+ syntax in app code audit-argument-checks@1.0.7 # ensure meteor method argument validation browser-policy@1.1.0 # security-related policies enforced by newer browsers juliancwirko:postcss # CSS post-processing plugin (replaces standard-minifier-css) abernix:standard-minifier-js # a minifier plugin used for Meteor apps by default session@1.1.7 # ReactiveDict whose contents are preserved across Hot Code Push -tracker@1.1.2 # Meteor transparent reactive programming library -mongo@1.1.16 +tracker@1.1.3 # Meteor transparent reactive programming library +mongo@1.1.17 random@1.0.10 reactive-var@1.0.11 reactive-dict@1.1.8 @@ -36,8 +36,8 @@ mdg:validated-method shell-server@0.2.3 # Meteor Auth Packages -accounts-base@1.2.16 -accounts-password@1.3.5 +accounts-base@1.2.17 +accounts-password@1.3.6 accounts-facebook@1.1.1 accounts-google@1.1.2 accounts-twitter@1.2.1 @@ -63,9 +63,7 @@ jeremy:stripe jparker:gravatar juliancwirko:s-alert juliancwirko:s-alert-stackslide -kadira:blaze-layout kadira:dochead -kadira:flow-router-ssr matb33:collection-hooks meteorhacks:ssr meteorhacks:subs-manager @@ -74,7 +72,7 @@ ongoworks:security raix:ui-dropped-event risul:moment-timezone tmeasday:publish-counts -vsivsi:job-collection +vsivsi:job-collection@1.4.0 react-meteor-data percolate:migrations gadicc:blaze-react-component diff --git a/.meteor/release b/.meteor/release index 605b4e1f031..fb6f3bc15e2 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.4.4.1 +METEOR@1.4.4.2 diff --git a/.meteor/versions b/.meteor/versions index d38b3d4bde6..eeb4ef9e5fd 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,14 +1,14 @@ abernix:minifier-js@1.3.19 abernix:standard-minifier-js@1.3.19 -accounts-base@1.2.16 +accounts-base@1.2.17 accounts-facebook@1.1.1 accounts-google@1.1.2 accounts-oauth@1.1.15 -accounts-password@1.3.5 +accounts-password@1.3.6 accounts-twitter@1.2.1 alanning:roles@1.2.16 aldeed:autoform@5.8.1 -aldeed:browser-tests@0.1.0 +aldeed:browser-tests@0.1.1 aldeed:collection2@2.10.0 aldeed:collection2-core@1.2.0 aldeed:schema-deny@1.1.0 @@ -55,8 +55,7 @@ cfs:ui@0.1.3 cfs:upload-http@0.0.20 cfs:worker@0.1.4 check@1.2.5 -chuangbo:cookie@1.1.0 -coffeescript@1.11.1_4 +coffeescript@1.12.3_1 dburles:factory@1.1.0 ddp@1.2.5 ddp-client@1.3.4 @@ -65,12 +64,12 @@ ddp-rate-limiter@1.0.7 ddp-server@1.3.14 deps@1.0.12 diff-sequence@1.0.7 -dispatch:mocha@0.3.0 +dispatch:mocha@0.4.1 dispatch:run-as-user@1.1.1 ecmascript@0.7.3 ecmascript-runtime@0.3.15 ejson@1.0.13 -email@1.2.0 +email@1.2.1 es5-shim@4.6.15 facebook-config-ui@1.0.0 facebook-oauth@1.3.0 @@ -78,7 +77,7 @@ fastclick@1.0.13 gadicc:blaze-react-component@1.4.0 geojson-utils@1.0.10 google-config-ui@1.0.0 -google-oauth@1.2.3 +google-oauth@1.2.4 hot-code-push@1.0.4 html-tools@1.0.11 htmljs@1.0.11 @@ -93,9 +92,7 @@ jquery@1.11.10 juliancwirko:postcss@1.2.0 juliancwirko:s-alert@3.2.0 juliancwirko:s-alert-stackslide@3.1.3 -kadira:blaze-layout@2.3.0 kadira:dochead@1.5.0 -kadira:flow-router-ssr@3.13.0 launch-screen@1.1.1 less@2.7.9 livedata@1.0.18 @@ -106,20 +103,16 @@ mdg:validated-method@1.1.0 mdg:validation-error@0.5.1 meteor@1.6.1 meteor-base@1.0.4 -meteorhacks:fast-render@2.16.0 -meteorhacks:inject-data@2.0.0 -meteorhacks:meteorx@1.4.1 -meteorhacks:picker@1.0.3 meteorhacks:ssr@2.2.0 meteorhacks:subs-manager@1.6.4 minifier-css@1.2.16 -minimongo@1.0.21 +minimongo@1.0.23 mobile-experience@1.0.4 mobile-status-bar@1.0.14 modules@0.8.2 modules-runtime@0.7.10 momentjs:moment@2.17.1 -mongo@1.1.16 +mongo@1.1.17 mongo-id@1.0.6 mongo-livedata@1.0.12 mrt:later@1.6.1 @@ -131,7 +124,7 @@ oauth-encryption@1.2.1 oauth1@1.1.11 oauth2@1.1.11 observe-sequence@1.0.16 -ongoworks:security@2.0.1 +ongoworks:security@2.1.0 ordered-dict@1.0.9 percolate:migrations@0.9.8 practicalmeteor:chai@2.1.0_1 @@ -162,7 +155,7 @@ templating-runtime@1.3.2 templating-tools@1.1.2 tmeasday:check-npm-versions@0.3.1 tmeasday:publish-counts@0.8.0 -tracker@1.1.2 +tracker@1.1.3 twitter-config-ui@1.0.0 twitter-oauth@1.2.0 ui@1.0.13 diff --git a/Dockerfile b/Dockerfile index adb8cc0a8c1..64239ed39ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM reactioncommerce/base:v1.3.0 +FROM reactioncommerce/base:v1.3.1 # Default environment variables ENV ROOT_URL "http://localhost" diff --git a/README.md b/README.md index fd77b3d0b62..269827b695d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![bitHound Overall Score](https://www.bithound.io/github/reactioncommerce/reaction/badges/score.svg)](https://www.bithound.io/github/reactioncommerce/reaction) [![bitHound Dev Dependencies](https://www.bithound.io/github/reactioncommerce/reaction/badges/devDependencies.svg)](https://www.bithound.io/github/reactioncommerce/reaction/9a858eb459d7260d5ae59124c2b364bc791a3e70/dependencies/npm) [![bitHound Code](https://www.bithound.io/github/reactioncommerce/reaction/badges/code.svg)](https://www.bithound.io/github/reactioncommerce/reaction) [![Circle CI](https://circleci.com/gh/reactioncommerce/reaction.svg?style=svg)](https://circleci.com/gh/reactioncommerce/reaction) [![Gitter](https://badges.gitter.im/JoinChat.svg)](https://gitter.im/reactioncommerce/reaction?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Reaction is an event-driven, real-time reactive commerce platform built with JavaScript (ES6). It plays nicely with npm and Docker, and is based entirely on JavaScript, CSS, and HTML. +Reaction is an event-driven, real-time reactive commerce platform built with JavaScript (ES6). It plays nicely with npm and Docker, and is based entirely on JavaScript, CSS, and HTML. ![Reaction v.1.0.0](https://raw.githubusercontent.com/reactioncommerce/reaction-docs/master/assets/rc-desktop.png) @@ -10,16 +10,16 @@ Reaction is an event-driven, real-time reactive commerce platform built with Jav Reaction’s out-of-the-box core features include: -* Drag-and-drop merchandising -* Order processing -* Payments -* Shipping -* Taxes -* Discounts -* Analytics -* Integration with dozens of third-party apps +- Drag-and-drop merchandising +- Order processing +- Payments +- Shipping +- Taxes +- Discounts +- Analytics +- Integration with dozens of third-party apps -And, since anything in our codebase can be extended, overwritten, or installed as a package, you may also develop, scale, and customize anything on our platform. +Since anything in our codebase can be extended, overwritten, or installed as a package, you may also develop, scale, and customize anything on our platform. ## Installation @@ -32,61 +32,53 @@ cd reaction reaction ``` -_Reaction requires Meteor, Git, MongoDB, OS Specific Build Tools, and (optionally) ImageMagick. See our [Requirements Docs](https://docs.reactioncommerce.com/reaction-docs/master/requirements) for requirements installation information._ +Reaction requires Meteor, Git, MongoDB, OS Specific Build Tools, and (optionally) ImageMagick. -For more information on setup and configuration, check out the [installation](https://docs.reactioncommerce.com/reaction-docs/development/installation) and [configuration](https://docs.reactioncommerce.com/reaction-docs/development/configuration) docs. - -## Participation - -If you are interested in participating in the development of Reaction, that's really great! +See our [Requirements Docs](https://docs.reactioncommerce.com/reaction-docs/master/requirements) for requirements that you may need to install for Reaction. -Our [community guidelines](https://docs.reactioncommerce.com/reaction-docs/master/guidelines) can be found in our [documentation](https://docs.reactioncommerce.com/). This is a good place to start getting more familar with Reaction. - -The [Reaction Gitter channel](https://gitter.im/reactioncommerce/reaction) and [forum](http://discourse.reactioncommerce.com/) are good places to engage with core contributors and the community. +For more information on setup and configuration, check out the [installation](https://docs.reactioncommerce.com/reaction-docs/development/installation) and [configuration](https://docs.reactioncommerce.com/reaction-docs/development/configuration) docs. ### Planning -For a high level review our roadmap, take a look at the [Reaction features page](http://reactioncommerce.com/features). +For an overview of our roadmap, visit our [Features & Roadmap page](https://reactioncommerce.com/roadmap). -For a kanban-esque, hardcore, real time progress overview of all Reaction Commerce projects use our [project board](https://waffle.io/reactioncommerce/reaction). +You will find the roadmap defined as projects on the [Reaction repository's project page](https://github.com/reactioncommerce/reaction/projects). -### Testing - -Testing is another great way to contribute. If you do discover a bug, [create an issue](https://github.com/reactioncommerce/reaction/issues/new) to report it. - -Integration tests can be run at the command line with `reaction test`. Use `npm run-script test-local` to run local tests. +Specific features in progress are found on the [Reaction repository's milestones page](https://github.com/reactioncommerce/reaction/milestones). ### Documentation -The Reaction documentation source is located in the [reaction-docs](https://github.com/reactioncommerce/reaction-docs) repository, while the documentation site is the [reactioncommerce/redoc](https://github.com/reactioncommerce/redoc) application. +Multiple branches, release documentation is found at +The Reaction documentation source is located in the [reaction-docs](https://github.com/reactioncommerce/reaction-docs) repository, while the documentation site is the [reactioncommerce/redoc](https://github.com/reactioncommerce/redoc) application. -### Deployment +### Contributing -We require that all releases are deployable as [Docker](https://www.docker.com/) containers. Athough we haven't tested out other methods of deployment, our community has documented deployment strategies for [Heroku](https://github.com/reactioncommerce/reaction/issues/1363), AWS, [Digital Ocean](https://gist.github.com/jshimko/745ca66748846551692e24c267a56060), and Galaxy. +Star us on GitHub, it helps! -##### Docker +If you are interested in participating in the development of Reaction, that's really great! -Docker images are pushed when Reaction sucessfully builds and passes all tests on the `master` or `development` branches. These images are released on [Reaction Commerce Docker Hub](https://hub.docker.com/u/reactioncommerce/). There are two images available: [reactioncommerce:prequel](https://hub.docker.com/r/reactioncommerce/prequel/) - the latest `development` image and [reactioncommerce:reaction](https://hub.docker.com/r/reactioncommerce/reaction/), the `master` image. +The [Reaction Gitter channel](https://gitter.im/reactioncommerce/reaction) and [forum](https://forums.reactioncommerce.com/) are good places to engage with core contributors, the community, and to get familiar with Reaction. Our [community guidelines](https://docs.reactioncommerce.com/reaction-docs/master/guidelines) can be found in our [documentation](https://docs.reactioncommerce.com/). +Check out the [issues](https://github.com/reactioncommerce/reaction/issues) page, and if you find something you want to work on, let us know in the comments. If you're interested in a particular [project](https://github.com/reactioncommerce/reaction/projects) and you aren’t sure where to begin, feel free to ask. Start small! -### Contributing +If your contribution doesn't fit with an existing issue, go ahead and [create an issue](https://github.com/reactioncommerce/reaction/issues/new) before submitting a [Pull Request](https://help.github.com/articles/about-pull-requests/). This will allow the Reaction team to give feedback if necessary. -Want to contribute? That's great! [Here's how you can get started](https://guides.github.com/activities/contributing-to-open-source/#contributing). +Pull Requests should: -Check out our Issues page, and if you find something you want to work on, let us know in the comments. If you're interested in a particular [project](https://github.com/reactioncommerce/reaction/projects) and you aren’t sure where to begin, feel free to ask. Start small! +- Be very focused in scope. Smaller scopes are easier for us to digest and approve. +- Note any existing associated issues. +- Lint and adhere to the [Reaction style guide](https://docs.reactioncommerce.com/reaction-docs/master/styleguide). +- Pass both [acceptance tests and unit testing](https://docs.reactioncommerce.com/reaction-docs/master/testing-reaction). -If your contribution doesn't fit with an existing issue, go ahead and [create an issue](https://github.com/reactioncommerce/reaction/issues/new) before submitting a [Pull Request](https://help.github.com/articles/about-pull-requests/). This will allow the Reaction team to give feedback if necessary. +### Testing -Pull Requests should: +Testing is another great way to contribute. If you do discover a bug, [create an issue](https://github.com/reactioncommerce/reaction/issues/new) to report it. -- Include an associated issue -- Comply with the Contributor License Agreement -- Adhere to the [Reaction style guide](https://docs.reactioncommerce.com/reaction-docs/master/styleguide) -- Pass both [acceptance tests and unit testing](https://docs.reactioncommerce.com/reaction-docs/master/testing-reaction) +Integration tests can be run at the command line with `reaction test`. -Be sure to read our [Community Guidelines](https://docs.reactioncommerce.com/reaction-docs/master/guidelines) to get more familiar with Reaction. And if you have any questions or comments, feel free to reach out via [Gitter](https://gitter.im/reactioncommerce/reaction) or our [forums](http://discourse.reactioncommerce.com/). +### Deployment -### What's Next +We ensure that all releases are deployable as [Docker](https://www.docker.com/) containers. While we don't regularly test other methods of deployment, our community has documented deployment strategies for AWS, [Digital Ocean](https://gist.github.com/jshimko/745ca66748846551692e24c267a56060), and Galaxy. -For an overview of our roadmap, visit our [Features & Roadmap page](https://reactioncommerce.com/roadmap). Or, if you'd like to see what we're doing in real time, check out our [Project Board](https://waffle.io/reactioncommerce/reaction). You can also see what we're doing [by project](https://github.com/reactioncommerce/reaction/projects) or [by release date](https://github.com/reactioncommerce/reaction/milestones). +For an introduction to Docker deployment, [the Reaction deployment guide](https://docs.reactioncommerce.com/reaction-docs/master/deploying) has detailed examples. Reaction Commerce also offers a managed deployment platform integrated with the Reaction command line. diff --git a/client/modules/accounts/templates/members/member.js b/client/modules/accounts/templates/members/member.js index 4f163ae9f88..48441f7ab70 100644 --- a/client/modules/accounts/templates/members/member.js +++ b/client/modules/accounts/templates/members/member.js @@ -80,8 +80,11 @@ Template.memberSettings.helpers({ // Get all permissions, add them to an array if (registryItem.permissions) { for (const permission of registryItem.permissions) { - permission.shopId = shopId; - permissions.push(permission); + // check needed because of non-object perms in the permissions array (e.g "admin", "owner") + if (typeof permission === "object") { + permission.shopId = shopId; + permissions.push(permission); + } } } diff --git a/client/modules/core/helpers/apps.js b/client/modules/core/helpers/apps.js index 9c767075730..07b1ab4f77d 100644 --- a/client/modules/core/helpers/apps.js +++ b/client/modules/core/helpers/apps.js @@ -47,11 +47,6 @@ export function Apps(optionHash) { const reactionApps = []; let options = {}; - // remove audience permissions for owner - if (Reaction.hasOwnerAccess() && optionHash.audience) { - delete optionHash.audience; - } - // allow for object or option.hash if (optionHash) { if (optionHash.hash) { @@ -66,6 +61,11 @@ export function Apps(optionHash) { options.shopId = Reaction.getShopId(); } + // remove audience permissions for owner (still needed here for older/legacy calls) + if (Reaction.hasOwnerAccess() && options.audience) { + delete options.audience; + } + // // build filter to only get matching registry elements // @@ -89,24 +89,33 @@ export function Apps(optionHash) { } } - // fetch the packages + delete filter["registry.audience"]; // Temporarily remove "audience" key (see comment below) + + // TODO: Review fix for filter on Packages.find(filter) + // The current "filter" setup uses "audience" field which is not present in the registry array in most (if not all) docs + // in the Packages coll. + // For now, the audience checks (after the Package.find call) filters out the registry items based on permissions. But + // part of the filtering should have been handled by the Package.find call, if the "audience" filter works as it should. Packages.find(filter).forEach((app) => { const matchingRegistry = _.filter(app.registry, function (item) { - const itemFilter = registryFilter; + const itemFilter = _.cloneDeep(registryFilter); // check audience permissions only if they exist as part of optionHash and are part of the registry item // ideally all routes should use it, safe for backwards compatibility though // owner bypasses permissions - if (!Reaction.hasOwnerAccess() && item.audience && registryFilter.audience) { + if (!Reaction.hasOwnerAccess() && item.permissions && registryFilter.audience) { let hasAccess; for (const permission of registryFilter.audience) { - if (item.audience.indexOf(permission) > -1) { - hasAccess = true; - } - // make sure user also has audience perms - if (Roles.userIsInRole(Meteor.userId(), permission, Reaction.getShopId())) { + // This checks that the registry item contains a permissions matches with the user's permission for the shop + const hasPermissionToRegistryItem = item.permissions.indexOf(permission) > -1; + // This checks that the user's permission set have the right value that is on the registry item + const hasRoleAccessForShop = Roles.userIsInRole(Meteor.userId(), permission, Reaction.getShopId()); + + // both checks must pass for access to be granted + if (hasPermissionToRegistryItem && hasRoleAccessForShop) { hasAccess = true; + break; } } diff --git a/client/modules/core/main.js b/client/modules/core/main.js index f2a1b649fd7..add5794a7f7 100644 --- a/client/modules/core/main.js +++ b/client/modules/core/main.js @@ -197,10 +197,15 @@ export default { }, getUserPreferences(packageName, preference, defaultValue) { - const profile = Meteor.user().profile; - if (profile && profile.preferences && profile.preferences[packageName] && profile.preferences[packageName][preference]) { - return profile.preferences[packageName][preference]; + const user = Meteor.user(); + + if (user) { + const profile = Meteor.user().profile; + if (profile && profile.preferences && profile.preferences[packageName] && profile.preferences[packageName][preference]) { + return profile.preferences[packageName][preference]; + } } + return defaultValue || undefined; }, diff --git a/client/modules/i18n/templates/currency/currency.js b/client/modules/i18n/templates/currency/currency.js index 165d2c17007..7aa0b39b26f 100644 --- a/client/modules/i18n/templates/currency/currency.js +++ b/client/modules/i18n/templates/currency/currency.js @@ -37,7 +37,9 @@ Template.currencySelect.helpers({ } } } - return currencies; + if (currencies.length > 1) { + return currencies; + } }, currentCurrency() { diff --git a/client/modules/i18n/templates/header/i18n.js b/client/modules/i18n/templates/header/i18n.js index 31d1fda097c..285eb4e68fb 100644 --- a/client/modules/i18n/templates/header/i18n.js +++ b/client/modules/i18n/templates/header/i18n.js @@ -33,7 +33,9 @@ Template.i18nChooser.helpers({ } } } - return languages; + if (languages.length > 1) { + return languages; + } } }); diff --git a/client/modules/i18n/templates/i18nSettings.html b/client/modules/i18n/templates/i18nSettings.html deleted file mode 100644 index 18adf912128..00000000000 --- a/client/modules/i18n/templates/i18nSettings.html +++ /dev/null @@ -1,27 +0,0 @@ - diff --git a/client/modules/i18n/templates/i18nSettings.js b/client/modules/i18n/templates/i18nSettings.js deleted file mode 100644 index 7f262430462..00000000000 --- a/client/modules/i18n/templates/i18nSettings.js +++ /dev/null @@ -1,102 +0,0 @@ -import i18next from "i18next"; -import { Countries } from "/client/collections"; -import { Reaction } from "/client/api"; -import { Shops } from "/lib/collections"; -import { Meteor } from "meteor/meteor"; -import { Template } from "meteor/templating"; - -Template.i18nSettings.helpers({ - shop() { - if (Reaction.Subscriptions.Shops.ready()) { - return Shops.findOne(); - } - return null; - }, - checked(enabled) { - return enabled === true ? "checked" : ""; - }, - countryOptions() { - return Countries.find().fetch(); - }, - currencyOptions() { - const currencies = Shops.findOne().currencies; - const currencyOptions = []; - for (const currency in currencies) { - if ({}.hasOwnProperty.call(currencies, currency)) { - const structure = currencies[currency]; - currencyOptions.push({ - label: currency + " | " + structure.symbol + " | " + - structure.format, - value: currency - }); - } - } - return currencyOptions; - }, - uomOptions() { - const unitsOfMeasure = Shops.findOne().unitsOfMeasure; - const uomOptions = []; - for (const measure of unitsOfMeasure) { - uomOptions.push({ - label: i18next.t(`uom.${measure.uom}`, { defaultValue: measure.uom }), - value: measure.uom - }); - } - return uomOptions; - }, - enabledLanguages() { - const languages = []; - const shop = Shops.findOne(); - if (typeof shop === "object" && shop.languages) { - for (const language of shop.languages) { - if (language.enabled === true) { - languages.push({ - label: language.label, - value: language.i18n - }); - } - } - return languages; - } - }, - languages() { - const languages = []; - const shop = Shops.findOne(); - if (typeof shop === "object" && shop.languages) { - for (const language of shop.languages) { - const i18nKey = "languages." + language.label.toLowerCase(); - languages.push({ - label: language.label, - value: language.i18n, - enabled: language.enabled, - i18nKey: i18nKey - }); - } - return languages; - } - } -}); - - -Template.i18nSettings.events({ - "change input[name=enabled]": (event) => { - const language = event.target.value; - const enabled = event.target.checked; - Meteor.call("shop/updateLanguageConfiguration", language, enabled); - } -}); - -AutoForm.hooks({ - shopEditLocalizationSettingsForm: { - onSuccess() { - return Alerts.toast(i18next.t("shopSettings.shopLocalizationSettingsSaved"), - "success"); - }, - onError(operation, error) { - return Alerts.toast( - `${i18next.t("shopSettings.shopLocalizationSettingsFailed")} ${error}`, - "error" - ); - } - } -}); diff --git a/client/modules/router/helpers.js b/client/modules/router/helpers.js index 3adf49d5edf..88deb0b1c21 100644 --- a/client/modules/router/helpers.js +++ b/client/modules/router/helpers.js @@ -1,11 +1,5 @@ -import { BlazeLayout } from "meteor/kadira:blaze-layout"; import Router from "./main"; -// -// Layout container uses body -// -BlazeLayout.setRoot("body"); - // // pathFor // template helper to return path diff --git a/client/modules/router/hooks.js b/client/modules/router/hooks.js index 7a77ec56411..6fbd6f26bd9 100644 --- a/client/modules/router/hooks.js +++ b/client/modules/router/hooks.js @@ -1,55 +1 @@ - -/** - * Route Hook Methods - */ -const Hooks = { - _hooks: { - onEnter: {}, - onExit: {} - }, - - _addHook(type, routeName, callback) { - if (typeof this._hooks[type][routeName] === "undefined") { - this._hooks[type][routeName] = []; - } - this._hooks[type][routeName].push(callback); - }, - - onEnter(routeName, callback) { - // global onEnter callback - if (arguments.length === 1 && typeof arguments[0] === "function") { - const cb = routeName; - return this._addHook("onEnter", "GLOBAL", cb); - } - // route-specific onEnter callback - return this._addHook("onEnter", routeName, callback); - }, - - onExit(routeName, callback) { - // global onExit callback - if (arguments.length === 1 && typeof arguments[0] === "function") { - const cb = routeName; - return this._addHook("onExit", "GLOBAL", cb); - } - // route-specific onExit callback - return this._addHook("onExit", routeName, callback); - }, - - get(type, name) { - const group = this._hooks[type] || {}; - const callbacks = group[name]; - return (typeof callbacks !== "undefined" && !!callbacks.length) ? callbacks : []; - }, - - run(type, name, constant) { - const callbacks = this.get(type, name); - if (typeof callbacks !== "undefined" && !!callbacks.length) { - return callbacks.forEach((callback) => { - return callback(constant); - }); - } - return null; - } -}; - -export default Hooks; +export default from "/imports/plugins/core/router/lib/hooks"; diff --git a/client/modules/router/index.js b/client/modules/router/index.js index 8a3ee9a0479..d4b6d98c75d 100644 --- a/client/modules/router/index.js +++ b/client/modules/router/index.js @@ -1 +1 @@ -export { default as Router } from "./main"; +export { default as Router } from "/imports/plugins/core/router/lib/router"; diff --git a/client/modules/router/main.js b/client/modules/router/main.js index 4fef3c98603..167a8b70bef 100644 --- a/client/modules/router/main.js +++ b/client/modules/router/main.js @@ -1,319 +1,3 @@ -import _ from "lodash"; -import { Session } from "meteor/session"; -import { Meteor } from "meteor/meteor"; -import { Tracker } from "meteor/tracker"; -import { FlowRouter as Router } from "meteor/kadira:flow-router-ssr"; -import { BlazeLayout } from "meteor/kadira:blaze-layout"; -import { Reaction, Logger } from "/client/api"; -import { Packages, Shops } from "/lib/collections"; -import { MetaData } from "/lib/api/router/metadata"; -import Hooks from "./hooks"; - - -// init flow-router -// -/* eslint no-loop-func: 0 */ - -// client should wait on subs -Router.wait(); - -Router.Hooks = Hooks; - -/** - * checkRouterPermissions - * check if user has route permissions - * @param {Object} context - route context - * @param {redirect} null object - * @return {Object} return context - */ -function checkRouterPermissions(context) { - const routeName = context.route.name; - - if (Reaction.hasPermission(routeName, Meteor.userId())) { - if (context.unauthorized === true) { - delete context.unauthorized; - return context; - } - return context; - } - // determine if this is a valid route or a 404 - const routeExists = _.find(Router._routes, function (route) { - return route.path === context.path; - }); - - // if route exists (otherwise this will return 404) - // return unauthorized flag on context - if (routeExists) { - context.unauthorized = true; - } - return context; -} - - -/** - * getRouteName - * assemble route name to be standard - * prefix/package name + registry name or route - * @param {String} packageName [package name] - * @param {Object} registryItem [registry object] - * @return {String} [route name] - */ -function getRegistryRouteName(packageName, registryItem) { - let routeName; - if (packageName && registryItem) { - if (registryItem.name) { - routeName = registryItem.name; - } else if (registryItem.template) { - routeName = `${packageName}/${registryItem.template}`; - } else { - routeName = packageName; - } - // dont include params in the name - routeName = routeName.split(":")[0]; - return routeName; - } - return null; -} - -/** - * selectLayout - * @param {Object} layout - element of shops.layout array - * @param {Object} setLayout - layout - * @param {Object} setWorkflow - workflow - * @returns {Object} layout - return object of template definitions for Blaze Layout - */ -function selectLayout(layout, setLayout, setWorkflow) { - const currentLayout = setLayout || Session.get("DEFAULT_LAYOUT") || "coreLayout"; - const currentWorkflow = setWorkflow || Session.get("DEFAULT_WORKFLOW") || "coreWorkflow"; - if (layout.layout === currentLayout && layout.workflow === currentWorkflow && layout.enabled === true) { - return layout; - } - return null; -} - -/** - * ReactionLayout - * sets and returns reaction layout structure - * @param {Object} options - this router context - * @param {String} options.layout - string of shop.layout.layout (defaults to coreLayout) - * @param {String} options.workflow - string of shop.layout.workflow (defaults to coreLayout) - * @returns {Object} layout - return object of template definitions for Blaze Layout - */ -export function ReactionLayout(options = {}) { - const layout = options.layout || Session.get("DEFAULT_LAYOUT") || "coreLayout"; - const workflow = options.workflow || Session.get("DEFAULT_WORKFLOW") || "coreWorkflow"; - if (!options.layout) { - options.layout = "coreLayout"; - } - if (!options.workflow) { - options.workflow = "coreWorkflow"; - } - - // check if router has denied permissions - // see: checkRouterPermissions - const unauthorized = {}; - if (Router.current().unauthorized) { - unauthorized.template = "unauthorized"; - } - - // autorun router rendering - Tracker.autorun(function () { - if (Reaction.Subscriptions.Shops.ready()) { - const shop = Shops.findOne(Reaction.getShopId()); - if (shop) { - const sortedLayout = shop.layout.sort((prev, next) => prev.priority - next.priority); - const newLayout = sortedLayout.find((x) => selectLayout(x, layout, workflow)); - - // oops this layout wasn't found. render notFound - if (!newLayout) { - BlazeLayout.render("notFound"); - } else { - const layoutToRender = Object.assign({}, newLayout.structure, options, unauthorized); - BlazeLayout.render(layout, layoutToRender); - } - } - } - }); - return options; -} - -// default not found route -Router.notFound = { - action() { - ReactionLayout({ - template: "notFound" - }); - } -}; - - -/** - * initPackageRoutes - * registers route and template when registry item has - * registryItem.route && registryItem.template - * @param {String} userId - userId - * @returns {undefined} returns undefined - */ -Router.initPackageRoutes = () => { - const pkgs = Packages.find().fetch(); - const prefix = Reaction.getShopPrefix(); - - // prefixing isnt necessary if we only have one shop - // but we need to bypass the current - // subscription to determine this. - const shopSub = Meteor.subscribe("shopsCount"); - if (shopSub.ready()) { - // using tmeasday:publish-counts - const shopCount = Counts.get("shops-count"); - - // initialize index - // define default routing groups - const shop = Router.group({ - name: "shop" - }); - - // - // index / home route - // to overide layout, ie: home page templates - // set INDEX_OPTIONS, in config.js - // - shop.route("/", { - name: "index", - action() { - ReactionLayout(Session.get("INDEX_OPTIONS") || {}); - } - }); - - // get package registry route configurations - for (const pkg of pkgs) { - const newRoutes = []; - // pkg registry - if (pkg.registry && pkg.enabled) { - const registry = Array.from(pkg.registry); - for (const registryItem of registry) { - // registryItems - if (registryItem.route) { - const { - meta, - route, - template, - layout, - workflow - } = registryItem; - - // get registry route name - const name = getRegistryRouteName(pkg.name, registryItem); - - // define new route - // we could allow the options to be passed in the registry if we need to be more flexible - const newRouteConfig = { - route, - options: { - meta, - name, - template, - layout, - triggersEnter: Router.Hooks.get("onEnter", name), - triggersExit: Router.Hooks.get("onExit", name), - action() { - ReactionLayout({ template, workflow, layout }); - } - } - }; - - // push new routes - newRoutes.push(newRouteConfig); - } // end registryItems - } // end package.registry - - // - // add group and routes to routing table - // - const uniqRoutes = new Set(newRoutes); - for (const route of uniqRoutes) { - // allow overriding of prefix in route definitions - // define an "absolute" url by excluding "/" - if (route.route.substring(0, 1) !== "/") { - route.route = "/" + route.route; - shop.newGroup = Router.group({ - prefix: "" - }); - } else if (shopCount <= 1) { - shop.newGroup = Router.group({ - prefix: "" - }); - } else { - shop.newGroup = Router.group({ - prefix: prefix - }); - } - - // todo: look for a cheap way to validate and prevent duplicate additions - shop.newGroup.route(route.route, route.options); - } - } - } // end package loop - - // - // initialize the router - // - try { - Router.initialize(); - } catch (e) { - Logger.error(e); - } - } -}; - - -/** - * pathFor - * @summary get current router path - * @param {String} path - path to fetch - * @param {Object} options - url params - * @return {String} returns current router path - */ -Router.pathFor = (path, options = {}) => { - const params = options.hash || {}; - const query = params.query ? Router._qs.parse(params.query) : {}; - // prevent undefined param error - for (const i in params) { - if (params[i] === null || params[i] === undefined) { - params[i] = "/"; - } - } - return Router.path(path, params, query); -}; - -/** - * isActive - * @summary general helper to return "active" when on current path - * @example {{active "name"}} - * @param {String} routeName - route name as defined in registry - * @return {String} return "active" or null - */ -Router.isActiveClassName = (routeName) => { - Router.watchPathChange(); - const group = Router.current().route.group; - let prefix; - if (group && group.prefix) { - prefix = Router.current().route.group.prefix; - } else { - prefix = ""; - } - const path = Router.current().route.path; - const routeDef = path.replace(prefix + "/", ""); - return routeDef === routeName ? "active" : ""; -}; - -// Register Global Route Hooks -Meteor.startup(() => { - Router.Hooks.onEnter(checkRouterPermissions); - Router.Hooks.onEnter(MetaData.init); - - Router.triggers.enter(Router.Hooks.get("onEnter", "GLOBAL")); - Router.triggers.exit(Router.Hooks.get("onExit", "GLOBAL")); -}); - +import { Router } from "/imports/plugins/core/router/lib"; export default Router; diff --git a/imports/plugins/core/checkout/client/components/cartDrawer.js b/imports/plugins/core/checkout/client/components/cartDrawer.js new file mode 100644 index 00000000000..4383649e920 --- /dev/null +++ b/imports/plugins/core/checkout/client/components/cartDrawer.js @@ -0,0 +1,49 @@ +import React, { PropTypes } from "react"; +import CartSubTotals from "../container/cartSubTotalContainer"; +import CartItems from "./cartItems"; + +const cartDrawer = ({ productItems, pdpPath, handleRemoveItem, handleCheckout, handleImage, handleLowInventory, handleShowProduct }) => { + return ( +
+
+
+
+ +
+ {productItems.map(item => { + return ( +
+ +
+ ); + })} +
+
+
+
+ + Checkout now + +
+
+ ); +}; + +cartDrawer.propTypes = { + handleCheckout: PropTypes.func, + handleImage: PropTypes.func, + handleLowInventory: PropTypes.func, + handleRemoveItem: PropTypes.func, + handleShowProduct: PropTypes.func, + pdpPath: PropTypes.func, + productItems: PropTypes.array +}; + +export default cartDrawer; diff --git a/imports/plugins/core/checkout/client/components/cartItems.js b/imports/plugins/core/checkout/client/components/cartItems.js new file mode 100644 index 00000000000..0c42ed2daf6 --- /dev/null +++ b/imports/plugins/core/checkout/client/components/cartItems.js @@ -0,0 +1,72 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +class CartItems extends Component { + static propTypes = { + handleImage: PropTypes.func, + handleLowInventory: PropTypes.func, + handleRemoveItem: PropTypes.func, + handleShowProduct: PropTypes.func, + item: PropTypes.object, + pdpPath: PropTypes.func + } + + handleClick = (event) => { + event.preventDefault(); + + if (typeof this.props.handleShowProduct === "function") { + this.props.handleShowProduct(this.props.item); + } + } + + render() { + const { + handleLowInventory, + pdpPath, + handleImage, + handleRemoveItem, + item + } = this.props; + + return ( +
+ + + {handleImage(item) ? +
+ +
: +
+ +
+ } +
+
+ {handleLowInventory(item) ? +
!
: +
+ {item.quantity} + + {item.title} +
+ {item.variants.title} +
+
+ } +
+
+ ); + } +} + +export default CartItems; diff --git a/imports/plugins/core/checkout/client/components/cartSubTotal.js b/imports/plugins/core/checkout/client/components/cartSubTotal.js new file mode 100644 index 00000000000..d9ec8d9f6ee --- /dev/null +++ b/imports/plugins/core/checkout/client/components/cartSubTotal.js @@ -0,0 +1,77 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Currency, Translation } from "/imports/plugins/core/ui/client/components/"; + +class CartSubTotal extends Component { + static propTypes = { + cartCount: PropTypes.number, + cartDiscount: PropTypes.string, + cartShipping: PropTypes.string, + cartSubTotal: PropTypes.string, + cartTaxes: PropTypes.string, + cartTotal: PropTypes.string + } + + validateDiscount() { + if (Number(this.props.cartDiscount) > 0) { + return ( + + + + + ); + } + } + validateShipping() { + if (Number(this.props.cartShipping) > 0) { + return ( + + + + + ); + } + } + validateTaxes() { + if (Number(this.props.cartTaxes) > 0) { + return ( + + + + + ); + } + } + render() { + return ( +
+
+ + + + + + + + + + + + + + {this.validateDiscount()} + {this.validateShipping()} + {this.validateTaxes()} + + + + + +
{this.props.cartCount}
+
+
+ ); + } +} + +export default CartSubTotal; diff --git a/imports/plugins/core/checkout/client/components/emptyCartDrawer.js b/imports/plugins/core/checkout/client/components/emptyCartDrawer.js new file mode 100644 index 00000000000..dd96037fb91 --- /dev/null +++ b/imports/plugins/core/checkout/client/components/emptyCartDrawer.js @@ -0,0 +1,37 @@ +import React, { PropTypes } from "react"; +import { Button, Translation } from "/imports/plugins/core/ui/client/components"; + + +const EmptyCartDrawer = ({ keepShopping }) => { + return ( +
+
+
+

+ +

+

+ +

+
+
+
+
+
+ ); +}; + +EmptyCartDrawer.propTypes = { + keepShopping: PropTypes.func +}; + +export default EmptyCartDrawer; diff --git a/imports/plugins/core/checkout/client/container/cartDrawerContainer.js b/imports/plugins/core/checkout/client/container/cartDrawerContainer.js new file mode 100644 index 00000000000..a89f4a3d3ca --- /dev/null +++ b/imports/plugins/core/checkout/client/container/cartDrawerContainer.js @@ -0,0 +1,117 @@ +import React, { Component, PropTypes } from "react"; +import { Session } from "meteor/session"; +import { Meteor } from "meteor/meteor"; +import { Cart, Media } from "/lib/collections"; +import { Reaction } from "/client/api"; +import { composeWithTracker } from "/lib/api/compose"; +import { Loading } from "/imports/plugins/core/ui/client/components"; +import CartDrawer from "../components/cartDrawer"; + +class CartDrawerContainer extends Component { + static propTypes = { + defaultImage: PropTypes.object, + lowInventory: PropTypes.bool, + productItems: PropTypes.array + } + + handleImage(item) { + const { defaultImage } = item; + if (defaultImage && defaultImage.url({ store: "small" })) { + return defaultImage; + } + return false; + } + /** + * showLowInventoryWarning + * @param {Object} productItem - product item object + * @return {Boolean} return true if low inventory on variant + */ + showItemLowInventoryWarning(productItem) { + const { variants } = productItem; + if (variants && variants.inventoryPolicy && + variants.lowInventoryWarningThreshold) { + return variants.inventoryQuantity <= + variants.lowInventoryWarningThreshold; + } + return false; + } + + handleLowInventory = (productItem) => { + return this.showItemLowInventoryWarning(productItem); + } + + handleShowProduct = (productItem) => { + if (productItem) { + Reaction.Router.go("product", { + handle: productItem.productId, + variantId: productItem.variants._id + }); + } + } + + pdpPath(productItem) { + if (productItem) { + const handle = productItem.productId; + return Reaction.Router.pathFor("product", { + hash: { + handle, + variantId: productItem.variants._id + } + }); + } + } + + handleRemoveItem(event) { + event.stopPropagation(); + event.preventDefault(); + const currentCartItemId = event.target.getAttribute("id"); + $(`#${currentCartItemId}`).fadeOut(500, () => { + return Meteor.call("cart/removeFromCart", currentCartItemId); + }); + } + handleCheckout() { + $("#cart-drawer-container").fadeOut(); + Session.set("displayCart", false); + return Reaction.Router.go("cart/checkout"); + } + render() { + const { productItems } = this.props; + return ( + + ); + } +} + +function composer(props, onData) { + const userId = Meteor.userId(); + const shopId = Reaction.getShopId(); + let productItems = Cart.findOne({ userId, shopId }).items; + let defaultImage; + + productItems = productItems.map((item) => { + Meteor.subscribe("CartItemImage", item); + defaultImage = Media.findOne({ + "metadata.variantId": item.variants._id + }); + if (defaultImage) { + return Object.assign({}, item, { defaultImage }); + } + defaultImage = Media.findOne({ + "metadata.productId": item.productId + }); + return Object.assign({}, item, { defaultImage }); + }); + onData(null, { + productItems + }); +} + +export default composeWithTracker(composer, Loading)(CartDrawerContainer); diff --git a/imports/plugins/core/checkout/client/container/cartSubTotalContainer.js b/imports/plugins/core/checkout/client/container/cartSubTotalContainer.js new file mode 100644 index 00000000000..2684d064487 --- /dev/null +++ b/imports/plugins/core/checkout/client/container/cartSubTotalContainer.js @@ -0,0 +1,34 @@ +import React, { Component } from "react"; +import { Cart } from "/lib/collections"; +import { composeWithTracker } from "/lib/api/compose"; +import { Loading } from "/imports/plugins/core/ui/client/components"; +import CartSubTotal from "../components/cartSubTotal"; + +class CartSubTotalContainer extends Component { + render() { + return ( + + ); + } + +} + +function composer(props, onData) { + const cart = Cart.findOne(); + if (cart) { + onData(null, { + cartSubTotal: cart.cartSubTotal(), + cartCount: cart.cartCount(), + cartShipping: cart.cartShipping(), + cartDiscount: cart.cartDiscounts(), + cartTaxes: cart.cartTaxes(), + cartTotal: cart.cartTotal() + }); + } else { + onData(null, {}); + } +} + +export default composeWithTracker(composer, Loading)(CartSubTotalContainer); diff --git a/imports/plugins/core/checkout/client/container/emptyCartContainer.js b/imports/plugins/core/checkout/client/container/emptyCartContainer.js new file mode 100644 index 00000000000..7173aef9802 --- /dev/null +++ b/imports/plugins/core/checkout/client/container/emptyCartContainer.js @@ -0,0 +1,22 @@ +import React, { Component } from "react"; +import { Reaction } from "/client/api"; +import EmptyCartDrawer from "../components/emptyCartDrawer"; + +class EmptyCartContainer extends Component { + handleKeepShopping(event) { + event.stopPropagation(); + event.preventDefault(); + return $("#cart-drawer-container").fadeOut(300, function () { + return Reaction.toggleSession("displayCart"); + }); + } + render() { + return ( +
+ +
+ ); + } +} + +export default EmptyCartContainer; diff --git a/imports/plugins/core/checkout/client/index.js b/imports/plugins/core/checkout/client/index.js index bdd23a38e29..788200366b5 100644 --- a/imports/plugins/core/checkout/client/index.js +++ b/imports/plugins/core/checkout/client/index.js @@ -1,19 +1,12 @@ import "./helpers/cart"; import "./methods/cart"; -import "./templates/cartDrawer/cartItems/cartItems.html"; -import "./templates/cartDrawer/cartItems/cartItems.js"; -import "./templates/cartDrawer/cartSubTotals/cartSubTotals.html"; -import "./templates/cartDrawer/cartSubTotals/cartSubTotals.js"; import "./templates/cartDrawer/cartDrawer.html"; import "./templates/cartDrawer/cartDrawer.js"; import "./templates/cartIcon/cartIcon.html"; import "./templates/cartIcon/cartIcon.js"; -import "./templates/cartPanel/cartPanel.html"; -import "./templates/cartPanel/cartPanel.js"; - import "./templates/checkout/addressBook/addressBook.html"; import "./templates/checkout/completed/completed.html"; import "./templates/checkout/completed/completed.js"; diff --git a/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.html b/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.html index 3b32b245562..94bf474624d 100644 --- a/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.html +++ b/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.html @@ -6,19 +6,8 @@ @@ -26,23 +15,6 @@

diff --git a/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.js b/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.js index ff0ff590ae5..733b188e1d2 100644 --- a/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.js +++ b/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.js @@ -1,10 +1,9 @@ -import { Reaction } from "/client/api"; import { Cart } from "/lib/collections"; import { Session } from "meteor/session"; -import { Meteor } from "meteor/meteor"; import { Template } from "meteor/templating"; import Swiper from "swiper"; - +import CartDrawerContainer from "../../container/cartDrawerContainer"; +import EmptyCartDrawer from "../../container/emptyCartContainer"; /** * cartDrawer helpers * @@ -65,46 +64,17 @@ Template.openCartDrawer.onRendered(function () { }); Template.openCartDrawer.helpers({ - cartItems: function () { - return Cart.findOne().items; + CartDrawerContainer() { + return CartDrawerContainer; } }); -/** - * openCartDrawer events - * - */ -Template.openCartDrawer.events({ - "click #btn-checkout": function () { - $("#cart-drawer-container").fadeOut(); - Session.set("displayCart", false); - return Reaction.Router.go("cart/checkout"); - }, - "click .remove-cart-item": function (event) { - event.stopPropagation(); - event.preventDefault(); - const currentCartItemId = this._id; - - return Template.instance().$(event.currentTarget).fadeOut(300, function () { - return Meteor.call("cart/removeFromCart", currentCartItemId); - }); - } +Template.emptyCartDrawer.onRendered(function () { + return $("#cart-drawer-container").fadeIn(); }); -/** - * emptyCartDrawer helpers - * - */ -Template.emptyCartDrawer.events({ - "click #btn-keep-shopping": function (event) { - event.stopPropagation(); - event.preventDefault(); - return $("#cart-drawer-container").fadeOut(300, function () { - return Reaction.toggleSession("displayCart"); - }); +Template.emptyCartDrawer.helpers({ + EmptyCartDrawer() { + return EmptyCartDrawer; } }); - -Template.emptyCartDrawer.onRendered(function () { - return $("#cart-drawer-container").fadeIn(); -}); diff --git a/imports/plugins/core/checkout/client/templates/cartDrawer/cartItems/cartItems.html b/imports/plugins/core/checkout/client/templates/cartDrawer/cartItems/cartItems.html deleted file mode 100644 index a41df81d4d4..00000000000 --- a/imports/plugins/core/checkout/client/templates/cartDrawer/cartItems/cartItems.html +++ /dev/null @@ -1,26 +0,0 @@ - diff --git a/imports/plugins/core/checkout/client/templates/cartDrawer/cartItems/cartItems.js b/imports/plugins/core/checkout/client/templates/cartDrawer/cartItems/cartItems.js deleted file mode 100644 index e8605909358..00000000000 --- a/imports/plugins/core/checkout/client/templates/cartDrawer/cartItems/cartItems.js +++ /dev/null @@ -1,33 +0,0 @@ -import _ from "lodash"; -import { Template } from "meteor/templating"; -import { Media } from "/lib/collections"; - -/** - * cartDrawerItems helpers - * - * @provides media - * @returns default product image - */ -Template.cartDrawerItems.helpers({ - product: function () { - return this; - }, - media: function () { - const product = this; - let defaultImage = Media.findOne({ - "metadata.variantId": this.variants._id - }); - - if (defaultImage) { - return defaultImage; - } else if (product) { - _.some(product.variants, function (variant) { - defaultImage = Media.findOne({ - "metadata.variantId": variant._id - }); - return !!defaultImage; - }); - } - return defaultImage; - } -}); diff --git a/imports/plugins/core/checkout/client/templates/cartDrawer/cartSubTotals/cartSubTotals.html b/imports/plugins/core/checkout/client/templates/cartDrawer/cartSubTotals/cartSubTotals.html deleted file mode 100644 index 39ea18f5949..00000000000 --- a/imports/plugins/core/checkout/client/templates/cartDrawer/cartSubTotals/cartSubTotals.html +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/imports/plugins/core/checkout/client/templates/cartDrawer/cartSubTotals/cartSubTotals.js b/imports/plugins/core/checkout/client/templates/cartDrawer/cartSubTotals/cartSubTotals.js deleted file mode 100644 index fa49b92d17e..00000000000 --- a/imports/plugins/core/checkout/client/templates/cartDrawer/cartSubTotals/cartSubTotals.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Cart } from "/lib/collections"; -import { Template } from "meteor/templating"; - -/** - * cartSubTotals helpers - * - * @returns cart - */ -Template.cartSubTotals.helpers({ - cart() { - return Cart.findOne(); - }, - isValid(option) { - return option > 0 ? true : false; - } -}); diff --git a/imports/plugins/core/checkout/client/templates/cartPanel/cartPanel.html b/imports/plugins/core/checkout/client/templates/cartPanel/cartPanel.html deleted file mode 100644 index 255e7e36be3..00000000000 --- a/imports/plugins/core/checkout/client/templates/cartPanel/cartPanel.html +++ /dev/null @@ -1,8 +0,0 @@ - \ No newline at end of file diff --git a/imports/plugins/core/checkout/client/templates/cartPanel/cartPanel.js b/imports/plugins/core/checkout/client/templates/cartPanel/cartPanel.js deleted file mode 100644 index 7566a4a267d..00000000000 --- a/imports/plugins/core/checkout/client/templates/cartPanel/cartPanel.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Reaction } from "/client/api"; -import { Session } from "meteor/session"; - -/** - * cartPanel events - * - * goes to checkout on btn-checkout click - * - */ -Template.cartPanel.events({ - "click #btn-checkout": function () { - $("#cart-drawer-container").fadeOut(); - Session.set("displayCart", false); - return Reaction.Router.go("cart/checkout"); - } -}); diff --git a/imports/plugins/core/checkout/client/templates/cartPanel/component/cartPanel.js b/imports/plugins/core/checkout/client/templates/cartPanel/component/cartPanel.js new file mode 100644 index 00000000000..6050ebc386c --- /dev/null +++ b/imports/plugins/core/checkout/client/templates/cartPanel/component/cartPanel.js @@ -0,0 +1,35 @@ +import React, { Component, PropTypes } from "react"; +import { Button } from "/imports/plugins/core/ui/client/components"; + +class CartPanel extends Component { + render() { + return ( +
+ + + +
{}
+
+
+
+ ); + } +} + +CartPanel.propTypes = { + checkout: PropTypes.func, + onClick: PropTypes.func +}; + +export default CartPanel; diff --git a/imports/plugins/core/checkout/client/templates/cartPanel/container/cartPanelContainer.js b/imports/plugins/core/checkout/client/templates/cartPanel/container/cartPanelContainer.js new file mode 100644 index 00000000000..098183e7237 --- /dev/null +++ b/imports/plugins/core/checkout/client/templates/cartPanel/container/cartPanelContainer.js @@ -0,0 +1,20 @@ +import React, { Component } from "react"; +import { Reaction } from "/client/api"; +import CartPanel from "../component/cartPanel"; + +class CartPanelContainer extends Component { + handleCheckout() { + $("#cart-drawer-container").fadeOut(); + Session.set("displayCart", false); + return Reaction.Router.go("cart/checkout"); + } + render() { + return ( + + ); + } +} + +export default CartPanelContainer; diff --git a/imports/plugins/core/checkout/client/templates/checkout/checkout.js b/imports/plugins/core/checkout/client/templates/checkout/checkout.js index 263dfdf8e6d..ce0f6fdf085 100644 --- a/imports/plugins/core/checkout/client/templates/checkout/checkout.js +++ b/imports/plugins/core/checkout/client/templates/checkout/checkout.js @@ -22,7 +22,7 @@ Template.cartCheckout.helpers({ Template.cartCheckout.onCreated(function () { if (Reaction.Subscriptions.Cart.ready()) { const cart = Cart.findOne(); - if (cart.workflow && cart.workflow.status === "new") { + if (cart && cart.workflow && cart.workflow.status === "new") { // if user logged in as normal user, we must pass it through the first stage Meteor.call("workflow/pushCartWorkflow", "coreCartWorkflow", "checkoutLogin", cart._id); } diff --git a/imports/plugins/core/checkout/client/templates/checkout/completed/completed.html b/imports/plugins/core/checkout/client/templates/checkout/completed/completed.html index 088fb4d7596..a04be56c664 100644 --- a/imports/plugins/core/checkout/client/templates/checkout/completed/completed.html +++ b/imports/plugins/core/checkout/client/templates/checkout/completed/completed.html @@ -1,6 +1,6 @@ diff --git a/imports/plugins/core/checkout/client/templates/checkout/completed/completed.js b/imports/plugins/core/checkout/client/templates/checkout/completed/completed.js index cbf3da70293..1a699ee4e7d 100644 --- a/imports/plugins/core/checkout/client/templates/checkout/completed/completed.js +++ b/imports/plugins/core/checkout/client/templates/checkout/completed/completed.js @@ -11,7 +11,7 @@ import { Template } from "meteor/templating"; */ Template.cartCompleted.helpers({ orderCompleted: function () { - const id = Reaction.Router.getQueryParam("_id"); + const id = Reaction.Router.getQueryParam("_id"); if (id) { const ccoSub = Meteor.subscribe("CompletedCartOrder", Meteor.userId(), id); if (ccoSub.ready()) { diff --git a/imports/plugins/core/checkout/client/templates/checkout/review/review.html b/imports/plugins/core/checkout/client/templates/checkout/review/review.html index aa9b94388cb..302546345ba 100644 --- a/imports/plugins/core/checkout/client/templates/checkout/review/review.html +++ b/imports/plugins/core/checkout/client/templates/checkout/review/review.html @@ -5,7 +5,7 @@

Review

- {{>cartSubTotals}} + {{> React component=CartSubTotals}}
diff --git a/imports/plugins/core/checkout/client/templates/checkout/review/review.js b/imports/plugins/core/checkout/client/templates/checkout/review/review.js index b80878fd855..fb832cececa 100644 --- a/imports/plugins/core/checkout/client/templates/checkout/review/review.js +++ b/imports/plugins/core/checkout/client/templates/checkout/review/review.js @@ -1,6 +1,7 @@ import "./review.html"; import { Meteor } from "meteor/meteor"; import { Template } from "meteor/templating"; +import CartSubTotals from "../../../container/cartSubTotalContainer"; /** * review status @@ -10,3 +11,9 @@ import { Template } from "meteor/templating"; Template.checkoutReview.onRendered(function () { Meteor.call("workflow/pushCartWorkflow", "coreCartWorkflow", "checkoutReview"); }); + +Template.checkoutReview.helpers({ + CartSubTotals() { + return CartSubTotals; + } +}); diff --git a/imports/plugins/core/dashboard/client/components/toolbar.js b/imports/plugins/core/dashboard/client/components/toolbar.js index 40c38931f0e..94d6e3228b7 100644 --- a/imports/plugins/core/dashboard/client/components/toolbar.js +++ b/imports/plugins/core/dashboard/client/components/toolbar.js @@ -16,6 +16,7 @@ class PublishControls extends Component { dashboardHeaderTemplate: PropTypes.oneOfType([PropTypes.func, PropTypes.node, PropTypes.string]), documentIds: PropTypes.arrayOf(PropTypes.string), documents: PropTypes.arrayOf(PropTypes.object), + hasCreateProductAccess: PropTypes.bool, isEnabled: PropTypes.bool, isPreview: PropTypes.bool, onAddProduct: PropTypes.func, @@ -61,16 +62,20 @@ class PublishControls extends Component { } renderVisibilitySwitch() { - return ( - - ); + if (this.props.hasCreateProductAccess) { + return ( + + ); + } + + return null; } renderAdminButton() { @@ -90,14 +95,18 @@ class PublishControls extends Component { } renderAddButton() { - return ( - - ); + if (this.props.hasCreateProductAccess) { + return ( + + ); + } + + return null; } renderPackageButons() { @@ -113,15 +122,15 @@ class PublishControls extends Component { } renderCustomControls() { - if (this.props.dashboardHeaderTemplate) { + if (this.props.dashboardHeaderTemplate && this.props.hasCreateProductAccess) { if (this.props.isEnabled) { return [ , - + ]; } return [ - + ]; } diff --git a/imports/plugins/core/dashboard/client/containers/packageListContainer.js b/imports/plugins/core/dashboard/client/containers/packageListContainer.js index 0e553152b0a..5e8ed51b1ce 100644 --- a/imports/plugins/core/dashboard/client/containers/packageListContainer.js +++ b/imports/plugins/core/dashboard/client/containers/packageListContainer.js @@ -15,9 +15,10 @@ function handleShowPackage(event, app) { } function composer(props, onData) { - const settings = Reaction.Apps({ provides: "settings", enabled: true }) || []; + const audience = Roles.getRolesForUser(Meteor.userId(), Reaction.getShopId()); + const settings = Reaction.Apps({ provides: "settings", enabled: true, audience }) || []; - const dashboard = Reaction.Apps({ provides: "dashboard", enabled: true }) + const dashboard = Reaction.Apps({ provides: "dashboard", enabled: true, audience }) .filter((d) => typeof Template[d.template] !== "undefined") || []; onData(null, { diff --git a/imports/plugins/core/dashboard/client/containers/toolbarContainer.js b/imports/plugins/core/dashboard/client/containers/toolbarContainer.js index b1fac1f286a..83d8952f94a 100644 --- a/imports/plugins/core/dashboard/client/containers/toolbarContainer.js +++ b/imports/plugins/core/dashboard/client/containers/toolbarContainer.js @@ -7,6 +7,7 @@ import { TranslationProvider, AdminContextProvider } from "/imports/plugins/core import { isRevisionControlEnabled } from "/imports/plugins/core/revisions/lib/api"; const handleAddProduct = () => { + Reaction.setUserPreferences("reaction-dashboard", "viewAs", "administrator"); Meteor.call("products/createProduct", (error, productId) => { if (Meteor.isClient) { let currentTag; @@ -56,8 +57,8 @@ function composer(props, onData) { // Standard variables const packageButtons = []; - if (routeName !== "dashboard") { - const registryItems = Reaction.Apps({ provides: "settings", container: routeName }); + if (routeName !== "dashboard" && props.showPackageShortcuts) { + const registryItems = Reaction.Apps({ provides: "settings", container: "dashboard" }); for (const item of registryItems) { if (Reaction.hasPermission(item.route, Meteor.userId())) { @@ -87,6 +88,7 @@ function composer(props, onData) { isEnabled: isRevisionControlEnabled(), isActionViewAtRootView: Reaction.isActionViewAtRootView(), actionViewIsOpen: Reaction.isActionViewOpen(), + hasCreateProductAccess: Reaction.hasPermission("createProduct", Meteor.userId(), Reaction.shopId), // Callbacks onAddProduct: handleAddProduct, diff --git a/imports/plugins/core/email/client/components/emailLogs.js b/imports/plugins/core/email/client/components/emailLogs.js index be4b33a04d2..9defc2ee9b4 100644 --- a/imports/plugins/core/email/client/components/emailLogs.js +++ b/imports/plugins/core/email/client/components/emailLogs.js @@ -53,7 +53,7 @@ class EmailLogs extends Component { return ( + ); + } +} + +export default CartDrawer; diff --git a/imports/plugins/core/layout/client/components/content.js b/imports/plugins/core/layout/client/components/content.js new file mode 100644 index 00000000000..e7f5380d5e2 --- /dev/null +++ b/imports/plugins/core/layout/client/components/content.js @@ -0,0 +1,18 @@ +import React, { Component, PropTypes } from "react"; +import Blaze from "meteor/gadicc:blaze-react-component"; + +class Content extends Component { + static propTypes = { + template: PropTypes.string + } + + render() { + return ( +
+ +
+ ); + } +} + +export default Content; diff --git a/imports/plugins/core/layout/client/components/coreLayout.js b/imports/plugins/core/layout/client/components/coreLayout.js new file mode 100644 index 00000000000..2e0183bc06a --- /dev/null +++ b/imports/plugins/core/layout/client/components/coreLayout.js @@ -0,0 +1,32 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import classnames from "classnames"; +import Header from "./header"; +import CartDrawer from "./cartDrawer"; +import { Content } from "./"; + + +class CoreLayout extends Component { + static propTypes = { + actionViewIsOpen: PropTypes.bool, + data: PropTypes.object, + structure: PropTypes.object + } + + render() { + const pageClassName = classnames({ + "page": true, + "show-settings": this.props.actionViewIsOpen + }); + + return ( +
+
+ + +
+ ); + } +} + +export default CoreLayout; diff --git a/imports/plugins/core/layout/client/components/header.js b/imports/plugins/core/layout/client/components/header.js new file mode 100644 index 00000000000..e403c295ced --- /dev/null +++ b/imports/plugins/core/layout/client/components/header.js @@ -0,0 +1,20 @@ +import React, { Component, PropTypes } from "react"; +import Blaze from "meteor/gadicc:blaze-react-component"; + +class Header extends Component { + static propTypes = { + template: PropTypes.string + } + + render() { + if (this.props.template) { + return ( + + ); + } + + return null; + } +} + +export default Header; diff --git a/imports/plugins/core/layout/client/components/index.js b/imports/plugins/core/layout/client/components/index.js new file mode 100644 index 00000000000..4034d62ff06 --- /dev/null +++ b/imports/plugins/core/layout/client/components/index.js @@ -0,0 +1,6 @@ +export { default as CoreLayout } from "./coreLayout"; +export { default as QuickMemu } from "./quickMenu"; +export { default as Header } from "./header"; +export { default as CartDrawer } from "./cartDrawer"; +export { default as Content } from "./content"; +export { default as PrintLayout } from "./printLayout"; diff --git a/imports/plugins/core/layout/client/components/printLayout.js b/imports/plugins/core/layout/client/components/printLayout.js new file mode 100644 index 00000000000..81f31edc187 --- /dev/null +++ b/imports/plugins/core/layout/client/components/printLayout.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import Blaze from "meteor/gadicc:blaze-react-component"; + +class PrintLayout extends Component { + static propTypes = { + structure: PropTypes.object + } + + render() { + return ( + + ); + } +} + +export default PrintLayout; diff --git a/imports/plugins/core/layout/client/components/quickMenu.js b/imports/plugins/core/layout/client/components/quickMenu.js new file mode 100644 index 00000000000..cfe434f921f --- /dev/null +++ b/imports/plugins/core/layout/client/components/quickMenu.js @@ -0,0 +1,45 @@ +import React, { Component, PropTypes } from "react"; +import { Button } from "/imports/plugins/core/ui/client/components"; + +class QuickMenu extends Component { + static propTypes = { + buttons: PropTypes.array + } + + renderButtons() { + if (Array.isArray(this.props.buttons)) { + return this.props.buttons.map((buttonProps, index) => { + if (buttonProps.type === "seperator") { + return ( +
+
+
+ ); + } + + const { type, ...otherButtonProps } = buttonProps; + + return ( + {{/if}} + {{#if showAfterPaymentCaptured}} + + {{/if}} + -
+
{{#if paymentPendingApproval}} - +
+ {{> React buttonSelectComponent}} +
{{/if}} {{#if paymentApproved}} @@ -75,16 +89,13 @@
- + + Print Invoice + {{else}} diff --git a/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js b/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js index c8272bdd083..aec08e4ac94 100644 --- a/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js +++ b/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js @@ -1,11 +1,13 @@ import accounting from "accounting-js"; import _ from "lodash"; import { Meteor } from "meteor/meteor"; +import $ from "jquery"; import { Template } from "meteor/templating"; import { ReactiveVar } from "meteor/reactive-var"; import { i18next, Logger, formatNumber, Reaction } from "/client/api"; import { NumericInput } from "/imports/plugins/core/ui/client/components"; -import { Orders, Shops } from "/lib/collections"; +import { Orders, Shops, Packages } from "/lib/collections"; +import { ButtonSelect } from "../../../../ui/client/components/button"; import DiscountList from "/imports/plugins/core/discounts/client/components/list"; import InvoiceContainer from "../../containers/invoiceContainer.js"; import LineItemsContainer from "../../containers/lineItemsContainer.js"; @@ -42,10 +44,9 @@ Template.coreOrderShippingInvoice.onCreated(function () { if (order) { Meteor.call("orders/refunds/list", order, (error, result) => { - if (!error) { - this.refunds.set(result); - this.state.set("isFetching", false); - } + if (error) Logger.warn(error); + this.refunds.set(result); + this.state.set("isFetching", false); }); } }); @@ -64,7 +65,7 @@ Template.coreOrderShippingInvoice.helpers({ isRefunding() { const instance = Template.instance(); if (instance.state.get("isRefunding")) { - instance.$("#btn-refund-payment").text("Refunding"); + instance.$("#btn-refund-payment").text(i18next.t("order.refunding")); return true; } return false; @@ -82,6 +83,30 @@ Template.coreOrderShippingInvoice.helpers({ InvoiceContainer() { return InvoiceContainer; }, + buttonSelectComponent() { + return { + component: ButtonSelect, + buttons: [ + { + name: "Approve", + i18nKeyLabel: "order.approveInvoice", + active: true, + status: "info", + eventAction: "approveInvoice", + bgColor: "bg-info", + buttonType: "submit" + }, { + name: "Cancel", + i18nKeyLabel: "order.cancelInvoice", + active: false, + status: "danger", + eventAction: "cancelOrder", + bgColor: "bg-danger", + buttonType: "button" + } + ] + }; + }, LineItemsContainer() { return LineItemsContainer; }, @@ -100,6 +125,66 @@ Template.coreOrderShippingInvoice.helpers({ * coreOrderAdjustments events */ Template.coreOrderShippingInvoice.events({ + /** + * Click Start Cancel Order + * @param {Event} event - Event Object + * @param {Template} instance - Blaze Template + * @return {void} + */ + "click [data-event-action=cancelOrder]": (event, instance) => { + event.preventDefault(); + const order = instance.state.get("order"); + const invoiceTotal = order.billing[0].invoice.total; + const currencySymbol = instance.state.get("currency").symbol; + + Meteor.subscribe("Packages"); + const packageId = order.billing[0].paymentMethod.paymentPackageId; + const settingsKey = order.billing[0].paymentMethod.paymentSettingsKey; + // check if payment provider supports de-authorize + const checkSupportedMethods = Packages.findOne({ + _id: packageId, + shopId: Reaction.getShopId() + }).settings[settingsKey].support; + + const orderStatus = order.billing[0].paymentMethod.status; + const orderMode = order.billing[0].paymentMethod.mode; + + let alertText; + if (_.includes(checkSupportedMethods, "de-authorize") || + (orderStatus === "completed" && orderMode === "capture")) { + alertText = i18next.t("order.applyRefundDuringCancelOrder", { currencySymbol, invoiceTotal }); + } + + Alerts.alert({ + title: i18next.t("order.cancelOrder"), + text: alertText, + type: "warning", + showCancelButton: true, + showCloseButton: true, + confirmButtonColor: "#98afbc", + cancelButtonColor: "#98afbc", + confirmButtonText: i18next.t("order.cancelOrderNoRestock"), + cancelButtonText: i18next.t("order.cancelOrderThenRestock") + }, (isConfirm, cancel)=> { + let returnToStock; + if (isConfirm) { + returnToStock = false; + return Meteor.call("orders/cancelOrder", order, returnToStock, err => { + if (err) { + $(".alert").removeClass("hidden").text(err.message); + } + }); + } + if (cancel === "cancel") { + returnToStock = true; + return Meteor.call("orders/cancelOrder", order, returnToStock, err => { + if (err) { + $(".alert").removeClass("hidden").text(err.message); + } + }); + } + }); + }, /** * Submit form * @param {Event} event - Event object @@ -203,11 +288,14 @@ Template.coreOrderShippingInvoice.events({ }, (isConfirm) => { if (isConfirm) { state.set("isRefunding", true); - Meteor.call("orders/refunds/create", order._id, paymentMethod, refund, (error) => { + Meteor.call("orders/refunds/create", order._id, paymentMethod, refund, (error, result) => { if (error) { Alerts.alert(error.reason); } - Alerts.toast(i18next.t("mail.alerts.emailSent"), "success"); + if (result) { + Alerts.toast(i18next.t("mail.alerts.emailSent"), "success"); + } + $("#btn-refund-payment").text(i18next.t("order.applyRefund")); state.set("field-refund", 0); state.set("isRefunding", false); }); @@ -294,7 +382,7 @@ Template.coreOrderShippingInvoice.helpers({ return { component: NumericInput, numericType: "currency", - value: 0, + value: state.get("field-refund") || 0, maxValue: adjustedTotal, format: state.get("currency"), classNames: { @@ -355,6 +443,13 @@ Template.coreOrderShippingInvoice.helpers({ return true; }, + showAfterPaymentCaptured() { + const instance = Template.instance(); + const order = instance.state.get("order"); + const orderStatus = orderCreditMethod(order).paymentMethod.status; + return orderStatus === "completed"; + }, + paymentApproved() { const instance = Template.instance(); const order = instance.state.get("order"); @@ -365,8 +460,9 @@ Template.coreOrderShippingInvoice.helpers({ paymentCaptured() { const instance = Template.instance(); const order = instance.state.get("order"); - - return orderCreditMethod(order).paymentMethod.status === "completed"; + const orderStatus = orderCreditMethod(order).paymentMethod.status; + const orderMode = orderCreditMethod(order).paymentMethod.mode; + return orderStatus === "completed" || (orderStatus === "refunded" && orderMode === "capture"); }, refundTransactions() { @@ -385,7 +481,7 @@ Template.coreOrderShippingInvoice.helpers({ return refunds.reverse(); } - return false; + return refunds; }, /** @@ -484,6 +580,7 @@ Template.coreOrderShippingInvoice.helpers({ let items; + // if avalara tax has been enabled it adds a "taxDetail" field for every item if (order.taxes !== undefined) { const taxes = order.taxes.slice(0, -1); diff --git a/imports/plugins/core/orders/client/templates/workflow/shippingSummary.html b/imports/plugins/core/orders/client/templates/workflow/shippingSummary.html index b5a1c840430..8659498c039 100644 --- a/imports/plugins/core/orders/client/templates/workflow/shippingSummary.html +++ b/imports/plugins/core/orders/client/templates/workflow/shippingSummary.html @@ -1,48 +1,7 @@ diff --git a/imports/plugins/core/orders/client/templates/workflow/shippingSummary.js b/imports/plugins/core/orders/client/templates/workflow/shippingSummary.js index 456a9f07f6d..478b8687a86 100644 --- a/imports/plugins/core/orders/client/templates/workflow/shippingSummary.js +++ b/imports/plugins/core/orders/client/templates/workflow/shippingSummary.js @@ -1,130 +1,11 @@ -import _ from "lodash"; -import { Meteor } from "meteor/meteor"; -import { Tracker } from "meteor/tracker"; import { Template } from "meteor/templating"; -import { i18next } from "/client/api"; -import { Orders } from "/lib/collections"; - -Template.coreOrderShippingSummary.onCreated(() => { - const template = Template.instance(); - const currentData = Template.currentData(); - - template.orderDep = new Tracker.Dependency; - - function getOrder(orderId, shipmentId) { - template.orderDep.depend(); - return Orders.findOne({ - "_id": orderId, - "shipping._id": shipmentId - }); - } - - Tracker.autorun(() => { - template.order = getOrder(currentData.orderId, currentData.fulfillment._id); - }); -}); - -/* - * automatically start order processing on first view - */ - -Template.coreOrderShippingSummary.onRendered(function () { - const template = Template.instance(); - const order = template.order; - - if (order.workflow) { - if (order.workflow.status === "coreOrderCreated") { - order.workflow.status = "coreOrderCreated"; - Meteor.call("workflow/pushOrderWorkflow", "coreOrderWorkflow", "coreOrderCreated", order); - } - } -}); - -/** - * coreOrderCreated events - * - */ -Template.coreOrderShippingSummary.events({ - "click .btn": function () { - Meteor.call("workflow/pushOrderWorkflow", "coreOrderWorkflow", "coreOrderCreated", this); - } -}); +import OrderSummaryContainer from "../../containers/orderSummaryContainer"; Template.coreOrderShippingSummary.helpers({ - order() { - const template = Template.instance(); - return template.order; - }, - shipment() { - return Template.instance().order.shipping[0]; - }, - - paymentProcessor() { - const processor = Template.instance().order.billing[0].paymentMethod.processor; - return { - name: processor.toLowerCase(), - label: processor - }; - }, - - tracking() { - const shipment = Template.instance().order.shipping[0]; - if (shipment.tracking) { - return shipment.tracking; - } - - return i18next.t("orderShipping.noTracking"); - }, - - printableLabels() { - const { shippingLabelUrl, customsLabelUrl } = Template.instance().order.shipping[0]; - if (shippingLabelUrl || customsLabelUrl) { - return { shippingLabelUrl, customsLabelUrl }; - } - - return false; - }, - - shipmentStatus() { - const order = Template.instance().order; - const shipment = Template.instance().order.shipping[0]; - - // check first if it was delivered - if (shipment.delivered) { - return { - delivered: true, - shipped: true, - status: "success", - label: i18next.t("orderShipping.delivered") - }; - } - - const shipped = _.every(shipment.items, (shipmentItem) => { - for (const fullItem of order.items) { - if (fullItem._id === shipmentItem._id) { - if (fullItem.workflow) { - if (_.isArray(fullItem.workflow.workflow)) { - return _.includes(fullItem.workflow.workflow, "coreOrderItemWorkflow/completed"); - } - } - } - } - }); - - if (shipped) { - return { - delivered: false, - shipped: true, - status: "success", - label: i18next.t("orderShipping.shipped") - }; - } - + orderSummary() { return { - delivered: false, - shipped: false, - status: "info", - label: i18next.t("orderShipping.notShipped") + component: OrderSummaryContainer, + ...Template.currentData() }; } }); diff --git a/imports/plugins/core/orders/client/templates/workflow/shippingTracking.html b/imports/plugins/core/orders/client/templates/workflow/shippingTracking.html index 62c02086b8a..7cc2481d26b 100644 --- a/imports/plugins/core/orders/client/templates/workflow/shippingTracking.html +++ b/imports/plugins/core/orders/client/templates/workflow/shippingTracking.html @@ -1,5 +1,6 @@ diff --git a/imports/plugins/core/orders/client/templates/workflow/shippingTracking.js b/imports/plugins/core/orders/client/templates/workflow/shippingTracking.js index 712112f06ed..d688335393b 100644 --- a/imports/plugins/core/orders/client/templates/workflow/shippingTracking.js +++ b/imports/plugins/core/orders/client/templates/workflow/shippingTracking.js @@ -121,12 +121,29 @@ Template.coreOrderShippingTracking.helpers({ } }); - return _.includes(fullItem.workflow.workflow, "coreOrderItemWorkflow/shipped"); + return !_.includes(fullItem.workflow.workflow, "coreOrderItemWorkflow/shipped"); }); return shippedItems; }, + isNotCanceled() { + const currentData = Template.currentData(); + const order = Template.instance().order; + + const canceledItems = _.every(currentData.fulfillment.items, (shipmentItem) => { + const fullItem = _.find(order.items, (orderItem) => { + if (orderItem._id === shipmentItem._id) { + return true; + } + }); + + return fullItem.workflow.status !== "coreOrderItemWorkflow/canceled"; + }); + + return canceledItems; + }, + isCompleted() { const currentData = Template.currentData(); const order = Template.instance().order; diff --git a/imports/plugins/core/router/client/app.js b/imports/plugins/core/router/client/app.js new file mode 100644 index 00000000000..4eaedc9a3c4 --- /dev/null +++ b/imports/plugins/core/router/client/app.js @@ -0,0 +1,109 @@ +import React, { Component, PropTypes } from "react"; +import classnames from "classnames"; +import { Reaction, Router } from "/client/api"; +import { composeWithTracker } from "/lib/api/compose"; +import { Loading } from "/imports/plugins/core/ui/client/components"; +import ToolbarContainer from "/imports/plugins/core/dashboard/client/containers/toolbarContainer"; +import Toolbar from "/imports/plugins/core/dashboard/client/components/toolbar"; +import { ActionViewContainer } from "/imports/plugins/core/dashboard/client/containers"; +import { ActionView } from "/imports/plugins/core/dashboard/client/components"; + +const ConnectedToolbarComponent = ToolbarContainer(Toolbar); +const ConnectedAdminViewComponent = ActionViewContainer(ActionView); + +const styles = { + customerApp: { + width: "100%" + }, + adminApp: { + width: "100%", + height: "100vh", + display: "flex", + overflow: "hidden" + }, + adminContentContainer: { + flex: "1 1 auto", + height: "100vh" + }, + adminContainer: { + display: "flex", + flex: "1 1 auto" + }, + scrollableContainer: { + overflow: "auto" + } +}; + +class App extends Component { + static propTypes = { + children: PropTypes.node, + currentRoute: PropTypes.object.isRequired, + hasDashboardAccess: PropTypes.bool, + isActionViewOpen: PropTypes.bool + } + + get isAdminApp() { + return this.props.hasDashboardAccess; + } + + renderAdminApp() { + const pageClassName = classnames({ + "admin": true, + "page": true, + "show-settings": this.props.isActionViewOpen + }); + + const currentRoute = this.props.currentRoute; + const routeOptions = currentRoute.route && currentRoute.route.options || {}; + const routeData = routeOptions && currentRoute.route.options.structure || {}; + + return ( +
+
+
+ +
+
+ {this.props.children} +
+
+ {this.props.hasDashboardAccess && } +
+ ); + } + + render() { + const pageClassName = classnames({ + "admin": true, + "show-settings": this.props.isActionViewOpen + }); + + const currentRoute = this.props.currentRoute; + const layout = currentRoute.route.options.layout; + + if (this.isAdminApp && layout !== "printLayout") { + return this.renderAdminApp(); + } + + return ( +
+ {this.props.children} +
+ ); + } +} + +function composer(props, onData) { + onData(null, { + isActionViewOpen: Reaction.isActionViewOpen(), + hasDashboardAccess: Reaction.hasDashboardAccess(), + currentRoute: Router.current() + }); +} + +export default composeWithTracker(composer, Loading)(App); diff --git a/imports/plugins/core/router/client/browserRouter.js b/imports/plugins/core/router/client/browserRouter.js new file mode 100644 index 00000000000..2dae2aa7686 --- /dev/null +++ b/imports/plugins/core/router/client/browserRouter.js @@ -0,0 +1,158 @@ +import React, { Component, PropTypes } from "react"; +import ReactDOM from "react-dom"; +import { matchPath } from "react-router"; +import { Router as ReactRouter } from "react-router-dom"; +import { Reaction } from "/client/api"; +import pathToRegexp from "path-to-regexp"; +import { isEqual } from "lodash"; +import queryParse from "query-parse"; +import { Session } from "meteor/session"; +import { Tracker } from "meteor/tracker"; +import App from "/imports/plugins/core/router/client/app"; +import { Router } from "../lib"; +import { MetaData } from "/lib/api/router/metadata"; +import { TranslationProvider } from "/imports/plugins/core/ui/client/providers"; + +const history = Router.history; + +class BrowserRouter extends Component { + static propTypes = { + children: PropTypes.node, + history: PropTypes.object, + store: PropTypes.object + } + + static contextTypes = { + store: PropTypes.object + } + + componentWillMount() { + this.unsubscribeFromHistory = history.listen(this.handleLocationChange); + this.handleLocationChange(history.location); + } + + componentWillUnmount() { + if (this.unsubscribeFromHistory) this.unsubscribeFromHistory(); + } + + handleLocationChange = location => { + // Find all matching paths + const foundPaths = Router.routes.filter((pathObject) => { + return matchPath(location.pathname, { + path: pathObject.route, + exact: true + }); + }); + + // If no matching pathis, redirect to the not found page + if (foundPaths.length === 0 && location.pathname !== "not-found") { + Router.replace("not-found"); + return undefined; + } + + // If we have a found path, take the first match + const foundPath = foundPaths.length && foundPaths[0]; + const params = {}; + + // Process the params from the found path definiton + if (foundPath) { + const keys = []; + const re = pathToRegexp(foundPath.route, keys); // Create parser with route regex + const values = re.exec(location.pathname); // Process values + + // Create params object + keys.forEach((key, index) => { + params[key.name] = values[index + 1]; + }); + } + + // Get serach (query) string from current location + let search = location.search; + + // Remove the ? if it exists at the beginning + if (typeof search === "string" && search.startsWith("?")) { + search = search.substr(1); + } + + // Create objext of all necessary data for the current route + const routeData = { + route: { + ...foundPath, + name: foundPath.name, + path: location.pathname, + fullPath: `${location.pathname}${location.search}` + }, + params, + query: queryParse.toObject(search), // Parse query string into object + payload: location + }; + + // Get the previousroute, which is the currentRoute just before it changes + const previousRoute = Router.current(); + + // If it seems like we've moved to a differen route, then run the onExit + // hooks for the previousRoute + const routesAreSame = isEqual(previousRoute.route, routeData.route); + const paramsAreSame = isEqual(previousRoute.params, routeData.params); + const queryParamsAreSame = isEqual(previousRoute.query, routeData.query); + + const routesDiffer = routesAreSame && paramsAreSame && queryParamsAreSame; + + if (routesDiffer === false) { + // Run on enter hooks + Router.Hooks.run("onExit", "GLOBAL", routeData); + Router.Hooks.run("onExit", previousRoute.name, previousRoute); + + // Set current route reactive-var + Router.setCurrentRoute(routeData); + + // Run on enter hooks for the new route + Router.Hooks.run("onEnter", "GLOBAL", routeData); + Router.Hooks.run("onEnter", routeData.name, routeData); + } + } + + render() { + return ( + + ); + } +} + +export function getRootNode() { + let rootNode = document.getElementById("react-root"); + + if (rootNode) { + return rootNode; + } + const rootNodeHtml = "
"; + const body = document.getElementsByTagName("body")[0]; + + body.insertAdjacentHTML("beforeend", rootNodeHtml); + rootNode = document.getElementById("react-root"); + + return rootNode; +} + +export function initBrowserRouter() { + Router.initPackageRoutes({ + reactionContext: Reaction, + indexRoute: Session.get("INDEX_OPTIONS") || {} + }); + + Router.Hooks.onEnter(MetaData.init); + + Tracker.autorun(() => { + if (Router.ready()) { + ReactDOM.render(( + + + + + + ), getRootNode()); + } + }); +} + +export default BrowserRouter; diff --git a/imports/plugins/core/router/client/index.js b/imports/plugins/core/router/client/index.js new file mode 100644 index 00000000000..bb97da4717b --- /dev/null +++ b/imports/plugins/core/router/client/index.js @@ -0,0 +1 @@ +import "./startup"; diff --git a/client/modules/router/startup.js b/imports/plugins/core/router/client/startup.js similarity index 81% rename from client/modules/router/startup.js rename to imports/plugins/core/router/client/startup.js index 1b86468352d..87dad3fff8f 100644 --- a/client/modules/router/startup.js +++ b/imports/plugins/core/router/client/startup.js @@ -1,15 +1,14 @@ import { Meteor } from "meteor/meteor"; import { Tracker } from "meteor/tracker"; import { Reaction } from "/client/api"; -import Router from "./main"; +import { initBrowserRouter } from "./browserRouter"; +import { Router } from "../lib"; Meteor.startup(function () { Tracker.autorun(function () { // initialize client routing if (Reaction.Subscriptions.Packages.ready() && Reaction.Subscriptions.Shops.ready()) { - if (!Router._initialized) { - Router.initPackageRoutes(); - } + initBrowserRouter(); } }); @@ -22,7 +21,7 @@ Meteor.startup(function () { // Accounts.onLogin(() => { if (Meteor.loggingIn() === false && Router._routes.length > 0) { - Router.reload(); + initBrowserRouter(); } }); }); diff --git a/imports/plugins/core/router/lib/hooks.js b/imports/plugins/core/router/lib/hooks.js new file mode 100644 index 00000000000..b27075c5eab --- /dev/null +++ b/imports/plugins/core/router/lib/hooks.js @@ -0,0 +1,75 @@ + +/** + * Route Hook Methods + */ +const Hooks = { + _hooks: { + onEnter: {}, + onExit: {} + }, + + _addHook(type, routeName, callback) { + if (typeof this._hooks[type][routeName] === "undefined") { + this._hooks[type][routeName] = []; + } + this._hooks[type][routeName].push(callback); + }, + + enter(callback) { + if (Array.isArray(callback)) { + callback.forEach((cb) => { + this.onEnter(cb); + }); + } else { + this.onEnter(callback); + } + }, + + leave(callback) { + if (Array.isArray(callback)) { + callback.forEach((cb) => { + this.onExit(cb); + }); + } else { + return this.onExit(callback); + } + }, + + onEnter(routeName, callback) { + // global onEnter callback + if (arguments.length === 1 && typeof arguments[0] === "function") { + const cb = routeName; + return this._addHook("onEnter", "GLOBAL", cb); + } + // route-specific onEnter callback + return this._addHook("onEnter", routeName, callback); + }, + + onExit(routeName, callback) { + // global onExit callback + if (arguments.length === 1 && typeof arguments[0] === "function") { + const cb = routeName; + return this._addHook("onExit", "GLOBAL", cb); + } + // route-specific onExit callback + return this._addHook("onExit", routeName, callback); + }, + + get(type, name) { + const group = this._hooks[type] || {}; + const callbacks = group[name]; + return (typeof callbacks !== "undefined" && !!callbacks.length) ? callbacks : []; + }, + + run(type, name, constant) { + const callbacks = this.get(type, name); + if (typeof callbacks !== "undefined" && !!callbacks.length) { + return callbacks.forEach((callback) => { + return callback(constant); + }); + } + return null; + } +}; + +export default Hooks; diff --git a/imports/plugins/core/router/lib/index.js b/imports/plugins/core/router/lib/index.js new file mode 100644 index 00000000000..79c04b5814c --- /dev/null +++ b/imports/plugins/core/router/lib/index.js @@ -0,0 +1 @@ +export { default as Router } from "./router"; diff --git a/imports/plugins/core/router/lib/router.js b/imports/plugins/core/router/lib/router.js new file mode 100644 index 00000000000..17dc70b88e2 --- /dev/null +++ b/imports/plugins/core/router/lib/router.js @@ -0,0 +1,527 @@ +import React from "react"; +import { Route } from "react-router"; +import createBrowserHistory from "history/createBrowserHistory"; +import createMemoryHistory from "history/createMemoryHistory"; +import pathToRegexp from "path-to-regexp"; +import queryParse from "query-parse"; +import Immutable from "immutable"; +import { Meteor } from "meteor/meteor"; +import Blaze from "meteor/gadicc:blaze-react-component"; +import { Tracker } from "meteor/tracker"; +import { Packages, Shops } from "/lib/collections"; +import { getComponent } from "/imports/plugins/core/layout/lib/components"; +import BlazeLayout from "/imports/plugins/core/layout/lib/blazeLayout"; +import Hooks from "./hooks"; + + +export let history; + +// Private vars +// const currentRoute = new ReactiveVar({}); +let currentRoute = Immutable.Map(); +const routerReadyDependency = new Tracker.Dependency; +const routerChangeDependency = new Tracker.Dependency; + +// Create history object depending on if this is client or server +if (Meteor.isClient) { + history = createBrowserHistory(); +} else { + history = createMemoryHistory(); +} + +// Base router class (static) +class Router { + static history = history + static Hooks = Hooks + static routes = [] + static _routes = Router.routes // for legacy + static _initialized = false; + + static ready() { + routerReadyDependency.depend(); + return Router._initialized; + } + + static triggerRouterReady() { + routerReadyDependency.changed(); + } + + static get triggers() { + return Hooks; + } + + static current() { + return currentRoute.toJS(); + } + + static setCurrentRoute(routeData) { + currentRoute = Immutable.Map(routeData); + routerChangeDependency.changed(); + } + + static getRouteName() { + const current = Router.current(); + + return current.options && current.options.name || ""; + } + + static getParam(name) { + routerChangeDependency.depend(); + const current = Router.current(); + + return current.params && current.params[name] || undefined; + } + + static getQueryParam(name) { + routerChangeDependency.depend(); + const current = Router.current(); + + return current.query && current.query[name] || undefined; + } + + static watchPathChange() { + routerChangeDependency.depend(); + } +} + +/** + * pathFor + * @summary get current router path + * @param {String} path - path to fetch + * @param {Object} options - url params + * @return {String} returns current router path + */ +Router.pathFor = (path, options = {}) => { + // const params = options.hash || {}; + // const query = params.query ? Router._qs.parse(params.query) : {}; + // // prevent undefined param error + // for (const i in params) { + // if (params[i] === null || params[i] === undefined) { + // params[i] = "/"; + // } + // } + // return Router.path(path, params, query); + + const foundPath = Router.routes.find((pathObject) => { + // console.log(pathObject.options.name, path); + if (pathObject.options.name === path) { + return true; + } + return false; + }); + + if (foundPath) { + // Pull the hash out of options + // + // This is becuase of Spacebars that we have hash. + // Spacebars takes all params passed into a template tag and places + // them into the options.hash object. This will also include any `query` params + const hash = options && options.hash || {}; + + // Create an executable function based on the route regex + const toPath = pathToRegexp.compile(foundPath.route); + + // Compile the regex path with the params from the hash + const compiledPath = toPath(hash); + + // Convert the query object to a string + // e.g. { a: "one", b: "two"} => "a=one&b=two" + const queryString = queryParse.toString(hash.query); + + // Return the compiled path + query string if we have one + if (typeof queryString === "string" && queryString.length) { + return `${compiledPath}?${queryString}`; + } + + // Return only the compiled path + return compiledPath; + } + + return "/"; +}; + + +Router.go = (path, params, query) => { + let actualPath; + + if (typeof path === "string" && path.startsWith("/")) { + actualPath = path; + } else { + actualPath = Router.pathFor(path, { + hash: { + ...params, + query + } + }); + } + + if (window) { + history.push(actualPath); + } +}; + +Router.replace = (path, params, query) => { + const actualPath = Router.pathFor(path, { + hash: { + ...params, + query + } + }); + + if (window) { + history.replace(actualPath); + } +}; + +Router.reload = () => { + const current = Router.current(); + + if (window) { + history.replace(current.route.fullPath || "/"); + } +}; + +/** + * isActive + * @summary general helper to return "active" when on current path + * @example {{active "name"}} + * @param {String} routeName - route name as defined in registry + * @return {String} return "active" or null + */ +Router.isActiveClassName = (routeName) => { + const current = Router.current(); + const group = current.route.group; + const path = current.route.path; + let prefix; + + if (group && group.prefix) { + prefix = current.route.group.prefix; + } else { + prefix = ""; + } + + if (typeof path === "string") { + const routeDef = path.replace(prefix + "/", ""); + return routeDef === routeName ? "active" : ""; + } + + return ""; +}; + +/** + * hasRoutePermission + * check if user has route permissions + * @param {Object} route - route context + * @return {Boolean} returns `true` if route is autoriized, `false` otherwise + */ +function hasRoutePermission(route) { + const routeName = route.name; + + if (routeName === "index" || routeName === "not-found") { + return true; + } else if (Router.Reaction.hasPermission(routeName, Meteor.userId())) { + return true; + } + + return false; +} + + +/** + * getRouteName + * assemble route name to be standard + * prefix/package name + registry name or route + * @param {String} packageName [package name] + * @param {Object} registryItem [registry object] + * @return {String} [route name] + */ +function getRegistryRouteName(packageName, registryItem) { + let routeName; + if (packageName && registryItem) { + if (registryItem.name) { + routeName = registryItem.name; + } else if (registryItem.template) { + routeName = `${packageName}/${registryItem.template}`; + } else { + routeName = packageName; + } + // dont include params in the name + routeName = routeName.split(":")[0]; + return routeName; + } + return null; +} + +/** + * selectLayout + * @param {Object} layout - element of shops.layout array + * @param {Object} setLayout - layout + * @param {Object} setWorkflow - workflow + * @returns {Object} layout - return object of template definitions for Blaze Layout + */ +function selectLayout(layout, setLayout, setWorkflow) { + const currentLayout = setLayout || Session.get("DEFAULT_LAYOUT") || "coreLayout"; + const currentWorkflow = setWorkflow || Session.get("DEFAULT_WORKFLOW") || "coreWorkflow"; + if (layout.layout === currentLayout && layout.workflow === currentWorkflow && layout.enabled === true) { + return layout; + } + return null; +} + +/** + * ReactionLayout + * sets and returns reaction layout structure + * @param {Object} options - this router context + * @param {String} options.layout - string of shop.layout.layout (defaults to coreLayout) + * @param {String} options.workflow - string of shop.layout.workflow (defaults to coreLayout) + * @returns {Object} layout - return object of template definitions for Blaze Layout + */ +export function ReactionLayout(options = {}) { + // Find a workflow layout to render + // Get the current shop data + const shop = Shops.findOne(Router.Reaction.getShopId()); + + // get the layout & workflow from options if they exist + // Otherwise get them from the Session. this is set in `/client/config/defaults` + // Otherwise, default to hard-coded values + const layoutName = options.layout || Session.get("DEFAULT_LAYOUT") || "coreLayout"; + const workflowName = options.workflow || Session.get("DEFAULT_WORKFLOW") || "coreWorkflow"; + + // Layout object used to render + // Defaults provided for reference + let layoutStructure = { + template: "", + layoutHeader: "", + layoutFooter: "", + notFound: "notFound", + dashboardHeader: "", + dashboardControls: "", + dashboardHeaderControls: "", + adminControlsFooter: "" + }; + + // Find a registered layout using the layoutName and workflowName + if (shop) { + const sortedLayout = shop.layout.sort((prev, next) => prev.priority - next.priority); + const foundLayout = sortedLayout.find((x) => selectLayout(x, layoutName, workflowName)); + + if (foundLayout && foundLayout.structure) { + layoutStructure = { + ...foundLayout.structure + }; + } + } + + // If the original options did not include a workflow, but did have a template, + // then we override the template from the layout with the one provided by the options. + // + // Why is this? We always need a workflow to render the entire layout of the app. + // The default layout has a default template that may not be the one we want to render. + // Some routes, such as `/account/profile` do no have a workflow, but define a template. + // Without the logic below, it would end up rendering the homepage instead of the profile + // page. + // const optionsHasWorkflow = typeof options.workflow === "string"; + const optionsHasTemplate = typeof options.template === "string"; + + if (optionsHasTemplate) { + layoutStructure.template = options.template; + } + + // If there is no Blaze Template (Template[]) or React Component (getComponent) + // Then use the notFound template instead + if (!Template[layoutStructure.template] && !getComponent(layoutStructure.template)) { + return ( + + ); + } + + // Render the layout + return { + structure: layoutStructure, + component: (props) => { // eslint-disable-line react/no-multi-comp, react/display-name + const route = Router.current().route; + const structure = { + ...layoutStructure + }; + + // If the current route is unauthorized, and is not the "not-found" route, + // then override the template to use the default unauthroized template + if (hasRoutePermission(route) === false && route.name !== "not-found") { + structure.template = "unauthorized"; + } + + if (getComponent(layoutName)) { + return React.createElement(getComponent(layoutName), { + ...props, + structure: structure + }); + } else if (Template[layoutName]) { + return ( + + ); + } + + return ; + } + }; +} + +/** + * initPackageRoutes + * registers route and template when registry item has + * registryItem.route && registryItem.template + * @param {Object} options - options and context for route creation + * @returns {undefined} returns undefined + */ +Router.initPackageRoutes = (options) => { + Router.Reaction = options.reactionContext; + Router.routes = []; + + const pkgs = Packages.find().fetch(); + const prefix = Router.Reaction.getShopPrefix(); + const reactRouterRoutes = []; + + // prefixing isnt necessary if we only have one shop + // but we need to bypass the current + // subscription to determine this. + const shopSub = Meteor.subscribe("shopsCount"); + if (shopSub.ready()) { + // using tmeasday:publish-counts + const shopCount = Counts.get("shops-count"); + + // Index layout + const indexLayout = ReactionLayout(options.indexRoute); + const indexRoute = { + route: "/", + name: "index", + options: { + name: "index", + ...options.indexRoute, + component: indexLayout.component, + structure: indexLayout.structure + } + }; + + reactRouterRoutes.push( + + ); + + const notFoundLayout = ReactionLayout({ template: "notFound" }); + const notFoundRoute = { + route: "/not-found", + name: "not-found", + options: { + name: "not-found", + ...notFoundLayout.indexRoute, + component: notFoundLayout.component, + structure: notFoundLayout.structure + } + }; + + reactRouterRoutes.push( + + ); + + Router.routes.push(indexRoute); + Router.routes.push(notFoundRoute); + + // get package registry route configurations + for (const pkg of pkgs) { + const newRoutes = []; + // pkg registry + if (pkg.registry && pkg.enabled) { + const registry = Array.from(pkg.registry); + for (const registryItem of registry) { + // registryItems + if (registryItem.route) { + const { + meta, + route, + template, + layout, + workflow + } = registryItem; + + // get registry route name + const name = getRegistryRouteName(pkg.name, registryItem); + + // define new route + // we could allow the options to be passed in the registry if we need to be more flexible + const reactionLayout = ReactionLayout({ template, workflow, layout }); + const newRouteConfig = { + route, + name, + options: { + meta, + name, + template, + layout, + triggersEnter: Router.Hooks.get("onEnter", name), + triggersExit: Router.Hooks.get("onExit", name), + component: reactionLayout.component, + structure: reactionLayout.structure + } + }; + + // push new routes + newRoutes.push(newRouteConfig); + } // end registryItems + } // end package.registry + + // + // add group and routes to routing table + // + const uniqRoutes = new Set(newRoutes); + let index = 0; + for (const route of uniqRoutes) { + // allow overriding of prefix in route definitions + // define an "absolute" url by excluding "/" + route.group = {}; + + if (route.route.substring(0, 1) !== "/") { + route.route = "/" + route.route; + route.group.prefix = ""; + } else if (shopCount <= 1) { + route.group.prefix = ""; + } else { + route.group.prefix = prefix; + route.route = `${prefix}${route.route}`; + } + + // Add the route to the routing table + reactRouterRoutes.push( + + ); + + Router.routes.push(route); + } + } + } // end package loop + + Router._initialized = true; + Router.reactComponents = reactRouterRoutes; + Router._routes = Router.routes; + + routerReadyDependency.changed(); + } +}; + + +export default Router; diff --git a/imports/plugins/core/router/register.js b/imports/plugins/core/router/register.js new file mode 100644 index 00000000000..8e5d4b8d077 --- /dev/null +++ b/imports/plugins/core/router/register.js @@ -0,0 +1,10 @@ +import { Reaction } from "/server/api"; + +Reaction.registerPackage({ + label: "Router", + name: "reaction-router", + icon: "fa fa-share", + autoEnable: true, + settings: {}, + registry: [] +}); diff --git a/imports/plugins/core/router/server/index.js b/imports/plugins/core/router/server/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/imports/plugins/core/ui-navbar/client/components/brand/brand.js b/imports/plugins/core/ui-navbar/client/components/brand/brand.js index 43a2a111a9d..5a60c341046 100644 --- a/imports/plugins/core/ui-navbar/client/components/brand/brand.js +++ b/imports/plugins/core/ui-navbar/client/components/brand/brand.js @@ -1,4 +1,4 @@ -import { Reaction } from "/client/api"; +import { Reaction, Router } from "/client/api"; import { Media, Shops } from "/lib/collections"; Template.coreNavigationBrand.helpers({ @@ -13,3 +13,10 @@ Template.coreNavigationBrand.helpers({ return false; } }); + +Template.coreNavigationBrand.events({ + "click a.brand"(event) { + event.preventDefault(); + Router.go("/"); + } +}); diff --git a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html index d29e3e10942..2717da455d6 100644 --- a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html +++ b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html @@ -17,6 +17,6 @@
{{> accounts tpl="loginDropdown"}}
{{> cartIcon}}
-
{{> cartPanel}}
+
{{> React component=cartPanel}}
diff --git a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js index 8e2cee7a305..5e3d17d59eb 100644 --- a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js +++ b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js @@ -2,6 +2,7 @@ import { FlatButton } from "/imports/plugins/core/ui/client/components"; import { NotificationContainer } from "/imports/plugins/included/notifications/client/containers"; import { Reaction } from "/client/api"; import { Tags } from "/lib/collections"; +import CartPanel from "../../../../checkout/client/templates/cartPanel/container/cartPanelContainer"; Template.CoreNavigationBar.onCreated(function () { @@ -96,5 +97,8 @@ Template.CoreNavigationBar.helpers({ instance.toggleMenuCallback = callback; } }; + }, + cartPanel() { + return CartPanel; } }); diff --git a/imports/plugins/core/ui/client/components/button/button.jsx b/imports/plugins/core/ui/client/components/button/button.jsx index 6d6540a92aa..92ddf363e25 100644 --- a/imports/plugins/core/ui/client/components/button/button.jsx +++ b/imports/plugins/core/ui/client/components/button/button.jsx @@ -122,14 +122,12 @@ class Button extends Component { } render() { - const { } = this.props; - const { active, status, toggleOn, primary, bezelStyle, className, containerStyle, // Destructure these vars as they aren't valid as attributes on the HTML element button iconAfter, label, i18nKeyTitle, i18nKeyLabel, i18nKeyTooltip, // eslint-disable-line no-unused-vars - tooltip, icon, toggle, onIcon, eventAction, // eslint-disable-line no-unused-vars + tooltip, icon, toggle, onIcon, eventAction, buttonType, // eslint-disable-line no-unused-vars toggleOnLabel, i18nKeyToggleOnLabel, tagName, onClick, onToggle, onValue, tooltipPosition, // eslint-disable-line no-unused-vars // Get the rest of the properties and put them in attrs @@ -164,7 +162,7 @@ class Button extends Component { "onMouseOut": this.handleButtonMouseOut, "onMouseOver": this.handleButtonMouseOver, "onClick": this.handleClick, - "type": "button" + "type": buttonType || "button" }, attrs, extraProps); @@ -213,6 +211,7 @@ class Button extends Component { Button.propTypes = { active: PropTypes.bool, bezelStyle: PropTypes.oneOf(["flat", "solid", "outline"]), + buttonType: PropTypes.string, children: PropTypes.node, className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), containerStyle: PropTypes.object, diff --git a/imports/plugins/core/ui/client/components/button/buttonSelect.js b/imports/plugins/core/ui/client/components/button/buttonSelect.js new file mode 100644 index 00000000000..f062323dbd9 --- /dev/null +++ b/imports/plugins/core/ui/client/components/button/buttonSelect.js @@ -0,0 +1,145 @@ +import React, { Component, PropTypes } from "react"; +import classnames from "classnames"; +import Button from "./button.jsx"; +import { Translation } from "/imports/plugins/core/ui/client/components"; + +class ButtonSelect extends Component { + static PropTypes = { + buttons: PropTypes.array, + currentButton: PropTypes.node, + defaultButton: PropTypes.object, + defaultNonActiveButtons: PropTypes.array, + nonActiveButtons: PropTypes.array + } + + state = { + toggle: "hidden", + currentButton: {}, + buttons: [], + activeButton: "", + nonActiveButtons: [], + defaultBgClassNames: "", + toggleIcon: classnames({ "fa": true, "fa-chevron-down": true, "text-center": true, "fa-icon": true }), + toggleClassNames: classnames({ "button-dropdown": true, "hidden": true }) + } + + componentWillMount() { + this.handleDefaultState(); + } + + handleDefaultState = () => { + const props = this.props; + let defaultButton = props.buttons.filter(button => { + if (button.active === true) { + return button; + } + }); + defaultButton = defaultButton[0]; + + const defaultBgClassNames = classnames({ "button-select": true, [defaultButton.bgColor]: true }); + + const defaultNonActiveButtons = props.buttons.filter(button => { + if (button.active === false || button.active === undefined) { + return button; + } + }); + const currentButton = ( +