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 @@ - 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 @@ - 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 @@ - 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 @@ - 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 && +
+ + TaxCloud +
+ } +
+
+ ); +}; + +/** + * @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 @@ 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" } }] });