diff --git a/client/config/defaults.js b/client/config/defaults.js index b03761b1e04..22f16beefec 100644 --- a/client/config/defaults.js +++ b/client/config/defaults.js @@ -23,3 +23,6 @@ Session.setDefault("DEFAULT_LAYOUT", DEFAULT_LAYOUT); Session.setDefault("DEFAULT_WORKFLOW", DEFAULT_WORKFLOW); Session.setDefault("INDEX_OPTIONS", INDEX_OPTIONS); Session.setDefault("productScrollLimit", ITEMS_INCREMENT); + +// autoform default template +AutoForm.setDefaultTemplate("bootstrap3"); diff --git a/imports/plugins/core/dashboard/client/components/actionView.js b/imports/plugins/core/dashboard/client/components/actionView.js index 49a088402db..573f33012b4 100644 --- a/imports/plugins/core/dashboard/client/components/actionView.js +++ b/imports/plugins/core/dashboard/client/components/actionView.js @@ -34,15 +34,15 @@ const getStyles = (props) => { "@media only screen and (max-width: 949px)": { width: "100vw" }, - boxShadow: isBigView ? "0 0 40px rgba(0,0,0,.1)" : "", - flex: "0 0 auto", - backgroundColor: "white", - borderLeft: "1px solid @black10", + "boxShadow": isBigView ? "0 0 40px rgba(0,0,0,.1)" : "", + "flex": "0 0 auto", + "backgroundColor": "white", + "borderLeft": "1px solid @black10", - overflow: "hidden", - transition: "width 300ms cubic-bezier(0.455, 0.03, 0.515, 0.955))", + "overflow": "hidden", + "transition": "width 300ms cubic-bezier(0.455, 0.03, 0.515, 0.955))", // boxShadow: "0 0 40px rgba(0,0,0,.1)", - zIndex: 100, + "zIndex": 100 // @media screen and (max-width: @screen-xs-max) { // transition: top 400ms cubic-bezier(0.645, 0.045, 0.355, 1); @@ -79,23 +79,23 @@ const getStyles = (props) => { masterViewPanel: { display: "flex", flexDirection: "column", - flex: "1 1 auto", + flex: "1 1 auto" // height: "100%" }, masterView: { flex: "1 1 auto", // height: "100%", - overflow: "auto", + overflow: "auto" // WebkitOverflowScrolling: "touch" }, detailViewPanel: { - display: "flex", - flexDirection: "column", - flex: "1 1 auto", - maxWidth: "400px", - height: "100vh", - backgroundColor: "white", - borderRight: "1px solid #ccc", + "display": "flex", + "flexDirection": "column", + "flex": "1 1 auto", + "maxWidth": "400px", + "height": "100vh", + "backgroundColor": "white", + "borderRight": "1px solid #ccc", "@media only screen and (max-width: 949px)": { position: "absolute", top: 0, @@ -202,13 +202,12 @@ class ActionView extends Component { ); } else { - return ( - ) + ); } } @@ -283,7 +282,7 @@ class ActionView extends Component { - ) + ); } renderDetailView() { @@ -328,12 +327,12 @@ class ActionView extends Component {
- {/*this.renderControlComponent() */} + {/* this.renderControlComponent() */} {this.renderDetailComponent()}
- ) + ); } } diff --git a/imports/plugins/core/dashboard/client/containers/actionViewContainer.js b/imports/plugins/core/dashboard/client/containers/actionViewContainer.js index fa543349a2e..41700c70606 100644 --- a/imports/plugins/core/dashboard/client/containers/actionViewContainer.js +++ b/imports/plugins/core/dashboard/client/containers/actionViewContainer.js @@ -1,6 +1,6 @@ import React, { Component, PropTypes } from "react"; import { composeWithTracker } from "/lib/api/compose"; -import { Admin } from "../components" +import { Admin } from "../components"; import { StyleRoot } from "radium"; import { Meteor } from "meteor/meteor"; import { Blaze } from "meteor/blaze"; @@ -54,7 +54,7 @@ function composer(props, onData) { icon: "plus", tooltip: "Create Content", i18nKeyTooltip: "app.createContent", - tooltipPosition: "left middle", + 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" }); @@ -67,8 +67,6 @@ function composer(props, onData) { }); - - onData(null, { isAdminArea: true, actionView: Reaction.getActionView(), diff --git a/imports/plugins/core/layout/client/index.js b/imports/plugins/core/layout/client/index.js index cc4e678bd5d..6a21a5735ab 100644 --- a/imports/plugins/core/layout/client/index.js +++ b/imports/plugins/core/layout/client/index.js @@ -12,7 +12,6 @@ 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/loading/loading.html"; import "./templates/layout/notFound/notFound.html"; import "./templates/layout/notFound/notFound.js"; import "./templates/layout/notice/unauthorized.html"; diff --git a/imports/plugins/core/layout/client/templates/layout/loading/loading.html b/imports/plugins/core/layout/client/templates/layout/loading/loading.html deleted file mode 100644 index ffe4e5703b1..00000000000 --- a/imports/plugins/core/layout/client/templates/layout/loading/loading.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/imports/plugins/core/orders/client/components/orderActions.js b/imports/plugins/core/orders/client/components/orderActions.js index a9ba81487cd..21da97a7f65 100644 --- a/imports/plugins/core/orders/client/components/orderActions.js +++ b/imports/plugins/core/orders/client/components/orderActions.js @@ -6,12 +6,12 @@ const styles = { display: "flex" }, item: { - display: "flex", - flex: "1 1 auto", - height: 100, - justifyContent: "center", - alignItems: "center", - cursor: "pointer", + "display": "flex", + "flex": "1 1 auto", + "height": 100, + "justifyContent": "center", + "alignItems": "center", + "cursor": "pointer", ":hover": { } diff --git a/imports/plugins/core/orders/client/containers/ordersActionContainer.js b/imports/plugins/core/orders/client/containers/ordersActionContainer.js index 5d70eb6c740..46ebfb5d53f 100644 --- a/imports/plugins/core/orders/client/containers/ordersActionContainer.js +++ b/imports/plugins/core/orders/client/containers/ordersActionContainer.js @@ -1,9 +1,9 @@ -import React from "react" +import React from "react"; import { Meteor } from "meteor/meteor"; import { Orders, Shops } from "/lib/collections"; import { Reaction, i18next } from "/client/api"; import { composeWithTracker } from "/lib/api/compose"; -import OrderActions from "../components/orderActions" +import OrderActions from "../components/orderActions"; const orderFilters = [{ name: "new", diff --git a/imports/plugins/core/payments/client/settings/settings.js b/imports/plugins/core/payments/client/settings/settings.js index 848508b1c95..bd194dc5146 100644 --- a/imports/plugins/core/payments/client/settings/settings.js +++ b/imports/plugins/core/payments/client/settings/settings.js @@ -39,37 +39,21 @@ Template.paymentSettings.helpers({ } }); +// toggle payment methods visibility Template.paymentSettings.events({ - /** - * toggle payment settings visibility - * also toggles payment method settings - * @param {event} event jQuery Event - * @return {void} - */ "change input[name=enabled]": (event) => { + event.preventDefault(); const settingsKey = event.target.getAttribute("data-key"); const packageId = event.target.getAttribute("data-id"); const fields = [{ property: "enabled", value: event.target.checked }]; - - Meteor.call("registry/update", packageId, settingsKey, fields, (error, result) => { - if (result.numberAffected > 0) { - // check to see if we should disable the package as well - const pkg = Packages.findOne(packageId); - const enabled = pkg.registry.find((registry) => { - return registry.enabled === true; - }); - // disable the package if no registry items are enabled. - // maybe this would be better placed in togglePackage - if (pkg.enabled !== true && enabled) { - Meteor.call("shop/togglePackage", packageId, true); - } else { - Meteor.call("shop/togglePackage", packageId, false); - } - } - }); + // update package registry + if (packageId) { + Meteor.call("registry/update", packageId, settingsKey, fields); + Meteor.call("shop/togglePackage", packageId, !event.target.checked); + } } }); diff --git a/imports/plugins/core/shipping/client/templates/checkout/shipping.html b/imports/plugins/core/shipping/client/templates/checkout/shipping.html index 75d04c7ed3c..a3b3c3a2f70 100644 --- a/imports/plugins/core/shipping/client/templates/checkout/shipping.html +++ b/imports/plugins/core/shipping/client/templates/checkout/shipping.html @@ -8,18 +8,25 @@

{{#if isReady}} - {{#each reactionApps provides='shippingMethod' enabled=true}} - {{> Template.dynamic template=template}} - {{else}} -
-
- No shipping packages are configured. - - Configure now. - -
-
- {{/each}} +
+ +
{{else}}
diff --git a/imports/plugins/core/shipping/client/templates/checkout/shipping.js b/imports/plugins/core/shipping/client/templates/checkout/shipping.js index 33f4ec4713d..b7c4575b6cf 100644 --- a/imports/plugins/core/shipping/client/templates/checkout/shipping.js +++ b/imports/plugins/core/shipping/client/templates/checkout/shipping.js @@ -1,37 +1,48 @@ import _ from "lodash"; -import { Cart, Shipping } from "/lib/collections"; import { Meteor } from "meteor/meteor"; import { Template } from "meteor/templating"; +import { Reaction } from "/client/api"; +import { Cart } from "/lib/collections"; -// -// These helpers can be used in general shipping packages -// cartShippingMethods to get current shipment methods -// until we handle multiple methods, we just use the first -function cartShippingMethods(currentCart) { +// cartShippingQuotes +// returns multiple methods +function cartShippingQuotes(currentCart) { const cart = currentCart || Cart.findOne(); + const shipmentQuotes = []; + if (cart) { if (cart.shipping) { - if (cart.shipping[0].shipmentQuotes) { - return cart.shipping[0].shipmentQuotes; + for (const shipping of cart.shipping) { + if (shipping.shipmentQuotes) { + for (quote of shipping.shipmentQuotes) { + shipmentQuotes.push(quote); + } + } } } } - return undefined; + return shipmentQuotes; } -// getShipmentMethod to get current shipment method -// until we handle multiple methods, we just use the first -function getShipmentMethod(currentCart) { +// cartShipmentMethods to get current shipment method +// this returns multiple methods, if more than one carrier +// has been chosen +function cartShipmentMethods(currentCart) { const cart = currentCart || Cart.findOne(); + const shipmentMethods = []; + if (cart) { if (cart.shipping) { - if (cart.shipping[0].shipmentMethod) { - return cart.shipping[0].shipmentMethod; + for (const shipping of cart.shipping) { + if (shipping.shipmentMethod) { + shipmentMethods.push(shipping.shipmentMethod); + } } } } - return undefined; + return shipmentMethods; } +// ensure new quotes are Template.coreCheckoutShipping.onCreated(function () { this.autorun(() => { this.subscribe("Shipping"); @@ -42,27 +53,22 @@ Template.coreCheckoutShipping.helpers({ // retrieves current rates and updates shipping rates // in the users cart collection (historical, and prevents repeated rate lookup) shipmentQuotes: function () { - const cart = Cart.findOne(); - return cartShippingMethods(cart); - }, - - // helper to make sure there are some shipping providers - shippingConfigured: function () { const instance = Template.instance(); if (instance.subscriptionsReady()) { - return Shipping.find({ - "methods.enabled": true - }).count(); + const cart = Cart.findOne(); + return cartShippingQuotes(cart); } }, - // helper to display currently selected shipmentMethod isSelected: function () { const self = this; - const shipmentMethod = getShipmentMethod(); - // if there is already a selected method, set active - if (_.isEqual(self.method, shipmentMethod)) { - return "active"; + const shipmentMethods = cartShipmentMethods(); + + for (method of shipmentMethods) { + // if there is already a selected method, set active + if (_.isEqual(self.method, method)) { + return "active"; + } } return null; }, @@ -71,8 +77,10 @@ Template.coreCheckoutShipping.helpers({ const instance = Template.instance(); const isReady = instance.subscriptionsReady(); - if (isReady) { - return true; + if (Reaction.Subscriptions.Cart.ready()) { + if (isReady) { + return true; + } } return false; diff --git a/imports/plugins/core/shipping/client/templates/flatRates/shipping.html b/imports/plugins/core/shipping/client/templates/flatRates/shipping.html deleted file mode 100644 index 856d6ab494b..00000000000 --- a/imports/plugins/core/shipping/client/templates/flatRates/shipping.html +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/imports/plugins/core/shipping/client/templates/flatRates/shipping.js b/imports/plugins/core/shipping/client/templates/flatRates/shipping.js deleted file mode 100644 index 041b312316c..00000000000 --- a/imports/plugins/core/shipping/client/templates/flatRates/shipping.js +++ /dev/null @@ -1,2 +0,0 @@ -Template.flatRateCheckoutShipping.inheritsHelpersFrom("coreCheckoutShipping"); -Template.flatRateCheckoutShipping.inheritsEventsFrom("coreCheckoutShipping"); diff --git a/imports/plugins/core/shipping/client/templates/index.js b/imports/plugins/core/shipping/client/templates/index.js index b3c7fc71161..9526b280965 100644 --- a/imports/plugins/core/shipping/client/templates/index.js +++ b/imports/plugins/core/shipping/client/templates/index.js @@ -1,9 +1,5 @@ import "./checkout/shipping.html"; import "./checkout/shipping.js"; -import "./flatRates/shipping.html"; -import "./flatRates/shipping.js"; - -import "./shipping.html"; -import "./shipping.js"; -import "./shipping.less"; +import "./settings/shipping.html"; +import "./settings/shipping.js"; diff --git a/imports/plugins/core/shipping/client/templates/settings/shipping.html b/imports/plugins/core/shipping/client/templates/settings/shipping.html new file mode 100644 index 00000000000..16e466d64de --- /dev/null +++ b/imports/plugins/core/shipping/client/templates/settings/shipping.html @@ -0,0 +1,21 @@ + diff --git a/imports/plugins/core/shipping/client/templates/settings/shipping.js b/imports/plugins/core/shipping/client/templates/settings/shipping.js new file mode 100644 index 00000000000..c08cdd857b5 --- /dev/null +++ b/imports/plugins/core/shipping/client/templates/settings/shipping.js @@ -0,0 +1,50 @@ +import { Template } from "meteor/templating"; +/* + * Template shippinges Helpers + */ +Template.shippingSettings.onCreated(function () { + this.autorun(() => { + this.subscribe("Shipping"); + }); +}); + +Template.shippingSettings.helpers({ + checked(enabled) { + if (enabled === true) { + return "checked"; + } + return ""; + }, + shown(enabled) { + if (enabled !== true) { + return "hidden"; + } + return ""; + } +}); + +// toggle shipping methods visibility +// also toggles shipping method settings +Template.shippingSettings.events({ + /** + * shippingSettings settings update enabled status for shipping service on change + * @param {event} event jQuery Event + * @return {void} + */ + "change input.checkbox-switch.shipping-settings[name=enabled]": (event) => { + event.preventDefault(); + const settingsKey = event.target.getAttribute("data-key"); + const packageId = event.target.getAttribute("data-id"); + const fields = [{ + property: "enabled", + value: event.target.checked + }]; + // save shipping registry updates + if (packageId) { + // update package registry + Meteor.call("registry/update", packageId, settingsKey, fields); + // also update shipping provider status + Meteor.call("shipping/provider/toggle", packageId, settingsKey); + } + } +}); diff --git a/imports/plugins/core/shipping/client/templates/shipping.html b/imports/plugins/core/shipping/client/templates/shipping.html deleted file mode 100644 index d9882d109ae..00000000000 --- a/imports/plugins/core/shipping/client/templates/shipping.html +++ /dev/null @@ -1,369 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/imports/plugins/core/shipping/client/templates/shipping.js b/imports/plugins/core/shipping/client/templates/shipping.js deleted file mode 100644 index 7741e0a8470..00000000000 --- a/imports/plugins/core/shipping/client/templates/shipping.js +++ /dev/null @@ -1,330 +0,0 @@ -import { Meteor } from "meteor/meteor"; -import { Session } from "meteor/session"; -import { Template } from "meteor/templating"; -import { Blaze } from "meteor/blaze"; -import { AutoForm } from "meteor/aldeed:autoform"; -import { Reaction, i18next } from "/client/api"; -import { Packages, Shipping } from "/lib/collections"; - -/* - * Template shipping Helpers - */ -Template.shippingDashboardControls.events({ - "click [data-event-action=addShippingProvider]": function () { - Reaction.setActionViewDetail({ - label: i18next.t("shipping.addShippingProvider"), - template: "addShippingProvider" - }); - } -}); - -Template.shippingSettings.onCreated(function () { - // don't show unless we have services - Reaction.hideActionView(); - this.autorun(() => { - this.subscribe("Shipping"); - }); -}); - -Template.shippingSettings.helpers({ - packageData() { - return Packages.findOne({ - name: "reaction-shipping" - }); - }, - shipping() { - const instance = Template.instance(); - if (instance.subscriptionsReady()) { - return Shipping.find({ - shopId: Reaction.getShopId() - }); - } - return []; - }, - selectedShippingProvider() { - return Session.equals("selectedShippingProvider", true); - } -}); - -Template.shippingProviderTable.onCreated(function () { - this.autorun(() => { - this.subscribe("Shipping"); - }); -}); - -Template.shippingProviderTable.helpers({ - shipping() { - const instance = Template.instance(); - if (instance.subscriptionsReady()) { - return Shipping.find({ - shopId: Reaction.getShopId() - }); - } - }, - isShippoProvider() { - if (this.provider.shippoProvider) { - return true; - } - return false; - } -}); - -/* - * Template Shipping Events - */ - -Template.shipping.events({ - "click"() { - return Alerts.removeSeen(); - }, - "click [data-action=addShippingProvider]"() { - Reaction.setActionViewDetail({ - label: i18next.t("shipping.addShippingProvider"), - template: "addShippingProvider" - }); - } -}); - -/* - * template editShippingMethod helpers - */ - -Template.editShippingMethod.helpers({ - selectedMethodDoc() { - const Doc = Session.get("updatedMethodObj") || Session.get("selectedMethodObj"); - if (Doc) { - return Doc; - } - } -}); - - -Template.afFormGroup_validLocales.helpers({ - afFieldInputAtts() { - return _.extend({ - template: "bootstrap3" - }, this.afFieldInputAtts); - } -}); - -Template.afFormGroup_validRanges.helpers({ - afFieldInputAtts() { - return _.extend({ - template: "bootstrap3" - }, this.afFieldInputAtts); - } -}); - -/* - * template addShippingProvider events - */ -Template.editShippingProvider.events({ - "click [data-event-action=cancelUpdateShippingProvider]"(event) { - event.preventDefault(); - Reaction.hideActionView(); - }, - "click [data-event-action=deleteShippingProvider]"(event) { - event.preventDefault(); - // confirm delete - Alerts.alert({ - title: i18next.t("shipping.removeShippingProvider"), - type: "warning", - showCancelButton: true, - confirmButtonText: i18next.t("shipping.removeShippingProviderConfirm", { provider: this.provider.name }) - }, (isConfirm) => { - if (isConfirm) { - Meteor.call("shipping/provider/remove", this._id); - Reaction.hideActionView(); - } - }); - } -}); - -Template.editShippingProvider.helpers({ - isShippoProvider() { - if (this.provider.shippoProvider) { - return true; - } - return false; - } -}); - -/* - * template addShippingProvider events - */ -Template.addShippingProvider.events({ - "click [data-event-action=cancelAddShippingProvider]"(event) { - event.preventDefault(); - Reaction.hideActionView(); - } -}); - -/* - * template addShippingMethods events - */ -Template.addShippingMethod.events({ - "click .cancel"(event) { - event.preventDefault(); - Reaction.toggleSession("selectedAddShippingMethod"); - Reaction.hideActionView(); - } -}); - -/* - * Template shippingProviderTable Helpers - */ -Template.shippingProviderTable.helpers({ - shipping() { - return Shipping.find(); - }, - selectedShippingMethod() { - const session = Session.get("selectedShippingMethod"); - if (_.isEqual(this, session)) { - return this; - } - }, - selectedAddShippingMethod() { - const session = Session.get("selectedAddShippingMethod"); - if (_.isEqual(this, session)) { - return this; - } - }, - selectedShippingProvider() { - const session = Session.get("selectedShippingProvider"); - if (_.isEqual(this, session)) { - return this; - } - } -}); - -/* - * template shippingProviderTable events - */ -Template.shippingProviderTable.events({ - "click [data-event-action=editShippingMethod]"(event) { - event.preventDefault(); - - Reaction.setActionViewDetail({ - label: i18next.t("shipping.editShippingMethod"), - data: this, - template: "editShippingMethod" - }); - - Session.set("updatedMethodObj", ""); - Session.set("selectedMethodObj", this); - }, - "click [data-event-action=editShippingProvider]"(event) { - event.preventDefault(); - - Reaction.setActionViewDetail({ - label: i18next.t("shipping.editShippingProvider"), - data: this, - template: "editShippingProvider" - }); - }, - "click [data-event-action=deleteShippingMethod]"(event) { - event.preventDefault(); - event.stopPropagation(); - - // confirm delete - Alerts.alert({ - title: i18next.t("shipping.removeShippingMethodTitle"), - type: "warning", - showCancelButton: true, - confirmButtonText: i18next.t("shipping.removeShippingMethodConfirm", { method: this.name }) - }, (isConfirm) => { - if (isConfirm) { - Meteor.call("shipping/methods/remove", $(event.currentTarget).data("provider-id"), this); - Reaction.hideActionView(); - } - }); - }, - "click [data-event-action=addShippingMethod]"(event) { - event.preventDefault(); - - Reaction.setActionViewDetail({ - label: i18next.t("shipping.addShippingMethod"), - template: "addShippingMethod" - }); - } -}); - -/* - * Autoform hooks - * Because these are some convoluted forms - */ - -AutoForm.hooks({ - "shipping-provider-add-form": { - onSuccess() { - Reaction.toggleSession("selectedShippingProvider"); - return Alerts.inline(i18next.t("shipping.shippingProviderSaved"), "success", { - autoHide: true, - placement: "shippingPackage" - }); - } - } -}); - -AutoForm.hooks({ - "shipping-method-add-form": { - onSubmit(insertDoc, updateDoc, currentDoc) { - const providerId = currentDoc ? currentDoc._id : Template.instance().parentTemplate(4).$(".delete-shipping-method").data("provider-id"); - let error; - try { - Meteor.call("shipping/methods/add", insertDoc, providerId); - this.done(); - } catch (_error) { - error = _error; - this.event.preventDefault(); - this.done(new Error("Submission failed")); - } - return error || false; - }, - onSuccess() { - Reaction.toggleSession("selectedAddShippingMethod"); - return Alerts.inline(i18next.t("shipping.shippingMethodRateAdded"), "success", { - autoHide: true, - placement: "shippingPackage" - }); - } - } -}); - -AutoForm.hooks({ - "shipping-method-edit-form": { - onSubmit(insertDoc, updateDoc, currentDoc) { - let error; - // handling case where we are either inserting inline this providers first methods - // or where we are adding additional methods to an existing array of provider methods in the admin panel. - const providerId = Template.instance().parentTemplate(4).$(".delete-shipping-method").data("provider-id"); - try { - _.extend(insertDoc, { _id: currentDoc._id }); - Meteor.call("shipping/methods/update", providerId, currentDoc._id, insertDoc); - Session.set("updatedMethodObj", insertDoc); - this.done(); - } catch (_error) { - error = _error; - this.done(new Error("Submission failed")); - } - return error || false; - }, - onSuccess() { - return Alerts.inline(i18next.t("shipping.shippingMethodRateUpdated"), "success", { - autoHide: true, - placement: "shippingPackage" - }); - } - } -}); - -Blaze.TemplateInstance.prototype.parentTemplate = function (levels = 1) { - let view = Blaze.currentView; - let numLevel = levels; - while (view) { - if (view.name.substring(0, 9) === "Template." && !numLevel--) { - return view.templateInstance(); - } - view = view.parentView; - } -}; diff --git a/imports/plugins/core/shipping/client/templates/shipping.less b/imports/plugins/core/shipping/client/templates/shipping.less deleted file mode 100644 index 7830d137635..00000000000 --- a/imports/plugins/core/shipping/client/templates/shipping.less +++ /dev/null @@ -1,21 +0,0 @@ -.add-shipping-method, -.edit-shipping-method, -.delete-shipping-method{ - cursor: pointer; - padding-right: 10px; -} - -.add-shipping-provider { - cursor: pointer; - padding-right: 33px; - margin-top: 30px; -} - -.edit-shipping-provider { - cursor: pointer; - padding-right: 18px; -} - -.shipping-rate-tables { - -} \ No newline at end of file diff --git a/imports/plugins/core/shipping/register.js b/imports/plugins/core/shipping/register.js index aaa9c2d957d..1df85678a7e 100644 --- a/imports/plugins/core/shipping/register.js +++ b/imports/plugins/core/shipping/register.js @@ -6,12 +6,9 @@ Reaction.registerPackage({ icon: "fa fa-truck", autoEnable: true, settings: { - name: "Flat Rate Service", + name: "Shipping", shipping: { enabled: true - }, - flatRates: { - enabled: true } }, registry: [ @@ -20,7 +17,7 @@ Reaction.registerPackage({ route: "/dashboard/shipping", name: "shipping", label: "Shipping", - description: "Provide shipping rates", + description: "Shipping dashboard", icon: "fa fa-truck", priority: 1, container: "core", @@ -30,14 +27,9 @@ Reaction.registerPackage({ provides: "settings", name: "settings/shipping", label: "Shipping", - description: "Provide shipping rates", + description: "Configure shipping", icon: "fa fa-truck", - template: "shipping" - }, - { - template: "flatRateCheckoutShipping", - name: "shipping/flatRates", - provides: "shippingMethod" + template: "shippingSettings" } ] }); diff --git a/imports/plugins/core/shipping/server/i18n/en.json b/imports/plugins/core/shipping/server/i18n/en.json index 6762b6b3ffb..2e2e3a0ff30 100644 --- a/imports/plugins/core/shipping/server/i18n/en.json +++ b/imports/plugins/core/shipping/server/i18n/en.json @@ -12,6 +12,68 @@ "settings": { "shippingLabel": "Shipping" } + }, + "checkoutShipping": { + "selectShippingOption": "Select shipping option", + "noShippingMethods": "No shipping methods are configured.", + "configureNow": "Configure now.", + "shipping": "Shipping" + }, + "shipping": { + "addShippingProvider": "Add shipping provider", + "editShippingProvider": "Edit shipping provider", + "addShippingMethod": "Add shipping method", + "editShippingMethod": "Edit shipping method", + "deleteShippingMethod": "Delete shipping method", + "noSettingsForThisView": "No settings for this view", + "noShippingMethods": "No shipping methods are configured.", + "removeShippingMethodConfirm": "Are you sure you want to delete {{method}}?", + "removeShippingMethodTitle": "Remove Shipping Method", + "shippingMethodDeleted": "This shipping method has been deleted.", + "removeShippingProvider": "Remove Shipping Provider", + "removeShippingProviderConfirm": "Are you sure you want to delete {{provider}}?", + "shippingProviderSaved": "Shipping provider saved.", + "shippingProviderUpdated": "Shipping provider data updated.", + "shippingMethodRateAdded": "Shipping method rate added.", + "shippingMethodRateUpdated": "Shipping method rate updated.", + "name": "Name", + "label": "Label", + "group": "Group", + "cost": "Cost", + "handling": "Handling", + "rate": "Rate", + "enabled": "Enabled", + "disabled": "Disabled", + "addRate": "Add rate", + "updateRate": "Update {{name}} rate", + "addNewCondition": "Add new condition", + "deleteCondition": "Delete condition", + "provider": { + "name": "Service Code", + "label": "Public Label", + "enabled": "Enabled" + } + }, + "shippingMethod": { + "name": "Method Name", + "label": "Public Label", + "group": "Group", + "cost": "Cost", + "handling": "Handling", + "rate": "Rate", + "enabled": "Enabled", + "matchingCartRanges": "Matching Cart Ranges", + "validRanges": { + "begin": "Begin", + "end": "End" + }, + "matchingLocales": "Matching Locales", + "validLocales": { + "origination": "From", + "destination": "To", + "deliveryBegin": "Shipping Est.", + "deliveryEnd": "Delivery Est." + } } } } diff --git a/imports/plugins/core/shipping/server/index.js b/imports/plugins/core/shipping/server/index.js index 6fd1abd7f17..580fa4381c9 100644 --- a/imports/plugins/core/shipping/server/index.js +++ b/imports/plugins/core/shipping/server/index.js @@ -1,2 +1,2 @@ -import "./methods/methods"; import "./i18n"; +import "./methods/methods"; diff --git a/imports/plugins/core/shipping/server/lib/roles.js b/imports/plugins/core/shipping/server/lib/roles.js new file mode 100644 index 00000000000..fb9f1fa2d12 --- /dev/null +++ b/imports/plugins/core/shipping/server/lib/roles.js @@ -0,0 +1 @@ +export const shippingRoles = ["admin", "owner", "shipping"]; diff --git a/imports/plugins/core/shipping/server/methods/methods.js b/imports/plugins/core/shipping/server/methods/methods.js index 8b9a3ed8132..b73642bb9d7 100644 --- a/imports/plugins/core/shipping/server/methods/methods.js +++ b/imports/plugins/core/shipping/server/methods/methods.js @@ -1,112 +1,41 @@ -import { Shipping } from "/lib/collections"; +import { Meteor } from "meteor/meteor"; +import { check } from "meteor/check"; +import { Shipping, Packages } from "/lib/collections"; import { Reaction } from "/server/api"; +import { shippingRoles } from "../lib/roles"; export const methods = { /** - * add new shipping methods - * @summary insert Shipping methods for a provider - * @param {String} insertDoc shipping method - * @param {String} currentDoc current providerId - * @return {Number} insert result + * shipping/provider/toggle + * @summary toggle enabled provider + * @param { String } packageId - packageId + * @param { String } provider - provider name + * @return { Number } update - result */ - "shipping/methods/add": function (insertDoc, currentDoc) { - check(insertDoc, Object); - check(currentDoc, Match.Optional(String)); - // if no currentDoc we need to insert a default provider. - const id = currentDoc || Random.id(); - - if (!Reaction.hasPermission("shipping")) { + "shipping/provider/toggle": function (packageId, provider) { + check(packageId, String); + check(provider, String); + if (!Reaction.hasPermission(shippingRoles)) { throw new Meteor.Error(403, "Access Denied"); } - - return Shipping.update({ - _id: id - }, { - $addToSet: { - methods: insertDoc + const pkg = Packages.findOne(packageId); + if (pkg && pkg.settings[provider]) { + const current = Shipping.findOne({ "provider.name": provider }); + const enabled = pkg.settings[provider].enabled; + // const enabled = !current.provider.enabled; + if (current && current.provider) { + return Shipping.update({ + "_id": current._id, + "provider.name": provider + }, { + $set: { + "provider.enabled": enabled + } + }, { + multi: true + }); } - }); - }, - - /** - * updateShippingMethods - * @summary update Shipping methods for a provider - * @param {String} providerId providerId - * @param {String} methodId methodId - * @param {Object} updateMethod - updated method itself - * @return {Number} update result - */ - "shipping/methods/update": function (providerId, methodId, updateMethod) { - check(providerId, String); - check(methodId, String); - check(updateMethod, Object); - if (!Reaction.hasPermission("shipping")) { - throw new Meteor.Error(403, "Access Denied"); - } - - return Shipping.update({ - "_id": providerId, - "methods._id": methodId - }, { - $set: { - "methods.$": updateMethod - } - }); - }, - - /* - * remove shipping method - */ - "shipping/methods/remove": function (providerId, removeDoc) { - check(providerId, String); - check(removeDoc, Object); - if (!Reaction.hasPermission("shipping")) { - throw new Meteor.Error(403, "Access Denied"); - } - - return Shipping.update({ - _id: providerId, - methods: removeDoc - }, { - $pull: { - methods: removeDoc - } - }); - }, - - /* - * add / insert shipping provider - */ - "shipping/provider/add": function (doc) { - check(doc, Object); - if (!Reaction.hasPermission("shipping")) { - throw new Meteor.Error(403, "Access Denied"); - } - return Shipping.insert(doc); - }, - - /* - * update shipping provider - */ - "shipping/provider/update": function (updateDoc, currentDoc) { - check(updateDoc, Object); - check(currentDoc, String); - if (!Reaction.hasPermission("shipping")) { - throw new Meteor.Error(403, "Access Denied"); - } - return Shipping.update({ - _id: currentDoc - }, updateDoc); - }, - /* - * remove shipping provider - */ - "shipping/provider/remove": function (providerId) { - check(providerId, String); - if (!Reaction.hasPermission("shipping")) { - throw new Meteor.Error(403, "Access Denied"); } - return Shipping.remove(providerId); } }; diff --git a/imports/plugins/core/taxes/server/methods/methods.js b/imports/plugins/core/taxes/server/methods/methods.js index c8d73336571..0234b69a961 100644 --- a/imports/plugins/core/taxes/server/methods/methods.js +++ b/imports/plugins/core/taxes/server/methods/methods.js @@ -97,7 +97,7 @@ export const methods = { if (pkg && pkg.enabled === true && pkg.settings.rates.enabled === true) { Logger.debug("Calculating custom tax rates"); - if (typeof cartToCalc.shipping !== "undefined") { + if (typeof cartToCalc.shipping !== "undefined" && typeof cartToCalc.items !== "undefined") { const shippingAddress = cartToCalc.shipping[0].address; // // custom rates that match shipping info diff --git a/imports/plugins/core/ui-grid/client/griddle.js b/imports/plugins/core/ui-grid/client/griddle.js index 11349ea835b..ccef8e0d832 100644 --- a/imports/plugins/core/ui-grid/client/griddle.js +++ b/imports/plugins/core/ui-grid/client/griddle.js @@ -15,12 +15,13 @@ const MeteorGriddle = React.createClass({ filteredFields: React.PropTypes.array, // an array of fields to search through when filtering matchingResultsCount: React.PropTypes.string, // the name of the matching results counter publication: React.PropTypes.string, // the publication that will provide the data - subsManager: React.PropTypes.object + subsManager: React.PropTypes.object, // subsManager sub + transform: React.PropTypes.func // external function to filter result source }, mixins: [ReactMeteorData], getDefaultProps() { - return { useExternal: false, externalFilterDebounceWait: 300, externalResultsPerPage: 10 }; + return { useExternal: false, externalFilterDebounceWait: 300, externalResultsPerPage: 10, query: {} }; }, getInitialState() { @@ -73,7 +74,11 @@ const MeteorGriddle = React.createClass({ }, options)); } - const results = this.props.collection.find(this.state.query, options).fetch(); + // optional transform of collection for grid results + let results = this.props.collection.find(this.state.query, options).fetch(); + if (this.props.transform) { + results = this.props.transform(results); + } return { loading: !pubHandle.ready(), 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 8263b31a2f9..3fd5efa8036 100644 --- a/imports/plugins/included/default-theme/client/styles/dashboard/console.less +++ b/imports/plugins/included/default-theme/client/styles/dashboard/console.less @@ -217,14 +217,14 @@ body.admin-vertical .admin-controls { } // patch for autoform's mysteriously -// // not getting the form-control class -// .admin-controls.show-settings label { -// font-weight: lighter; -// } -// .admin-controls.show-settings .form-group input { -// .form-control; -// } -// .admin-controls.show-settings .form-group select { +// not getting the form-control class +.admin-controls-content label { + font-weight: lighter; +} +.admin-controls-content .form-group input:not([type=checkbox]) { + .form-control; +} +// .admin-controls-content .form-group select { // .form-control; // } diff --git a/imports/plugins/included/notifications/server/i18n/en.json b/imports/plugins/included/notifications/server/i18n/en.json index c5f612543fd..adf1086d74a 100644 --- a/imports/plugins/included/notifications/server/i18n/en.json +++ b/imports/plugins/included/notifications/server/i18n/en.json @@ -5,12 +5,12 @@ "reaction-notification": { "admin": { "dashboard": { - "smsLabel": "SMS", + "smsLabel": "SMS Notifications", "smsTitle": "SMS", "smsDescription": "Notifications" }, "settings": { - "smsSettingsLabel": "SMS settings" + "smsSettingsLabel": "SMS Notifications" } } } diff --git a/imports/plugins/included/payments-authnet/register.js b/imports/plugins/included/payments-authnet/register.js index 31151f45e59..209e4234c54 100644 --- a/imports/plugins/included/payments-authnet/register.js +++ b/imports/plugins/included/payments-authnet/register.js @@ -5,6 +5,7 @@ Reaction.registerPackage({ label: "Authorize.net", name: "reaction-auth-net", icon: "fa fa-credit-card", + autoEnable: true, settings: { "api_id": "", "transaction_key": "", diff --git a/imports/plugins/included/payments-braintree/register.js b/imports/plugins/included/payments-braintree/register.js index e62d33b05fe..8281595a4d6 100644 --- a/imports/plugins/included/payments-braintree/register.js +++ b/imports/plugins/included/payments-braintree/register.js @@ -4,7 +4,7 @@ import { Reaction } from "/server/api"; Reaction.registerPackage({ label: "BrainTree", icon: "fa fa-credit-card", - autoEnable: false, + autoEnable: true, name: "reaction-braintree", // usually same as meteor package settings: // private package settings config (blackbox) { diff --git a/imports/plugins/included/payments-example/register.js b/imports/plugins/included/payments-example/register.js index 4f3b01d3709..86b5b40c4a3 100644 --- a/imports/plugins/included/payments-example/register.js +++ b/imports/plugins/included/payments-example/register.js @@ -10,10 +10,10 @@ Reaction.registerPackage({ "mode": false, "apiKey": "", "example": { - enabled: true + enabled: false }, "example-paymentmethod": { - enabled: true + enabled: false } }, registry: [ diff --git a/imports/plugins/included/payments-paypal/client/templates/checkout/express/checkoutButton.html b/imports/plugins/included/payments-paypal/client/templates/checkout/express/checkoutButton.html index 8979fa20bba..b8e6a133bed 100644 --- a/imports/plugins/included/payments-paypal/client/templates/checkout/express/checkoutButton.html +++ b/imports/plugins/included/payments-paypal/client/templates/checkout/express/checkoutButton.html @@ -1,23 +1,29 @@ diff --git a/imports/plugins/included/payments-paypal/client/templates/checkout/express/checkoutButton.js b/imports/plugins/included/payments-paypal/client/templates/checkout/express/checkoutButton.js index 67f46509b15..98e5d4db5f0 100644 --- a/imports/plugins/included/payments-paypal/client/templates/checkout/express/checkoutButton.js +++ b/imports/plugins/included/payments-paypal/client/templates/checkout/express/checkoutButton.js @@ -13,22 +13,6 @@ import "./checkoutButton.html"; * provided by paypal. */ -/** - * Setup PayPal Express Checkout - * @param {Element} element DOM element - * @param {element} expressCheckoutSettings checkout settings - * @return {undefined} no return value - */ -function doSetup(element, expressCheckoutSettings) { - Session.set("paypalExpressSetup", true); - paypal.checkout.setup(expressCheckoutSettings.merchantId, { - environment: expressCheckoutSettings.mode, - button: element, - // Blank function to disable default paypal onClick functionality - click: function () {} - }); -} - /** * Checkout - Open PayPal Express popup * @return {undefined} no return value @@ -76,7 +60,8 @@ Template.paypalCheckoutButton.onCreated(function () { PaypalClientAPI.load(); this.state = new ReactiveDict(); this.state.setDefault({ - isConfigured: false + isConfigured: true, + isLoading: true }); }); @@ -92,11 +77,21 @@ Template.paypalCheckoutButton.onRendered(function () { if (PaypalClientAPI.loaded()) { const expressCheckoutSettings = Session.get("expressCheckoutSettings"); if (expressCheckoutSettingsValid(expressCheckoutSettings)) { - this.state.set("isConfigured", true); - doSetup(element, expressCheckoutSettings); + // setup paypal button for this checkout + // gives nada back to us? + paypal.checkout.setup(expressCheckoutSettings.merchantId, { + environment: expressCheckoutSettings.mode, + button: element, + // Blank function to disable default paypal onClick functionality + click: function () {} + }); + this.state.set("isLoading", false); } else { this.state.set("isConfigured", false); + this.state.set("isLoading", false); } + } else { + this.state.set("isLoading", true); } }); }); diff --git a/imports/plugins/included/payments-paypal/register.js b/imports/plugins/included/payments-paypal/register.js index 3edf1a6c455..c53d6f45646 100644 --- a/imports/plugins/included/payments-paypal/register.js +++ b/imports/plugins/included/payments-paypal/register.js @@ -4,6 +4,7 @@ Reaction.registerPackage({ label: "PayPal", name: "reaction-paypal", icon: "fa fa-paypal", + autoEnable: true, settings: { express: { enabled: false diff --git a/imports/plugins/included/payments-stripe/register.js b/imports/plugins/included/payments-stripe/register.js index dcedefc5de4..5eee1966275 100644 --- a/imports/plugins/included/payments-stripe/register.js +++ b/imports/plugins/included/payments-stripe/register.js @@ -5,6 +5,7 @@ Reaction.registerPackage({ label: "Stripe", name: "reaction-stripe", icon: "fa fa-cc-stripe", + autoEnable: true, settings: { "mode": false, "api_key": "", diff --git a/imports/plugins/included/product-variant/client/templates/products/productGrid/item.js b/imports/plugins/included/product-variant/client/templates/products/productGrid/item.js index 98de31af776..91ce82c1bf5 100644 --- a/imports/plugins/included/product-variant/client/templates/products/productGrid/item.js +++ b/imports/plugins/included/product-variant/client/templates/products/productGrid/item.js @@ -11,8 +11,8 @@ import { Media } from "/lib/collections"; import { isRevisionControlEnabled } from "/imports/plugins/core/revisions/lib/api"; -Template.productGridItems.onRendered( function () { - $(".container-main").on("click", function(event) { +Template.productGridItems.onRendered(function () { + $(".container-main").on("click", function (event) { if ($(event.target).closest(".product-grid-item").length === 0) { Session.set("productGrid/selectedProducts", []); @@ -27,7 +27,7 @@ Template.productGridItems.onRendered( function () { }); }); -Template.productGridItems.onDestroyed( function() { +Template.productGridItems.onDestroyed(function () { $(".container-main").off("click"); }); @@ -224,7 +224,7 @@ Template.productGridItems.events({ $checkbox.prop("checked", !$checkbox.prop("checked")).trigger("change"); } } else { - let $checkbox = template.$(`input[type=checkbox][value=${this._id}]`); + const $checkbox = template.$(`input[type=checkbox][value=${this._id}]`); Session.set("productGrid/selectedProducts", []); $checkbox.prop("checked", true).trigger("change"); diff --git a/imports/plugins/included/product-variant/client/templates/products/productGrid/notice.js b/imports/plugins/included/product-variant/client/templates/products/productGrid/notice.js index 405d7860e1d..2a05cace920 100644 --- a/imports/plugins/included/product-variant/client/templates/products/productGrid/notice.js +++ b/imports/plugins/included/product-variant/client/templates/products/productGrid/notice.js @@ -12,7 +12,7 @@ Template.gridNotice.helpers({ const inventoryThreshold = topVariant.lowInventoryWarningThreshold; const inventoryQuantity = ReactionProduct.getVariantQuantity(topVariant); - if(inventoryQuantity !== 0 && inventoryThreshold >= inventoryQuantity){ + if (inventoryQuantity !== 0 && inventoryThreshold >= inventoryQuantity) { return true; } } @@ -24,7 +24,7 @@ Template.gridNotice.helpers({ for (const topVariant of topVariants) { const inventoryQuantity = ReactionProduct.getVariantQuantity(topVariant); - if(inventoryQuantity > 0){ + if (inventoryQuantity > 0) { return false; } } diff --git a/imports/plugins/included/shipping-rates/client/index.js b/imports/plugins/included/shipping-rates/client/index.js new file mode 100644 index 00000000000..fa549ed7853 --- /dev/null +++ b/imports/plugins/included/shipping-rates/client/index.js @@ -0,0 +1 @@ +import "./templates"; diff --git a/imports/plugins/included/shipping-rates/client/templates/index.js b/imports/plugins/included/shipping-rates/client/templates/index.js new file mode 100644 index 00000000000..89769f8d6c6 --- /dev/null +++ b/imports/plugins/included/shipping-rates/client/templates/index.js @@ -0,0 +1,2 @@ +import "./settings/rates.html"; +import "./settings/rates.js"; diff --git a/imports/plugins/included/shipping-rates/client/templates/settings/rates.html b/imports/plugins/included/shipping-rates/client/templates/settings/rates.html new file mode 100644 index 00000000000..c72e62b614c --- /dev/null +++ b/imports/plugins/included/shipping-rates/client/templates/settings/rates.html @@ -0,0 +1,77 @@ + + + + diff --git a/imports/plugins/included/shipping-rates/client/templates/settings/rates.js b/imports/plugins/included/shipping-rates/client/templates/settings/rates.js new file mode 100644 index 00000000000..16d15fbe435 --- /dev/null +++ b/imports/plugins/included/shipping-rates/client/templates/settings/rates.js @@ -0,0 +1,228 @@ +import { Template } from "meteor/templating"; +import { ReactiveDict } from "meteor/reactive-dict"; +import { AutoForm } from "meteor/aldeed:autoform"; +import { Shipping } from "/lib/collections"; +import { i18next } from "/client/api"; +import MeteorGriddle from "/imports/plugins/core/ui-grid/client/griddle"; +import { IconButton, Loading } from "/imports/plugins/core/ui/client/components"; + +Template.shippingRatesSettings.onCreated(function () { + this.autorun(() => { + this.subscribe("Shipping"); + }); + + this.state = new ReactiveDict(); + this.state.setDefault({ + isEditing: false, + editingId: null + }); +}); + +Template.shippingRatesSettings.helpers({ + editButton() { + const instance = Template.instance(); + const state = instance.state; + const isEditing = state.equals("isEditing", true); + let editingId = state.get("editingId"); + // toggle edit state + if (!isEditing) { + editingId = null; + } + // return icon + return { + component: IconButton, + icon: "fa fa-plus", + onIcon: "fa fa-pencil", + toggle: true, + toggleOn: isEditing, + style: { + position: "relative", + top: "-25px", + right: "8px" + }, + onClick() { + // remove active rows from grid + $(".shipping-grid-row").removeClass("active"); + return state.set({ + isEditing: !isEditing, + editingId: editingId + }); + } + }; + }, + shippingGrid() { + const filteredFields = ["name", "group", "label", "rate"]; + const noDataMessage = i18next.t("admin.shippingSettings.noRatesFound"); + const instance = Template.instance(); + + // griddle helper to select row + function editRow(options) { + const currentId = instance.state.get("editingId"); + // isEditing is shipping rate object + instance.state.set("isEditing", options.props.data); + instance.state.set("editingId", options.props.data._id); + // toggle edit mode clicking on same row + if (currentId === options.props.data._id) { + instance.state.set("isEditing", null); + instance.state.set("editingId", null); + } + } + + // add shipping-grid-row class + const customRowMetaData = { + bodyCssClassName: () => { + return "shipping-grid-row"; + } + }; + + // add i18n handling to headers + const customColumnMetadata = []; + filteredFields.forEach(function (field) { + const columnMeta = { + columnName: field, + displayName: i18next.t(`admin.shippingGrid.${field}`) + }; + customColumnMetadata.push(columnMeta); + }); + + // filter and extract shipping methods + // from flat rate shipping provider + function transform(results) { + const result = []; + for (method of results) { + if (method.provider && method.provider.name === "flatRates") { + result.push(method.methods); + } + } + return result[0]; + } + + // return shipping Grid + return { + component: MeteorGriddle, + publication: "Shipping", + transform: transform, + collection: Shipping, + matchingResultsCount: "shipping-count", + showFilter: true, + useGriddleStyles: false, + rowMetadata: customRowMetaData, + filteredFields: filteredFields, + columns: filteredFields, + noDataMessage: noDataMessage, + onRowClick: editRow, + columnMetadata: customColumnMetadata, + externalLoadingComponent: Loading + }; + }, + + instance() { + const instance = Template.instance(); + return instance; + }, + + shippingRate() { + const instance = Template.instance(); + const id = instance.state.get("editingId"); + const providerRates = Shipping.findOne({ "provider.name": "flatRates" }) || {}; + let rate = {}; + if (providerRates && providerRates.methods) { + if (id) { + for (method of providerRates.methods) { + if (method._id === id) { + rate = method; + } + } + } else { + // a little trick to provide _id for insert + rate._id = providerRates._id; + } + } + return rate; + } +}); + +// +// on submit lets clear the form state +// +Template.shippingRatesSettings.events({ + "submit #shipping-rates-update-form": function () { + const instance = Template.instance(); + instance.state.set({ + isEditing: false, + editingId: null + }); + }, + "submit #shipping-rates-insert-form": function () { + const instance = Template.instance(); + instance.state.set({ + isEditing: true, + editingId: null + }); + }, + "click .cancel, .shipping-grid-row .active": function () { + instance = Template.instance(); + // remove active rows from grid + instance.state.set({ + isEditing: false, + editingId: null + }); + // ugly hack + $(".shipping-grid-row").removeClass("active"); + }, + "click .delete": function () { + const confirmTitle = i18next.t("admin.shippingSettings.confirmRateDelete"); + const confirmButtonText = i18next.t("app.delete"); + const instance = Template.instance(); + const id = instance.state.get("editingId"); + // confirm delete + Alerts.alert({ + title: confirmTitle, + type: "warning", + showCancelButton: true, + confirmButtonText: confirmButtonText + }, (isConfirm) => { + if (isConfirm) { + if (id) { + Meteor.call("shipping/rates/delete", id); + instance.state.set({ + isEditing: false, + editingId: null + }); + } + } + }); + }, + "click .shipping-grid-row": function (event) { + // toggle all rows off, then add our active row + $(".shipping-grid-row").removeClass("active"); + $(event.currentTarget).addClass("active"); + } +}); + +// +// Hooks for update and insert forms +// +AutoForm.hooks({ + "shipping-rates-update-form": { + onSuccess: function () { + return Alerts.toast(i18next.t("admin.shippingSettings.rateSaved"), + "success"); + }, + onError: function (operation, error) { + return Alerts.toast( + `${i18next.t("admin.shippingSettings.rateFailed")} ${error}`, "error" + ); + } + }, + "shipping-rates-insert-form": { + onSuccess: function () { + return Alerts.toast(i18next.t("admin.shippingSettings.rateSaved"), "success"); + }, + onError: function (operation, error) { + return Alerts.toast( + `${i18next.t("admin.shippingSettings.rateFailed")} ${error}`, "error" + ); + } + } +}); diff --git a/imports/plugins/included/shipping-rates/register.js b/imports/plugins/included/shipping-rates/register.js new file mode 100644 index 00000000000..725cc64d8dc --- /dev/null +++ b/imports/plugins/included/shipping-rates/register.js @@ -0,0 +1,40 @@ +import { Reaction } from "/server/api"; + +Reaction.registerPackage({ + label: "Shipping Rates", + name: "reaction-shipping-rates", + icon: "fa fa-truck-o", + autoEnable: true, + settings: { + name: "Flat Rate Service", + flatRates: { + enabled: false + } + }, + registry: [ + { + provides: "dashboard", + route: "/shipping/rates", + name: "shipping", + label: "Shipping", + description: "Provide shipping rates", + icon: "fa fa-truck", + priority: 1, + container: "core", + workflow: "coreDashboardWorkflow" + }, + { + provides: "shippingSettings", + name: "shipping/settings/flatRates", + label: "Flat Rate", + description: "Provide shipping rates", + icon: "fa fa-truck", + template: "shippingRatesSettings" + }, + { + template: "flatRateCheckoutShipping", + name: "shipping/flatRates", + provides: "shippingMethod" + } + ] +}); diff --git a/imports/plugins/included/shipping-rates/server/hooks/hooks.js b/imports/plugins/included/shipping-rates/server/hooks/hooks.js new file mode 100644 index 00000000000..e4f06953e02 --- /dev/null +++ b/imports/plugins/included/shipping-rates/server/hooks/hooks.js @@ -0,0 +1,80 @@ +import { check } from "meteor/check"; +import { Shipping, Packages } from "/lib/collections"; +import { Logger, Reaction, Hooks } from "/server/api"; +import { Cart as CartSchema } from "/lib/collections/schemas"; + +// callback ran on getShippingRates hook +function getShippingRates(rates, cart) { + check(cart, CartSchema); + const shops = []; + const products = cart.items; + + const { settings } = Packages.findOne({ + name: "reaction-shipping-rates", + shopId: Reaction.getShopId() + }); + + // must have cart items and package enabled to calculate shipping + if (!cart.items || settings.flatRates.enabled !== true) { + return rates; + } + + // default selector is current shop + let selector = { + "shopId": Reaction.getShopId(), + "provider.enabled": true + }; + + // create an array of shops, allowing + // the cart to have products from multiple shops + for (const product of products) { + if (product.shopId) { + shops.push(product.shopId); + } + } + // if we have multiple shops in cart + if ((shops !== null ? shops.length : void 0) > 0) { + selector = { + "shopId": { + $in: shops + }, + "provider.enabled": true + }; + } + + const shippingCollection = Shipping.find(selector); + shippingCollection.forEach(function (doc) { + const _results = []; + for (const method of doc.methods) { + if (!method.enabled) { + continue; + } + if (!method.rate) { + method.rate = 0; + } + if (!method.handling) { + method.handling = 0; + } + // Store shipping provider here in order to have it available in shipmentMethod + // for cart and order usage + if (!method.carrier) { + method.carrier = doc.provider.label; + } + const rate = method.rate + method.handling; + _results.push( + rates.push({ + carrier: doc.provider.label, + method: method, + rate: rate, + shopId: doc.shopId + }) + ); + } + return _results; + }); + + Logger.debug("Flat rate onGetShippingRates", rates); + return rates; +} +// run getShippingRates when the onGetShippingRates event runs +Hooks.Events.add("onGetShippingRates", getShippingRates); diff --git a/imports/plugins/included/shipping-rates/server/i18n/en.json b/imports/plugins/included/shipping-rates/server/i18n/en.json new file mode 100644 index 00000000000..5e32bef5e7a --- /dev/null +++ b/imports/plugins/included/shipping-rates/server/i18n/en.json @@ -0,0 +1,23 @@ +[{ + "i18n": "en", + "ns": "reaction-shipping-rates", + "translation": { + "reaction-shipping-rates": { + "admin": { + "shippingSettings": { + "flatRateLabel": "Flat Rate", + "confirmRateDelete": "Delete rate?", + "rateSaved": "Flat rate saved.", + "rateFailed": "Flat rate update failed.", + "noRatesFound": "No rates found." + }, + "shippingGrid": { + "name": "Name", + "group": "Group", + "rate": "Rate", + "label": "Label" + } + } + } + } +}] diff --git a/imports/plugins/included/shipping-rates/server/i18n/index.js b/imports/plugins/included/shipping-rates/server/i18n/index.js new file mode 100644 index 00000000000..0624a680704 --- /dev/null +++ b/imports/plugins/included/shipping-rates/server/i18n/index.js @@ -0,0 +1,32 @@ +import { loadTranslations } from "/server/startup/i18n"; + +// import ar from "./ar.json"; +// import bg from "./bg.json"; +// import de from "./de.json"; +// import el from "./el.json"; +import en from "./en.json"; +// import es from "./es.json"; +// import fr from "./fr.json"; +// import he from "./he.json"; +// import hr from "./hr.json"; +// import it from "./it.json"; +// import my from "./my.json"; +// import nb from "./nb.json"; +// import nl from "./nl.json"; +// import pl from "./pl.json"; +// import pt from "./pt.json"; +// import ro from "./ro.json"; +// import ru from "./ru.json"; +// import sl from "./sl.json"; +// import sv from "./sv.json"; +// import tr from "./tr.json"; +// import vi from "./vi.json"; +// import zh from "./zh.json"; + +// +// we want all the files in individual +// imports for easier handling by +// automated translation software +// +// loadTranslations([ar, bg, de, el, en, es, fr, he, hr, it, my, nb, nl, pl, pt, ro, ru, sl, sv, tr, vi, zh]); +loadTranslations([en]); diff --git a/imports/plugins/included/shipping-rates/server/index.js b/imports/plugins/included/shipping-rates/server/index.js new file mode 100644 index 00000000000..36bf69c9d0b --- /dev/null +++ b/imports/plugins/included/shipping-rates/server/index.js @@ -0,0 +1,3 @@ +import "./i18n"; +import "./methods/rates"; +import "./hooks/hooks"; diff --git a/imports/plugins/included/shipping-rates/server/lib/roles.js b/imports/plugins/included/shipping-rates/server/lib/roles.js new file mode 100644 index 00000000000..4e5805b7973 --- /dev/null +++ b/imports/plugins/included/shipping-rates/server/lib/roles.js @@ -0,0 +1 @@ +export const shippingRoles = ["admin", "owner", "shipping", "reaction-shippo"]; diff --git a/imports/plugins/included/shipping-rates/server/methods/rates.js b/imports/plugins/included/shipping-rates/server/methods/rates.js new file mode 100644 index 00000000000..5aa2eec4910 --- /dev/null +++ b/imports/plugins/included/shipping-rates/server/methods/rates.js @@ -0,0 +1,82 @@ +import { Meteor } from "meteor/meteor"; +import { check } from "meteor/check"; +import { Shipping } from "/lib/collections"; +import { ShippingMethod } from "/lib/collections/schemas"; +import { Reaction } from "/server/api"; +import { shippingRoles } from "../lib/roles"; + +export const methods = { + /** + * shipping/rates/add + * add new shipping flat rate methods + * @summary insert shipping method for a flat rate provider + * @param { Object } rate a valid ShippingMethod object + * @return { Number } insert result + */ + "shipping/rates/add": function (rate) { + check(rate, ShippingMethod); + // a little trickery + // we passed in the providerId + // as _id, perhaps cleanup + const providerId = rate._id; + rate._id = Random.id(); + + if (!Reaction.hasPermission(shippingRoles)) { + throw new Meteor.Error(403, "Access Denied"); + } + + return Shipping.update({ + _id: providerId + }, { + $addToSet: { + methods: rate + } + }); + }, + + /** + * shipping/rates/update + * @summary update shipping rate methods + * @param { Object } method shipping method object + * @return { Number } update result + */ + "shipping/rates/update": function (method) { + check(method, ShippingMethod); + if (!Reaction.hasPermission(shippingRoles)) { + throw new Meteor.Error(403, "Access Denied"); + } + const methodId = method._id; + + return Shipping.update({ + "methods._id": methodId + }, { + $set: { + "methods.$": method + } + }); + }, + + /** + * shipping/rates/delete + * @summary delete shipping rate method + * @param { String } rateId id of method to delete + * @return { Number } update result + */ + "shipping/rates/delete": function (rateId) { + check(rateId, String); + + if (!Reaction.hasPermission(shippingRoles)) { + throw new Meteor.Error(403, "Access Denied"); + } + + return Shipping.update({ + "methods._id": rateId + }, { + $pull: { + methods: { _id: rateId } + } + }); + } +}; + +Meteor.methods(methods); diff --git a/imports/plugins/included/shippo/client/index.js b/imports/plugins/included/shippo/client/index.js index d8463aa733a..2595a765e00 100644 --- a/imports/plugins/included/shippo/client/index.js +++ b/imports/plugins/included/shippo/client/index.js @@ -1 +1,2 @@ import "./settings/shippo"; +import "./settings/carriers"; diff --git a/imports/plugins/included/shippo/client/settings/carriers.html b/imports/plugins/included/shippo/client/settings/carriers.html new file mode 100644 index 00000000000..ae0bc734f48 --- /dev/null +++ b/imports/plugins/included/shippo/client/settings/carriers.html @@ -0,0 +1,39 @@ + + + diff --git a/imports/plugins/included/shippo/client/settings/carriers.js b/imports/plugins/included/shippo/client/settings/carriers.js new file mode 100644 index 00000000000..07f1174732c --- /dev/null +++ b/imports/plugins/included/shippo/client/settings/carriers.js @@ -0,0 +1,146 @@ +import { Template } from "meteor/templating"; +import { ReactiveDict } from "meteor/reactive-dict"; +import { AutoForm } from "meteor/aldeed:autoform"; +import { Shipping } from "/lib/collections"; +import { i18next } from "/client/api"; +import MeteorGriddle from "/imports/plugins/core/ui-grid/client/griddle"; +import { Loading } from "/imports/plugins/core/ui/client/components"; + +import "./carriers.html"; + +Template.shippoCarriers.onCreated(function () { + this.autorun(() => { + this.subscribe("Shipping"); + }); + + this.state = new ReactiveDict(); + this.state.setDefault({ + isEditing: false, + editingId: null + }); +}); + +Template.shippoCarriers.helpers({ + carrierGrid() { + const filteredFields = ["name", "carrier", "label", "enabled"]; + const noDataMessage = i18next.t("admin.shippingSettings.noCarriersFound"); + const instance = Template.instance(); + + // griddle helper to select row + function editRow(options) { + const currentId = instance.state.get("editingId"); + // isEditing is shipping rate object + instance.state.set("isEditing", options.props.data); + instance.state.set("editingId", options.props.data._id); + // toggle edit mode clicking on same row + if (currentId === options.props.data._id) { + instance.state.set("isEditing", null); + instance.state.set("editingId", null); + } + } + + // add shipping-carriers-grid-row class + const customRowMetaData = { + bodyCssClassName: () => { + return "shipping-carriers-grid-row"; + } + }; + + // add i18n handling to headers + const customColumnMetadata = []; + filteredFields.forEach(function (field) { + const columnMeta = { + columnName: field, + displayName: i18next.t(`admin.shippingGrid.${field}`) + }; + customColumnMetadata.push(columnMeta); + }); + + // filter and extract shipping methods + // from flat rate shipping provider + function transform(results) { + const result = []; + for (const method of results) { + if (method.provider && typeof method.provider.shippoProvider === "object") { + method.provider.carrier = method.name; + result.push(method.provider); + } + } + return result; + } + + // return shipping Grid + return { + component: MeteorGriddle, + publication: "Shipping", + transform: transform, + collection: Shipping, + showFilter: true, + useGriddleStyles: false, + rowMetadata: customRowMetaData, + filteredFields: filteredFields, + columns: filteredFields, + noDataMessage: noDataMessage, + onRowClick: editRow, + columnMetadata: customColumnMetadata, + externalLoadingComponent: Loading + }; + }, + + instance() { + const instance = Template.instance(); + return instance; + }, + + shippoCarrier() { + const instance = Template.instance(); + const id = instance.state.get("editingId"); + const shippoCarriers = Shipping.findOne({ "provider._id": id }); + return shippoCarriers.provider; + } +}); + +// +// on submit lets clear the form state +// +Template.shippoCarriers.events({ + "submit #shipping-carrier-insert-form": function () { + const instance = Template.instance(); + instance.state.set({ + isEditing: true, + editingId: null + }); + }, + "click .cancel, .shipping-carriers-grid-row .active": function () { + instance = Template.instance(); + // remove active rows from grid + instance.state.set({ + isEditing: false, + editingId: null + }); + // ugly hack + $(".shipping-carriers-grid-row").removeClass("active"); + }, + "click .shipping-carriers-grid-row": function (event) { + // toggle all rows off, then add our active row + $(".shipping-carriers-grid-row").removeClass("active"); + $(event.currentTarget).addClass("active"); + } +}); + +// +// Hooks for update and insert forms +// +AutoForm.hooks({ + "shipping-carrier-update-form": { + onSuccess: function () { + return Alerts.toast(i18next.t("admin.shippingSettings.carrierSaved"), + "success"); + }, + onError: function (operation, error) { + return Alerts.toast( + `${i18next.t("admin.shippingSettings.carrierFailed")} ${error}`, "error" + ); + } + } +}); diff --git a/imports/plugins/included/shippo/client/settings/shippo.html b/imports/plugins/included/shippo/client/settings/shippo.html index 3da92a2de9a..e1f87c996fa 100644 --- a/imports/plugins/included/shippo/client/settings/shippo.html +++ b/imports/plugins/included/shippo/client/settings/shippo.html @@ -1,9 +1,19 @@ diff --git a/imports/plugins/included/shippo/register.js b/imports/plugins/included/shippo/register.js index 2842ad83751..63ddc68569f 100644 --- a/imports/plugins/included/shippo/register.js +++ b/imports/plugins/included/shippo/register.js @@ -7,7 +7,7 @@ Reaction.registerPackage({ autoEnable: true, settings: { shippo: { - enabled: true + enabled: false }, // todo: move all settings in shippo subfield apiKey: "", @@ -27,22 +27,23 @@ Reaction.registerPackage({ }, { label: "Shippo", icon: "fa fa-plane", - route: "/dashboard/shippo", - provides: "settings", + name: "shipping/settings/shippo", + provides: "shippingSettings", container: "connection", template: "shippoSettings" } - // WIP: - // For now we use Flat Rate's checkout template( which inherits its methods from coreCheckoutShipping - // to show all shipping methods in the same panel. - // .If we are gonna proceed with different panel per provider, we need to enable the 'provides:"Shipping Method"', - // alter coreCheckoutShipping checkout.js and inherit from there (or write specific logic) for a shippo's - // checkout template. - // - // provides: "shippingMethod", - // name: "shipping/methods/shippo", - // template: "shippoCheckoutShipping" - // Not needed at the time cause the coreCheckoutShipping is enough(inherited from Flatrate) - //} +// WIP: +// TODO: Review custom shipping in checkout, are layout handling this requirement +// For now we use Flat Rate's checkout template( which inherits its methods from coreCheckoutShipping +// to show all shipping methods in the same panel. +// .If we are gonna proceed with different panel per provider, we need to enable the 'provides:"Shipping Method"', +// alter coreCheckoutShipping checkout.js and inherit from there (or write specific logic) for a shippo's +// checkout template. +// +// provides: "shippingMethod", +// name: "shipping/methods/shippo", +// template: "shippoCheckoutShipping" +// Not needed at the time cause the coreCheckoutShipping is enough(inherited from Flatrate) +// } ] }); diff --git a/imports/plugins/included/shippo/server/hooks/hooks.js b/imports/plugins/included/shippo/server/hooks/hooks.js new file mode 100644 index 00000000000..d796f210843 --- /dev/null +++ b/imports/plugins/included/shippo/server/hooks/hooks.js @@ -0,0 +1,65 @@ +import { Meteor } from "meteor/meteor"; +import { Shipping, Packages } from "/lib/collections"; +import { Logger, Reaction, Hooks } from "/server/api"; + +// callback ran on getShippingRates hook +function getShippingRates(rates, cart) { + const shops = []; + const products = cart.items; + + const { settings } = Packages.findOne({ + name: "reaction-shippo", + shopId: Reaction.getShopId() + }); + + // must have cart items and package enabled to calculate shipping + if (!cart.items || settings.shippo.enabled !== true) { + return rates; + } + + // default selector is current shop + let selector = { + "shopId": Reaction.getShopId(), + "provider.enabled": true + }; + + // create an array of shops, allowing + // the cart to have products from multiple shops + for (const product of products) { + if (product.shopId) { + shops.push(product.shopId); + } + } + // if we have multiple shops in cart + if ((shops !== null ? shops.length : void 0) > 0) { + selector = { + "shopId": { + $in: shops + }, + "provider.enabled": true + }; + } + + const shippingCollection = Shipping.find(selector); + const shippoDocs = {}; + if (shippingCollection) { + shippingCollection.forEach(function (doc) { + // If provider is from Shippo, put it in an object to get rates dynamically(shippoApi) for all of them after. + if (doc.provider.shippoProvider) { + shippoDocs[doc.provider.shippoProvider.carrierAccountId] = doc; + } + }); + + // Get shippingRates from Shippo + if (Object.keys(shippoDocs).length > 0) { + const shippoRates = Meteor.call("shippo/getShippingRatesForCart", cart._id, shippoDocs); + rates.push(...shippoRates); + } + } + + Logger.debug("Shippo onGetShippingRates", rates); + return rates; +} + +// run getShippingRates when the onGetShippingRates event runs +Hooks.Events.add("onGetShippingRates", getShippingRates); diff --git a/imports/plugins/included/shippo/server/i18n/en.json b/imports/plugins/included/shippo/server/i18n/en.json index f73387951d7..833d502f572 100644 --- a/imports/plugins/included/shippo/server/i18n/en.json +++ b/imports/plugins/included/shippo/server/i18n/en.json @@ -8,6 +8,16 @@ "shippoLabel": "Shippo", "shippoTitle": "Shippo", "shippoDescription": "Shipping labels and package tracking" + }, + "shippingGrid": { + "carrier": "Carrier", + "enabled": "Enabled" + }, + "shippingSettings": { + "noCarriersFound": "No shipping carriers configured.", + "editApiKey": "Edit API Key", + "carrierSaved": "Carrier saved successfully.", + "carrierFailed": "Error updating carrier." } }, "shippo": { diff --git a/imports/plugins/included/shippo/server/index.js b/imports/plugins/included/shippo/server/index.js index 7c8a97225e1..b573d50cab5 100644 --- a/imports/plugins/included/shippo/server/index.js +++ b/imports/plugins/included/shippo/server/index.js @@ -1,3 +1,4 @@ import "./methods"; import "./jobs"; import "./i18n"; +import "./hooks/hooks"; diff --git a/imports/plugins/included/shippo/server/jobs/shippo.js b/imports/plugins/included/shippo/server/jobs/shippo.js index 30e94ad38ec..74ba64b6d68 100644 --- a/imports/plugins/included/shippo/server/jobs/shippo.js +++ b/imports/plugins/included/shippo/server/jobs/shippo.js @@ -27,8 +27,8 @@ Hooks.Events.add("afterCoreInit", () => { if (!config.shippo.enabled || !refreshPeriod) { return; } - - Logger.info(`Adding shippo/fetchTrackingStatusForOrders to JobControl. Refresh ${refreshPeriod}`); + // there might be some validity to this being Logger.info. + Logger.debug(`Adding shippo/fetchTrackingStatusForOrders to JobControl. Refresh ${refreshPeriod}`); new Job(Jobs, "shippo/fetchTrackingStatusForOrdersJob", {}) .priority("normal") .retry({ @@ -56,6 +56,7 @@ export default function () { workTimeout: 180 * 1000 }, (job, callback) => { + // TODO review meteor runAsUser and add to project documentation // As this is run by the Server and we don't have userId()/this.userId // which "shippo/fetchTrackingStatusForOrders" need, we use dispatch:run-as-user // An alternative way is https://forums.meteor.com/t/cant-set-logged-in-user-for-rest-calls/18656/3 @@ -64,8 +65,8 @@ export default function () { if (error) { job.done(error.toString(), { repeatId: true }); } else { - const success = "Latest Shippo's Tracking Status of Orders fetched successfully."; - Logger.info(success); + const success = "Shippo tracking status updated."; + Logger.debug(success); job.done(success, { repeatId: true }); } }); diff --git a/imports/plugins/included/shippo/server/lib/roles.js b/imports/plugins/included/shippo/server/lib/roles.js new file mode 100644 index 00000000000..6ddb33fb351 --- /dev/null +++ b/imports/plugins/included/shippo/server/lib/roles.js @@ -0,0 +1 @@ +export const shippingRoles = ["admin", "owner", "shipping", "reaction-shipping-rates"]; diff --git a/imports/plugins/included/shippo/server/methods/carriers.js b/imports/plugins/included/shippo/server/methods/carriers.js new file mode 100644 index 00000000000..d952c977058 --- /dev/null +++ b/imports/plugins/included/shippo/server/methods/carriers.js @@ -0,0 +1,31 @@ +import { Meteor } from "meteor/meteor"; +import { check } from "meteor/check"; +import { Shipping } from "/lib/collections"; +import { Reaction } from "/server/api"; +import { shippingRoles } from "../lib/roles"; + +export const methods = { + /** + * shippo/carrier/update + * @summary update Shipping methods for a provider + * @param {String} provider provider object + * @return {Number} update result + */ + "shippo/carrier/update": function (provider) { + check(provider, Object); // ShippingProvider + if (!Reaction.hasPermission(shippingRoles)) { + throw new Meteor.Error(403, "Access Denied"); + } + const method = {}; + method.provider = provider; + const flatten = require("flatten-obj")(); + const update = flatten(method); + return Shipping.update({ + "provider._id": provider._id + }, { + $set: update + }); + } +}; + +Meteor.methods(methods); diff --git a/imports/plugins/included/shippo/server/methods/index.js b/imports/plugins/included/shippo/server/methods/index.js index d8ba8f4d4ca..95523e245b7 100644 --- a/imports/plugins/included/shippo/server/methods/index.js +++ b/imports/plugins/included/shippo/server/methods/index.js @@ -1 +1,2 @@ import "./shippo"; +import "./carriers"; diff --git a/imports/plugins/included/shippo/server/methods/shippo.js b/imports/plugins/included/shippo/server/methods/shippo.js index 9810f375911..d06020d91ad 100644 --- a/imports/plugins/included/shippo/server/methods/shippo.js +++ b/imports/plugins/included/shippo/server/methods/shippo.js @@ -1,8 +1,11 @@ /* eslint camelcase: 0 */ +import { Meteor } from "meteor/meteor"; +import { check } from "meteor/check"; import { Reaction } from "/server/api"; import { Packages, Accounts, Shops, Shipping, Cart, Orders } from "/lib/collections"; import { ShippoPackageConfig } from "../../lib/collections/schemas"; import { ShippoApi } from "./shippoapi"; +import { shippingRoles } from "../lib/roles"; // Creates an address (for sender or recipient) suitable for Shippo Api Calls given // a reaction address an email and a purpose("QUOTE"|"PURCHASE") @@ -53,7 +56,7 @@ function ratesParser(shippoRates, shippoDocs) { rate: rateAmount, handling: 0, carrier: rate.provider, - shippoMethod: { + settings: { // carrierAccount: rate.carrier_account, rateId: rate.object_id, serviceLevelToken: rate.servicelevel_token @@ -69,7 +72,7 @@ function ratesParser(shippoRates, shippoDocs) { // Filters the carrier list and gets and parses only the ones that are activated in the Shippo Account function filterActiveCarriers(carrierList) { - let activeCarriers = []; + const activeCarriers = []; if (carrierList.results && carrierList.count) { carrierList.results.forEach(carrier => { if (carrier.active) { @@ -155,8 +158,8 @@ function updateShippoProviders(activeCarriers, shopId = Reaction.getShopId()) { // Ids of Shippo Carriers that exist currently as docs in Shipping Collection const currentCarriersIds = currentShippoProviders.map(doc => doc.provider.shippoProvider.carrierAccountId); - let newActiveCarriers = []; - let unchangedActiveCarriersIds = []; + const newActiveCarriers = []; + const unchangedActiveCarriersIds = []; activeCarriers.forEach(carrier => { const carrierId = carrier.carrierAccountId; if (!currentCarriersIds.includes(carrierId)) { @@ -177,8 +180,7 @@ function updateShippoProviders(activeCarriers, shopId = Reaction.getShopId()) { return true; } -Meteor.methods({ - +export const methods = { /** * Updates the Api key(Live/Test Token) used for connection with the Shippo account. * Also inserts(and deletes if already exist) docs in the Shipping collection each of the @@ -197,7 +199,7 @@ Meteor.methods({ // Make sure user has proper rights to this package const { shopId } = Packages.findOne({ _id }, { field: { shopId: 1 } }); - if (shopId && Roles.userIsInRole(this.userId, ["admin", "owner"], shopId)) { + if (shopId && Roles.userIsInRole(this.userId, shippingRoles, shopId)) { // If user wants to delete existing key if (modifier.hasOwnProperty("$unset")) { const customModifier = { $set: { "settings.apiKey": null } }; @@ -238,7 +240,7 @@ Meteor.methods({ "shippo/fetchProviders"() { const shopId = Reaction.getShopId(); - if (Roles.userIsInRole(this.userId, ["admin", "owner"], shopId)) { + if (Roles.userIsInRole(this.userId, shippingRoles, shopId)) { const apiKey = getApiKey(shopId); if (!apiKey) { return false; @@ -337,7 +339,6 @@ Meteor.methods({ check(shippoDocs, Object); const cart = Cart.findOne(cartId); if (cart && cart.userId === this.userId) { // confirm user has the right - let shippoAddressFrom; let shippoAddressTo; let shippoParcel; const purpose = "PURCHASE"; @@ -357,8 +358,8 @@ Meteor.methods({ if (!apiKey) { return []; } - - shippoAddressFrom = createShippoAddress(shop.addressBook[0], shop.emails[0].address, purpose); + // TODO create a shipping address book record for shop. + const shippoAddressFrom = createShippoAddress(shop.addressBook[0], shop.emails[0].address, purpose); // product in the cart has to have parcel property with the dimensions if (cart.items && cart.items[0] && cart.items[0].parcel) { const unitOfMeasure = shop && shop.unitsOfMeasure && shop.unitsOfMeasure[0].uom || "KG"; @@ -376,7 +377,15 @@ Meteor.methods({ }); // check that there is address available in cart if (cart.shipping && cart.shipping[0] && cart.shipping[0].address) { - shippoAddressTo = createShippoAddress(cart.shipping[0].address, buyer.emails[0].address, purpose); + // TODO take a more elegant approach to guest checkout -> no email address + // add Logger.trace if this smells + let email = shop.emails[0].address || "noreply@localhost"; + if (buyer.emails.length > 0) { + if (buyer.emails[0].address) { + email = buyer.emails[0].address; + } + } + shippoAddressTo = createShippoAddress(cart.shipping[0].address, email, purpose); } else { return []; } @@ -408,17 +417,17 @@ Meteor.methods({ check(orderId, String); const order = Orders.findOne(orderId); // Make sure user has permissions in the shop's order - if (Roles.userIsInRole(this.userId, ["admin", "owner"], order.shopId)) { + if (Roles.userIsInRole(this.userId, shippingRoles, order.shopId)) { // Here we done it for the first/unique Shipment only . in the near future it will be done for multiple ones - if (order.shipping[0].shipmentMethod.shippoMethod && - order.shipping[0].shipmentMethod.shippoMethod.rateId) { + if (order.shipping[0].shipmentMethod.settings && + order.shipping[0].shipmentMethod.settings.rateId) { const apiKey = getApiKey(order.shopId); // If for a weird reason Shop hasn't a Shippo Api key anymore you have to throw an error // cause the Shippo label purchasing is not gonna happen. if (!apiKey) { throw new Meteor.Error("403", "Invalid Shippo Credentials"); } - const rateId = order.shipping[0].shipmentMethod.shippoMethod.rateId; + const rateId = order.shipping[0].shipmentMethod.settings.rateId; // make the actual purchase const transaction = ShippoApi.methods.createTransaction.call({ rateId, apiKey }); @@ -438,4 +447,6 @@ Meteor.methods({ return false; } -}); +}; + +Meteor.methods(methods); diff --git a/lib/collections/schemas/shipping.js b/lib/collections/schemas/shipping.js index 6f892da35d5..fe174932647 100644 --- a/lib/collections/schemas/shipping.js +++ b/lib/collections/schemas/shipping.js @@ -7,6 +7,7 @@ import { Workflow } from "./workflow"; /** * ShippoShippingMethod Schema + * TODO move shippo related schema to shippo module * This will only exist in ShippingMethods Inside Cart/Order and not DB shipping Collection * as Shippo Methods are Dynamic. */ @@ -42,7 +43,8 @@ export const ShippingMethod = new SimpleSchema({ }, "group": { type: String, - label: "Group" + label: "Group", + allowedValues: ["Ground", "Priority", "One Day", "Free"] }, "cost": { type: Number, @@ -67,7 +69,7 @@ export const ShippingMethod = new SimpleSchema({ "enabled": { type: Boolean, label: "Enabled", - defaultValue: true + defaultValue: false }, "validRanges": { type: Array, @@ -123,7 +125,7 @@ export const ShippingMethod = new SimpleSchema({ type: String, // Alternatively we can make an extra Schema:ShipmentMethod, that inherits optional: true // ShippingMethod and add the optional carrier field }, - "shippoMethod": { + "settings": { type: ShippoShippingMethod, optional: true } @@ -324,9 +326,16 @@ export const ShippoShippingProvider = new SimpleSchema({ */ export const ShippingProvider = new SimpleSchema({ + _id: { + type: String, + label: "Provider Id", + optional: true, + autoValue: schemaIdAutoValue + }, name: { type: String, - label: "Service Code" + label: "Service Code", + optional: true }, label: { type: String, diff --git a/package.json b/package.json index ce551870978..571c1c15c94 100644 --- a/package.json +++ b/package.json @@ -19,96 +19,97 @@ "url": "https://github.com/reactioncommerce/reaction/issues" }, "dependencies": { - "@reactioncommerce/authorize-net": "^1.0.7", + "@reactioncommerce/authorize-net": "^1.0.8", "accounting-js": "^1.1.1", - "autoprefixer": "^6.5.3", - "autosize": "^3.0.19", + "autoprefixer": "^6.7.1", + "autosize": "^3.0.20", "avalara-taxrates": "^1.0.1", - "babel-runtime": "^6.18.0", + "babel-runtime": "^6.22.0", "bcrypt": "^1.0.2", "bootstrap": "^3.3.7", - "braintree": "^1.41.0", - "bunyan": "^1.8.1", + "braintree": "^1.47.0", + "bunyan": "^1.8.5", "bunyan-format": "^0.2.1", - "bunyan-loggly": "^1.1.0", + "bunyan-loggly": "^1.2.0", "classnames": "^2.2.5", "country-data": "^0.0.31", "css-annotation": "^0.6.2", "deep-diff": "^0.3.4", "dnd-core": "^2.0.2", "faker": "^3.1.0", - "fibers": "^1.0.14", - "font-awesome": "^4.6.3", - "griddle-react": "^0.7.0", - "handlebars": "^4.0.5", - "i18next": "^4.1.0", - "i18next-browser-languagedetector": "^1.0.0", + "fibers": "^1.0.15", + "flatten-obj": "^3.1.0", + "font-awesome": "^4.7.0", + "griddle-react": "^0.7.1", + "handlebars": "^4.0.6", + "i18next": "^6.0.3", + "i18next-browser-languagedetector": "^1.0.1", "i18next-localstorage-cache": "^0.3.0", "i18next-sprintf-postprocessor": "^0.2.2", "immutable": "^3.8.1", "jquery": "^3.1.1", - "jquery-i18next": "^1.1.0", + "jquery-i18next": "^1.2.0", "later": "^1.2.0", - "lodash": "^4.17.2", + "lodash": "^4.17.4", "lodash.pick": "^4.4.0", - "meteor-node-stubs": "^0.2.3", - "moment": "^2.17.0", - "moment-timezone": "^0.5.9", - "nexmo": "^1.1.0", - "node-geocoder": "^3.15.0", - "nodemailer": "^2.6.4", - "nodemailer-wellknown": "^0.2.0", + "meteor-node-stubs": "^0.2.5", + "moment": "^2.17.1", + "moment-timezone": "^0.5.11", + "nexmo": "^1.1.2", + "node-geocoder": "^3.16.0", + "nodemailer": "^2.7.2", + "nodemailer-wellknown": "^0.2.1", "npm-shrinkwrap": "^6.0.2", - "paypal-rest-sdk": "^1.6.9", - "postcss": "^5.2.5", + "paypal-rest-sdk": "^1.7.1", + "postcss": "^5.2.11", "postcss-js": "^0.2.0", - "prerender-node": "^2.6.0", + "prerender-node": "^2.7.0", "radium": "^0.18.1", - "react": "^15.3.2", - "react-addons-create-fragment": "^15.3.2", - "react-addons-pure-render-mixin": "^15.3.2", - "react-autosuggest": "^7.0.1", - "react-bootstrap": "^0.30.5", - "react-color": "^2.3.2", + "react": "^15.4.2", + "react-addons-create-fragment": "^15.4.2", + "react-addons-pure-render-mixin": "^15.4.2", + "react-autosuggest": "^8.0.0", + "react-bootstrap": "^0.30.7", + "react-color": "^2.11.1", "react-dnd": "^2.1.4", "react-dnd-html5-backend": "^2.1.2", - "react-dom": "^15.3.2", - "react-dropzone": "^3.6.0", - "react-helmet": "^3.1.0", + "react-dom": "^15.4.2", + "react-dropzone": "^3.9.2", + "react-helmet": "^4.0.0", "react-komposer": "^2.0.0", "react-nouislider": "^1.14.2", - "react-onclickoutside": "^5.7.1", + "react-onclickoutside": "^5.8.4", "react-select": "^1.0.0-rc.2", "react-simple-di": "^1.2.0", "react-taco-table": "^0.5.0", - "react-tether": "^0.5.2", + "react-tether": "^0.5.5", "react-textarea-autosize": "^4.0.5", - "shippo": "^1.1.3", + "shippo": "^1.2.0", "sortablejs": "^1.5.0-rc1", - "stripe": "^4.11.0", - "sweetalert2": "^6.1.0", - "swiper": "^3.3.1", + "stripe": "^4.15.0", + "sweetalert2": "^6.3.2", + "swiper": "^3.4.1", "tether-drop": "^1.4.2", "tether-tooltip": "^1.2.0", - "transliteration": "^1.1.9", + "transliteration": "^1.2.3", "twilio": "^2.11.1", "url": "^0.11.0", - "velocity-animate": "^1.3.1", - "velocity-react": "^1.1.11" + "velocity-animate": "^1.4.0", + "velocity-react": "^1.2.1" }, "devDependencies": { - "babel-eslint": "^7.0.0", - "babel-plugin-lodash": "^3.2.9", - "babel-preset-stage-2": "^6.17.0", - "browserstack-local": "^1.0.0", + "babel-eslint": "^7.1.1", + "babel-plugin-lodash": "^3.2.11", + "babel-preset-stage-2": "^6.22.0", + "browserstack-local": "^1.3.0", "chai": "^3.5.0", - "eslint": "^3.7.1", - "eslint-plugin-react": "^6.3.0", - "js-yaml": "^3.6.1", - "react-addons-test-utils": "^15.3.2", - "wdio-allure-reporter": "^0.1.1", - "wdio-mocha-framework": "^0.5.4", - "webdriverio": "^4.2.16" + "eslint": "^3.14.1", + "eslint-plugin-react": "^6.9.0", + "js-yaml": "^3.7.0", + "react-addons-test-utils": "^15.4.2", + "wdio-allure-reporter": "^0.1.2", + "wdio-mocha-framework": "^0.5.8", + "webdriverio": "^4.6.2" }, "postcss": { "plugins": { diff --git a/private/data/Shipping.json b/private/data/Shipping.json index 1f40d8f817f..a3498100d9f 100644 --- a/private/data/Shipping.json +++ b/private/data/Shipping.json @@ -4,7 +4,6 @@ "name": "Free", "label": "Free Shipping", "group": "Ground", - "enabled": true, "rate": 0, "validLocales": [{ "deliveryBegin": 2, @@ -17,7 +16,6 @@ "name": "Standard", "label": "Standard", "group": "Ground", - "enabled": true, "rate": 2.99, "validLocales": [{ "deliveryBegin": 2, @@ -27,7 +25,6 @@ "name": "Priority", "label": "Priority", "group": "Priority", - "enabled": true, "rate": 6.99, "validLocales": [{ "deliveryBegin": 1, @@ -35,8 +32,7 @@ }] }], "provider": { - "name": "Flat Rate", - "label": "Flat Rate", - "enabled": true + "name": "flatRates", + "label": "Flat Rate" } }] diff --git a/private/data/i18n/en.json b/private/data/i18n/en.json index 6b715ce0c77..18170900002 100644 --- a/private/data/i18n/en.json +++ b/private/data/i18n/en.json @@ -576,68 +576,6 @@ "checkoutReview": { "review": "Review" }, - "checkoutShipping": { - "selectShippingOption": "Select shipping option", - "noShippingMethods": "No shipping methods are configured.", - "configureNow": "Configure now.", - "shipping": "Shipping" - }, - "shipping": { - "addShippingProvider": "Add shipping provider", - "editShippingProvider": "Edit shipping provider", - "addShippingMethod": "Add shipping method", - "editShippingMethod": "Edit shipping method", - "deleteShippingMethod": "Delete shipping method", - "noSettingsForThisView": "No settings for this view", - "noShippingMethods": "No shipping methods are configured.", - "removeShippingMethodConfirm": "Are you sure you want to delete {{method}}?", - "removeShippingMethodTitle": "Remove Shipping Method", - "shippingMethodDeleted": "This shipping method has been deleted.", - "removeShippingProvider": "Remove Shipping Provider", - "removeShippingProviderConfirm": "Are you sure you want to delete {{provider}}?", - "shippingProviderSaved": "Shipping provider saved.", - "shippingProviderUpdated": "Shipping provider data updated.", - "shippingMethodRateAdded": "Shipping method rate added.", - "shippingMethodRateUpdated": "Shipping method rate updated.", - "name": "Name", - "label": "Label", - "group": "Group", - "cost": "Cost", - "handling": "Handling", - "rate": "Rate", - "enabled": "Enabled", - "disabled": "Disabled", - "addRate": "Add rate", - "updateRate": "Update {{name}} rate", - "addNewCondition": "Add new condition", - "deleteCondition": "Delete condition", - "provider": { - "name": "Service Code", - "label": "Public Label", - "enabled": "Enabled" - } - }, - "shippingMethod": { - "name": "Method Name", - "label": "Public Label", - "group": "Group", - "cost": "Cost", - "handling": "Handling", - "rate": "Rate", - "enabled": "Enabled", - "matchingCartRanges": "Matching Cart Ranges", - "validRanges": { - "begin": "Begin", - "end": "End" - }, - "matchingLocales": "Matching Locales", - "validLocales": { - "origination": "From", - "destination": "To", - "deliveryBegin": "Shipping Est.", - "deliveryEnd": "Delivery Est." - } - }, "uom": { "OZ": "Ounces", "LB": "Pounds", diff --git a/server/methods/core/orders.js b/server/methods/core/orders.js index 129fd9b973f..6f6dadea068 100644 --- a/server/methods/core/orders.js +++ b/server/methods/core/orders.js @@ -843,11 +843,9 @@ export const methods = { }); // Temporarily(?) put here the Shippo's method/label purchasing.After a succesfull capture fund - if (order.shipping[0].shipmentMethod.shippoMethod) { + if (order.shipping[0].shipmentMethod.settings) { Meteor.call("shippo/confirmShippingMethodForOrder", orderId); } - - } else { if (result && result.error) { Logger.fatal("Failed to capture transaction.", order, paymentMethod.transactionId, result.error); diff --git a/server/methods/core/shipping.js b/server/methods/core/shipping.js index 511633e544d..7310d7364f2 100644 --- a/server/methods/core/shipping.js +++ b/server/methods/core/shipping.js @@ -1,15 +1,14 @@ -import _ from "lodash"; import { Meteor } from "meteor/meteor"; import { check } from "meteor/check"; -import { Cart, Shipping } from "/lib/collections"; -import { Logger, Reaction } from "/server/api"; +import { Cart } from "/lib/collections"; +import { Logger, Hooks } from "/server/api"; import { Cart as CartSchema } from "/lib/collections/schemas"; /* * Reaction Shipping Methods * methods typically used for checkout (shipping, taxes, etc) */ -Meteor.methods({ +export const methods = { /** * shipping/updateShipmentQuotes * @summary gets shipping rates and updates the users cart methods @@ -24,17 +23,15 @@ Meteor.methods({ check(cartId, String); this.unblock(); const cart = Cart.findOne(cartId); + + check(cart, CartSchema); + if (cart) { const rates = Meteor.call("shipping/getShippingRates", cart); - // no rates found - if (!rates) { - return []; - } let selector; let update; - // temp hack until we build out multiple shipment handlers - // if we have an existing item update it, otherwise add to set. - if (cart.shipping && rates.length > 0) { + // temp hack until we build out multiple shipment handlers if we have an existing item update it, otherwise add to set. + if (cart.shipping) { selector = { "_id": cartId, "shipping._id": cart.shipping[0]._id @@ -57,15 +54,13 @@ Meteor.methods({ }; } // add quotes to the cart - if (rates.length > 0) { - Cart.update(selector, update, function (error) { - if (error) { - Logger.warn(`Error adding rates to cart ${cartId}`, error); - return; - } - Logger.debug(`Success adding rates to cart ${cartId}`, rates); - }); - } + Cart.update(selector, update, function (error) { + if (error) { + Logger.warn(`Error adding rates to cart ${cartId}`, error); + return; + } + Logger.debug(`Success adding rates to cart ${cartId}`, rates); + }); } }, @@ -78,77 +73,16 @@ Meteor.methods({ "shipping/getShippingRates": function (cart) { check(cart, CartSchema); const rates = []; - const shops = []; - const products = cart.items; - // default selector is current shop - let selector = { - "shopId": Reaction.getShopId(), - "provider.enabled": true - }; - // must have products to calculate shipping + // must have items to calculate shipping if (!cart.items) { return []; } - // create an array of shops, allowing - // the cart to have products from multiple shops - for (const product of products) { - if (product.shopId) { - shops.push(product.shopId); - } - } - // if we have multiple shops in cart - if ((shops !== null ? shops.length : void 0) > 0) { - selector = { - "shopId": { - $in: shops - }, - "provider.enabled": true - }; - } - - const shippingCollection = Shipping.find(selector); - let shippoDocs = {}; - - shippingCollection.forEach(function (doc) { - const _results = []; - // If provider is from Shippo, put it in an object to get rates dynamically(shippoApi) for all of them after. - if (doc.provider.shippoProvider) { - shippoDocs[doc.provider.shippoProvider.carrierAccountId] = doc; - } else { - for (const method of doc.methods) { - if (!method.enabled) { - continue; - } - if (!method.rate) { - method.rate = 0; - } - if (!method.handling) { - method.handling = 0; - } - // Store shipping provider here in order to have it available in shipmentMethod - // for cart and order usage - if (!method.carrier) { - method.carrier = doc.provider.label; - } - const rate = method.rate + method.handling; - _results.push( - rates.push({ - carrier: doc.provider.label, - method: method, - rate: rate, - shopId: doc.shopId - }) - ); - } - return _results; - } - }); - // Get shippingRates from Shippo - if (!_.isEmpty(shippoDocs)) { - const shippoRates = Meteor.call("shippo/getShippingRatesForCart", cart._id, shippoDocs); - rates.push(...shippoRates); - } + // hooks for other shipping rate events + // all callbacks should return rates + Hooks.Events.run("onGetShippingRates", rates, cart); Logger.debug("getShippingRates returning rates", rates); return rates; } -}); +}; + +Meteor.methods(methods); diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index 48dbcf2197b..324e9990844 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -1,5 +1,5 @@ import { Products, Revisions } from "/lib/collections"; -import { Reaction } from "/server/api"; +import { Reaction, Logger } from "/server/api"; import { RevisionApi } from "/imports/plugins/core/revisions/lib/api/revisions"; // @@ -69,11 +69,19 @@ const filters = new SimpleSchema({ */ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, sort = {}) { check(productScrollLimit, Number); - check(productFilters, Match.OneOf(undefined, filters)); + check(productFilters, Match.OneOf(undefined, Object)); check(sort, Match.OneOf(undefined, Object)); + // if there are filter/params that don't match the schema + // validate, catch except but return no results + try { + check(productFilters, Match.OneOf(undefined, filters)); + } catch (e) { + Logger.debug(e, "Invalid Product Filters"); + return this.ready(); + } + // ensure that we've got a shop instance const shop = Reaction.getCurrentShop(); - if (typeof shop !== "object") { return this.ready(); } diff --git a/server/publications/collections/shipping.js b/server/publications/collections/shipping.js index bcead6dd656..7606a159652 100644 --- a/server/publications/collections/shipping.js +++ b/server/publications/collections/shipping.js @@ -1,16 +1,32 @@ +import { Meteor } from "meteor/meteor"; +import { Match, check } from "meteor/check"; import { Shipping } from "/lib/collections"; import { Reaction } from "/server/api"; - +import { Counts } from "meteor/tmeasday:publish-counts"; /** * shipping */ -Meteor.publish("Shipping", function () { +Meteor.publish("Shipping", function (query, options) { + check(query, Match.Optional(Object)); + check(options, Match.Optional(Object)); + const shopId = Reaction.getShopId(); if (!shopId) { return this.ready(); } - return Shipping.find({ - shopId: shopId - }); + const select = query || {}; + select.shopId = shopId; + + // appends a count to the collection + // we're doing this for use with griddleTable + Counts.publish(this, "shipping-count", Shipping.find( + select, + options + )); + + return Shipping.find( + select, + options + ); });