diff --git a/client/templates/layout.html b/client/templates/layout.html
deleted file mode 100644
index 6de1a5cc271..00000000000
--- a/client/templates/layout.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- {{> Template.dynamic template=dashboard}}
-
-
- {{> inlineAlerts}}
- {{> Template.dynamic template=template}}
-
-
-
-
-
diff --git a/client/templates/layout.js b/client/templates/layout.js
deleted file mode 100644
index 1a3e2e07609..00000000000
--- a/client/templates/layout.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * This is an example of a customized template.
- * This layout replaces the "coreLayout" template defined in the reactioncommerce:core package.
- * https://github.com/reactioncommerce/reaction-core/blob/master/client/templates/layout/layout.html
- * To use custom template in layout.html uncomment
- *
- * Template.layout.replaces "coreLayout"
- */
-
-
-// Template.layout.replaces("coreLayout");
diff --git a/imports/plugins/core/accounts/register.js b/imports/plugins/core/accounts/register.js
index 4a0334c9d7f..969a92d6226 100644
--- a/imports/plugins/core/accounts/register.js
+++ b/imports/plugins/core/accounts/register.js
@@ -30,7 +30,8 @@ Reaction.registerPackage({
route: "/dashboard/account/settings",
container: "accounts",
workflow: "coreAccountsWorkflow",
- template: "accountsSettings"
+ template: "accountsSettings",
+ showForShopTypes: ["primary"]
}, {
route: "/dashboard/accounts",
name: "dashboard/accounts",
@@ -57,7 +58,7 @@ Reaction.registerPackage({
enabled: true,
structure: {
template: "accountsDashboard",
- layoutHeader: "layoutHeader",
+ layoutHeader: "NavBar",
layoutFooter: "",
notFound: "notFound",
dashboardHeader: "dashboardHeader",
diff --git a/imports/plugins/core/checkout/client/index.js b/imports/plugins/core/checkout/client/index.js
index d851c168016..967e3d8c955 100644
--- a/imports/plugins/core/checkout/client/index.js
+++ b/imports/plugins/core/checkout/client/index.js
@@ -10,7 +10,6 @@ import "./templates/cartIcon/cartIcon.js";
import "./templates/checkout/addressBook/addressBook.html";
import "./templates/checkout/completed/completed.html";
import "./templates/checkout/completed/completed.js";
-import "./templates/checkout/header/header.html";
import "./templates/checkout/login/login.html";
import "./templates/checkout/login/login.js";
import "./templates/checkout/progressBar/progressBar.html";
diff --git a/imports/plugins/core/checkout/client/templates/checkout/header/header.html b/imports/plugins/core/checkout/client/templates/checkout/header/header.html
deleted file mode 100644
index 92cadc5a346..00000000000
--- a/imports/plugins/core/checkout/client/templates/checkout/header/header.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
{{> coreNavigationBrand}}
-
-
-
-
-
-
-
diff --git a/imports/plugins/core/checkout/register.js b/imports/plugins/core/checkout/register.js
index e3e6028ac43..768d13d407c 100644
--- a/imports/plugins/core/checkout/register.js
+++ b/imports/plugins/core/checkout/register.js
@@ -28,7 +28,7 @@ Reaction.registerPackage({
enabled: true,
structure: {
template: "cartCheckout",
- layoutHeader: "checkoutHeader",
+ layoutHeader: "NavBarCheckout",
layoutFooter: "",
notFound: "notFound",
dashboardHeader: "",
diff --git a/imports/plugins/core/dashboard/register.js b/imports/plugins/core/dashboard/register.js
index 1b2e333e2c9..acc18e4be2b 100644
--- a/imports/plugins/core/dashboard/register.js
+++ b/imports/plugins/core/dashboard/register.js
@@ -56,7 +56,7 @@ Reaction.registerPackage({
enabled: true,
structure: {
template: "dashboardPackages",
- layoutHeader: "layoutHeader",
+ layoutHeader: "NavBar",
layoutFooter: "",
notFound: "notFound",
dashboardHeader: "dashboardHeader",
diff --git a/imports/plugins/core/email/register.js b/imports/plugins/core/email/register.js
index efbfb36c0b1..da80ae404a3 100644
--- a/imports/plugins/core/email/register.js
+++ b/imports/plugins/core/email/register.js
@@ -29,7 +29,7 @@ Reaction.registerPackage({
enabled: true,
structure: {
template: "email",
- layoutHeader: "layoutHeader",
+ layoutHeader: "NavBar",
layoutFooter: "",
notFound: "notFound",
dashboardHeader: "dashboardHeader",
diff --git a/imports/plugins/core/layout/client/components/coreLayout.js b/imports/plugins/core/layout/client/components/coreLayout.js
index 39fc003bd3c..6c46cf2ef84 100644
--- a/imports/plugins/core/layout/client/components/coreLayout.js
+++ b/imports/plugins/core/layout/client/components/coreLayout.js
@@ -1,33 +1,34 @@
import React from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
-import { Components, registerComponent } from "@reactioncommerce/reaction-components";
+import { getComponent, registerComponent } from "@reactioncommerce/reaction-components";
import Blaze from "meteor/gadicc:blaze-react-component";
import { Template } from "meteor/templating";
const CoreLayout = ({ actionViewIsOpen, structure }) => {
- const { layoutFooter, template } = structure || {};
+ const { layoutHeader, layoutFooter, template } = structure || {};
const pageClassName = classnames({
"page": true,
"show-settings": actionViewIsOpen
});
+ const headerComponent = layoutHeader && getComponent(layoutHeader);
+ const footerComponent = layoutFooter && getComponent(layoutFooter);
+
return (
-
+
+ {headerComponent && React.createElement(headerComponent, {})}
- { Template[template] &&
+ {Template[template] &&
-
- }
+ }
- { Template[layoutFooter] &&
-
- }
+ {footerComponent && React.createElement(footerComponent, {})}
);
};
diff --git a/imports/plugins/core/layout/client/components/footer.js b/imports/plugins/core/layout/client/components/footer.js
new file mode 100644
index 00000000000..8e7bc511a78
--- /dev/null
+++ b/imports/plugins/core/layout/client/components/footer.js
@@ -0,0 +1,17 @@
+import React from "react";
+import { registerComponent } from "/imports/plugins/core/components/lib";
+
+const Footer = () => (
+
+
+
+);
+
+
+registerComponent("Footer", Footer);
+
+export default Footer;
diff --git a/imports/plugins/core/layout/client/index.js b/imports/plugins/core/layout/client/index.js
index 34f0dcdfe39..04cf45f5af5 100644
--- a/imports/plugins/core/layout/client/index.js
+++ b/imports/plugins/core/layout/client/index.js
@@ -1,16 +1,11 @@
-import "./templates/layout/admin/admin.html";
-import "./templates/layout/admin/admin.js";
import "./templates/layout/alerts/alerts.html";
import "./templates/layout/alerts/alerts.js";
import "./templates/layout/alerts/inlineAlerts.js";
import "./templates/layout/alerts/reactionAlerts.js";
import "./templates/layout/createContentMenu/createContentMenu.html";
import "./templates/layout/createContentMenu/createContentMenu.js";
-import "./templates/layout/footer/footer.html";
import "./templates/layout/header/brand.html";
import "./templates/layout/header/button.html";
-import "./templates/layout/header/header.html";
-import "./templates/layout/header/header.js";
import "./templates/layout/header/tags.html";
import "./templates/layout/notFound/notFound.html";
import "./templates/layout/notFound/notFound.js";
@@ -20,5 +15,7 @@ import "./templates/layout/notice/unauthorized.js";
import "./templates/theme/theme.html";
import "./templates/theme/theme.js";
+import "./components/footer";
+
export CoreLayout from "./components/coreLayout";
export PrintLayout from "./components/printLayout";
diff --git a/imports/plugins/core/layout/client/templates/layout/admin/admin.html b/imports/plugins/core/layout/client/templates/layout/admin/admin.html
deleted file mode 100644
index a7dfed557ec..00000000000
--- a/imports/plugins/core/layout/client/templates/layout/admin/admin.html
+++ /dev/null
@@ -1,35 +0,0 @@
-
- {{> inlineAlerts}}
-
-
-
- {{> React PublishContainerComponent }}
-
-
-
-
-
-
-
-
- {{> Template.dynamic template=template}}
-
-
-
-
-
-
-
- {{> React ActionViewComponent }}
-
-
diff --git a/imports/plugins/core/layout/client/templates/layout/admin/admin.js b/imports/plugins/core/layout/client/templates/layout/admin/admin.js
deleted file mode 100644
index 808a50fa050..00000000000
--- a/imports/plugins/core/layout/client/templates/layout/admin/admin.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import _ from "lodash";
-import Drop from "tether-drop";
-import { Meteor } from "meteor/meteor";
-import { Blaze } from "meteor/blaze";
-import { $ } from "meteor/jquery";
-import { Template } from "meteor/templating";
-import { Reaction, i18next } from "/client/api";
-import { Packages } from "/lib/collections";
-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";
-
-Template.coreAdminLayout.onRendered(function () {
- $("body").addClass("admin");
-});
-
-Template.coreAdminLayout.onDestroyed(() => {
- $("body").removeClass("admin");
-});
-
-Template.coreAdminLayout.helpers({
- PublishContainerComponent() {
- return {
- component: ToolbarContainer(Toolbar),
- data: Template.currentData()
- };
- },
- ActionViewComponent() {
- return {
- component: ActionViewContainer(ActionView),
- data: Template.currentData()
- };
- },
- shortcutButtons() {
- const instance = Template.instance();
- const shortcuts = Reaction.Apps({ provides: "shortcut", enabled: true });
- const items = [];
-
- if (_.isArray(shortcuts)) {
- for (const shortcut of shortcuts) {
- if (!shortcut.container) {
- items.push({
- type: "link",
- href: Reaction.Router.pathFor(shortcut.name),
- className: Reaction.Router.isActiveClassName(shortcut.name),
- icon: shortcut.icon,
- tooltip: shortcut.label || "",
- i18nKeyTooltip: shortcut.i18nKeyLabel,
- tooltipPosition: "left middle"
- });
- }
- }
- }
-
- items.push({ type: "seperator" });
-
- items.push({
- icon: "plus",
- tooltip: "Create Content",
- i18nKeyTooltip: "app.createContent",
- tooltipPosition: "left middle",
- onClick(event) {
- if (!instance.dropInstance) {
- instance.dropInstance = new Drop({
- target: event.currentTarget,
- content: "",
- constrainToWindow: true,
- classes: "drop-theme-arrows",
- position: "right center"
- });
-
- Blaze.renderWithData(Template.createContentMenu, {}, instance.dropInstance.content);
- }
-
- instance.dropInstance.open();
- }
- });
-
- return items;
- },
-
- isSeperator(props) {
- if (props.type === "seperator") {
- return true;
- }
- return false;
- },
-
- packageButtons() {
- const routeName = Reaction.Router.getRouteName();
-
- if (routeName !== "dashboard") {
- const registryItems = Reaction.Apps({ provides: "settings", container: routeName });
- const buttons = [];
-
- for (const item of registryItems) {
- if (Reaction.hasPermission(item.route, Meteor.userId())) {
- let icon = item.icon;
-
- if (!item.icon && item.provides && item.provides.includes("settings")) {
- icon = "gear";
- }
-
- buttons.push({
- href: item.route,
- icon: icon,
- tooltip: i18next.t(item.i18nKeyLabel, item.i18n),
- tooltipPosition: "left middle",
- onClick() {
- Reaction.showActionView(item);
- }
- });
- }
- }
-
- return buttons;
- }
- return [];
- },
-
- control: function () {
- return Reaction.getActionView();
- },
-
- adminControlsClassname: function () {
- if (Reaction.isActionViewOpen()) {
- return "show-settings";
- }
- return "";
- },
-
- /**
- * thisApp
- * @return {Object} Registry entry for item
- */
- thisApp() {
- const reactionApp = Packages.findOne({
- "registry.provides": "settings",
- "registry.route": Reaction.Router.getRouteName()
- }, {
- enabled: 1,
- registry: 1,
- name: 1,
- route: 1
- });
-
- if (reactionApp) {
- const settingsData = _.find(reactionApp.registry, function (item) {
- return item.route === Reaction.Router.getRouteName() && item.provides && item.provides.includes("settings");
- });
-
- return settingsData;
- }
- return reactionApp;
- }
-});
diff --git a/imports/plugins/core/layout/client/templates/layout/footer/footer.html b/imports/plugins/core/layout/client/templates/layout/footer/footer.html
deleted file mode 100644
index e3e546d90b8..00000000000
--- a/imports/plugins/core/layout/client/templates/layout/footer/footer.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
diff --git a/imports/plugins/core/layout/client/templates/layout/header/header.html b/imports/plugins/core/layout/client/templates/layout/header/header.html
deleted file mode 100644
index 3a183d998eb..00000000000
--- a/imports/plugins/core/layout/client/templates/layout/header/header.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
- {{> CoreNavigationBar coreNavProps}}
-
-
-
- {{!-- {{> tagNav tagNavProps}} --}}
-
-
-
-
diff --git a/imports/plugins/core/layout/client/templates/layout/header/header.js b/imports/plugins/core/layout/client/templates/layout/header/header.js
deleted file mode 100644
index 25c14747219..00000000000
--- a/imports/plugins/core/layout/client/templates/layout/header/header.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Template } from "meteor/templating";
-import { $ } from "meteor/jquery";
-
-/**
- * layoutHeader events
- */
-Template.layoutHeader.events({
- "click .navbar-accounts .dropdown-toggle": function () {
- return setTimeout(function () {
- return $("#login-email").focus();
- }, 100);
- },
- "click .header-tag, click .navbar-brand": function () {
- return $(".dashboard-navbar-packages ul li").removeClass("active");
- }
-});
-
-Template.layoutHeader.helpers({
- coreNavProps() {
- const instance = Template.instance();
- return {
- onMenuButtonClick() {
- instance.toggleMenuCallback();
- }
- };
- }
-});
diff --git a/imports/plugins/core/orders/client/containers/invoiceContainer.js b/imports/plugins/core/orders/client/containers/invoiceContainer.js
index 781ab06c5fc..c5f65613565 100644
--- a/imports/plugins/core/orders/client/containers/invoiceContainer.js
+++ b/imports/plugins/core/orders/client/containers/invoiceContainer.js
@@ -679,6 +679,7 @@ const composer = (props, onData) => {
return tax.lineNumber === item._id;
});
item.taxDetail = taxDetail;
+ return item;
}
});
} else {
diff --git a/imports/plugins/core/orders/register.js b/imports/plugins/core/orders/register.js
index 5017d0ffb9a..51c10ef7bb9 100644
--- a/imports/plugins/core/orders/register.js
+++ b/imports/plugins/core/orders/register.js
@@ -45,8 +45,8 @@ Reaction.registerPackage({
enabled: true,
structure: {
template: "orders",
- layoutHeader: "layoutHeader",
- layoutFooter: "layoutFooter",
+ layoutHeader: "NavBar",
+ layoutFooter: "Footer",
notFound: "notFound",
dashboardHeader: "dashboardHeader",
dashboardHeaderControls: "orderListFilters",
@@ -60,8 +60,8 @@ Reaction.registerPackage({
enabled: true,
structure: {
template: "completedPDFLayout",
- layoutHeader: "layoutHeader",
- layoutFooter: "layoutFooter"
+ layoutHeader: "NavBar",
+ layoutFooter: "Footer"
}
}, {
layout: "coreLayout",
diff --git a/imports/plugins/core/ui-navbar/client/components/navbar.js b/imports/plugins/core/ui-navbar/client/components/navbar.js
index d14dc07ea71..f841c7abfb1 100644
--- a/imports/plugins/core/ui-navbar/client/components/navbar.js
+++ b/imports/plugins/core/ui-navbar/client/components/navbar.js
@@ -24,8 +24,23 @@ class NavBar extends Component {
brandMedia: PropTypes.object,
hasProperPermission: PropTypes.bool,
searchEnabled: PropTypes.bool,
- shop: PropTypes.object
- }
+ shop: PropTypes.object,
+ visibility: PropTypes.object.isRequired
+ };
+
+ static defaultProps = {
+ visibility: {
+ hamburger: true,
+ brand: true,
+ tags: true,
+ search: true,
+ notifications: true,
+ languages: true,
+ currency: true,
+ mainDropdown: true,
+ cartContainer: true
+ }
+ };
state = {
navBarVisible: false
@@ -135,15 +150,15 @@ class NavBar extends Component {
render() {
return (
- {this.renderHamburgerButton()}
- {this.renderBrand()}
- {this.renderTagNav()}
- {this.renderSearchButton()}
- {this.renderNotificationIcon()}
- {this.renderLanguage()}
- {this.renderCurrency()}
- {this.renderMainDropdown()}
- {this.renderCartContainerAndPanel()}
+ {this.props.visibility.hamburger && this.renderHamburgerButton()}
+ {this.props.visibility.brand && this.renderBrand()}
+ {this.props.visibility.tags && this.renderTagNav()}
+ {this.props.visibility.search && this.renderSearchButton()}
+ {this.props.visibility.notifications && this.renderNotificationIcon()}
+ {this.props.visibility.languages && this.renderLanguage()}
+ {this.props.visibility.currency && this.renderCurrency()}
+ {this.props.visibility.mainDropdown && this.renderMainDropdown()}
+ {this.props.visibility.cartContainer && this.renderCartContainerAndPanel()}
);
}
diff --git a/imports/plugins/core/ui-navbar/client/components/navbarCheckout.js b/imports/plugins/core/ui-navbar/client/components/navbarCheckout.js
new file mode 100644
index 00000000000..7e1c5f3af23
--- /dev/null
+++ b/imports/plugins/core/ui-navbar/client/components/navbarCheckout.js
@@ -0,0 +1,23 @@
+import React from "react";
+import NavBar from "../components/navbar";
+
+const NavBarCheckout = (props, context) => {
+ const visibility = {
+ hamburger: false,
+ brand: true,
+ tags: false,
+ search: false,
+ notifications: false,
+ languages: false,
+ currency: false,
+ mainDropdown: false,
+ cartContainer: false
+ };
+ const newProps = {
+ ...props,
+ visibility
+ };
+ return React.createElement(NavBar, newProps, context);
+};
+
+export default NavBarCheckout;
diff --git a/imports/plugins/core/ui-navbar/client/containers/navbar.js b/imports/plugins/core/ui-navbar/client/containers/navbar.js
index b46ba3429e5..31dcd53a6c8 100644
--- a/imports/plugins/core/ui-navbar/client/containers/navbar.js
+++ b/imports/plugins/core/ui-navbar/client/containers/navbar.js
@@ -4,7 +4,7 @@ import { Reaction } from "/client/api";
import NavBar from "../components/navbar";
import { Media, Shops } from "/lib/collections";
-function composer(props, onData) {
+export function composer(props, onData) {
const shop = Shops.findOne(Reaction.getShopId());
const searchPackage = Reaction.Apps({ provides: "ui-search" });
let searchEnabled;
diff --git a/imports/plugins/core/ui-navbar/client/containers/navbarCheckout.js b/imports/plugins/core/ui-navbar/client/containers/navbarCheckout.js
new file mode 100644
index 00000000000..7a59f0475d4
--- /dev/null
+++ b/imports/plugins/core/ui-navbar/client/containers/navbarCheckout.js
@@ -0,0 +1,7 @@
+import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components";
+import NavBarCheckout from "../components/navbarCheckout";
+import { composer } from "./navbar";
+
+registerComponent("NavBarCheckout", NavBarCheckout, composeWithTracker(composer));
+
+export default composeWithTracker(composer)(NavBarCheckout);
diff --git a/imports/plugins/core/ui-navbar/client/index.js b/imports/plugins/core/ui-navbar/client/index.js
index 54221bda4b4..639529c622a 100644
--- a/imports/plugins/core/ui-navbar/client/index.js
+++ b/imports/plugins/core/ui-navbar/client/index.js
@@ -1,2 +1,3 @@
export { default as Brand } from "./components/brand";
export { default as Navbar } from "./containers/navbar";
+export { default as NavBarCheckout } from "./containers/navbarCheckout";
diff --git a/imports/plugins/core/ui/client/components/translation/translation.js b/imports/plugins/core/ui/client/components/translation/translation.js
index d05050f3d6d..507db1f0185 100644
--- a/imports/plugins/core/ui/client/components/translation/translation.js
+++ b/imports/plugins/core/ui/client/components/translation/translation.js
@@ -8,6 +8,22 @@ const Translation = ({ i18nKey, defaultValue, ...rest }) => {
const key = i18nKey || camelCase(defaultValue);
const translation = i18next.t(key, { defaultValue });
+ // i18next returns 'undefined' if the default value happens to be the key for a set of definitions
+ // ```
+ // "components": {
+ // "componentDef": "Translated Component Def"
+ // }
+ // ```
+ // In this case, a request for i18next.t("components", "defaultValue") will return undefined
+ // but i18next.t("components.componentDef", "defaultValue") will return correctly
+ //
+ // This checks to see if translation is undefined and returns the default value instead
+ if (typeof translation === "undefined") {
+ return (
+ {defaultValue}
+ );
+ }
+
return (
{translation}
);
diff --git a/imports/plugins/core/ui/register.js b/imports/plugins/core/ui/register.js
index a9894763690..671f87de283 100644
--- a/imports/plugins/core/ui/register.js
+++ b/imports/plugins/core/ui/register.js
@@ -30,7 +30,7 @@ Reaction.registerPackage({
enabled: true,
structure: {
template: "uiDashboard",
- layoutHeader: "layoutHeader",
+ layoutHeader: "NavBar",
layoutFooter: "",
notFound: "notFound",
dashboardHeader: "dashboardHeader",
diff --git a/imports/plugins/core/versions/server/migrations/18_use_react_for_header_and_footer_layout.js b/imports/plugins/core/versions/server/migrations/18_use_react_for_header_and_footer_layout.js
new file mode 100644
index 00000000000..d50bc8c31bc
--- /dev/null
+++ b/imports/plugins/core/versions/server/migrations/18_use_react_for_header_and_footer_layout.js
@@ -0,0 +1,94 @@
+import { Migrations } from "meteor/percolate:migrations";
+import { Shops, Packages } from "/lib/collections";
+
+const pkgs = [
+ "reaction-accounts",
+ "reaction-checkout",
+ "reaction-dashboard",
+ "reaction-email",
+ "reaction-orders",
+ "reaction-ui",
+ "reaction-product-variant",
+ "product-detail-simple",
+ "core"
+];
+
+const query = {
+ name: { $in: pkgs },
+ layout: { $type: 3 } // docs with layouts set
+};
+
+Migrations.add({
+ version: 18,
+ up() {
+ const packages = Packages.find(query).fetch();
+ packages.forEach(updateHandler(
+ Packages
+ ));
+
+ const shops = Shops.find().fetch();
+ shops.forEach(updateHandler(
+ Shops
+ ));
+ },
+ down() {
+ const packages = Packages.find(query).fetch();
+ packages.forEach(downgradeHandler(
+ Packages
+ ));
+
+ const shops = Shops.find().fetch();
+ shops.forEach(downgradeHandler(
+ Shops
+ ));
+ }
+});
+
+function updateHandler(collection) {
+ return function (doc) {
+ let changed = false;
+ for (const layout of doc.layout) {
+ if (layout.structure && layout.structure.template === "cartCheckout") {
+ layout.structure.layoutHeader = "NavBarCheckout";
+ changed = true;
+ } else if (layout.structure && layout.structure.layoutHeader === "layoutHeader") {
+ layout.structure.layoutHeader = "NavBar";
+ changed = true;
+ }
+ if (layout.structure && layout.structure.layoutFooter === "layoutFooter") {
+ layout.structure.layoutFooter = "Footer";
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ collection.update(
+ { _id: doc._id }, {
+ $set: { layout: doc.layout }
+ });
+ }
+ };
+}
+
+function downgradeHandler(collection) {
+ return function (doc) {
+ let changed = false;
+ for (const layout of doc.layout) {
+ if (layout.structure && layout.structure.layoutHeader === "NavBar") {
+ layout.structure.layoutHeader = "layoutHeader";
+ changed = true;
+ }
+ if (layout.structure && layout.structure.layoutFooter === "Footer") {
+ layout.structure.layoutFooter = "layoutFooter";
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ collection.update(
+ { _id: doc._id }, {
+ $set: { layout: doc.layout }
+ });
+ }
+ };
+}
diff --git a/imports/plugins/core/versions/server/migrations/index.js b/imports/plugins/core/versions/server/migrations/index.js
index 5298528e16a..de38509089a 100644
--- a/imports/plugins/core/versions/server/migrations/index.js
+++ b/imports/plugins/core/versions/server/migrations/index.js
@@ -15,3 +15,4 @@ import "./14_rebuild_order_search_collection";
import "./15_update_shipping_status_to_workflow";
import "./16_update_billing_paymentMethod";
import "./17_set_shop_uols";
+import "./18_use_react_for_header_and_footer_layout";
diff --git a/imports/plugins/included/analytics/register.js b/imports/plugins/included/analytics/register.js
index ed649b71426..c7e835dc632 100644
--- a/imports/plugins/included/analytics/register.js
+++ b/imports/plugins/included/analytics/register.js
@@ -39,6 +39,7 @@ Reaction.registerPackage({
route: "/dashboard/analytics/settings",
provides: ["settings"],
container: "dashboard",
- template: "reactionAnalyticsSettings"
+ template: "reactionAnalyticsSettings",
+ showForShopTypes: ["primary"]
}]
});
diff --git a/imports/plugins/included/default-theme/client/styles/dashboard/console.less b/imports/plugins/included/default-theme/client/styles/dashboard/console.less
index 8b0213fd649..dc5510955e4 100644
--- a/imports/plugins/included/default-theme/client/styles/dashboard/console.less
+++ b/imports/plugins/included/default-theme/client/styles/dashboard/console.less
@@ -3,11 +3,11 @@
}
html.rtl .rui.admin.action-view {
- border-right: 1px solid @black10;
+ border-right: 1px solid @black20;
}
html:not(.rtl) .rui.admin.action-view {
- border-left: 1px solid @black10;
+ border-left: 1px solid @black20;
}
.rui.admin.action-view {
@@ -155,7 +155,7 @@ html:not(.rtl) .rui.admin.action-view {
}
.admin-controls-content .panel-default > .panel-heading {
- background-color: darken(@white, 2%);
+ background-color: lighten(@black05, 2%);
&.validation {
background-color: @rui-danger-bg;
@@ -201,6 +201,8 @@ body.admin-vertical .admin-controls {
// not getting the form-control class
.admin-controls-content label {
font-weight: lighter;
+ color: @rui-default-text;
+ letter-spacing: .02rem;
}
@@ -236,6 +238,8 @@ body.admin-vertical .admin-controls {
.rui.admin.action-view-pane .header .title {
margin: 0;
+ letter-spacing: 0.03rem;
+ color: @rui-default-text;
}
.rui.admin.action-view-detail {
diff --git a/imports/plugins/included/default-theme/client/styles/panels.less b/imports/plugins/included/default-theme/client/styles/panels.less
index 1ce8a5b2d32..62300b399b2 100644
--- a/imports/plugins/included/default-theme/client/styles/panels.less
+++ b/imports/plugins/included/default-theme/client/styles/panels.less
@@ -33,6 +33,11 @@
border: @border-thin;
}
+.panel-title {
+ color: @rui-default-text;
+ letter-spacing: .03rem;
+}
+
label.panel-title {
font-weight: normal;
}
diff --git a/imports/plugins/included/default-theme/client/styles/products/productDetail.less b/imports/plugins/included/default-theme/client/styles/products/productDetail.less
index 564d04b1480..aa698233bc5 100644
--- a/imports/plugins/included/default-theme/client/styles/products/productDetail.less
+++ b/imports/plugins/included/default-theme/client/styles/products/productDetail.less
@@ -37,6 +37,8 @@
font-size: @product-page-title-font-size;
font-weight: @headings-font-weight-h2;
color: @headings-color-h2;
+ letter-spacing: .01em;
+ margin-top: 0px;
}
// Product edit fields
@@ -132,6 +134,7 @@
.pdp.price-range {
font-weight: bold;
font-size: @product-price-font-size;
+ letter-spacing: 0rem;
overflow: visible;
display:inline-block;
diff --git a/imports/plugins/included/default-theme/client/styles/select.less b/imports/plugins/included/default-theme/client/styles/select.less
index e8fb4dd1518..328ed7d45a7 100644
--- a/imports/plugins/included/default-theme/client/styles/select.less
+++ b/imports/plugins/included/default-theme/client/styles/select.less
@@ -25,3 +25,7 @@
width: 44px;
pointer-events: none;
}
+
+.rui.multiselect .Select-control {
+ background-color: lighten(@black05, 2%);
+}
diff --git a/imports/plugins/included/default-theme/client/styles/textfield.less b/imports/plugins/included/default-theme/client/styles/textfield.less
index ca466058d9f..b27960a183b 100644
--- a/imports/plugins/included/default-theme/client/styles/textfield.less
+++ b/imports/plugins/included/default-theme/client/styles/textfield.less
@@ -45,6 +45,11 @@
border-radius: @input-border-radius;
}
+.action-view-body .rui.textfield input, .action-view-body .rui.textfield textarea {
+ background-color: @black02;
+ color: @rui-default-text;
+}
+
.rui.textfield.has-error input {
border-color: @rui-danger;
}
diff --git a/imports/plugins/included/default-theme/client/styles/variables.less b/imports/plugins/included/default-theme/client/styles/variables.less
index 64adae7e99d..ab427797400 100644
--- a/imports/plugins/included/default-theme/client/styles/variables.less
+++ b/imports/plugins/included/default-theme/client/styles/variables.less
@@ -1,6 +1,6 @@
//== Google Fonts
//*requires browser-policy to be set in client
-@import url(//fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,700,900);
+@import url(//fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,500,700,800);
//
@@ -67,7 +67,7 @@
@black: #000;
@black02: darken(#fff, 2%);
-@black05: darken(#fff, 5%);
+@black05: darken(#fff, 4%);
@black10: darken(#fff, 10%);
@black15: darken(#fff, 15%);
@black20: darken(#fff, 20%);
@@ -159,7 +159,7 @@
//** Global text color on ``.
@text-color: @gray-dark;
//** Global border-color and border-width.
-@border-color: @black10;
+@border-color: @black20;
@border-width: 1px;
@border-thin: 1px solid @black10;
diff --git a/imports/plugins/included/product-detail-simple/register.js b/imports/plugins/included/product-detail-simple/register.js
index 9c2dcb3cbb5..e9d49ae27b4 100644
--- a/imports/plugins/included/product-detail-simple/register.js
+++ b/imports/plugins/included/product-detail-simple/register.js
@@ -19,7 +19,7 @@ Reaction.registerPackage({
enabled: true,
structure: {
template: "productDetailSimple",
- layoutHeader: "layoutHeader",
+ layoutHeader: "NavBar",
layoutFooter: "",
notFound: "productNotFound",
dashboardHeader: "productDetailSimpleToolbar",
diff --git a/imports/plugins/included/product-variant/components/gridItemControls.js b/imports/plugins/included/product-variant/components/gridItemControls.js
index 8c166e4239a..a14e8e1ac90 100644
--- a/imports/plugins/included/product-variant/components/gridItemControls.js
+++ b/imports/plugins/included/product-variant/components/gridItemControls.js
@@ -7,6 +7,7 @@ class GridItemControls extends Component {
checked: PropTypes.func,
hasChanges: PropTypes.func,
hasCreateProductPermission: PropTypes.func,
+ isValid: PropTypes.bool,
product: PropTypes.object
}
@@ -21,7 +22,17 @@ class GridItemControls extends Component {
}
renderVisibilityButton() {
- if (this.props.hasChanges()) {
+ if (!this.props.product.__isValid && this.props.hasChanges()) {
+ return (
+
+
+
+ );
+ } else if (this.props.hasChanges()) {
return (
(
class GridItemControlsContainer extends Component {
@@ -13,12 +16,20 @@ const wrapComponent = (Comp) => (
product: PropTypes.object
}
- constructor() {
- super();
+ constructor(props) {
+ super(props);
+
+ this.validation = new Validation(ProductVariant);
+ this.validProduct = props.product;
this.hasCreateProductPermission = this.hasCreateProductPermission.bind(this);
this.hasChanges = this.hasChanges.bind(this);
this.checked = this.checked.bind(this);
+ this.checkValidation = this.checkValidation.bind(this);
+ }
+
+ componentWillMount() {
+ this.checkValidation();
}
hasCreateProductPermission = () => {
@@ -29,6 +40,16 @@ const wrapComponent = (Comp) => (
return this.props.product.__draft ? true : false;
}
+ // This method checks validation of the variants of the all the products on the Products grid to
+ // check whether all required fields have been submitted before publishing
+ checkValidation = () => {
+ // this returns an array with a single object
+ const variants = ReactionProduct.getVariants(this.props.product._id).map((variant) => this.validation.validate(variant));
+ this.setState({
+ validProduct: Object.assign({}, this.props.product, { __isValid: variants[0].isValid })
+ });
+ }
+
checked = () => {
return this.props.isSelected === true;
}
@@ -36,7 +57,7 @@ const wrapComponent = (Comp) => (
render() {
return (
= Session.get("productScrollLimit");
const stateProducts = sortedProducts;
diff --git a/imports/plugins/included/product-variant/register.js b/imports/plugins/included/product-variant/register.js
index 243adb9f46b..6bd45c0aa5d 100644
--- a/imports/plugins/included/product-variant/register.js
+++ b/imports/plugins/included/product-variant/register.js
@@ -32,7 +32,7 @@ Reaction.registerPackage({
enabled: true,
structure: {
template: "productDetail",
- layoutHeader: "layoutHeader",
+ layoutHeader: "NavBar",
layoutFooter: "",
notFound: "productNotFound",
dashboardHeader: "productDetailSimpleToolbar",
@@ -48,7 +48,7 @@ Reaction.registerPackage({
enabled: true,
structure: {
template: "products",
- layoutHeader: "layoutHeader",
+ layoutHeader: "NavBar",
layoutFooter: "",
notFound: "productNotFound",
dashboardHeader: "gridPublishControls",
diff --git a/imports/plugins/included/sms/register.js b/imports/plugins/included/sms/register.js
index f11c5553f6f..cf6af14042e 100644
--- a/imports/plugins/included/sms/register.js
+++ b/imports/plugins/included/sms/register.js
@@ -22,6 +22,7 @@ Reaction.registerPackage({
route: "/dashboard/sms",
provides: ["settings"],
container: "dashboard",
- template: "smsSettings"
+ template: "smsSettings",
+ showForShopTypes: ["primary"]
}]
});
diff --git a/imports/plugins/included/taxes-taxcloud/client/components/index.js b/imports/plugins/included/taxes-taxcloud/client/components/index.js
new file mode 100644
index 00000000000..0b20db9f7c7
--- /dev/null
+++ b/imports/plugins/included/taxes-taxcloud/client/components/index.js
@@ -0,0 +1 @@
+export { default as TaxCloudSettingsForm } from "./taxCloudSettingsForm";
diff --git a/imports/plugins/included/taxes-taxcloud/client/components/taxCloudSettingsForm.js b/imports/plugins/included/taxes-taxcloud/client/components/taxCloudSettingsForm.js
new file mode 100644
index 00000000000..61279561e5a
--- /dev/null
+++ b/imports/plugins/included/taxes-taxcloud/client/components/taxCloudSettingsForm.js
@@ -0,0 +1,66 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Form } from "/imports/plugins/core/ui/client/components";
+import { Components } from "@reactioncommerce/reaction-components";
+import { TaxCloudPackageConfig } from "../../lib/collections/schemas";
+
+/**
+ * @file TaxCloudSettingsForm is a React Component used to change TaxCloud
+ * settings.
+ * @module TaxCloudSettingsForm
+ */
+
+/**
+ * @method TaxCloudSettingsForm
+ * @summary renders a form for updating TaxCloud settings.
+ * @param {Object} props - some data for use by this component.
+ * @property {Function} handleSubmit - a function for saving new TaxCloud settings.
+ * @property {Array} hiddenFields - the fields (of the TaxCloud Package) to hide from the form.
+ * @property {Object} settings - the value of the "settings" field in the TaxCloud Package.
+ * @property {Object} shownFields - info about the fields the form is to show.
+ * @since 1.5.2
+ * @return {Node} - a React node containing the TaxCloud settings form.
+ */
+const TaxCloudSettingsForm = (props) => {
+ const { handleSubmit, hiddenFields, settings, shownFields } = props;
+ return (
+
+ {!settings.taxcloud.apiLoginId &&
+
+ }
+
+
+ );
+};
+
+/**
+ * @name TaxCloudSettingsForm propTypes
+ * @type {propTypes}
+ * @param {Object} props - React PropTypes
+ * @property {Function} handleSubmit - a function that saves new TaxCloud settings.
+ * @property {Array} hiddenFields - an array of the TaxCloud Package's fields
+ * to hide from the settings form.
+ * @property {Object} settings - the value of the "settings" field in the TaxCloud Package.
+ * @property {Object} shownFields - info about the fields of the TaxCloud Package
+ * that the settings form will allow users to change.
+ * @return {Array} React propTypes
+ */
+TaxCloudSettingsForm.propTypes = {
+ handleSubmit: PropTypes.func,
+ hiddenFields: PropTypes.arrayOf(PropTypes.string),
+ settings: PropTypes.object,
+ shownFields: PropTypes.object
+};
+
+export default TaxCloudSettingsForm;
diff --git a/imports/plugins/included/taxes-taxcloud/client/containers/index.js b/imports/plugins/included/taxes-taxcloud/client/containers/index.js
new file mode 100644
index 00000000000..49b21108c07
--- /dev/null
+++ b/imports/plugins/included/taxes-taxcloud/client/containers/index.js
@@ -0,0 +1 @@
+export { default as TaxCloudSettingsFormContainer } from "./taxCloudSettingsFormContainer";
diff --git a/imports/plugins/included/taxes-taxcloud/client/containers/taxCloudSettingsFormContainer.js b/imports/plugins/included/taxes-taxcloud/client/containers/taxCloudSettingsFormContainer.js
new file mode 100644
index 00000000000..20180678769
--- /dev/null
+++ b/imports/plugins/included/taxes-taxcloud/client/containers/taxCloudSettingsFormContainer.js
@@ -0,0 +1,67 @@
+import { compose, withProps } from "recompose";
+import { composeWithTracker, registerComponent } from "@reactioncommerce/reaction-components";
+import { Meteor } from "meteor/meteor";
+import { Reaction, i18next } from "/client/api";
+import { TaxCloudPackageConfig } from "../../lib/collections/schemas";
+import { TaxCloudSettingsForm } from "../components";
+
+/**
+ * @file This is a container for TaxCloudSettingsForm.
+ * @module taxCloudSettingsFormContainer
+ */
+
+const handlers = {
+ /**
+ * handleSubmit
+ * @method
+ * @summary event handler for when new TaxCloud settings are submitted.
+ * @param {Object} event - event info.
+ * @param {Object} changedInfo - info about the new TaxCloud settings.
+ * @param {String} targetField - where to save the new settings in the TaxCloud Package.
+ * @since 1.5.2
+ * @return {null} - returns nothing
+ */
+ handleSubmit(event, changedInfo, targetField) {
+ if (!changedInfo.isValid) {
+ return;
+ }
+ Meteor.call("package/update", "taxes-taxcloud", targetField, changedInfo.doc.settings.taxcloud, (error) => {
+ if (error) {
+ Alerts.toast(
+ i18next.t("admin.update.updateFailed", { defaultValue: "Failed to update TaxCloud settings." }),
+ "error"
+ );
+ return;
+ }
+ Alerts.toast(
+ i18next.t("admin.update.updateSucceeded", { defaultValue: "TaxCloud settings updated." }),
+ "success"
+ );
+ });
+ }
+};
+
+const composer = (props, onData) => {
+ const shownFields = {
+ ["settings.taxcloud.apiKey"]: TaxCloudPackageConfig._schema["settings.taxcloud.apiKey"],
+ ["settings.taxcloud.apiLoginId"]: TaxCloudPackageConfig._schema["settings.taxcloud.apiLoginId"]
+ };
+ const hiddenFields = [
+ "settings.taxcloud.enabled",
+ "settings.taxcloud.refreshPeriod",
+ "settings.taxcloud.taxCodeUrl"
+ ];
+
+ const shopId = Reaction.getShopId();
+ const packageSub = Meteor.subscribe("Packages", shopId);
+ if (packageSub.ready()) {
+ const packageData = Reaction.getPackageSettings("taxes-taxcloud");
+ onData(null, { settings: packageData.settings, shownFields, hiddenFields });
+ }
+};
+
+registerComponent("TaxCloudSettingsForm", TaxCloudSettingsForm, [
+ withProps(handlers), composeWithTracker(composer)
+]);
+
+export default compose(withProps(handlers), composeWithTracker(composer))(TaxCloudSettingsForm);
diff --git a/imports/plugins/included/taxes-taxcloud/client/settings/taxcloud.html b/imports/plugins/included/taxes-taxcloud/client/settings/taxcloud.html
index 8f7a3c8b232..c6de090e8b3 100644
--- a/imports/plugins/included/taxes-taxcloud/client/settings/taxcloud.html
+++ b/imports/plugins/included/taxes-taxcloud/client/settings/taxcloud.html
@@ -1,21 +1,5 @@
-
- {{#unless packageData.settings.taxcloud.apiLoginId}}
-
-
Add API Login ID to enable
-
TaxCloud
-
- {{/unless}}
-
- {{#autoForm collection=Collections.Packages schema=packageConfigSchema doc=packageData type="update" id="taxcloud-update-form"}}
-
- {{> afQuickField name='settings.taxcloud.apiLoginId' class='form-control'}}
- {{> afQuickField name='settings.taxcloud.apiKey' class='form-control'}}
-
-
- {{> shopSettingsSubmitButton}}
- {{/autoForm}}
+ {{> React taxCloudForm}}
diff --git a/imports/plugins/included/taxes-taxcloud/client/settings/taxcloud.js b/imports/plugins/included/taxes-taxcloud/client/settings/taxcloud.js
index ab58042b49b..6faa9111db8 100644
--- a/imports/plugins/included/taxes-taxcloud/client/settings/taxcloud.js
+++ b/imports/plugins/included/taxes-taxcloud/client/settings/taxcloud.js
@@ -1,47 +1,15 @@
-import { Meteor } from "meteor/meteor";
import { Template } from "meteor/templating";
-import { AutoForm } from "meteor/aldeed:autoform";
-import { Packages } from "/lib/collections";
-import { TaxCodes } from "/imports/plugins/core/taxes/lib/collections";
-import { Reaction, i18next } from "/client/api";
-import { TaxCloudPackageConfig } from "../../lib/collections/schemas";
+import { TaxCloudSettingsFormContainer } from "../containers";
Template.taxCloudSettings.helpers({
- packageConfigSchema() {
- return TaxCloudPackageConfig;
- },
- packageData() {
- return Packages.findOne({
- name: "taxes-taxcloud",
- shopId: Reaction.getShopId()
- });
- }
-});
-
-
-AutoForm.hooks({
- "taxcloud-update-form": {
- onSuccess: function () {
- if (!TaxCodes.findOne({ taxCodeProvider: "taxes-taxcloud" })) {
- Meteor.call("taxcloud/getTaxCodes", (err, res) => {
- if (res && Array.isArray(res)) {
- Alerts.toast(i18next.t("admin.taxSettings.shopTaxMethodsSaved"),
- "success");
- res.forEach((code) => {
- Meteor.call("taxes/insertTaxCodes", Reaction.getShopId(), code,
- "taxes-taxcloud");
- });
- }
- });
- } else {
- Alerts.toast(i18next.t("admin.taxSettings.shopTaxMethodsAlreadySaved"),
- "success");
- }
- },
- onError: function (operation, error) {
- return Alerts.toast(
- `${i18next.t("admin.taxSettings.shopTaxMethodsFailed")} ${error}`,
- "error");
- }
+ /**
+ * @method taxCloudForm
+ * @summary returns a component for updating the TaxCloud settings for
+ * this app.
+ * @since 1.5.2
+ * @return {Object} - an object containing the component to render.
+ */
+ taxCloudForm() {
+ return { component: TaxCloudSettingsFormContainer };
}
});
diff --git a/imports/plugins/included/taxes-taxcloud/server/i18n/en.json b/imports/plugins/included/taxes-taxcloud/server/i18n/en.json
index 5d27574389f..32cec532462 100644
--- a/imports/plugins/included/taxes-taxcloud/server/i18n/en.json
+++ b/imports/plugins/included/taxes-taxcloud/server/i18n/en.json
@@ -19,6 +19,10 @@
"taxcloudSettingsLabel": "TaxCloud",
"taxcloudCredentials": "Add credentials to enable",
"taxcloudGetCredentialsURL": "Get them here"
+ },
+ "update": {
+ "updateSucceeded": "TaxCloud settings updated.",
+ "updateFailed": "Failed to update TaxCloud settings."
}
}
}
diff --git a/lib/api/products.js b/lib/api/products.js
index ea79f44f744..e17d637f34a 100644
--- a/lib/api/products.js
+++ b/lib/api/products.js
@@ -166,7 +166,7 @@ ReactionProduct.setProduct = (currentProductId, currentVariantId) => {
if (product) {
// set the default variant
// as the default.
- if (!variantId) {
+ if (!variantId || !variantIsSelected(variantId)) {
const variants = ReactionProduct.getTopVariants(productId);
variantId = Array.isArray(variants) && variants.length &&
variants[0]._id || null;
diff --git a/lib/collections/schemas/cart.js b/lib/collections/schemas/cart.js
index a78431dcdd8..798155c2566 100644
--- a/lib/collections/schemas/cart.js
+++ b/lib/collections/schemas/cart.js
@@ -179,8 +179,7 @@ export const Cart = new SimpleSchema({
},
billing: {
type: [Payment],
- optional: true,
- blackbox: true
+ optional: true
},
tax: {
type: Number,
diff --git a/lib/collections/schemas/payments.js b/lib/collections/schemas/payments.js
index d7271fdc0ec..e7853945ae0 100644
--- a/lib/collections/schemas/payments.js
+++ b/lib/collections/schemas/payments.js
@@ -122,8 +122,7 @@ export const PaymentMethod = new SimpleSchema({
$setOnInsert: new Date
};
}
- },
- denyUpdate: true
+ }
},
updatedAt: {
type: Date,
@@ -247,7 +246,8 @@ export const Payment = new SimpleSchema({
},
paymentMethod: {
type: PaymentMethod,
- optional: true
+ optional: true,
+ blackbox: true
},
invoice: {
type: Invoice,
diff --git a/lib/collections/transform/cartOrder.js b/lib/collections/transform/cartOrder.js
index 942350ebafa..d7995e12669 100644
--- a/lib/collections/transform/cartOrder.js
+++ b/lib/collections/transform/cartOrder.js
@@ -20,6 +20,11 @@ function getSummary(items, prop, prop2, shopId) {
if (shopId) {
if (shopId === item.shopId) {
// if we're looking for a specific shop's items and this item does match
+ // if prop2 is an empty array
+ if (!prop2.length) {
+ return sum + (prop.length === 1 ? item[prop[0]] :
+ item[prop[0]][prop[1]]);
+ }
return sum + item[prop[0]] * (prop2.length === 1 ? item[prop2[0]] :
item[prop2[0]][prop2[1]]);
}
@@ -82,11 +87,16 @@ export const cartOrderTransform = {
* @returns {{Object}} - Total price of shipping, broken down by shop
*/
getShippingTotalByShop() {
- const billingObject = {};
- for (const billingItem of this.billing) {
- billingObject[billingItem.shopId] = billingItem.invoice.shipping;
- }
- return billingObject;
+ return this.shipping.reduce((uniqueShopShippingTotals, shippingRec) => {
+ if (!uniqueShopShippingTotals[shippingRec.shopId]) {
+ const rate = getSummary(this.shipping, ["shipmentMethod", "rate"], [], shippingRec.shopId);
+ const handling = getSummary(this.shipping, ["shipmentMethod", "handling"], [], shippingRec.shopId);
+ const shipping = handling + rate || 0;
+ uniqueShopShippingTotals[shippingRec.shopId] = accounting.toFixed(shipping, 2);
+ return uniqueShopShippingTotals;
+ }
+ return uniqueShopShippingTotals;
+ }, {});
},
/**
* @summary Return the total price of goods on an order
@@ -200,7 +210,7 @@ export const cartOrderTransform = {
getTotalByShop() {
const subtotals = this.getSubtotalByShop();
const taxes = this.getTaxesByShop();
- const shipping = parseFloat(this.getShippingTotal());
+ const shippingTotalByShop = this.getShippingTotalByShop();
// no discounts right now because that will need to support multi-shop
// TODO: Build out shop-by-shop discounts and permit discounts to reduce application fee
@@ -215,6 +225,7 @@ export const cartOrderTransform = {
}
const shopTaxes = parseFloat(taxes[shopId]);
+ const shipping = parseFloat(shippingTotalByShop[shopId]);
const shopTotal = shopSubtotal + shopTaxes + shipping;
shopTotals[shopId] = accounting.toFixed(shopTotal, 2);
}
diff --git a/package.json b/package.json
index 7a0069a6d57..3d772fd0f82 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "reaction",
"description": "Reaction is a modern reactive, real-time event driven ecommerce platform.",
- "version": "1.5.8",
+ "version": "1.5.9",
"main": "main.js",
"directories": {
"test": "tests"
diff --git a/server/imports/fixtures/index.js b/server/imports/fixtures/index.js
index dd15cc483cc..0a9ddf7c193 100644
--- a/server/imports/fixtures/index.js
+++ b/server/imports/fixtures/index.js
@@ -2,7 +2,7 @@ import accounts from "./accounts";
import cart from "./cart";
import orders from "./orders";
import products from "./products";
-import examplePaymentMethod from "./packages";
+import { examplePaymentMethod, examplePackage } from "./packages";
// import shipping from "./shipping";
import shops from "./shops";
import users from "./users";
@@ -12,6 +12,7 @@ export default function () {
shops();
users();
examplePaymentMethod();
+ examplePackage();
accounts();
products();
cart();
diff --git a/server/imports/fixtures/packages.js b/server/imports/fixtures/packages.js
index d83a40c0f6a..cc58ed37310 100644
--- a/server/imports/fixtures/packages.js
+++ b/server/imports/fixtures/packages.js
@@ -10,7 +10,7 @@ export const getPkgData = (pkgName) => {
};
-export default function () {
+export function examplePaymentMethod() {
const examplePaymentMethodPackage = {
name: "example-paymentmethod",
icon: "fa fa-credit-card-alt",
@@ -34,3 +34,21 @@ export default function () {
Factory.define("examplePaymentPackage", Packages, Object.assign({}, examplePaymentMethodPackage));
}
+/**
+ * @method examplePackage
+ * @summary creates a new fixture based off of the Packages collection.
+ * @since 1.5.5
+ * @return {undefined} - returns nothing.
+ */
+export function examplePackage() {
+ const examplePkg = {
+ name: "example-package",
+ settings: {
+ enabled: false,
+ apiUrl: "http://example.com/api"
+ },
+ shopId: "random-shop-101"
+ };
+
+ Factory.define("examplePackage", Packages, examplePkg);
+}
diff --git a/server/methods/core/cart.js b/server/methods/core/cart.js
index b8d7267b63e..b6b138c4568 100644
--- a/server/methods/core/cart.js
+++ b/server/methods/core/cart.js
@@ -108,7 +108,7 @@ function removeShippingAddresses(cart) {
* @file Methods for Cart - Use these methods by running `Meteor.call()`
* @example Meteor.call("cart/createCart", this.userId, sessionId)
* @namespace Methods/Cart
-*/
+ */
Meteor.methods({
/**
@@ -1020,6 +1020,7 @@ Meteor.methods({
const cartId = cart._id;
const cartShipping = cart.getShippingTotal();
+ const cartShippingByShop = cart.getShippingTotalByShop();
const cartSubTotal = cart.getSubTotal();
const cartSubtotalByShop = cart.getSubtotalByShop();
const cartTaxes = cart.getTaxTotal();
@@ -1046,7 +1047,7 @@ Meteor.methods({
paymentMethods.forEach((paymentMethod) => {
const shopId = paymentMethod.shopId;
const invoice = {
- shipping: parseFloat(cartShipping),
+ shipping: parseFloat(cartShippingByShop[shopId]),
subtotal: parseFloat(cartSubtotalByShop[shopId]),
taxes: parseFloat(cartTaxesByShop[shopId]),
discounts: parseFloat(cartDiscounts),
diff --git a/server/methods/core/packages-update.app-test.js b/server/methods/core/packages-update.app-test.js
new file mode 100644
index 00000000000..346bec03eb6
--- /dev/null
+++ b/server/methods/core/packages-update.app-test.js
@@ -0,0 +1,70 @@
+import { Meteor } from "meteor/meteor";
+import { Match } from "meteor/check";
+import { Factory } from "meteor/dburles:factory";
+import { expect } from "meteor/practicalmeteor:chai";
+import { sinon } from "meteor/practicalmeteor:sinon";
+import { Packages } from "/lib/collections";
+import { Reaction } from "/server/api";
+
+describe("Update Package", function () {
+ let sandbox;
+
+ beforeEach(function () {
+ sandbox = sinon.sandbox.create();
+ });
+
+ afterEach(function () {
+ sandbox.restore();
+ });
+
+ describe("package/update", function () {
+ it("should throw an 'Access Denied' error for non-admins", function (done) {
+ const pkgUpdateSpy = sandbox.spy(Packages, "update");
+ const examplePackage = Factory.create("examplePackage");
+
+ function updatePackage() {
+ return Meteor.call("package/update", examplePackage.name, "settings", {});
+ }
+ expect(updatePackage).to.throw(Meteor.Error, /Access Denied/);
+ expect(pkgUpdateSpy).to.not.have.been.called;
+
+ return done();
+ });
+
+ it("should throw an error when supplied with an argument of the wrong type", function (done) {
+ const pkgUpdateSpy = sandbox.spy(Packages, "update");
+ sandbox.stub(Reaction, "getShopId", () => "randomId");
+ sandbox.stub(Reaction, "hasPermission", () => true);
+
+ function updatePackage(packageName, field, value) {
+ return Meteor.call("package/update", packageName, field, value);
+ }
+ expect(() => updatePackage([], "someField", { foo: "bar" })).to.throw(Match.Error, /Match error: Expected string, got object/);
+ expect(() => updatePackage("somePackage", [], { foo: "bar" })).to.throw(Match.Error, /Match error: Expected string, got object/);
+ expect(() => updatePackage("somePackage", "someField", "")).to.throw(Match.Error, /Match error: Expected object, got string/);
+ expect(pkgUpdateSpy).to.not.have.been.called;
+
+ return done();
+ });
+
+ it("should be able to update any Package", function (done) {
+ const packageUpdateSpy = sandbox.spy(Packages, "update");
+ const oldPackage = Factory.create("examplePackage");
+
+ sandbox.stub(Reaction, "getShopId", () => oldPackage.shopId);
+ sandbox.stub(Reaction, "hasPermission", () => true);
+ const packageName = oldPackage.name;
+ const newValues = {
+ enabled: true,
+ apiUrl: "http://foo-bar.com/api/v1"
+ };
+ Meteor.call("package/update", packageName, "settings", newValues);
+ expect(packageUpdateSpy).to.have.been.called;
+ const updatedPackage = Packages.findOne({ name: packageName });
+ expect(oldPackage.settings.enabled).to.not.equal(updatedPackage.settings.enabled);
+ expect(oldPackage.settings.apiUrl).to.not.equal(updatedPackage.settings.apiUrl);
+
+ return done();
+ });
+ });
+});
diff --git a/server/methods/core/packages.js b/server/methods/core/packages.js
new file mode 100644
index 00000000000..e24f08ec4b8
--- /dev/null
+++ b/server/methods/core/packages.js
@@ -0,0 +1,44 @@
+import { Meteor } from "meteor/meteor";
+import { check } from "meteor/check";
+import { Packages } from "/lib/collections";
+import { Reaction } from "/server/api";
+
+/**
+ * @method updatePackage
+ * @summary updates the data stored for a certain Package.
+ * @param {String} packageName - the name of the Package to update.
+ * @param {String} field - the part of the Package's data that is to
+ * be updated.
+ * @param {Object} value - the new data that's to be stored for the said
+ * Package.
+ * @since 1.5.1
+ * @return {Object} - returns an object with info about the update operation.
+ */
+export function updatePackage(packageName, field, value) {
+ check(packageName, String);
+ check(field, String);
+ check(value, Object);
+
+ const userId = Meteor.userId();
+ const shopId = Reaction.getShopId();
+ if (!Reaction.hasPermission([packageName], userId, shopId)) {
+ throw new Meteor.Error("access-denied", `Access Denied. You don't have permissions for the ${packageName} package.`);
+ }
+
+ const updateResult = Packages.update({
+ name: packageName,
+ shopId: shopId
+ }, {
+ $set: {
+ [field]: value
+ }
+ });
+ if (updateResult !== 1) {
+ throw new Meteor.Error("server-error", `An error occurred while updating the package ${packageName}.`);
+ }
+ return updateResult;
+}
+
+Meteor.methods({
+ "package/update": updatePackage
+});
diff --git a/server/startup/registry/core.js b/server/startup/registry/core.js
index 03d087cd2b6..0f7e59497da 100644
--- a/server/startup/registry/core.js
+++ b/server/startup/registry/core.js
@@ -34,8 +34,8 @@ export default function () {
enabled: true,
structure: {
template: "products",
- layoutHeader: "layoutHeader",
- layoutFooter: "layoutFooter",
+ layoutHeader: "NavBar",
+ layoutFooter: "Footer",
notFound: "productNotFound",
dashboardControls: "dashboardControls",
adminControlsFooter: "adminControlsFooter"
@@ -47,8 +47,8 @@ export default function () {
enabled: true,
structure: {
template: "unauthorized",
- layoutHeader: "layoutHeader",
- layoutFooter: "layoutFooter"
+ layoutHeader: "NavBar",
+ layoutFooter: "Footer"
}
}]
});