diff --git a/client/modules/core/helpers/apps.js b/client/modules/core/helpers/apps.js index 94f34af1c73..54da9f6d3f4 100644 --- a/client/modules/core/helpers/apps.js +++ b/client/modules/core/helpers/apps.js @@ -4,6 +4,7 @@ import { Meteor } from "meteor/meteor"; import { Roles } from "meteor/alanning:roles"; import { Reaction } from "/client/api"; import { Packages, Shops } from "/lib/collections"; +import { Registry } from "/lib/collections/schemas/registry"; /** @@ -152,7 +153,24 @@ export function Apps(optionHash) { return false; } - return _.isMatch(item, itemFilter); + const filterKeys = Object.keys(itemFilter); + // Loop through all keys in the itemFilter + // each filter item should match exactly with the property in the registry or + // should be included in the array if that property is an array + return filterKeys.every((property) => { + // Check to see if the schema for this property is an array + // if so, we want to make sure that this item is included in the array + if (Array.isArray(Registry._schema[property].type())) { + // Check to see if the registry entry is an array. + // Legacy registry entries could exist that use a string even when the schema requires an array. + if (Array.isArray(item[property])) { + return item[property].includes(itemFilter[property]); + } + } + + // If it's not an array, the filter should match exactly + return item[property] === itemFilter[property]; + }); }); for (const registry of matchingRegistry) { diff --git a/client/modules/core/main.js b/client/modules/core/main.js index cc8b8c83366..31e21d60b90 100644 --- a/client/modules/core/main.js +++ b/client/modules/core/main.js @@ -47,46 +47,42 @@ export default { Tracker.autorun(() => { let shop; if (this.Subscriptions.PrimaryShop.ready()) { - // if we've already set the primaryShopId, carry on. - // otherwise we need to define it. - if (!this.primaryShopId) { - // There should only ever be one "primary" shop - shop = Shops.findOne({ - shopType: "primary" - }); - - if (shop) { - this.primaryShopId = shop._id; - this.primaryShopName = shop.name; - - // We'll initialize locale and currency for the primary shop unless - // marketplace settings exist and merchantLocale is set to true - if (this.marketplace.merchantLocale !== true) { - // initialize local client Countries collection - if (!Countries.findOne()) { - createCountryCollection(shop.locales.countries); - } + // There should only ever be one "primary" shop + shop = Shops.findOne({ + shopType: "primary" + }); + + if (shop) { + this.primaryShopId = shop._id; + this.primaryShopName = shop.name; + + // We'll initialize locale and currency for the primary shop unless + // marketplace settings exist and merchantLocale is set to true + if (this.marketplace.merchantLocale !== true) { + // initialize local client Countries collection + if (!Countries.findOne()) { + createCountryCollection(shop.locales.countries); + } - const locale = this.Locale.get() || {}; + const locale = this.Locale.get() || {}; - // fix for https://github.com/reactioncommerce/reaction/issues/248 - // we need to keep an eye for rates changes - if (typeof locale.locale === "object" && + // fix for https://github.com/reactioncommerce/reaction/issues/248 + // we need to keep an eye for rates changes + if (typeof locale.locale === "object" && typeof locale.currency === "object" && typeof locale.locale.currency === "string") { - const localeCurrency = locale.locale.currency.split(",")[0]; - if (typeof shop.currencies[localeCurrency] === "object") { - if (typeof shop.currencies[localeCurrency].rate === "number") { - locale.currency.rate = shop.currencies[localeCurrency].rate; - localeDep.changed(); - } + const localeCurrency = locale.locale.currency.split(",")[0]; + if (typeof shop.currencies[localeCurrency] === "object") { + if (typeof shop.currencies[localeCurrency].rate === "number") { + locale.currency.rate = shop.currencies[localeCurrency].rate; + localeDep.changed(); } } - // we are looking for a shopCurrency changes here - if (typeof locale.shopCurrency === "object") { - locale.shopCurrency = shop.currencies[shop.currency]; - localeDep.changed(); - } + } + // we are looking for a shopCurrency changes here + if (typeof locale.shopCurrency === "object") { + locale.shopCurrency = shop.currencies[shop.currency]; + localeDep.changed(); } } } @@ -199,7 +195,10 @@ export default { } else { permissions = checkPermissions; } - // if the user has admin, owner permissions we'll always check if those roles are enough + // if the user has owner permissions we'll always check if those roles are enough + // By adding the "owner" role to the permissions list, we are making hasPermission always return + // true for "owners". This gives owners global access. + // TODO: Review this way of granting global access for owners permissions.push("owner"); permissions = _.uniq(permissions); @@ -736,7 +735,7 @@ export default { // valid application if (reactionApp) { const settingsData = _.find(reactionApp.registry, function (item) { - return item.provides === provides && item.template === template; + return item.provides && item.provides.includes(provides) && item.template === template; }); return settingsData; } diff --git a/client/modules/i18n/main.js b/client/modules/i18n/main.js index 0953519a31d..cb6720b042f 100644 --- a/client/modules/i18n/main.js +++ b/client/modules/i18n/main.js @@ -119,7 +119,7 @@ Meteor.startup(() => { locale.language = getBrowserLanguage(); moment.locale(locale.language); // flag in case the locale currency isn't enabled - locale.currencyEnabled = locale.currencyEnabled; + locale.currencyEnabled = locale.currency.enabled; const user = Meteor.user(); if (user && user.profile && user.profile.currency) { localStorage.setItem("currency", user.profile.currency); diff --git a/imports/plugins/core/accounts/client/helpers/accountsHelper.js b/imports/plugins/core/accounts/client/helpers/accountsHelper.js index fa2e75ee4d1..2aa24744094 100644 --- a/imports/plugins/core/accounts/client/helpers/accountsHelper.js +++ b/imports/plugins/core/accounts/client/helpers/accountsHelper.js @@ -111,6 +111,7 @@ export function groupPermissions(packages) { shopId: pkg.shopId, permission: registryItem.name || pkg.name + "/" + registryItem.template, icon: registryItem.icon, + // TODO: Rethink naming convention for permissions list label: registryItem.label || registryItem.provides || registryItem.route }); } diff --git a/imports/plugins/core/accounts/register.js b/imports/plugins/core/accounts/register.js index 34de811a562..4a0334c9d7f 100644 --- a/imports/plugins/core/accounts/register.js +++ b/imports/plugins/core/accounts/register.js @@ -9,7 +9,7 @@ Reaction.registerPackage({ registry: [{ route: "/dashboard/accounts", name: "accounts", - provides: "dashboard", + provides: ["dashboard"], label: "Accounts", description: "Manage how members sign into your shop.", icon: "fa fa-users", @@ -26,7 +26,7 @@ Reaction.registerPackage({ }, { label: "Account Settings", icon: "fa fa-sign-in", - provides: "settings", + provides: ["settings"], route: "/dashboard/account/settings", container: "accounts", workflow: "coreAccountsWorkflow", @@ -35,7 +35,7 @@ Reaction.registerPackage({ route: "/dashboard/accounts", name: "dashboard/accounts", workflow: "coreAccountsWorkflow", - provides: "shortcut", + provides: ["shortcut"], label: "Accounts", icon: "fa fa-users", priority: 1, @@ -47,7 +47,7 @@ Reaction.registerPackage({ name: "account/profile", label: "Profile", icon: "fa fa-user", - provides: "userAccountDropdown" + provides: ["userAccountDropdown"] }], layout: [{ layout: "coreLayout", diff --git a/imports/plugins/core/catalog/register.js b/imports/plugins/core/catalog/register.js index 9bf19f8d529..a7e877bc664 100644 --- a/imports/plugins/core/catalog/register.js +++ b/imports/plugins/core/catalog/register.js @@ -10,7 +10,7 @@ Reaction.registerPackage({ }, registry: [ { - provides: "dashboard", + provides: ["dashboard"], label: "Catalog", description: "Product catalog", icon: "fa fa-book", @@ -20,7 +20,7 @@ Reaction.registerPackage({ label: "Catalog Settings", icon: "fa fa-book", name: "catalog/settings", - provides: "settings", + provides: ["settings"], template: "catalogSettings" } ] diff --git a/imports/plugins/core/checkout/client/helpers/cart.js b/imports/plugins/core/checkout/client/helpers/cart.js index 8b9c8556c85..bb14649bc49 100644 --- a/imports/plugins/core/checkout/client/helpers/cart.js +++ b/imports/plugins/core/checkout/client/helpers/cart.js @@ -13,8 +13,8 @@ import { Template } from "meteor/templating"; * cartCount, cartSubTotal, cartShipping, cartTaxes, cartTotal * are calculated by a transformation on the collection * and are available to use in template as cart.xxx - * in template: {{cart.cartCount}} - * in code: Cart.findOne().cartTotal() + * in template: {{cart.getCount}} + * in code: Cart.findOne().getTotal() * @return {Object} returns inventory helpers */ Template.registerHelper("cart", function () { diff --git a/imports/plugins/core/components/lib/hoc.js b/imports/plugins/core/components/lib/hoc.js index b8aab6a92f3..49a48a6fa12 100644 --- a/imports/plugins/core/components/lib/hoc.js +++ b/imports/plugins/core/components/lib/hoc.js @@ -40,14 +40,19 @@ export function withCurrentAccount(component) { return null; } - // shoppers should always be guests - const isGuest = Roles.userIsInRole(user, "guest", shopId); - // but if a user has never logged in then they are anonymous - const isAnonymous = Roles.userIsInRole(user, "anonymous", shopId); - - const account = Accounts.findOne(user._id); - - onData(null, { currentAccount: isGuest && !isAnonymous && account }); + const accSub = Meteor.subscribe("Accounts", user._id); + if (accSub.ready()) { + // shoppers should always be guests + const isGuest = Reaction.hasPermission("guest"); + // but if a user has never logged in then they are anonymous + const isAnonymous = Roles.userIsInRole(user, "anonymous", shopId); + // this check for "anonymous" uses userIsInRole instead of hasPermission because hasPermission + // always return `true` when logged in as the owner. + // But in this case, the anonymous check should be false when a user is logged in + const account = Accounts.findOne(user._id); + + onData(null, { currentAccount: isGuest && !isAnonymous && account }); + } })(component); } diff --git a/imports/plugins/core/connectors/register.js b/imports/plugins/core/connectors/register.js index e9de82be1db..0eaddd16e5a 100644 --- a/imports/plugins/core/connectors/register.js +++ b/imports/plugins/core/connectors/register.js @@ -9,7 +9,7 @@ Reaction.registerPackage({ name: "Connectors" }, registry: [{ - provides: "dashboard", + provides: ["dashboard"], route: "/dashboard/connectors", name: "connectors", label: "Connectors", @@ -19,7 +19,7 @@ Reaction.registerPackage({ container: "core", workflow: "coreDashboardWorkflow" }, { - provides: "settings", + provides: ["settings"], name: "settings/connectors", label: "Connectors", description: "Configure connectors", diff --git a/imports/plugins/core/dashboard/client/components/actionView.js b/imports/plugins/core/dashboard/client/components/actionView.js index e02ded1c093..381dcb738a8 100644 --- a/imports/plugins/core/dashboard/client/components/actionView.js +++ b/imports/plugins/core/dashboard/client/components/actionView.js @@ -19,7 +19,11 @@ import { getComponent } from "@reactioncommerce/reaction-components"; const getStyles = (props) => { let viewSize = 400; const actionView = props.actionView || {}; - const isBigView = actionView.provides === "dashboard" || (actionView.provides === "shortcut" && actionView.container === "dashboard"); + const provides = actionView.provides || []; + // legacy provides could be a string, is an array since 1.5.0, check for either. + // prototype.includes has the fortunate side affect of checking string equality as well as array inclusion. + const isBigView = provides.includes("dashboard") || + (provides.includes("shortcut") && actionView.container === "dashboard"); if (isBigView) { viewSize = "90vw"; @@ -277,8 +281,9 @@ class ActionView extends Component { get actionViewIsLargeSize() { const { meta } = this.props.actionView; const dashboardSize = meta && meta.actionView && meta.actionView.dashboardSize || "sm"; + const includesDashboard = this.props.actionView.provides && this.props.actionView.provides.includes("dashboard"); - return this.props.actionView.provides === "dashboard" || dashboardSize !== "sm"; + return includesDashboard || dashboardSize !== "sm"; } get showOverlay() { diff --git a/imports/plugins/core/dashboard/client/components/shortcutBar.js b/imports/plugins/core/dashboard/client/components/shortcutBar.js index 64ca5e2e5f3..bbcc4ad85e4 100644 --- a/imports/plugins/core/dashboard/client/components/shortcutBar.js +++ b/imports/plugins/core/dashboard/client/components/shortcutBar.js @@ -18,9 +18,6 @@ class ShortcutBar extends Component { ); diff --git a/imports/plugins/core/dashboard/client/components/toolbar.js b/imports/plugins/core/dashboard/client/components/toolbar.js index 3d306bba231..c61d7a0dbaa 100644 --- a/imports/plugins/core/dashboard/client/components/toolbar.js +++ b/imports/plugins/core/dashboard/client/components/toolbar.js @@ -131,7 +131,7 @@ class PublishControls extends Component { }); }} > - + ); diff --git a/imports/plugins/core/dashboard/client/containers/toolbarContainer.js b/imports/plugins/core/dashboard/client/containers/toolbarContainer.js index 66dde17524b..470bb17e220 100644 --- a/imports/plugins/core/dashboard/client/containers/toolbarContainer.js +++ b/imports/plugins/core/dashboard/client/containers/toolbarContainer.js @@ -83,8 +83,7 @@ function composer(props, onData) { for (const item of registryItems) { if (Reaction.hasPermission(item.route, Meteor.userId())) { let icon = item.icon; - - if (!item.icon && item.provides === "settings") { + if (!item.icon && item.provides && item.provides.includes("settings")) { icon = "gear"; } diff --git a/imports/plugins/core/dashboard/client/templates/settings/settings.js b/imports/plugins/core/dashboard/client/templates/settings/settings.js index fb2e14f4ccf..45f3cbcdb08 100644 --- a/imports/plugins/core/dashboard/client/templates/settings/settings.js +++ b/imports/plugins/core/dashboard/client/templates/settings/settings.js @@ -37,7 +37,7 @@ Template.settingsHeader.helpers({ if (reactionApp) { const settingsData = _.find(reactionApp.registry, function (item) { - return item.route === Reaction.Router.getRouteName() && item.provides === "settings"; + return item.route === Reaction.Router.getRouteName() && item.provides && item.provides.includes("settings"); }); return settingsData; diff --git a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html index 40a50baa4f5..873f9e3516e 100644 --- a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html +++ b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html @@ -39,6 +39,7 @@
{{#autoForm collection=Collections.Shops doc=shop id="shopEditForm" type="update"}} {{> afQuickField name='name' placeholder="Shop Name"}} + {{> afQuickField name='slug' placeholder="Shop Slug"}} {{> afQuickField name='emails.0.address' label="Email" placeholder="Primary Contact Email"}} {{> afQuickField name='description' placeholder="Description"}} {{> afQuickField name='keywords' placeholder="Keywords"}} diff --git a/imports/plugins/core/dashboard/register.js b/imports/plugins/core/dashboard/register.js index 0dd9a8a4a6b..f98027c1557 100644 --- a/imports/plugins/core/dashboard/register.js +++ b/imports/plugins/core/dashboard/register.js @@ -9,7 +9,7 @@ Reaction.registerPackage({ name: "Dashboard" }, registry: [{ - provides: "dashboard", + provides: ["dashboard"], workflow: "coreDashboardWorkflow", name: "dashboardPackages", label: "Core", @@ -25,7 +25,7 @@ Reaction.registerPackage({ route: "/dashboard", name: "dashboard", workflow: "coreDashboardWorkflow", - provides: "shortcut", + provides: ["shortcut"], label: "Dashboard", template: "dashboardPackages", icon: "icon-reaction-logo", @@ -40,7 +40,7 @@ Reaction.registerPackage({ name: "shopSettings", label: "Shop Settings", icon: "fa fa-th", - provides: "settings", + provides: ["settings"], container: "dashboard" }], layout: [{ diff --git a/imports/plugins/core/email/register.js b/imports/plugins/core/email/register.js index b0ae656655f..efbfb36c0b1 100644 --- a/imports/plugins/core/email/register.js +++ b/imports/plugins/core/email/register.js @@ -13,7 +13,7 @@ Reaction.registerPackage({ description: "Email settings", icon: "fa fa-envelope-o", name: "email/settings", - provides: "settings", + provides: ["settings"], workflow: "coreEmailWorkflow", template: "emailSettings", meta: { diff --git a/imports/plugins/core/i18n/register.js b/imports/plugins/core/i18n/register.js index 941cfb7467b..6e208a5f815 100644 --- a/imports/plugins/core/i18n/register.js +++ b/imports/plugins/core/i18n/register.js @@ -9,14 +9,14 @@ Reaction.registerPackage({ name: "i18n" }, registry: [{ - provides: "dashboard", + provides: ["dashboard"], label: "i18n", description: "Internationalization utilities", icon: "fa fa-language", priority: 1, container: "utilities" }, { - provides: "settings", + provides: ["settings"], template: "i18nSettings", label: "Localization and i18n", icon: "fa fa-language", diff --git a/imports/plugins/core/layout/client/templates/layout/admin/admin.js b/imports/plugins/core/layout/client/templates/layout/admin/admin.js index e9c77d5310b..808a50fa050 100644 --- a/imports/plugins/core/layout/client/templates/layout/admin/admin.js +++ b/imports/plugins/core/layout/client/templates/layout/admin/admin.js @@ -98,7 +98,7 @@ Template.coreAdminLayout.helpers({ if (Reaction.hasPermission(item.route, Meteor.userId())) { let icon = item.icon; - if (!item.icon && item.provides === "settings") { + if (!item.icon && item.provides && item.provides.includes("settings")) { icon = "gear"; } @@ -147,7 +147,7 @@ Template.coreAdminLayout.helpers({ if (reactionApp) { const settingsData = _.find(reactionApp.registry, function (item) { - return item.route === Reaction.Router.getRouteName() && item.provides === "settings"; + return item.route === Reaction.Router.getRouteName() && item.provides && item.provides.includes("settings"); }); return settingsData; diff --git a/imports/plugins/core/layout/register.js b/imports/plugins/core/layout/register.js index fd752cd0292..3942e83a646 100644 --- a/imports/plugins/core/layout/register.js +++ b/imports/plugins/core/layout/register.js @@ -9,7 +9,7 @@ Reaction.registerPackage({ name: "Layout" }, registry: [{ - provides: "dashboard", + provides: ["dashboard"], label: "Layout", description: "Layout utilities", icon: "fa fa-object-group", diff --git a/imports/plugins/core/orders/client/components/orderActions.js b/imports/plugins/core/orders/client/components/orderActions.js index 9375bb905c0..5ebb7f56c77 100644 --- a/imports/plugins/core/orders/client/components/orderActions.js +++ b/imports/plugins/core/orders/client/components/orderActions.js @@ -1,112 +1,177 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import classnames from "classnames/dedupe"; -import { Button, DropDownMenu, MenuItem } from "@reactioncommerce/reaction-ui"; +import { i18next } from "/client/api"; +import { Components } from "@reactioncommerce/reaction-components"; class OrderActions extends Component { static propTypes = { - className: PropTypes.string, + classNamesContainer: PropTypes.object, clearFilter: PropTypes.func, filter: PropTypes.string, + filterDates: PropTypes.func, handleMenuClick: PropTypes.func } + constructor(props) { + super(props); + this.state = { + startDate: null, + endDate: null + }; + } + + handleDatesChange = (startDate, endDate) => { + this.setState({ + startDate, + endDate + }); + this.props.filterDates(startDate, endDate); + } + buttonElement() { return ( - + ); } - render() { - const filterClassName = classnames({ - "order-filter-button": true - }, this.props.className); + dateLabel() { + if (this.state.startDate && this.state.endDate) { + return ( + {this.state.startDate.format("MM/DD")} - {this.state.endDate.format("MM/DD")} + ); + } + return ( + + ); + } - const labelClassName = classnames({ - "order-filter-name": true - }, this.props.className); + + render() { + const attachmentDirection = i18next.dir() === "rtl" ? "left" : "right"; return (
- {this.props.filter} + + {this.props.filter} +
- - + - - - - - - - +
- -
- This Week + + {this.dateLabel()} +
- - {this.buttonElement()} + + + +
- Shipping Status + + +
- + {this.buttonElement()}
-
diff --git a/imports/plugins/core/orders/client/components/orderDashboard.js b/imports/plugins/core/orders/client/components/orderDashboard.js index 61cd99f1e70..238c31275bb 100644 --- a/imports/plugins/core/orders/client/components/orderDashboard.js +++ b/imports/plugins/core/orders/client/components/orderDashboard.js @@ -7,10 +7,11 @@ import OrderSearch from "../components/orderSearch"; class OrderDashboard extends Component { static propTypes = { - className: PropTypes.string, + classNamesContainer: PropTypes.object, clearFilter: PropTypes.func, displayMedia: PropTypes.func, filter: PropTypes.string, + filterDates: PropTypes.func, handleBulkPaymentCapture: PropTypes.func, handleChange: PropTypes.func, handleClick: PropTypes.func, @@ -71,7 +72,8 @@ class OrderDashboard extends Component { handleMenuClick={this.props.handleMenuClick} clearFilter={this.props.clearFilter} filter={this.props.filter} - className={this.props.className} + classNamesContainer={this.props.classNamesContainer} + filterDates={this.props.filterDates} /> {this.state.orders.length ?
diff --git a/imports/plugins/core/orders/client/components/orderSummary.js b/imports/plugins/core/orders/client/components/orderSummary.js index 6eee3a77b50..dbf48dd1a0e 100644 --- a/imports/plugins/core/orders/client/components/orderSummary.js +++ b/imports/plugins/core/orders/client/components/orderSummary.js @@ -2,6 +2,7 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import moment from "moment"; import { Badge, ClickToCopy } from "@reactioncommerce/reaction-ui"; +import { getOrderRiskBadge, getOrderRiskStatus } from "../helpers"; class OrderSummary extends Component { static propTypes = { @@ -43,6 +44,7 @@ class OrderSummary extends Component { render() { const { dateFormat, tracking, order, profileShippingAddress, printableLabels } = this.props; + const orderRisk = getOrderRiskStatus(order); return (
@@ -62,6 +64,15 @@ class OrderSummary extends Component { label={order.workflow.status} status={this.badgeStatus()} /> + {orderRisk && + + }
diff --git a/imports/plugins/core/orders/client/components/orderTable.js b/imports/plugins/core/orders/client/components/orderTable.js index 4d55f643f4c..a5520b8cc7c 100644 --- a/imports/plugins/core/orders/client/components/orderTable.js +++ b/imports/plugins/core/orders/client/components/orderTable.js @@ -3,33 +3,34 @@ import PropTypes from "prop-types"; import Avatar from "react-avatar"; import moment from "moment"; import classnames from "classnames/dedupe"; -import { Reaction } from "/client/api"; +import { Reaction, i18next } from "/client/api"; import { Orders } from "/lib/collections"; import { Badge, ClickToCopy, Icon, Translation, Checkbox, Loading, SortableTable } from "@reactioncommerce/reaction-ui"; import OrderTableColumn from "./orderTableColumn"; import OrderBulkActionsBar from "./orderBulkActionsBar"; import { formatPriceString } from "/client/api"; import ProductImage from "./productImage"; +import { getOrderRiskBadge, getOrderRiskStatus } from "../helpers"; const classNames = { colClassNames: { - "Name": "order-table-column-name", - "Email": "order-table-column-email", - "Date": "order-table-column-date hidden-xs hidden-sm", - "ID": "order-table-column-id hidden-xs hidden-sm", - "Total": "order-table-column-total", - "Shipping": "order-table-column-shipping hidden-xs hidden-sm", - "Status": "order-table-column-status", + "name": "order-table-column-name", + "email": "order-table-column-email", + "date": "order-table-column-date hidden-xs hidden-sm", + "id": "order-table-column-id hidden-xs hidden-sm", + "total": "order-table-column-total", + "shipping": "order-table-column-shipping hidden-xs hidden-sm", + "status": "order-table-column-status", "": "order-table-column-control" }, headerClassNames: { - "Name": "order-table-header-name", - "Email": "order-table-header-email", - "Date": "order-table-header-date hidden-xs hidden-sm", - "ID": "order-table-header-id hidden-xs hidden-sm", - "Total": "order-table-header-total", - "Shipping": "order-table-header-shipping hidden-xs hidden-sm", - "Status": "order-table-header-status", + "name": "order-table-header-name", + "email": "order-table-header-email", + "date": "order-table-header-date hidden-xs hidden-sm", + "id": "order-table-header-id hidden-xs hidden-sm", + "total": "order-table-header-total", + "shipping": "order-table-header-shipping hidden-xs hidden-sm", + "status": "order-table-header-status", "": "order-table-header-control" } }; @@ -88,10 +89,11 @@ class OrderTable extends Component { "btn": true, "btn-success": startWorkflow }); + const chevronDirection = i18next.dir() === "rtl" ? "left" : "right"; return ( ); } @@ -144,6 +146,8 @@ class OrderTable extends Component { renderShipmentInfo(order) { const emailAddress = order.email || ; + const orderRisk = getOrderRiskStatus(order); + return (
@@ -155,6 +159,13 @@ class OrderTable extends Component { className="rui-order-avatar" /> {order.shipping[0].address.fullName} | {emailAddress} + {orderRisk && + + }
(
this.props.selectAllOrders(this.props.orders, this.props.multipleSelect)} /> - {columnName} + + +
); - } - - if (columnName === "") { + } else if (columnName === "") { + columnNameLabel = ""; resizable = false; sortable = false; + } else { + columnNameLabel = i18next.t(`admin.table.headers.${columnName}`); } const columnMeta = { accessor: filteredFields[columnName], - Header: colHeader ? colHeader : columnName, + Header: colHeader ? colHeader : columnNameLabel, headerClassName: classNames.headerClassNames[columnName], className: classNames.colClassNames[columnName], resizable: resizable, diff --git a/imports/plugins/core/orders/client/components/orderTableColumn.js b/imports/plugins/core/orders/client/components/orderTableColumn.js index a1317be8bf3..6e2bb06ff80 100644 --- a/imports/plugins/core/orders/client/components/orderTableColumn.js +++ b/imports/plugins/core/orders/client/components/orderTableColumn.js @@ -2,9 +2,10 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import classnames from "classnames/dedupe"; import moment from "moment"; -import { formatPriceString } from "/client/api"; +import { formatPriceString, i18next } from "/client/api"; import Avatar from "react-avatar"; import { Badge, ClickToCopy, Icon, RolloverCheckbox, Checkbox } from "@reactioncommerce/reaction-ui"; +import { getOrderRiskBadge, getOrderRiskStatus } from "../helpers"; class OrderTableColumn extends Component { static propTypes = { @@ -48,12 +49,22 @@ class OrderTableColumn extends Component { render() { const columnAccessor = this.props.row.column.id; + const orderRisk = getOrderRiskStatus(this.props.row.original); if (columnAccessor === "shipping[0].address.fullName") { return (
{this.renderCheckboxOnSelect(this.props.row)} - {this.props.row.value} + + {this.props.row.value} + {orderRisk && + + } +
); } @@ -102,12 +113,14 @@ class OrderTableColumn extends Component { } if (columnAccessor === "workflow.status") { return ( - +
+ +
); } if (columnAccessor === "") { @@ -117,10 +130,11 @@ class OrderTableColumn extends Component { "btn": true, "btn-success": startWorkflow }); + const chevronDirection = i18next.dir() === "rtl" ? "left" : "right"; return ( ); } diff --git a/imports/plugins/core/orders/client/containers/invoiceContainer.js b/imports/plugins/core/orders/client/containers/invoiceContainer.js index fe27871388f..f2b1ba65518 100644 --- a/imports/plugins/core/orders/client/containers/invoiceContainer.js +++ b/imports/plugins/core/orders/client/containers/invoiceContainer.js @@ -7,6 +7,7 @@ import { i18next, Logger, Reaction, formatPriceString } from "/client/api"; import { Media, Packages } from "/lib/collections"; import { composeWithTracker, registerComponent } from "@reactioncommerce/reaction-components"; import Invoice from "../components/invoice.js"; +import { getOrderRiskStatus, getOrderRiskBadge } from "../helpers"; class InvoiceContainer extends Component { static propTypes = { @@ -242,7 +243,11 @@ class InvoiceContainer extends Component { }); const order = this.state.order; - capturePayments(order); + capturePayments(order, () => { + this.setState({ + isCapturing: false + }); + }); } handleCancelPayment = (event) => { @@ -567,17 +572,47 @@ function approvePayment(order) { /** * @method capturePayments * @summary helper method to capture payments - * @param {Object} order - object representing an order + * @param {object} order - object representing an order + * @param {function} onCancel - called on clicking cancel in alert dialog * @return {null} null */ -function capturePayments(order) { - Meteor.call("orders/capturePayments", order._id); - if (order.workflow.status === "new") { - Meteor.call("workflow/pushOrderWorkflow", "coreOrderWorkflow", "processing", order); - - Reaction.Router.setQueryParams({ - filter: "processing", - _id: order._id +function capturePayments(order, onCancel) { + const capture = () => { + Meteor.call("orders/capturePayments", order._id); + if (order.workflow.status === "new") { + Meteor.call("workflow/pushOrderWorkflow", "coreOrderWorkflow", "processing", order); + + Reaction.Router.setQueryParams({ + filter: "processing", + _id: order._id + }); + } + }; + + // before capturing, check if there's a payment risk on order; alert admin before capture + if (getOrderRiskStatus(order)) { + alertDialog() + .then(capture) + .catch(onCancel); + } else { + capture(); + } + + function alertDialog() { + let alertType = "warning"; + const riskBadge = getOrderRiskBadge(getOrderRiskStatus(order)); + // use red alert color for high risk level + if (riskBadge === "danger") { + alertType = "error"; + } + + return Alerts.alert({ + title: i18next.t("admin.orderRisk.riskCapture"), + text: i18next.t("admin.orderRisk.riskCaptureWarn"), + type: alertType, + showCancelButton: true, + cancelButtonText: i18next.t("admin.settings.cancel"), + confirmButtonText: i18next.t("admin.settings.continue") }); } } diff --git a/imports/plugins/core/orders/client/containers/orderDashboardContainer.js b/imports/plugins/core/orders/client/containers/orderDashboardContainer.js index 0ffc1beb966..43464c5a0eb 100644 --- a/imports/plugins/core/orders/client/containers/orderDashboardContainer.js +++ b/imports/plugins/core/orders/client/containers/orderDashboardContainer.js @@ -102,7 +102,7 @@ class OrderDashboardContainer extends Component { ready: false, query: {}, filter: i18next.t("order.filter.status"), - className: "", + classNamesContainer: {}, searchQuery: "" }; @@ -154,6 +154,27 @@ class OrderDashboardContainer extends Component { } } + filterDates = (startDate, endDate) => { + const query = this.state.query; + + if (startDate && endDate) { + // generate time for start and end of day + const formattedEndDate = endDate.endOf("day"); + const formattedStartDate = startDate.startOf("day"); + + query.createdAt = { + $gte: new Date(formattedStartDate.toISOString()), + $lte: new Date(formattedEndDate.toISOString()) + }; + this.setState({ + classNamesContainer: Object.assign({}, this.state.classNamesContainer, { + date: "active" + }), + query + }); + } + } + handleMenuClick = (event, value) => { let query = OrderHelper.makeQuery(value); // ensure other fields (e.g ids) on query are kept @@ -161,7 +182,9 @@ class OrderDashboardContainer extends Component { this.setState({ query, filter: i18next.t(`order.filter.${value}`), - className: "active" + classNamesContainer: Object.assign({}, this.state.classNamesContainer, { + status: "active" + }) }); } @@ -191,19 +214,39 @@ class OrderDashboardContainer extends Component { }); } - clearFilter = () => { + clearFilter = (filterString) => { + let query; + let filter = this.state.filter; const oldQuery = this.state.query; - const query = OrderHelper.makeQuery(""); + const classNamesContainer = this.state.classNamesContainer; + + if (filterString === "status") { + query = OrderHelper.makeQuery(""); + filter = i18next.t("order.filter.status"); + + // if there was another filter active reattach it to the query object + if (oldQuery.createdAt) { + query.createdAt = oldQuery.createdAt; + } + } else if (filterString === "date") { + query = OrderHelper.makeQuery(filter.toLowerCase()); + } + // id is set by the searchbar in setupTracker. Here we check if there's a current value in it before // the filter was cleared. If there is, we attach it back to the queryObj if (oldQuery._id) { query._id = oldQuery._id; } + // clear filter for a particular search + const filterClassName = Object.assign({}, classNamesContainer, { + [filterString]: "" + }); + this.setState({ query, - filter: i18next.t("order.filter.status"), - className: "" + filter, + classNamesContainer: filterClassName }); } @@ -799,8 +842,9 @@ class OrderDashboardContainer extends Component { searchQuery={this.state.searchQuery} query={this.state.query} filter={this.state.filter} - className={this.state.className} + classNamesContainer={this.state.classNamesContainer} clearFilter={this.clearFilter} + filterDates={this.filterDates} handleClick={this.handleClick} displayMedia={this.handleDisplayMedia} selectedItems={this.state.selectedItems} diff --git a/imports/plugins/core/orders/client/helpers/index.js b/imports/plugins/core/orders/client/helpers/index.js new file mode 100644 index 00000000000..112930a3340 --- /dev/null +++ b/imports/plugins/core/orders/client/helpers/index.js @@ -0,0 +1,51 @@ +import { Reaction } from "/client/api"; + +/* + * @method getOrderRiskBadge + * @private + * @summary Selects appropriate color badge (e.g danger, warning) value based on risklevel + * @param {string} riskLevel - risk level value on the paymentMethod + * @return {string} label - style color class based on risk level + */ +export function getOrderRiskBadge(riskLevel) { + let label; + switch (riskLevel) { + case "high": + label = "danger"; + break; + case "elevated": + label = "warning"; + break; + default: + label = ""; + } + return label; +} + +/* + * @method getOrderRiskStatus + * @private + * @summary Gets the risk label on the paymentMethod object for a shop on an order. + * An empty string is returned if the value is "normal" becuase we don't flag a normal charge + * @param {object} order - order object + * @return {string} label - risklevel value (if risklevel is not normal) + */ +export function getOrderRiskStatus(order) { + let riskLevel; + const billingForShop = order.billing.find((billing) => billing.shopId === Reaction.getShopId()); + + if (billingForShop && billingForShop.paymentMethod && billingForShop.paymentMethod.riskLevel) { + riskLevel = billingForShop.paymentMethod.riskLevel; + } + + // normal transactions do not need to be flagged + if (riskLevel === "normal") { + return ""; + } + + if (!riskLevel) { + return ""; + } + + return riskLevel; +} diff --git a/imports/plugins/core/orders/client/index.js b/imports/plugins/core/orders/client/index.js index f52800d3bb7..cf8ea840ea7 100644 --- a/imports/plugins/core/orders/client/index.js +++ b/imports/plugins/core/orders/client/index.js @@ -1,3 +1,5 @@ +import "./helpers"; + import "./templates/list/items.html"; import "./templates/list/items.js"; import "./templates/list/ordersList.html"; diff --git a/imports/plugins/core/orders/register.js b/imports/plugins/core/orders/register.js index 91e7d132a4e..5017d0ffb9a 100644 --- a/imports/plugins/core/orders/register.js +++ b/imports/plugins/core/orders/register.js @@ -10,7 +10,7 @@ Reaction.registerPackage({ }, registry: [{ route: "/dashboard/orders", - provides: "dashboard", + provides: ["dashboard"], workflow: "coreOrderWorkflow", name: "orders", label: "Orders", @@ -22,7 +22,7 @@ Reaction.registerPackage({ }, { route: "/dashboard/orders", name: "dashboard/orders", - provides: "shortcut", + provides: ["shortcut"], label: "Orders", description: "Fulfill your orders", icon: "fa fa-sun-o", diff --git a/imports/plugins/core/orders/server/i18n/en.json b/imports/plugins/core/orders/server/i18n/en.json index 3d951ab4345..faf497e84b2 100644 --- a/imports/plugins/core/orders/server/i18n/en.json +++ b/imports/plugins/core/orders/server/i18n/en.json @@ -8,6 +8,11 @@ "shortcut": { "ordersLabel": "Orders" }, + "orderRisk": { + "high": "High Risk", + "riskCapture": "Capture Order with Risk Charge?", + "riskCaptureWarn": "You are about to capture order with a charge risk. Confirm before proceeding." + }, "dashboard": { "ordersLabel": "Orders", "ordersTitle": "Orders", @@ -38,6 +43,17 @@ "fulfillment": "Fulfillment", "orderDetails": "Order Details", "shipmentTracking": "Shipment" + }, + "table": { + "headers" : { + "name": "Name", + "email": "Email", + "date": "Date", + "id": "ID", + "total": "Total", + "shipping": "Shipping", + "status": "Status" + } } } } diff --git a/imports/plugins/core/payments/register.js b/imports/plugins/core/payments/register.js index ec5e359ede5..f4d24750b18 100644 --- a/imports/plugins/core/payments/register.js +++ b/imports/plugins/core/payments/register.js @@ -12,7 +12,7 @@ Reaction.registerPackage({ }, registry: [ { - provides: "dashboard", + provides: ["dashboard"], name: "payments", label: "Payments", description: "Payment Methods", @@ -25,7 +25,7 @@ Reaction.registerPackage({ label: "Payment Settings", icon: "fa fa-credit-card", name: "payment/settings", - provides: "settings", + provides: ["settings"], template: "paymentSettings" } ] diff --git a/imports/plugins/core/revisions/register.js b/imports/plugins/core/revisions/register.js index dfd35de07d7..fa545f380bd 100644 --- a/imports/plugins/core/revisions/register.js +++ b/imports/plugins/core/revisions/register.js @@ -14,7 +14,7 @@ Reaction.registerPackage({ { label: "Product Revisions", name: "catalog/settings/revisions/general", - provides: "catalogSettings", + provides: ["catalogSettings"], template: "revisionControlSettings" } ] diff --git a/imports/plugins/core/router/client/browserRouter.js b/imports/plugins/core/router/client/browserRouter.js index 7d7c279371e..b943dcb1a23 100644 --- a/imports/plugins/core/router/client/browserRouter.js +++ b/imports/plugins/core/router/client/browserRouter.js @@ -9,10 +9,10 @@ import { isEqual } from "lodash"; import queryParse from "query-parse"; import { Session } from "meteor/session"; import { Tracker } from "meteor/tracker"; -import App from "/imports/plugins/core/router/client/app"; import { Router } from "../lib"; import { MetaData } from "/lib/api/router/metadata"; import { TranslationProvider } from "/imports/plugins/core/ui/client/providers"; +import { Components } from "@reactioncommerce/reaction-components"; const history = Router.history; @@ -152,7 +152,7 @@ export function initBrowserRouter() { ReactDOM.render(( - + ), getRootNode()); diff --git a/imports/plugins/core/router/lib/router.js b/imports/plugins/core/router/lib/router.js index 04ab278c4f6..8b1a477744b 100644 --- a/imports/plugins/core/router/lib/router.js +++ b/imports/plugins/core/router/lib/router.js @@ -194,9 +194,17 @@ class Router { */ Router.pathFor = (path, options = {}) => { const foundPath = Router.routes.find((pathObject) => { - if (pathObject.options.name === path) { - return true; + if (pathObject.route) { + if (options.hash && options.hash.shopSlug) { + if (pathObject.options.name === path && pathObject.route.includes("shopSlug")) { + return true; + } + } else if (pathObject.options.name === path && !pathObject.route.includes("shopSlug")) { + return true; + } } + + // No path found return false; }); @@ -607,6 +615,18 @@ Router.initPackageRoutes = (options) => { } }); + routeDefinitions.push({ + route: "/shop/:shopSlug", + name: "index", + options: { + name: "index", + type: "shop-prefix", + ...options.indexRoute, + component: indexLayout.component, + structure: indexLayout.structure + } + }); + // Not-found route routeDefinitions.push({ route: "/not-found", @@ -635,9 +655,9 @@ Router.initPackageRoutes = (options) => { template, layout, workflow + // provides } = registryItem; - // get registry route name const name = getRegistryRouteName(pkg.name, registryItem); // define new route @@ -657,7 +677,14 @@ Router.initPackageRoutes = (options) => { structure: reactionLayout.structure } }; - + newRoutes.push({ + ...newRouteConfig, + route: `/shop/:shopSlug${route}`, + options: { + ...newRouteConfig.options, + type: "shop-prefix" + } + }); // push new routes newRoutes.push(newRouteConfig); } // end registryItems diff --git a/imports/plugins/core/shipping/register.js b/imports/plugins/core/shipping/register.js index 1df85678a7e..a27b2d02951 100644 --- a/imports/plugins/core/shipping/register.js +++ b/imports/plugins/core/shipping/register.js @@ -13,7 +13,7 @@ Reaction.registerPackage({ }, registry: [ { - provides: "dashboard", + provides: ["dashboard"], route: "/dashboard/shipping", name: "shipping", label: "Shipping", @@ -24,7 +24,7 @@ Reaction.registerPackage({ workflow: "coreDashboardWorkflow" }, { - provides: "settings", + provides: ["settings"], name: "settings/shipping", label: "Shipping", description: "Configure shipping", diff --git a/imports/plugins/core/taxes/register.js b/imports/plugins/core/taxes/register.js index 9ea0c768150..da279ef2756 100644 --- a/imports/plugins/core/taxes/register.js +++ b/imports/plugins/core/taxes/register.js @@ -15,7 +15,7 @@ Reaction.registerPackage({ }, registry: [ { - provides: "dashboard", + provides: ["dashboard"], name: "taxes", label: "Taxes", description: "Provide tax rates", @@ -28,18 +28,18 @@ Reaction.registerPackage({ label: "Tax Settings", icon: "fa fa-university", name: "taxes/settings", - provides: "settings", + provides: ["settings"], template: "taxSettings" }, { label: "Custom Rates", name: "taxes/settings/rates", - provides: "taxSettings", + provides: ["taxSettings"], template: "customTaxRates" }, { template: "flatRateCheckoutTaxes", - provides: "taxMethod" + provides: ["taxMethod"] } ] }); diff --git a/imports/plugins/core/templates/register.js b/imports/plugins/core/templates/register.js index 34537564fcb..bd6f0533798 100644 --- a/imports/plugins/core/templates/register.js +++ b/imports/plugins/core/templates/register.js @@ -13,7 +13,7 @@ Reaction.registerPackage({ }, registry: [ { - provides: "dashboard", + provides: ["dashboard"], workflow: "coreDashboardWorkflow", name: "Templates", label: "Templates", @@ -26,7 +26,7 @@ Reaction.registerPackage({ label: "Template Settings", icon: "fa fa-columns", name: "templates/settings", - provides: "settings", + provides: ["settings"], template: "templateSettings", meta: { actionView: { @@ -37,7 +37,7 @@ Reaction.registerPackage({ { label: "Email Templates", name: "templates/settings/email", - provides: "templateSettings", + provides: ["templateSettings"], template: "emailTemplates" } ] diff --git a/imports/plugins/core/router/client/app.js b/imports/plugins/core/ui/client/components/app/app.js similarity index 88% rename from imports/plugins/core/router/client/app.js rename to imports/plugins/core/ui/client/components/app/app.js index 08ee9c383d8..f722e9b5b46 100644 --- a/imports/plugins/core/router/client/app.js +++ b/imports/plugins/core/ui/client/components/app/app.js @@ -1,9 +1,7 @@ +import { Switch } from "react-router-dom"; import React, { Component } from "react"; import PropTypes from "prop-types"; import classnames from "classnames"; -import { Switch } from "react-router-dom"; -import { composeWithTracker } from "@reactioncommerce/reaction-components"; -import { Reaction, Router } from "/client/api"; import ToolbarContainer from "/imports/plugins/core/dashboard/client/containers/toolbarContainer"; import Toolbar from "/imports/plugins/core/dashboard/client/components/toolbar"; import { ActionViewContainer, PackageListContainer } from "/imports/plugins/core/dashboard/client/containers"; @@ -105,12 +103,4 @@ class App extends Component { } } -function composer(props, onData) { - onData(null, { - isActionViewOpen: Reaction.isActionViewOpen(), - hasDashboardAccess: Reaction.hasDashboardAccessForAnyShop(), - currentRoute: Router.current() - }); -} - -export default composeWithTracker(composer)(App); +export default App; diff --git a/imports/plugins/core/ui/client/components/app/index.js b/imports/plugins/core/ui/client/components/app/index.js new file mode 100644 index 00000000000..205253594de --- /dev/null +++ b/imports/plugins/core/ui/client/components/app/index.js @@ -0,0 +1 @@ +export { default as App } from "./app"; diff --git a/imports/plugins/core/ui/client/components/calendarPicker/calendarPicker.js b/imports/plugins/core/ui/client/components/calendarPicker/calendarPicker.js new file mode 100644 index 00000000000..e76ef7e6378 --- /dev/null +++ b/imports/plugins/core/ui/client/components/calendarPicker/calendarPicker.js @@ -0,0 +1,123 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { DayPickerRangeController } from "react-dates"; +import omit from "lodash/omit"; +import { registerComponent } from "@reactioncommerce/reaction-components"; + +// CalendarPicker is a wrapper around react-dates DayPickerRangeController. +// Anything that works in react-dates should work in CalendarPicker +// react-dates docs are available at: https://github.com/airbnb/react-dates + +class CalendarPicker extends Component { + constructor(props) { + super(props); + this.state = { + startDate: props.initialStartDate, + endDate: props.initialEndDate, + focusedInput: props.autoFocusEndDate ? "endDate" : "startDate" + }; + } + + onDatesChange = ({ startDate, endDate }) => { + this.setState({ + startDate, + endDate + }); + + if (this.props.onDatesChange) { + this.props.onDatesChange(startDate, endDate); + } + } + +onFocusChange = (focusedInput) => { + this.setState({ + // Force the focusedInput to always be truthy so that dates are always selectable + focusedInput: !focusedInput ? "startDate" : focusedInput + }); +} + +render() { + const { focusedInput, startDate, endDate } = this.state; + + const props = omit(this.props, [ + "autoFocus", + "autoFocusEndDate", + "initialStartDate", + "initialEndDate" + ]); + + return ( + } + navNext={} + hideKeyboardShortcutsPanel={true} + /> + ); + } +} + +CalendarPicker.defaultProps = { + autoFocusEndDate: false, + initialStartDate: null, + initialEndDate: null, + + // day presentation and interaction related props + renderDay: null, + minimumNights: 1, + isDayBlocked: () => false, + isDayHighlighted: () => false, + enableOutsideDays: false, + + // calendar presentation and interaction related props + withPortal: false, + initialVisibleMonth: null, + numberOfMonths: 1, + onOutsideClick() {}, + keepOpenOnDateSelect: false, + renderCalendarInfo: null, + isRTL: false, + + // navigation related props + navPrev: null, + navNext: null, + onPrevMonthClick() {}, + onNextMonthClick() {}, + + // internationalization + monthFormat: "MMMM YYYY" +}; + +CalendarPicker.propTypes = { + autoFocusEndDate: PropTypes.bool, + enableOutsideDays: PropTypes.bool, + initialEndDate: PropTypes.object, + initialStartDate: PropTypes.object, + initialVisibleMonth: PropTypes.func, + isDayBlocked: PropTypes.func, + isDayHighlighted: PropTypes.func, + isOutsideRange: PropTypes.func, + isRTL: PropTypes.bool, + keepOpenOnDateSelect: PropTypes.bool, + minimumNights: PropTypes.number, + monthFormat: PropTypes.string, + navNext: PropTypes.node, + navPrev: PropTypes.node, + numberOfMonths: PropTypes.number, + onDatesChange: PropTypes.func, + onNextMonthClick: PropTypes.func, + onOutsideClick: PropTypes.func, + onPrevMonthClick: PropTypes.func, + renderCalendarInfo: PropTypes.func, + renderDay: PropTypes.func, + withPortal: PropTypes.bool +}; + +registerComponent("CalendarPicker", CalendarPicker); + +export default CalendarPicker; diff --git a/imports/plugins/core/ui/client/components/index.js b/imports/plugins/core/ui/client/components/index.js index e0bb2b892e6..17fdc18b345 100644 --- a/imports/plugins/core/ui/client/components/index.js +++ b/imports/plugins/core/ui/client/components/index.js @@ -1,6 +1,7 @@ // export ButtonGroup from "./buttonGroup/buttonGroup"; export { Alerts, Alert } from "./alerts"; +export { App } from "./app"; export { default as Icon } from "./icon/icon"; export { default as CircularProgress } from "./progress/circularProgress"; export { default as Divider } from "./divider/divider"; @@ -37,3 +38,4 @@ export { default as Select } from "./select/select.react"; export { default as ClickToCopy } from "./clickToCopy/clickToCopy"; export { ReactionAvatar } from "./avatar"; export * from "./notFound"; +export { CalendarPicker } from "./calendarPicker/calendarPicker"; diff --git a/imports/plugins/core/ui/client/components/menu/dropDownMenu.js b/imports/plugins/core/ui/client/components/menu/dropDownMenu.js index c6e7617265a..df0260dd731 100644 --- a/imports/plugins/core/ui/client/components/menu/dropDownMenu.js +++ b/imports/plugins/core/ui/client/components/menu/dropDownMenu.js @@ -102,6 +102,7 @@ class DropDownMenu extends Component { value={this.props.value} onChange={this.handleMenuItemChange} style={this.props.menuStyle} + isClickable={this.props.isClickable} > {this.props.children} @@ -116,6 +117,7 @@ DropDownMenu.propTypes = { children: PropTypes.node, className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), closeOnClick: PropTypes.bool, + isClickable: PropTypes.bool, isEnabled: PropTypes.bool, isOpen: PropTypes.bool, menuClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), diff --git a/imports/plugins/core/ui/client/components/menu/menu.js b/imports/plugins/core/ui/client/components/menu/menu.js index 5e6914ecc85..ff2716d79b2 100644 --- a/imports/plugins/core/ui/client/components/menu/menu.js +++ b/imports/plugins/core/ui/client/components/menu/menu.js @@ -20,7 +20,7 @@ class Menu extends Component { active: element.props.value === this.props.value }, this.props.className); return ( -
  • {newChild}
  • +
  • {this.props.isClickable ? newChild : element}
  • ); }); } @@ -45,6 +45,7 @@ Menu.propTypes = { attachment: PropTypes.string, children: PropTypes.node, className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + isClickable: PropTypes.bool, menuClassName: PropTypes.string, onChange: PropTypes.func, style: PropTypes.object, @@ -52,7 +53,8 @@ Menu.propTypes = { }; Menu.defaultProps = { - attachment: "top" + attachment: "top", + isClickable: true }; registerComponent("Menu", Menu); diff --git a/imports/plugins/core/ui/client/components/table/sortableTable.js b/imports/plugins/core/ui/client/components/table/sortableTable.js index 51ed410ea84..bad9d97d343 100644 --- a/imports/plugins/core/ui/client/components/table/sortableTable.js +++ b/imports/plugins/core/ui/client/components/table/sortableTable.js @@ -5,10 +5,11 @@ import ReactTable from "react-table"; import { Meteor } from "meteor/meteor"; import { Counts } from "meteor/tmeasday:publish-counts"; import { SortableTableFilter, SortableTablePagination } from "./sortableTableComponents"; +import { registerComponent } from "@reactioncommerce/reaction-components"; // SortableTable is a wrapper around ReactTable. // Anything that works in ReactTable should work in SortableTable OOTB (although it may not be styled). -// ReactTable docs are available here: https://react-table.js.org/#/story/readme +// ReactTable docs are available at: https://react-table.js.org/#/story/readme class SortableTable extends Component { constructor(props) { @@ -360,4 +361,6 @@ SortableTable.defaultProps = { // rowsText: }; +registerComponent("SortableTable", SortableTable); + export default SortableTable; diff --git a/imports/plugins/core/ui/client/components/table/sortableTableComponents/pagination.js b/imports/plugins/core/ui/client/components/table/sortableTableComponents/pagination.js index 39f357052cc..151ae7749a2 100644 --- a/imports/plugins/core/ui/client/components/table/sortableTableComponents/pagination.js +++ b/imports/plugins/core/ui/client/components/table/sortableTableComponents/pagination.js @@ -114,7 +114,7 @@ class SortableTablePagination extends Component {
    { // eslint-disable-line no-unused-vars - if (!canPrevious) { + if (canPrevious) { return this.changePage(page - 1); } }} @@ -127,7 +127,7 @@ class SortableTablePagination extends Component {
    { // eslint-disable-line no-unused-vars - if (!canNext) { + if (canNext) { return this.changePage(page + 1); } }} diff --git a/imports/plugins/core/ui/client/components/table/sortableTableComponents/paginationButtons.js b/imports/plugins/core/ui/client/components/table/sortableTableComponents/paginationButtons.js index 3dd7bf0e2cf..61429b09a44 100644 --- a/imports/plugins/core/ui/client/components/table/sortableTableComponents/paginationButtons.js +++ b/imports/plugins/core/ui/client/components/table/sortableTableComponents/paginationButtons.js @@ -1,6 +1,7 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import { Button } from "/imports/plugins/core/ui/client/components"; +import { i18next } from "/client/api"; class PaginationButtons extends Component { @@ -12,11 +13,13 @@ class PaginationButtons extends Component { const { children } = this.props; if (children === "Previous") { - return "fa fa-angle-left"; + const angleDirection = i18next.dir() === "rtl" ? "right" : "left"; + return `fa fa-angle-${angleDirection}`; } if (children === "Next") { - return "fa fa-angle-right"; + const angleDirection = i18next.dir() === "rtl" ? "left" : "right"; + return `fa fa-angle-${angleDirection}`; } return null; diff --git a/imports/plugins/core/ui/client/containers/appContainer.js b/imports/plugins/core/ui/client/containers/appContainer.js new file mode 100644 index 00000000000..844e1b1614d --- /dev/null +++ b/imports/plugins/core/ui/client/containers/appContainer.js @@ -0,0 +1,20 @@ +import { compose } from "recompose"; +import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; +import { Reaction, Router } from "/client/api"; +import { App } from "../components"; + +function composer(props, onData) { + onData(null, { + isActionViewOpen: Reaction.isActionViewOpen(), + hasDashboardAccess: Reaction.hasDashboardAccessForAnyShop(), + currentRoute: Router.current() + }); +} + +registerComponent("App", App, [ + composeWithTracker(composer) +]); + +export default compose( + composeWithTracker(composer), +)(App); diff --git a/imports/plugins/core/ui/client/containers/index.js b/imports/plugins/core/ui/client/containers/index.js index c00ec48dbcf..27ac027aabe 100644 --- a/imports/plugins/core/ui/client/containers/index.js +++ b/imports/plugins/core/ui/client/containers/index.js @@ -1,5 +1,6 @@ export { default as EditContainer } from "./edit"; export { default as Alerts } from "./alerts"; +export { default as App } from "./appContainer"; export { default as ReactionAvatar } from "./avatar"; export { default as SortableItem } from "./sortableItem"; export { default as MediaGalleryContainer } from "./mediaGallery"; diff --git a/imports/plugins/core/ui/register.js b/imports/plugins/core/ui/register.js index 56760e087da..a9894763690 100644 --- a/imports/plugins/core/ui/register.js +++ b/imports/plugins/core/ui/register.js @@ -9,7 +9,7 @@ Reaction.registerPackage({ registry: [{ route: "/dashboard/ui", name: "reaction-ui/uiDashboard", - provides: "dashboard", + provides: ["dashboard"], workflow: "coreUIWorkflow", container: "appearance", label: "Themes", diff --git a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js index b53a0376d95..c1500cf254f 100644 --- a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js +++ b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js @@ -10,7 +10,7 @@ Migrations.add({ $set: { registry: [{ name: "Search Modal", - provides: "ui-search", + provides: ["ui-search"], template: "searchModal" }] } diff --git a/imports/plugins/core/versions/server/migrations/7_add_shop_slugs_to_schema.js b/imports/plugins/core/versions/server/migrations/7_add_shop_slugs_to_schema.js new file mode 100644 index 00000000000..dbd82264adf --- /dev/null +++ b/imports/plugins/core/versions/server/migrations/7_add_shop_slugs_to_schema.js @@ -0,0 +1,58 @@ +import { Migrations } from "meteor/percolate:migrations"; +import { Shops } from "/lib/collections"; +import { getSlug } from "/lib/api"; + +Migrations.add({ + version: 7, + up() { + // Get all shops + const shops = Shops.find(); + // Get primary shop + const primaryShop = Shops.findOne({ shopType: "primary" }); + // Init list of merchant shops + const merchantShops = []; + + // Loop through all shops creating slugs and creating merchant shop objects + shops.forEach((shop) => { + // create slug from shop name + const shopSlug = getSlug(shop.name); + + // If a shop doesn't have a slug, add one + if (typeof shop.slug === "undefined") { + Shops.update({ _id: shop._id }, { + $set: { + slug: shopSlug + } + }); + } + + // if the shop is a merchant shop, create an obeject for it to - these will be used to create shop routes + if (shop.shopType === "merchant") { + merchantShops.push({ + _id: shop._id, + name: shop.name, + slug: shopSlug + }); + } + }); + + // if we added any merchant shops, add those to the primary shop + if (merchantShops.length > 0) { + Shops.update({ _id: primaryShop._id }, { + $set: { + merchantShops: merchantShops + } + }); + } + }, + + down() { + // Remove slugs and merchant shops + Shops.update({}, { + $unset: { + slug: null, + merchantShops: null + } + }, { multi: true }); + } +}); diff --git a/imports/plugins/core/versions/server/migrations/8_update_registry_provides_to_array.js b/imports/plugins/core/versions/server/migrations/8_update_registry_provides_to_array.js new file mode 100644 index 00000000000..9a46ba9d02a --- /dev/null +++ b/imports/plugins/core/versions/server/migrations/8_update_registry_provides_to_array.js @@ -0,0 +1,53 @@ +import { Migrations } from "meteor/percolate:migrations"; +import { Packages } from "/lib/collections"; + +Migrations.add({ + version: 6, + up() { + const packages = Packages.find(); + + // Loop through all packages and update provides to use an array + packages.forEach((pkg) => { + // Map the existing registry into an updated registry with the existing "provides" string wrapped in an array + // We use the term "app" to refer to individual registry entries + const updatedRegistry = pkg.registry.map((app) => { + if (typeof app.provides === "string") { + app.provides = [app.provides]; + } + return app; + }); + + // Update the package document with the new registry + Packages.update({ _id: pkg._id }, { + $set: { + registry: updatedRegistry + } + }); + }); + }, + + down() { + const packages = Packages.find(); + + // Loop through all packages and update provides to use an array + packages.forEach((pkg) => { + // Map the existing registry into an updated registry with any provides arrays changed to use the first element + // of the array. We discussed reducing the array and creating an entry for each provides here, but felt that + // since versions of the app before this would have only had one entry, it's safer to just take the first element + // of the array + const updatedRegistry = pkg.registry.map((entry) => { + if (Array.isArray(entry.provides)) { + entry.provides = entry.provides[0]; + } + return entry; + }); + + // Update the package document with the new registry + Packages.update({ _id: pkg._id }, { + $set: { + registry: updatedRegistry + } + }); + }); + } +}); diff --git a/imports/plugins/core/versions/server/migrations/index.js b/imports/plugins/core/versions/server/migrations/index.js index 0a2cf0f3e1e..e06e4ffd7c8 100644 --- a/imports/plugins/core/versions/server/migrations/index.js +++ b/imports/plugins/core/versions/server/migrations/index.js @@ -4,3 +4,4 @@ import "./3_reset_package_registry"; import "./4_update_templates_priority"; import "./5_update_defaultRoles_to_groups"; import "./6_update_tags_is_visible"; +import "./7_add_shop_slugs_to_schema"; diff --git a/imports/plugins/included/analytics/register.js b/imports/plugins/included/analytics/register.js index 44b07af2433..ed649b71426 100644 --- a/imports/plugins/included/analytics/register.js +++ b/imports/plugins/included/analytics/register.js @@ -22,7 +22,7 @@ Reaction.registerPackage({ } }, registry: [{ - provides: "dashboard", + provides: ["dashboard"], label: "Analytics", description: "Analytics and tracking integrations", template: "reactionAnalytics", @@ -37,7 +37,7 @@ Reaction.registerPackage({ label: "Analytics Settings", icon: "fa fa-bar-chart-o", route: "/dashboard/analytics/settings", - provides: "settings", + provides: ["settings"], container: "dashboard", template: "reactionAnalyticsSettings" }] diff --git a/imports/plugins/included/connectors-shopify/client/settings/shopify.html b/imports/plugins/included/connectors-shopify/client/settings/shopify.html index 4ba6b9e41f5..a6c74a9306c 100644 --- a/imports/plugins/included/connectors-shopify/client/settings/shopify.html +++ b/imports/plugins/included/connectors-shopify/client/settings/shopify.html @@ -10,13 +10,51 @@
    {{#if packageData.settings.apiKey}}
    - {{> shopifyProductImport}} + {{> shopifyImport}} +
    +
    + {{> shopifySync}}
    {{/if}} -