From 168a2ed505bc43fdd00720291e6c6506062722ca Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Wed, 28 Feb 2018 09:49:00 -0600 Subject: [PATCH 01/77] refactor: WIP creating a new product grid publication, updated productsContainer to sub to the new product grid publication --- .../containers/productsContainer.js | 2 +- server/publications/collections/products.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/imports/plugins/included/product-variant/containers/productsContainer.js b/imports/plugins/included/product-variant/containers/productsContainer.js index d2a310f0041..08926e1dc04 100644 --- a/imports/plugins/included/product-variant/containers/productsContainer.js +++ b/imports/plugins/included/product-variant/containers/productsContainer.js @@ -160,7 +160,7 @@ function composer(props, onData) { } const queryParams = Object.assign({}, tags, Reaction.Router.current().query, shopIds); - const productsSubscription = Meteor.subscribe("Products", scrollLimit, queryParams, sort, editMode); + const productsSubscription = Meteor.subscribe("Products/grid", scrollLimit, queryParams, sort, editMode); if (productsSubscription.ready()) { window.prerenderReady = true; diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index 077924cb877..0400ca934e0 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -529,3 +529,20 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so // limit: productScrollLimit }); }); + + +Meteor.publish("Products/grid", (productScrollLimit = 24, productFilters, sort = {}, editMode = true) => { + check(productScrollLimit, Number); + check(productFilters, Match.OneOf(undefined, Object)); + check(sort, Match.OneOf(undefined, Object)); + check(editMode, Match.Maybe(Boolean)); + + console.log("new prod grid publication") + + const selector = { + ancestors: [], // Lookup top-level products + isDeleted: { $in: [null, false] }, // by default, we don't publish deleted products + isVisible: true // by default, only lookup visible products + }; + return Products.find(selector, { sort }); +}); From 61899c9970137cab39a6a0f48ff7922c2353178f Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Wed, 28 Feb 2018 10:00:27 -0800 Subject: [PATCH 02/77] refactor: add observable to "Products/grid" publication --- server/publications/collections/products.js | 46 ++++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index 0400ca934e0..3c7a1573d4f 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -531,7 +531,7 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so }); -Meteor.publish("Products/grid", (productScrollLimit = 24, productFilters, sort = {}, editMode = true) => { +Meteor.publish("Products/grid", function (productScrollLimit = 24, productFilters, sort = {}, editMode = true) { check(productScrollLimit, Number); check(productFilters, Match.OneOf(undefined, Object)); check(sort, Match.OneOf(undefined, Object)); @@ -544,5 +544,47 @@ Meteor.publish("Products/grid", (productScrollLimit = 24, productFilters, sort = isDeleted: { $in: [null, false] }, // by default, we don't publish deleted products isVisible: true // by default, only lookup visible products }; - return Products.find(selector, { sort }); + + const productCursor = Products.find(selector, { sort }); + + const handle = productCursor.observeChanges({ + added: (id, fields) => { + const variants = Products.find({ + ancestors: { + $in: [id] + } + }).fetch(); + + // const media = Media.find().map((media) => { + // return media.url() + // }) + + fields.varaints = variants; + fields.isSoldOut = true; + // fields.media = media; + + this.added("Products", id, fields); + }, + changed: (id, fields) => { + const variants = Products.find({ + ancestors: { + $in: [id] + } + }).fetch(); + + fields.varaints = variants; + fields.isSoldOut = true; + + this.changed("Products", id, fields); + }, + removed: (id) => { + this.removed("Products", id); + } + }); + + this.onStop(() => { + handle.stop(); + }); + + return this.ready(); }); From 27ca7726b340c972c822e661c182b2f3aa889471 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Wed, 28 Feb 2018 12:44:35 -0800 Subject: [PATCH 03/77] feat: publish product media on product document --- server/publications/collections/products.js | 33 ++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index 3c7a1573d4f..367fe5996b8 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -6,6 +6,7 @@ import { registerSchema } from "@reactioncommerce/reaction-collections"; import { Products, Shops, Revisions } from "/lib/collections"; import { Reaction, Logger } from "/server/api"; import { RevisionApi } from "/imports/plugins/core/revisions/lib/api/revisions"; +import { Media } from "/imports/plugins/core/files/server"; // // define search filters as a schema so we can validate @@ -545,35 +546,53 @@ Meteor.publish("Products/grid", function (productScrollLimit = 24, productFilter isVisible: true // by default, only lookup visible products }; - const productCursor = Products.find(selector, { sort }); + const productCursor = Products.find(selector, { + sort, + limit: productScrollLimit + }); const handle = productCursor.observeChanges({ - added: (id, fields) => { + added: async (id, fields) => { const variants = Products.find({ ancestors: { $in: [id] } }).fetch(); - // const media = Media.find().map((media) => { - // return media.url() - // }) + const mediaArray = await Media.find({ + "metadata.productId": id + }); + + const productMedia = mediaArray.map((media) => ({ + url: `/assets${media.url({ store: "medium" })}` + })); + + console.log("mediaArray", productMedia); fields.varaints = variants; fields.isSoldOut = true; - // fields.media = media; + fields.media = productMedia; this.added("Products", id, fields); }, - changed: (id, fields) => { + changed: async (id, fields) => { const variants = Products.find({ ancestors: { $in: [id] } }).fetch(); + const mediaArray = await Media.find({ + "metadata.productId": id + }); + + const productMedia = mediaArray.map((media) => ({ + url: `/assets${media.url({ store: "medium" })}` + })); + fields.varaints = variants; fields.isSoldOut = true; + fields.media = productMedia; this.changed("Products", id, fields); }, From 3f5232c1b8d2d3a8d245d3508e2f43731b3bd659 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Wed, 28 Feb 2018 12:45:15 -0800 Subject: [PATCH 04/77] feat: add products container for customer grid --- .../included/product-variant/client/index.js | 1 + .../containers/productsContainerCustomer.js | 256 ++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 imports/plugins/included/product-variant/containers/productsContainerCustomer.js diff --git a/imports/plugins/included/product-variant/client/index.js b/imports/plugins/included/product-variant/client/index.js index 8e3886e4358..e41e1e029a0 100644 --- a/imports/plugins/included/product-variant/client/index.js +++ b/imports/plugins/included/product-variant/client/index.js @@ -24,5 +24,6 @@ export { default as GridPublishContainer } from "../containers/gridPublishContai export { default as ProductGridContainer } from "../containers/productGridContainer"; export { default as ProductGridItemsContainer } from "../containers/productGridItemsContainer"; export { default as ProductsContainer } from "../containers/productsContainer"; +export { default as ProductsContainerCustomer } from "../containers/productsContainerCustomer"; export { default as VariantFormContainer } from "../containers/variantFormContainer"; export { default as VariantEditContainer } from "../containers/variantEditContainer"; diff --git a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js new file mode 100644 index 00000000000..e72c7179ccb --- /dev/null +++ b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js @@ -0,0 +1,256 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { compose } from "recompose"; +import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; +import { Meteor } from "meteor/meteor"; +import { ReactiveVar } from "meteor/reactive-var"; +import { Session } from "meteor/session"; +import { Tracker } from "meteor/tracker"; +import { Reaction } from "/client/api"; +import { ITEMS_INCREMENT } from "/client/config/defaults"; +import { ReactionProduct } from "/lib/api"; +import { applyProductRevision } from "/lib/api/products"; +import { Products, Tags, Shops } from "/lib/collections"; +import { Media } from "/imports/plugins/core/files/client"; +import ProductsComponent from "../components/customer/productGrid"; + +const reactiveProductIds = new ReactiveVar([], (oldVal, newVal) => JSON.stringify(oldVal.sort()) === JSON.stringify(newVal.sort())); + +// Isolated resubscribe to product grid images, only when the list of product IDs changes +// Tracker.autorun(() => { +// Meteor.subscribe("ProductGridMedia", reactiveProductIds.get()); +// }); + + +/** + * loadMoreProducts + * @summary whenever #productScrollLimitLoader becomes visible, retrieve more results + * this basically runs this: + * Session.set('productScrollLimit', Session.get('productScrollLimit') + ITEMS_INCREMENT); + * @return {undefined} + */ +function loadMoreProducts() { + let threshold; + const target = document.querySelectorAll("#productScrollLimitLoader"); + let scrollContainer = document.querySelectorAll("#container-main"); + if (scrollContainer.length === 0) { + scrollContainer = window; + } + + if (target.length) { + threshold = scrollContainer[0].scrollHeight - scrollContainer[0].scrollTop === scrollContainer[0].clientHeight; + + if (threshold) { + if (!target[0].getAttribute("visible")) { + target[0].setAttribute("productScrollLimit", true); + Session.set("productScrollLimit", Session.get("productScrollLimit") + ITEMS_INCREMENT || 24); + } + } else if (target[0].getAttribute("visible")) { + target[0].setAttribute("visible", false); + } + } +} + +const wrapComponent = (Comp) => ( + class ProductsContainer extends Component { + static propTypes = { + canLoadMoreProducts: PropTypes.bool, + productsSubscription: PropTypes.object, + showNotFound: PropTypes.bool + }; + + constructor(props) { + super(props); + this.state = { + initialLoad: true + }; + + this.ready = this.ready.bind(this); + this.loadMoreProducts = this.loadMoreProducts.bind(this); + } + + ready = () => { + if (this.props.showNotFound === true) { + return false; + } + + const isInitialLoad = this.state.initialLoad === true; + const isReady = this.props.productsSubscription.ready(); + + if (isInitialLoad === false) { + return true; + } + + if (isReady) { + return true; + } + + return false; + } + + loadMoreProducts = () => this.props.canLoadMoreProducts === true + + loadProducts = (event) => { + event.preventDefault(); + this.setState({ + initialLoad: false + }); + loadMoreProducts(); + } + + render() { + return ( + + ); + } + } +); + +function composer(props, onData) { + window.prerenderReady = false; + + let canLoadMoreProducts = false; + + const slug = Reaction.Router.getParam("slug"); + const shopIdOrSlug = Reaction.Router.getParam("shopSlug"); + + const tag = Tags.findOne({ slug }) || Tags.findOne(slug); + const scrollLimit = Session.get("productScrollLimit"); + let tags = {}; // this could be shop default implementation needed + let shopIds = {}; + + if (tag) { + tags = { tags: [tag._id] }; + } + + if (shopIdOrSlug) { + shopIds = { shops: [shopIdOrSlug] }; + } + + // if we get an invalid slug, don't return all products + if (!tag && slug) { + onData(null, { + showNotFound: true + }); + + return; + } + + const currentTag = ReactionProduct.getTag(); + + const sort = { + [`positions.${currentTag}.position`]: 1, + [`positions.${currentTag}.createdAt`]: 1, + createdAt: 1 + }; + + // TODO: Remove + // const viewAsPref = Reaction.getUserPreferences("reaction-dashboard", "viewAs"); + + // TODO: Remove + // Edit mode is true by default + // let editMode = true; + + // TODO: Remove + // if we have a "viewAs" preference and the preference is not set to "administrator", then edit mode is false + // if (viewAsPref && viewAsPref !== "administrator") { + // editMode = false; + // } + + const queryParams = Object.assign({}, tags, Reaction.Router.current().query, shopIds); + const productsSubscription = Meteor.subscribe("Products/grid", scrollLimit, queryParams, sort, false); + + if (productsSubscription.ready()) { + window.prerenderReady = true; + } + + const activeShopsIds = Shops.find({ + $or: [ + { "workflow.status": "active" }, + { _id: Reaction.getPrimaryShopId() } + ] + }).map((activeShop) => activeShop._id); + + const productCursor = Products.find({ + ancestors: [], + type: { $in: ["simple"] }, + shopId: { $in: activeShopsIds } + }, { + $sort: sort + }); + + // TODO: Remove + // const sortedProducts = ReactionProduct.sortProducts(productCursor.fetch(), currentTag); + // Session.set("productGrid/products", sortedProducts); + + // TODO: Remove + // const productIds = []; + // // Instantiate an object for use as a map. This object does not inherit prototype or methods from `Object` + // const productMediaById = Object.create(null); + // const stateProducts = sortedProducts.map((product) => { + // productIds.push(product._id); + + // const primaryMedia = Media.findOneLocal({ + // "metadata.productId": product._id, + // "metadata.toGrid": 1, + // "metadata.workflow": { $nin: ["archived", "unpublished"] } + // }, { + // sort: { "metadata.priority": 1, "uploadedAt": 1 } + // }); + + // const variantIds = ReactionProduct.getVariants(product._id).map((variant) => variant._id); + // let additionalMedia = Media.findLocal({ + // "metadata.productId": product._id, + // "metadata.variantId": { $in: variantIds }, + // "metadata.workflow": { $nin: ["archived", "unpublished"] } + // }, { + // limit: 3, + // sort: { "metadata.priority": 1, "uploadedAt": 1 } + // }); + + // if (additionalMedia.length < 2) additionalMedia = null; + + // productMediaById[product._id] = { + // additionalMedia, + // primaryMedia + // }; + + // return { + // // ...applyProductRevision(product), + // // additionalMedia, + // // primaryMedia + // }; + // }); + + // reactiveProductIds.set(productIds); + + canLoadMoreProducts = productCursor.count() >= Session.get("productScrollLimit"); + + // const isActionViewOpen = Reaction.isActionViewOpen(); + // if (isActionViewOpen === false) { + // Session.set("productGrid/selectedProducts", []); + // } + const products = productCursor.fetch(); + onData(null, { + canLoadMoreProducts, + products + // productMediaById, + // products: stateProducts, + // productsSubscription + }); +} + +registerComponent("Products", ProductsComponent, [ + composeWithTracker(composer), + // wrapComponent +]); + +export default compose( + composeWithTracker(composer), + wrapComponent +)(ProductsComponent); From 216ba0bb7634c42f9a096b826ad87086fe9c07b7 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Wed, 28 Feb 2018 12:46:11 -0800 Subject: [PATCH 05/77] refactor: change name of admin product container --- .../included/product-variant/containers/productsContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/included/product-variant/containers/productsContainer.js b/imports/plugins/included/product-variant/containers/productsContainer.js index 08926e1dc04..52f0edfdb58 100644 --- a/imports/plugins/included/product-variant/containers/productsContainer.js +++ b/imports/plugins/included/product-variant/containers/productsContainer.js @@ -237,7 +237,7 @@ function composer(props, onData) { }); } -registerComponent("Products", ProductsComponent, [ +registerComponent("ProductsAdmin", ProductsComponent, [ composeWithTracker(composer), wrapComponent ]); From f82b6e0b28e0aea6d64644df7b5d93455994a171 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Wed, 28 Feb 2018 12:46:53 -0800 Subject: [PATCH 06/77] feat: add customer product grid item component --- .../components/customer/productGridItem.js | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 imports/plugins/included/product-variant/components/customer/productGridItem.js diff --git a/imports/plugins/included/product-variant/components/customer/productGridItem.js b/imports/plugins/included/product-variant/components/customer/productGridItem.js new file mode 100644 index 00000000000..ec6dd1610ae --- /dev/null +++ b/imports/plugins/included/product-variant/components/customer/productGridItem.js @@ -0,0 +1,175 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Components } from "@reactioncommerce/reaction-components"; +import { formatPriceString } from "/client/api"; + +class ProductGridItems extends Component { + static propTypes = { + canEdit: PropTypes.bool, + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + displayPrice: PropTypes.func, + isMediumWeight: PropTypes.func, + isSearch: PropTypes.bool, + isSelected: PropTypes.func, + onClick: PropTypes.func, + onDoubleClick: PropTypes.func, + pdpPath: PropTypes.func, + positions: PropTypes.func, + product: PropTypes.object, + productMedia: PropTypes.object, + weightClass: PropTypes.func + } + + handleDoubleClick = (event) => { + if (this.props.onDoubleClick) { + this.props.onDoubleClick(event); + } + } + + handleClick = (event) => { + if (this.props.onClick) { + this.props.onClick(event); + } + } + + renderPinned() { + return this.props.positions().pinned ? "pinned" : ""; + } + + renderVisible() { + return this.props.product.isVisible ? "" : "not-visible"; + } + + renderOverlay() { + if (this.props.product.isVisible === false) { + return ( +
+ ); + } + } + + renderMedia() { + const { product, productMedia } = this.props; + + return ( + productMedia.primaryMedia} item={product} size="large" mode="span" /> + ); + } + + renderAdditionalMedia() { + const { isMediumWeight, productMedia } = this.props; + if (!isMediumWeight()) return null; + + const mediaArray = productMedia.additionalMedia; + if (!mediaArray || mediaArray.length === 0) return null; + + return ( +
+ {mediaArray.map((media) => ( + + ))} + {this.renderOverlay()} +
+ ); + } + + renderNotices() { + return null + // return ( + //
+ // this.props.product.isSoldOut} /> + + //
+ + // ); + } + + renderGridContent() { + const { product } = this.props; + + return ( + + ); + } + + renderHoverClassName() { + return this.props.isSearch ? "item-content" : ""; + } + + render() { + const { product } = this.props; + const MEDIA_PLACEHOLDER = "/resources/placeholder.gif"; + + // TODO: use this to get product url + // Reaction.Router.pathFor("product", { + // hash: { + // handle + // } + // }); + + // TODO: isSelected is not needed. Others may not need to be functions + // ${this.renderPinned()} ${this.props.weightClass()} ${this.props.isSelected()} + + const { url } = (Array.isArray(product.media) && product.media[0]) || { url: MEDIA_PLACEHOLDER }; + return ( +
  • +
    + + + +
    + + {/* {this.renderMedia()} */} + {/* {this.renderOverlay()} */} +
    + + {/* {this.renderAdditionalMedia()} */} +
    + + {/* {!this.props.isSearch && this.renderNotices()} */} + {this.renderGridContent()} +
    +
  • + ); + } +} + +export default ProductGridItems; From c38266cda57b7a594c8042d58d3571279e59d5b4 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Wed, 28 Feb 2018 12:47:03 -0800 Subject: [PATCH 07/77] feat: add customer product grid component --- .../components/customer/productGrid.js | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 imports/plugins/included/product-variant/components/customer/productGrid.js diff --git a/imports/plugins/included/product-variant/components/customer/productGrid.js b/imports/plugins/included/product-variant/components/customer/productGrid.js new file mode 100644 index 00000000000..518ce16d498 --- /dev/null +++ b/imports/plugins/included/product-variant/components/customer/productGrid.js @@ -0,0 +1,58 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Components } from "@reactioncommerce/reaction-components"; +import ProductGridItem from "./productGridItem"; +import { ReactionProduct } from "/lib/api"; + +class ProductGrid extends Component { + static propTypes = { + productMediaById: PropTypes.object, + products: PropTypes.array + } + + renderProductGridItems() { + const { products } = this.props; + const currentTag = ReactionProduct.getTag(); + + if (Array.isArray(products)) { + return products.map((product, index) => ( + + )); + } + + return ( +
    +
    +

    + +

    +
    +
    + ); + } + + render() { + console.log("this.props", this.props); + + return ( +
    +
    + +
      + {this.renderProductGridItems()} +
    +
    +
    +
    + ); + } +} + +export default ProductGrid; From 5fb0f64db17ad0ccf6fcbe81e54ad71a32968abd Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Thu, 1 Mar 2018 09:57:23 -0600 Subject: [PATCH 08/77] refactor: cleaning up code, moved temp product image into renderMedia method, added getter for the product url --- .../components/customer/productGridItem.js | 86 +++++++++++-------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGridItem.js b/imports/plugins/included/product-variant/components/customer/productGridItem.js index ec6dd1610ae..57b2bc521d0 100644 --- a/imports/plugins/included/product-variant/components/customer/productGridItem.js +++ b/imports/plugins/included/product-variant/components/customer/productGridItem.js @@ -1,7 +1,6 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import { Components } from "@reactioncommerce/reaction-components"; -import { formatPriceString } from "/client/api"; +import { formatPriceString, Reaction } from "/client/api"; class ProductGridItems extends Component { static propTypes = { @@ -21,6 +20,7 @@ class ProductGridItems extends Component { weightClass: PropTypes.func } + // action event handlers handleDoubleClick = (event) => { if (this.props.onDoubleClick) { this.props.onDoubleClick(event); @@ -33,6 +33,7 @@ class ProductGridItems extends Component { } } + // modifiers renderPinned() { return this.props.positions().pinned ? "pinned" : ""; } @@ -41,6 +42,11 @@ class ProductGridItems extends Component { return this.props.product.isVisible ? "" : "not-visible"; } + // render class names + renderHoverClassName() { + return this.props.isSearch ? "item-content" : ""; + } + renderOverlay() { if (this.props.product.isVisible === false) { return ( @@ -49,14 +55,50 @@ class ProductGridItems extends Component { } } + // notice + renderNotices() { + return null + // return ( + //
    + // this.props.product.isSoldOut} /> + + //
    + + // ); + } + + // getters + + // get product detail page URL + get productURL() { + const { product: { handle } } = this.props; + return Reaction.Router.pathFor("product", { + hash: { + handle + } + }); + } + + // render product image renderMedia() { - const { product, productMedia } = this.props; + // const { product, productMedia } = this.props; + + // return ( + // productMedia.primaryMedia} item={product} size="large" mode="span" /> + // ); + const { product } = this.props; + const MEDIA_PLACEHOLDER = "/resources/placeholder.gif"; + const { url } = (Array.isArray(product.media) && product.media[0]) || { url: MEDIA_PLACEHOLDER }; return ( - productMedia.primaryMedia} item={product} size="large" mode="span" /> + ); } + renderAdditionalMedia() { const { isMediumWeight, productMedia } = this.props; if (!isMediumWeight()) return null; @@ -78,17 +120,6 @@ class ProductGridItems extends Component { ); } - renderNotices() { - return null - // return ( - //
    - // this.props.product.isSoldOut} /> - - //
    - - // ); - } - renderGridContent() { const { product } = this.props; @@ -96,7 +127,7 @@ class ProductGridItems extends Component {
    - - {/* {this.renderMedia()} */} - {/* {this.renderOverlay()} */} + {this.renderMedia()}
    {/* {this.renderAdditionalMedia()} */} From 67f9d2c29b80938649d1caa4e926273507d3d980 Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Thu, 1 Mar 2018 09:58:29 -0600 Subject: [PATCH 09/77] refactor: updated renderPRoductGridItems to not descruture container pros onto each product component --- .../included/product-variant/components/customer/productGrid.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGrid.js b/imports/plugins/included/product-variant/components/customer/productGrid.js index 518ce16d498..e976cbe01ef 100644 --- a/imports/plugins/included/product-variant/components/customer/productGrid.js +++ b/imports/plugins/included/product-variant/components/customer/productGrid.js @@ -17,10 +17,8 @@ class ProductGrid extends Component { if (Array.isArray(products)) { return products.map((product, index) => ( From c1088612f979376541df7d0709fb23743bf264c0 Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Thu, 1 Mar 2018 10:30:33 -0600 Subject: [PATCH 10/77] fix: updated media path inside of the customer grid publication, updated the media return to have all avalible image crops --- server/publications/collections/products.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index 367fe5996b8..2a7241a1f1a 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -564,7 +564,10 @@ Meteor.publish("Products/grid", function (productScrollLimit = 24, productFilter }); const productMedia = mediaArray.map((media) => ({ - url: `/assets${media.url({ store: "medium" })}` + thumbnail: `${media.url({ store: "thumbnail" })}`, + small: `${media.url({ store: "small" })}`, + medium: `${media.url({ store: "medium" })}`, + large: `${media.url({ store: "large" })}` })); console.log("mediaArray", productMedia); @@ -587,7 +590,10 @@ Meteor.publish("Products/grid", function (productScrollLimit = 24, productFilter }); const productMedia = mediaArray.map((media) => ({ - url: `/assets${media.url({ store: "medium" })}` + thumbnail: `${media.url({ store: "thumbnail" })}`, + small: `${media.url({ store: "small" })}`, + medium: `${media.url({ store: "medium" })}`, + large: `${media.url({ store: "large" })}` })); fields.varaints = variants; From 0efb321a9167ac4638ad973213a2901c76d90baa Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Thu, 1 Mar 2018 10:31:09 -0600 Subject: [PATCH 11/77] fix: gridItem using the correct image crop --- .../components/customer/productGridItem.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGridItem.js b/imports/plugins/included/product-variant/components/customer/productGridItem.js index 57b2bc521d0..2a834e0a9c5 100644 --- a/imports/plugins/included/product-variant/components/customer/productGridItem.js +++ b/imports/plugins/included/product-variant/components/customer/productGridItem.js @@ -88,12 +88,12 @@ class ProductGridItems extends Component { // ); const { product } = this.props; const MEDIA_PLACEHOLDER = "/resources/placeholder.gif"; - const { url } = (Array.isArray(product.media) && product.media[0]) || { url: MEDIA_PLACEHOLDER }; + const { large } = (Array.isArray(product.media) && product.media[0]) || { large: MEDIA_PLACEHOLDER }; return ( ); } @@ -131,7 +131,7 @@ class ProductGridItems extends Component { data-event-category="grid" data-event-action="product-click" data-event-label="grid product click" - data-event-value={this.props.product._id} + data-event-value={product._id} onDoubleClick={this.handleDoubleClick} onClick={this.handleClick} > @@ -139,7 +139,7 @@ class ProductGridItems extends Component {
    {product.title}
    {formatPriceString(product.price.range)}
    {this.props.isSearch && -
    {this.props.product.description}
    +
    {product.description}
    }
    @@ -157,8 +157,8 @@ class ProductGridItems extends Component { return (
  • @@ -167,7 +167,7 @@ class ProductGridItems extends Component { href={this.productURL} data-event-category="grid" data-event-label="grid product click" - data-event-value={this.props.product._id} + data-event-value={product._id} onDoubleClick={this.handleDoubleClick} onClick={this.handleClick} > From 55fef379c04db2357824a8adedf74d9ed8441bd2 Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Thu, 1 Mar 2018 10:47:31 -0600 Subject: [PATCH 12/77] refactor: grid item component clean up, removing unneeded proptypes --- .../components/customer/productGridItem.js | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGridItem.js b/imports/plugins/included/product-variant/components/customer/productGridItem.js index 2a834e0a9c5..1d0511711bd 100644 --- a/imports/plugins/included/product-variant/components/customer/productGridItem.js +++ b/imports/plugins/included/product-variant/components/customer/productGridItem.js @@ -4,19 +4,13 @@ import { formatPriceString, Reaction } from "/client/api"; class ProductGridItems extends Component { static propTypes = { - canEdit: PropTypes.bool, - connectDragSource: PropTypes.func, - connectDropTarget: PropTypes.func, displayPrice: PropTypes.func, isMediumWeight: PropTypes.func, isSearch: PropTypes.bool, - isSelected: PropTypes.func, onClick: PropTypes.func, onDoubleClick: PropTypes.func, - pdpPath: PropTypes.func, positions: PropTypes.func, product: PropTypes.object, - productMedia: PropTypes.object, weightClass: PropTypes.func } @@ -57,7 +51,7 @@ class ProductGridItems extends Component { // notice renderNotices() { - return null + return null; // return ( //
    // this.props.product.isSoldOut} /> @@ -81,11 +75,6 @@ class ProductGridItems extends Component { // render product image renderMedia() { - // const { product, productMedia } = this.props; - - // return ( - // productMedia.primaryMedia} item={product} size="large" mode="span" /> - // ); const { product } = this.props; const MEDIA_PLACEHOLDER = "/resources/placeholder.gif"; const { large } = (Array.isArray(product.media) && product.media[0]) || { large: MEDIA_PLACEHOLDER }; @@ -100,24 +89,24 @@ class ProductGridItems extends Component { renderAdditionalMedia() { - const { isMediumWeight, productMedia } = this.props; - if (!isMediumWeight()) return null; + // const { isMediumWeight, productMedia } = this.props; + // if (!isMediumWeight()) return null; - const mediaArray = productMedia.additionalMedia; - if (!mediaArray || mediaArray.length === 0) return null; + // const mediaArray = productMedia.additionalMedia; + // if (!mediaArray || mediaArray.length === 0) return null; - return ( -
    - {mediaArray.map((media) => ( - - ))} - {this.renderOverlay()} -
    - ); + // return ( + //
    + // {mediaArray.map((media) => ( + // + // ))} + // {this.renderOverlay()} + //
    + // ); } renderGridContent() { @@ -126,7 +115,6 @@ class ProductGridItems extends Component { return (
    -
    +
    {this.renderMedia()}
    From 197fa61fc0d237d2fda399932d8b2b5ef1d81728 Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Thu, 1 Mar 2018 12:35:19 -0600 Subject: [PATCH 13/77] refactor: cleaning up gridItem component, added pinned and weight classes, updated prodCridContainer to check if posiiton is there before passing it down as a prop --- .../components/customer/productGrid.js | 2 +- .../components/customer/productGridItem.js | 87 +++++++++---------- 2 files changed, 42 insertions(+), 47 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGrid.js b/imports/plugins/included/product-variant/components/customer/productGrid.js index e976cbe01ef..8ae29fdeeb4 100644 --- a/imports/plugins/included/product-variant/components/customer/productGrid.js +++ b/imports/plugins/included/product-variant/components/customer/productGrid.js @@ -18,7 +18,7 @@ class ProductGrid extends Component { return products.map((product, index) => ( diff --git a/imports/plugins/included/product-variant/components/customer/productGridItem.js b/imports/plugins/included/product-variant/components/customer/productGridItem.js index 1d0511711bd..f77a9cd8b45 100644 --- a/imports/plugins/included/product-variant/components/customer/productGridItem.js +++ b/imports/plugins/included/product-variant/components/customer/productGridItem.js @@ -1,44 +1,51 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import { formatPriceString, Reaction } from "/client/api"; +import classnames from "classnames"; +import { formatPriceString, Router } from "/client/api"; -class ProductGridItems extends Component { +class ProductGridItem extends Component { static propTypes = { displayPrice: PropTypes.func, isMediumWeight: PropTypes.func, isSearch: PropTypes.bool, - onClick: PropTypes.func, - onDoubleClick: PropTypes.func, - positions: PropTypes.func, + position: PropTypes.object, product: PropTypes.object, weightClass: PropTypes.func } - // action event handlers - handleDoubleClick = (event) => { - if (this.props.onDoubleClick) { - this.props.onDoubleClick(event); - } - } - - handleClick = (event) => { - if (this.props.onClick) { - this.props.onClick(event); - } - } + // getters - // modifiers - renderPinned() { - return this.props.positions().pinned ? "pinned" : ""; + // get product detail page URL + get productURL() { + const { product: { handle } } = this.props; + return Router.pathFor("product", { + hash: { + handle + } + }); } - renderVisible() { - return this.props.product.isVisible ? "" : "not-visible"; + // get weight class name + get weightClass() { + const { weight } = this.props.position || { weight: 0 }; + switch (weight) { + case 1: + return "product-medium"; + case 2: + return "product-large"; + default: + return "product-small"; + } } - // render class names - renderHoverClassName() { - return this.props.isSearch ? "item-content" : ""; + // get product item class names + get productClassNames() { + const { position } = this.props; + return classnames({ + "product-grid-item": true, + [this.weightClass]: true, + "pinned": position.pinned + }); } renderOverlay() { @@ -49,6 +56,11 @@ class ProductGridItems extends Component { } } + handleClick = (event) => { + event.preventDefault(); + Router.go(this.productURL); + } + // notice renderNotices() { return null; @@ -61,18 +73,6 @@ class ProductGridItems extends Component { // ); } - // getters - - // get product detail page URL - get productURL() { - const { product: { handle } } = this.props; - return Reaction.Router.pathFor("product", { - hash: { - handle - } - }); - } - // render product image renderMedia() { const { product } = this.props; @@ -136,19 +136,14 @@ class ProductGridItems extends Component { } render() { - const { product } = this.props; - - // TODO: isSelected is not needed. Others may not need to be functions - // ${this.renderPinned()} ${this.props.weightClass()} ${this.props.isSelected()} - - + const { product, isSearch } = this.props; return (
  • -
    +
    Date: Thu, 1 Mar 2018 13:06:40 -0600 Subject: [PATCH 14/77] refactoir: custom grid item now has soldOut/backoder notices --- .../components/customer/productGridItem.js | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGridItem.js b/imports/plugins/included/product-variant/components/customer/productGridItem.js index f77a9cd8b45..6360670d331 100644 --- a/imports/plugins/included/product-variant/components/customer/productGridItem.js +++ b/imports/plugins/included/product-variant/components/customer/productGridItem.js @@ -2,15 +2,13 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import classnames from "classnames"; import { formatPriceString, Router } from "/client/api"; +import { Components } from "@reactioncommerce/reaction-components"; class ProductGridItem extends Component { static propTypes = { - displayPrice: PropTypes.func, - isMediumWeight: PropTypes.func, isSearch: PropTypes.bool, position: PropTypes.object, - product: PropTypes.object, - weightClass: PropTypes.func + product: PropTypes.object } // getters @@ -48,14 +46,6 @@ class ProductGridItem extends Component { }); } - renderOverlay() { - if (this.props.product.isVisible === false) { - return ( -
    - ); - } - } - handleClick = (event) => { event.preventDefault(); Router.go(this.productURL); @@ -63,14 +53,38 @@ class ProductGridItem extends Component { // notice renderNotices() { - return null; - // return ( - //
    - // this.props.product.isSoldOut} /> - - //
    + const { isSoldOut, isLowQuantity, isBackorder } = this.props.product; + let noticeEl; + // TODO: revisit this if + if (isSoldOut) { + if (isBackorder) { + noticeEl = ( + + + + ); + } else { + noticeEl = ( + + + + ); + } + } else if (isLowQuantity) { + noticeEl = ( + + + + ); + } - // ); + return ( +
    +
    + {noticeEl} +
    +
    + ); } // render product image @@ -82,7 +96,7 @@ class ProductGridItem extends Component { return ( ); } @@ -120,7 +134,6 @@ class ProductGridItem extends Component { data-event-action="product-click" data-event-label="grid product click" data-event-value={product._id} - onDoubleClick={this.handleDoubleClick} onClick={this.handleClick} >
  • From 86b50bb32575d1a686dcbc4a0b9499ef622c48f3 Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Thu, 1 Mar 2018 13:29:53 -0600 Subject: [PATCH 15/77] refactor: created new productsContainer component to swicth between the cusotmer and admin conatiner --- .../containers/productsContainer.js | 241 +---------------- .../containers/productsContainerAdmin.js | 248 ++++++++++++++++++ .../containers/productsContainerCustomer.js | 4 +- 3 files changed, 261 insertions(+), 232 deletions(-) create mode 100644 imports/plugins/included/product-variant/containers/productsContainerAdmin.js diff --git a/imports/plugins/included/product-variant/containers/productsContainer.js b/imports/plugins/included/product-variant/containers/productsContainer.js index 52f0edfdb58..5bb4eb93f5e 100644 --- a/imports/plugins/included/product-variant/containers/productsContainer.js +++ b/imports/plugins/included/product-variant/containers/productsContainer.js @@ -3,246 +3,27 @@ import PropTypes from "prop-types"; import { compose } from "recompose"; import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; -import { ReactiveVar } from "meteor/reactive-var"; -import { Session } from "meteor/session"; -import { Tracker } from "meteor/tracker"; import { Reaction } from "/client/api"; -import { ITEMS_INCREMENT } from "/client/config/defaults"; -import { ReactionProduct } from "/lib/api"; -import { applyProductRevision } from "/lib/api/products"; import { Products, Tags, Shops } from "/lib/collections"; -import { Media } from "/imports/plugins/core/files/client"; -import ProductsComponent from "../components/products"; +import ProductsContainerAdmin from "./productsContainerAdmin.js"; +import ProductsContainerCustomer from "./productsContainerCustomer.js"; -const reactiveProductIds = new ReactiveVar([], (oldVal, newVal) => JSON.stringify(oldVal.sort()) === JSON.stringify(newVal.sort())); - -// Isolated resubscribe to product grid images, only when the list of product IDs changes -Tracker.autorun(() => { - Meteor.subscribe("ProductGridMedia", reactiveProductIds.get()); -}); - - -/** - * loadMoreProducts - * @summary whenever #productScrollLimitLoader becomes visible, retrieve more results - * this basically runs this: - * Session.set('productScrollLimit', Session.get('productScrollLimit') + ITEMS_INCREMENT); - * @return {undefined} - */ -function loadMoreProducts() { - let threshold; - const target = document.querySelectorAll("#productScrollLimitLoader"); - let scrollContainer = document.querySelectorAll("#container-main"); - if (scrollContainer.length === 0) { - scrollContainer = window; - } - - if (target.length) { - threshold = scrollContainer[0].scrollHeight - scrollContainer[0].scrollTop === scrollContainer[0].clientHeight; - - if (threshold) { - if (!target[0].getAttribute("visible")) { - target[0].setAttribute("productScrollLimit", true); - Session.set("productScrollLimit", Session.get("productScrollLimit") + ITEMS_INCREMENT || 24); - } - } else if (target[0].getAttribute("visible")) { - target[0].setAttribute("visible", false); - } - } -} - -const wrapComponent = (Comp) => ( - class ProductsContainer extends Component { - static propTypes = { - canLoadMoreProducts: PropTypes.bool, - productsSubscription: PropTypes.object, - showNotFound: PropTypes.bool - }; - - constructor(props) { - super(props); - this.state = { - initialLoad: true - }; - - this.ready = this.ready.bind(this); - this.loadMoreProducts = this.loadMoreProducts.bind(this); - } - - ready = () => { - if (this.props.showNotFound === true) { - return false; - } - - const isInitialLoad = this.state.initialLoad === true; - const isReady = this.props.productsSubscription.ready(); - - if (isInitialLoad === false) { - return true; - } - - if (isReady) { - return true; - } - - return false; - } - - loadMoreProducts = () => this.props.canLoadMoreProducts === true - - loadProducts = (event) => { - event.preventDefault(); - this.setState({ - initialLoad: false - }); - loadMoreProducts(); - } - - render() { - return ( - - ); - } - } -); +const ProductsContainer = ({ isAdmin }) => { + return (isAdmin) ? : ; +}; function composer(props, onData) { - window.prerenderReady = false; - - let canLoadMoreProducts = false; - - const slug = Reaction.Router.getParam("slug"); - const shopIdOrSlug = Reaction.Router.getParam("shopSlug"); - - const tag = Tags.findOne({ slug }) || Tags.findOne(slug); - const scrollLimit = Session.get("productScrollLimit"); - let tags = {}; // this could be shop default implementation needed - let shopIds = {}; - - if (tag) { - tags = { tags: [tag._id] }; - } - - if (shopIdOrSlug) { - shopIds = { shops: [shopIdOrSlug] }; - } - - // if we get an invalid slug, don't return all products - if (!tag && slug) { - onData(null, { - showNotFound: true - }); - - return; - } - - const currentTag = ReactionProduct.getTag(); - - const sort = { - [`positions.${currentTag}.position`]: 1, - [`positions.${currentTag}.createdAt`]: 1, - createdAt: 1 - }; - - const viewAsPref = Reaction.getUserPreferences("reaction-dashboard", "viewAs"); - - // Edit mode is true by default - let editMode = true; - - // if we have a "viewAs" preference and the preference is not set to "administrator", then edit mode is false - if (viewAsPref && viewAsPref !== "administrator") { - editMode = false; - } - - const queryParams = Object.assign({}, tags, Reaction.Router.current().query, shopIds); - const productsSubscription = Meteor.subscribe("Products/grid", scrollLimit, queryParams, sort, editMode); - - if (productsSubscription.ready()) { - window.prerenderReady = true; - } - - const activeShopsIds = Shops.find({ - $or: [ - { "workflow.status": "active" }, - { _id: Reaction.getPrimaryShopId() } - ] - }).map((activeShop) => activeShop._id); - - const productCursor = Products.find({ - ancestors: [], - type: { $in: ["simple"] }, - shopId: { $in: activeShopsIds } - }); - - const sortedProducts = ReactionProduct.sortProducts(productCursor.fetch(), currentTag); - Session.set("productGrid/products", sortedProducts); - - const productIds = []; - // Instantiate an object for use as a map. This object does not inherit prototype or methods from `Object` - const productMediaById = Object.create(null); - const stateProducts = sortedProducts.map((product) => { - productIds.push(product._id); - - const primaryMedia = Media.findOneLocal({ - "metadata.productId": product._id, - "metadata.toGrid": 1, - "metadata.workflow": { $nin: ["archived", "unpublished"] } - }, { - sort: { "metadata.priority": 1, "uploadedAt": 1 } - }); - - const variantIds = ReactionProduct.getVariants(product._id).map((variant) => variant._id); - let additionalMedia = Media.findLocal({ - "metadata.productId": product._id, - "metadata.variantId": { $in: variantIds }, - "metadata.workflow": { $nin: ["archived", "unpublished"] } - }, { - limit: 3, - sort: { "metadata.priority": 1, "uploadedAt": 1 } - }); - - if (additionalMedia.length < 2) additionalMedia = null; - - productMediaById[product._id] = { - additionalMedia, - primaryMedia - }; - - return { - ...applyProductRevision(product), - // additionalMedia, - // primaryMedia - }; - }); - - reactiveProductIds.set(productIds); - - canLoadMoreProducts = productCursor.count() >= Session.get("productScrollLimit"); - - const isActionViewOpen = Reaction.isActionViewOpen(); - if (isActionViewOpen === false) { - Session.set("productGrid/selectedProducts", []); - } + const isAdmin = Reaction.hasPermission("createProduct", Meteor.userId()); onData(null, { - canLoadMoreProducts, - productMediaById, - products: stateProducts, - productsSubscription + isAdmin }); } -registerComponent("ProductsAdmin", ProductsComponent, [ - composeWithTracker(composer), - wrapComponent +registerComponent("Products", ProductsContainer, [ + composeWithTracker(composer) ]); export default compose( - composeWithTracker(composer), - wrapComponent -)(ProductsComponent); + composeWithTracker(composer) +)(ProductsContainer); diff --git a/imports/plugins/included/product-variant/containers/productsContainerAdmin.js b/imports/plugins/included/product-variant/containers/productsContainerAdmin.js new file mode 100644 index 00000000000..52f0edfdb58 --- /dev/null +++ b/imports/plugins/included/product-variant/containers/productsContainerAdmin.js @@ -0,0 +1,248 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { compose } from "recompose"; +import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; +import { Meteor } from "meteor/meteor"; +import { ReactiveVar } from "meteor/reactive-var"; +import { Session } from "meteor/session"; +import { Tracker } from "meteor/tracker"; +import { Reaction } from "/client/api"; +import { ITEMS_INCREMENT } from "/client/config/defaults"; +import { ReactionProduct } from "/lib/api"; +import { applyProductRevision } from "/lib/api/products"; +import { Products, Tags, Shops } from "/lib/collections"; +import { Media } from "/imports/plugins/core/files/client"; +import ProductsComponent from "../components/products"; + +const reactiveProductIds = new ReactiveVar([], (oldVal, newVal) => JSON.stringify(oldVal.sort()) === JSON.stringify(newVal.sort())); + +// Isolated resubscribe to product grid images, only when the list of product IDs changes +Tracker.autorun(() => { + Meteor.subscribe("ProductGridMedia", reactiveProductIds.get()); +}); + + +/** + * loadMoreProducts + * @summary whenever #productScrollLimitLoader becomes visible, retrieve more results + * this basically runs this: + * Session.set('productScrollLimit', Session.get('productScrollLimit') + ITEMS_INCREMENT); + * @return {undefined} + */ +function loadMoreProducts() { + let threshold; + const target = document.querySelectorAll("#productScrollLimitLoader"); + let scrollContainer = document.querySelectorAll("#container-main"); + if (scrollContainer.length === 0) { + scrollContainer = window; + } + + if (target.length) { + threshold = scrollContainer[0].scrollHeight - scrollContainer[0].scrollTop === scrollContainer[0].clientHeight; + + if (threshold) { + if (!target[0].getAttribute("visible")) { + target[0].setAttribute("productScrollLimit", true); + Session.set("productScrollLimit", Session.get("productScrollLimit") + ITEMS_INCREMENT || 24); + } + } else if (target[0].getAttribute("visible")) { + target[0].setAttribute("visible", false); + } + } +} + +const wrapComponent = (Comp) => ( + class ProductsContainer extends Component { + static propTypes = { + canLoadMoreProducts: PropTypes.bool, + productsSubscription: PropTypes.object, + showNotFound: PropTypes.bool + }; + + constructor(props) { + super(props); + this.state = { + initialLoad: true + }; + + this.ready = this.ready.bind(this); + this.loadMoreProducts = this.loadMoreProducts.bind(this); + } + + ready = () => { + if (this.props.showNotFound === true) { + return false; + } + + const isInitialLoad = this.state.initialLoad === true; + const isReady = this.props.productsSubscription.ready(); + + if (isInitialLoad === false) { + return true; + } + + if (isReady) { + return true; + } + + return false; + } + + loadMoreProducts = () => this.props.canLoadMoreProducts === true + + loadProducts = (event) => { + event.preventDefault(); + this.setState({ + initialLoad: false + }); + loadMoreProducts(); + } + + render() { + return ( + + ); + } + } +); + +function composer(props, onData) { + window.prerenderReady = false; + + let canLoadMoreProducts = false; + + const slug = Reaction.Router.getParam("slug"); + const shopIdOrSlug = Reaction.Router.getParam("shopSlug"); + + const tag = Tags.findOne({ slug }) || Tags.findOne(slug); + const scrollLimit = Session.get("productScrollLimit"); + let tags = {}; // this could be shop default implementation needed + let shopIds = {}; + + if (tag) { + tags = { tags: [tag._id] }; + } + + if (shopIdOrSlug) { + shopIds = { shops: [shopIdOrSlug] }; + } + + // if we get an invalid slug, don't return all products + if (!tag && slug) { + onData(null, { + showNotFound: true + }); + + return; + } + + const currentTag = ReactionProduct.getTag(); + + const sort = { + [`positions.${currentTag}.position`]: 1, + [`positions.${currentTag}.createdAt`]: 1, + createdAt: 1 + }; + + const viewAsPref = Reaction.getUserPreferences("reaction-dashboard", "viewAs"); + + // Edit mode is true by default + let editMode = true; + + // if we have a "viewAs" preference and the preference is not set to "administrator", then edit mode is false + if (viewAsPref && viewAsPref !== "administrator") { + editMode = false; + } + + const queryParams = Object.assign({}, tags, Reaction.Router.current().query, shopIds); + const productsSubscription = Meteor.subscribe("Products/grid", scrollLimit, queryParams, sort, editMode); + + if (productsSubscription.ready()) { + window.prerenderReady = true; + } + + const activeShopsIds = Shops.find({ + $or: [ + { "workflow.status": "active" }, + { _id: Reaction.getPrimaryShopId() } + ] + }).map((activeShop) => activeShop._id); + + const productCursor = Products.find({ + ancestors: [], + type: { $in: ["simple"] }, + shopId: { $in: activeShopsIds } + }); + + const sortedProducts = ReactionProduct.sortProducts(productCursor.fetch(), currentTag); + Session.set("productGrid/products", sortedProducts); + + const productIds = []; + // Instantiate an object for use as a map. This object does not inherit prototype or methods from `Object` + const productMediaById = Object.create(null); + const stateProducts = sortedProducts.map((product) => { + productIds.push(product._id); + + const primaryMedia = Media.findOneLocal({ + "metadata.productId": product._id, + "metadata.toGrid": 1, + "metadata.workflow": { $nin: ["archived", "unpublished"] } + }, { + sort: { "metadata.priority": 1, "uploadedAt": 1 } + }); + + const variantIds = ReactionProduct.getVariants(product._id).map((variant) => variant._id); + let additionalMedia = Media.findLocal({ + "metadata.productId": product._id, + "metadata.variantId": { $in: variantIds }, + "metadata.workflow": { $nin: ["archived", "unpublished"] } + }, { + limit: 3, + sort: { "metadata.priority": 1, "uploadedAt": 1 } + }); + + if (additionalMedia.length < 2) additionalMedia = null; + + productMediaById[product._id] = { + additionalMedia, + primaryMedia + }; + + return { + ...applyProductRevision(product), + // additionalMedia, + // primaryMedia + }; + }); + + reactiveProductIds.set(productIds); + + canLoadMoreProducts = productCursor.count() >= Session.get("productScrollLimit"); + + const isActionViewOpen = Reaction.isActionViewOpen(); + if (isActionViewOpen === false) { + Session.set("productGrid/selectedProducts", []); + } + + onData(null, { + canLoadMoreProducts, + productMediaById, + products: stateProducts, + productsSubscription + }); +} + +registerComponent("ProductsAdmin", ProductsComponent, [ + composeWithTracker(composer), + wrapComponent +]); + +export default compose( + composeWithTracker(composer), + wrapComponent +)(ProductsComponent); diff --git a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js index e72c7179ccb..e85197d82ae 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js +++ b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js @@ -245,8 +245,8 @@ function composer(props, onData) { }); } -registerComponent("Products", ProductsComponent, [ - composeWithTracker(composer), +registerComponent("ProductsCustomer", ProductsComponent, [ + composeWithTracker(composer) // wrapComponent ]); From ca9cb36a0b6080a5df47c7f8d46561b9d1df66de Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Thu, 1 Mar 2018 13:35:25 -0600 Subject: [PATCH 16/77] refactor: added proptypes to productsContainer, updated producstContainerAdmin to use old products publication --- .../included/product-variant/containers/productsContainer.js | 4 ++++ .../product-variant/containers/productsContainerAdmin.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/imports/plugins/included/product-variant/containers/productsContainer.js b/imports/plugins/included/product-variant/containers/productsContainer.js index 5bb4eb93f5e..0a1d6dafdb3 100644 --- a/imports/plugins/included/product-variant/containers/productsContainer.js +++ b/imports/plugins/included/product-variant/containers/productsContainer.js @@ -12,6 +12,10 @@ const ProductsContainer = ({ isAdmin }) => { return (isAdmin) ? : ; }; +ProductsContainer.propTypes = { + isAdmin: PropTypes.bool +}; + function composer(props, onData) { const isAdmin = Reaction.hasPermission("createProduct", Meteor.userId()); diff --git a/imports/plugins/included/product-variant/containers/productsContainerAdmin.js b/imports/plugins/included/product-variant/containers/productsContainerAdmin.js index 52f0edfdb58..eae3e1b6a66 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerAdmin.js +++ b/imports/plugins/included/product-variant/containers/productsContainerAdmin.js @@ -160,7 +160,7 @@ function composer(props, onData) { } const queryParams = Object.assign({}, tags, Reaction.Router.current().query, shopIds); - const productsSubscription = Meteor.subscribe("Products/grid", scrollLimit, queryParams, sort, editMode); + const productsSubscription = Meteor.subscribe("Products", scrollLimit, queryParams, sort, editMode); if (productsSubscription.ready()) { window.prerenderReady = true; From 46bfb25fe61d0b9cdc2f642775f50d282e39a1dc Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Thu, 1 Mar 2018 13:44:29 -0600 Subject: [PATCH 17/77] refactor: updated product-variant index to import the new ProductsContainer and the renamed ProductsContainerAdmin --- imports/plugins/included/product-variant/client/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/imports/plugins/included/product-variant/client/index.js b/imports/plugins/included/product-variant/client/index.js index e41e1e029a0..be764e791c9 100644 --- a/imports/plugins/included/product-variant/client/index.js +++ b/imports/plugins/included/product-variant/client/index.js @@ -24,6 +24,7 @@ export { default as GridPublishContainer } from "../containers/gridPublishContai export { default as ProductGridContainer } from "../containers/productGridContainer"; export { default as ProductGridItemsContainer } from "../containers/productGridItemsContainer"; export { default as ProductsContainer } from "../containers/productsContainer"; +export { default as ProductsContainerAdmin } from "../containers/productsContainerAdmin"; export { default as ProductsContainerCustomer } from "../containers/productsContainerCustomer"; export { default as VariantFormContainer } from "../containers/variantFormContainer"; export { default as VariantEditContainer } from "../containers/variantEditContainer"; From e6017441c419bc0753f31a38af43b7c3a032a64d Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Thu, 1 Mar 2018 15:16:04 -0600 Subject: [PATCH 18/77] refactor: removed drag and drop wrapper component from customer ProductGrid component --- .../product-variant/components/customer/productGrid.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGrid.js b/imports/plugins/included/product-variant/components/customer/productGrid.js index 8ae29fdeeb4..590cd339cc6 100644 --- a/imports/plugins/included/product-variant/components/customer/productGrid.js +++ b/imports/plugins/included/product-variant/components/customer/productGrid.js @@ -42,11 +42,9 @@ class ProductGrid extends Component { return (
    - -
      - {this.renderProductGridItems()} -
    -
    +
      + {this.renderProductGridItems()} +
    ); From 9a8dc7c88d3e64e44bae416e1bf94a101db7604e Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Thu, 1 Mar 2018 13:46:35 -0800 Subject: [PATCH 19/77] feat: add catalog schema --- lib/collections/schemas/catalog.js | 17 +++++++++++++++++ lib/collections/schemas/index.js | 1 + 2 files changed, 18 insertions(+) create mode 100644 lib/collections/schemas/catalog.js diff --git a/lib/collections/schemas/catalog.js b/lib/collections/schemas/catalog.js new file mode 100644 index 00000000000..e1c292cb986 --- /dev/null +++ b/lib/collections/schemas/catalog.js @@ -0,0 +1,17 @@ + +import { SimpleSchema } from "meteor/aldeed:simple-schema"; +import { Product } from "./products"; + +export const Catalog = new SimpleSchema({ + contentType: { + type: String + }, + media: { + type: Object, + blackbox: true + }, + variants: { + type: Object, + blackbox: true + } +}); diff --git a/lib/collections/schemas/index.js b/lib/collections/schemas/index.js index 242bc73d767..bcdb4fb754a 100644 --- a/lib/collections/schemas/index.js +++ b/lib/collections/schemas/index.js @@ -2,6 +2,7 @@ export * from "./accounts"; export * from "./address"; export * from "./analytics"; export * from "./assets"; +export * from "./catalog"; export * from "./cart"; export * from "./emails"; export * from "./inventory"; From f6352876d5c590cfaa3ffa9216fac6a90fc74391 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Thu, 1 Mar 2018 13:46:56 -0800 Subject: [PATCH 20/77] feat: add catalog collection --- lib/collections/collections.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/collections/collections.js b/lib/collections/collections.js index 9c392ccc2f0..7e2e73d979f 100644 --- a/lib/collections/collections.js +++ b/lib/collections/collections.js @@ -118,6 +118,14 @@ export const Packages = new Mongo.Collection("Packages"); Packages.attachSchema(Schemas.PackageConfig); +/** + * Catalog Collection + * @ignore + */ +export const Catalog = new Mongo.Collection("Catalog"); + +// Catalog.attachSchema(Schemas.Catalog); + /** * Products Collection From d4c2e5370eb3bd85cae8e7610c130590998e6661 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Thu, 1 Mar 2018 13:47:11 -0800 Subject: [PATCH 21/77] feat: fetch products from Catalog collection --- .../product-variant/containers/productsContainerCustomer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js index e85197d82ae..8824bbbd4cb 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js +++ b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js @@ -10,7 +10,7 @@ import { Reaction } from "/client/api"; import { ITEMS_INCREMENT } from "/client/config/defaults"; import { ReactionProduct } from "/lib/api"; import { applyProductRevision } from "/lib/api/products"; -import { Products, Tags, Shops } from "/lib/collections"; +import { Catalog, Products, Tags, Shops } from "/lib/collections"; import { Media } from "/imports/plugins/core/files/client"; import ProductsComponent from "../components/customer/productGrid"; @@ -176,7 +176,7 @@ function composer(props, onData) { ] }).map((activeShop) => activeShop._id); - const productCursor = Products.find({ + const productCursor = Catalog.find({ ancestors: [], type: { $in: ["simple"] }, shopId: { $in: activeShopsIds } From a97e79abb3344d485448454c2275863e3be17761 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Thu, 1 Mar 2018 13:48:24 -0800 Subject: [PATCH 22/77] feat: always allow the publish button to be clicked --- .../plugins/core/revisions/client/components/publishControls.js | 1 - 1 file changed, 1 deletion(-) diff --git a/imports/plugins/core/revisions/client/components/publishControls.js b/imports/plugins/core/revisions/client/components/publishControls.js index 057602a039c..0453e6a0d56 100644 --- a/imports/plugins/core/revisions/client/components/publishControls.js +++ b/imports/plugins/core/revisions/client/components/publishControls.js @@ -218,7 +218,6 @@ class PublishControls extends Component {
    - {/* {this.renderAdditionalMedia()} */} + {this.renderAdditionalMedia()} {!isSearch && this.renderNotices()} From 857f23d5be78fa9a2f4911de16866e2e278dd007 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 10:41:16 -0800 Subject: [PATCH 29/77] feat: move product filters to a shared function Move product filters to a shared function. Use shared `filterProducts` function in `Products` and `Products/grid` publication. --- server/publications/collections/products.js | 107 +++++++++++--------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index bd9b210ccc9..b2dd87d48cf 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -68,47 +68,14 @@ const filters = new SimpleSchema({ registerSchema("filters", filters); -/** - * products publication - * @param {Number} [productScrollLimit] - optional, defaults to 24 - * @param {Array} shops - array of shopId to retrieve product from. - * @return {Object} return product cursor - */ -Meteor.publish("Products", function (productScrollLimit = 24, productFilters, sort = {}, editMode = true) { - check(productScrollLimit, Number); - check(productFilters, Match.OneOf(undefined, Object)); - check(sort, Match.OneOf(undefined, Object)); - check(editMode, Match.Maybe(Boolean)); - - // TODO: Consider publishing the non-admin publication if a user is not in "edit mode" to see what is published - - // Active shop - const shopId = Reaction.getShopId(); - const primaryShopId = Reaction.getPrimaryShopId(); - - // Get a list of shopIds that this user has "createProduct" permissions for (owner permission is checked by default) - const userAdminShopIds = Reaction.getShopsWithRoles(["createProduct"], this.userId); - - // Don't publish if we're missing an active or primary shopId - if (!shopId || !primaryShopId) { - return this.ready(); - } - - // Get active shop id's to use for filtering - const activeShopsIds = Shops.find({ - $or: [ - { "workflow.status": "active" }, - { _id: Reaction.getPrimaryShopId() } - ] - }).fetch().map((activeShop) => activeShop._id); - +function filterProducts(productFilters) { // 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(); + return false; } const shopIdsOrSlugs = productFilters && productFilters.shops; @@ -117,13 +84,9 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so // Get all shopIds associated with the slug or Id const shopIds = Shops.find({ $or: [{ - _id: { - $in: shopIdsOrSlugs - } + _id: { $in: shopIdsOrSlugs } }, { - slug: { - $in: shopIdsOrSlugs - } + slug: { $in: shopIdsOrSlugs } }] }).map((shop) => shop._id); @@ -131,7 +94,7 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so if (shopIds) { productFilters.shops = shopIds; } else { - return this.ready(); + return false; } } @@ -147,11 +110,13 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so if (productFilters.shops) { _.extend(selector, { $or: [{ - shopId: { + "workflow.status": "active", + "shopId": { $in: productFilters.shops } }, { - slug: { + "workflow.status": "active", + "slug": { $in: productFilters.shops } }] @@ -275,6 +240,48 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so } } // end if productFilters + return selector; +} + +/** + * products publication + * @param {Number} [productScrollLimit] - optional, defaults to 24 + * @param {Array} shops - array of shopId to retrieve product from. + * @return {Object} return product cursor + */ +Meteor.publish("Products", function (productScrollLimit = 24, productFilters, sort = {}, editMode = true) { + check(productScrollLimit, Number); + check(productFilters, Match.OneOf(undefined, Object)); + check(sort, Match.OneOf(undefined, Object)); + check(editMode, Match.Maybe(Boolean)); + + // TODO: Consider publishing the non-admin publication if a user is not in "edit mode" to see what is published + + // Active shop + const shopId = Reaction.getShopId(); + const primaryShopId = Reaction.getPrimaryShopId(); + + // Get a list of shopIds that this user has "createProduct" permissions for (owner permission is checked by default) + const userAdminShopIds = Reaction.getShopsWithRoles(["createProduct"], this.userId); + + // Don't publish if we're missing an active or primary shopId + if (!shopId || !primaryShopId) { + return this.ready(); + } + + // Get active shop id's to use for filtering + const activeShopsIds = Shops.find({ + $or: [ + { "workflow.status": "active" }, + { _id: Reaction.getPrimaryShopId() } + ] + }).fetch().map((activeShop) => activeShop._id); + + const selector = filterProducts(productFilters); + + if (selector === false) { + return this.ready(); + } // We publish an admin version of this publication to admins of products who are in "Edit Mode" // Authorized content curators for shops get special publication of the product @@ -537,13 +544,13 @@ Meteor.publish("Products/grid", (productScrollLimit = 24, productFilters, sort = check(sort, Match.OneOf(undefined, Object)); check(editMode, Match.Maybe(Boolean)); - const selector = { - ancestors: [], // Lookup top-level products - isDeleted: { $in: [null, false] }, // by default, we don't publish deleted products - isVisible: true // by default, only lookup visible products - }; + const newSelector = filterProducts(productFilters); + + if (newSelector === false) { + return this.ready(); + } - const productCursor = Catalog.find(selector, { + const productCursor = Catalog.find(newSelector, { sort, limit: productScrollLimit }); From 5ec460fd07f1e3c05fbe03b04b110d01312e8d6d Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 10:41:45 -0800 Subject: [PATCH 30/77] feat: remove limit on media --- imports/plugins/core/catalog/server/methods/catalog.js | 1 - 1 file changed, 1 deletion(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index 33b6af97e1a..b38b2a13335 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -83,7 +83,6 @@ export async function publishProductToCatalog(productId) { "metadata.toGrid": 1, "metadata.workflow": { $nin: ["archived", "unpublished"] } }, { - limit: 4, sort: { "metadata.priority": 1, "uploadedAt": 1 } }); From cacfbf280098b2a0ef177d10f54eeed2dcec28be Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 10:42:44 -0800 Subject: [PATCH 31/77] feat: include source image store `image` --- imports/plugins/core/catalog/server/methods/catalog.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index b38b2a13335..6db9553dbeb 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -90,7 +90,8 @@ export async function publishProductToCatalog(productId) { thumbnail: `${media.url({ store: "thumbnail" })}`, small: `${media.url({ store: "small" })}`, medium: `${media.url({ store: "medium" })}`, - large: `${media.url({ store: "large" })}` + large: `${media.url({ store: "large" })}`, + image: `${media.url({ store: "image" })}` })); console.log("mediaArray", productMedia); From 57e610fc8e73dc2e2c84ae5ea2f4775b4a6e8119 Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Fri, 2 Mar 2018 13:17:11 -0600 Subject: [PATCH 32/77] fix: updated customer product grid item component to only use images 2-4 in the product.media array --- .../components/customer/productGridItem.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGridItem.js b/imports/plugins/included/product-variant/components/customer/productGridItem.js index c0c42b7205b..4e90e621c97 100644 --- a/imports/plugins/included/product-variant/components/customer/productGridItem.js +++ b/imports/plugins/included/product-variant/components/customer/productGridItem.js @@ -25,12 +25,12 @@ class ProductGridItem extends Component { get weightClass() { const { weight } = this.props.position || { weight: 0 }; switch (weight) { - case 1: - return "product-medium"; - case 2: - return "product-large"; - default: - return "product-small"; + case 1: + return "product-medium"; + case 2: + return "product-large"; + default: + return "product-small"; } } @@ -108,9 +108,10 @@ class ProductGridItem extends Component { // or the media object is empty exit if (weight !== 1 || (!media || media.length === 0)) return; - // removing the first image in the media array - // since it's being used as the primary product image - const additionalMedia = [...media.slice(1)]; + // creating an additional madia array with + // the 2nd, 3rd and 4th images returned + // in the media array + const additionalMedia = [...media.slice(1, 4)]; return (
    From 5818a9611efa437cbfb9929ebc75afdb234871fe Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 11:17:43 -0800 Subject: [PATCH 33/77] feat: remove inventory and price fields --- .../core/catalog/server/methods/catalog.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index 6db9553dbeb..9131dba559f 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -12,7 +12,7 @@ import { ProductRevision as Catalog } from "/imports/plugins/core/revisions/serv * @param {Array} variants - Array with top-level variants * @return {Boolean} true if summary product quantity is zero. */ -function isSoldOut(variants) { +export function isSoldOut(variants) { return variants.every((variant) => { if (variant.inventoryManagement) { return Catalog.getVariantQuantity(variant) <= 0; @@ -28,7 +28,7 @@ function isSoldOut(variants) { * @param {Array} variants - array of child variants * @return {boolean} low quantity or not */ -function isLowQuantity(variants) { +export function isLowQuantity(variants) { return variants.some((variant) => { const quantity = Catalog.getVariantQuantity(variant); // we need to keep an eye on `inventoryPolicy` too and qty > 0 @@ -46,7 +46,7 @@ function isLowQuantity(variants) { * @param {Array} variants - array with variant objects * @return {boolean} is backorder allowed or not for a product */ -function isBackorder(variants) { +export function isBackorder(variants) { return variants.every((variant) => variant.inventoryPolicy && variant.inventoryManagement && variant.inventoryQuantity === 0); } @@ -94,14 +94,15 @@ export async function publishProductToCatalog(productId) { image: `${media.url({ store: "image" })}` })); - console.log("mediaArray", productMedia); - - product.varaints = variants; - product.isSoldOut = isSoldOut(variants); - product.isBackorder = isBackorder(variants); - product.isLowQuantity = isLowQuantity(variants); + product.variants = variants; product.media = productMedia; + // Remove inventory fields + delete product.price; + delete product.isSoldOut; + delete product.isLowQuantity; + delete product.isBackorder; + return CatalogCollection.upsert({ _id: productId }, { From 16fd3f3fa927b599e8f44021ae5fcf1ce8953e09 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 11:43:22 -0800 Subject: [PATCH 34/77] feat: set type of product to `simple-product` --- imports/plugins/core/catalog/server/methods/catalog.js | 1 + server/publications/collections/products.js | 1 + 2 files changed, 2 insertions(+) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index 9131dba559f..5d8a2e26fe2 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -96,6 +96,7 @@ export async function publishProductToCatalog(productId) { product.variants = variants; product.media = productMedia; + product.type = "simple-product"; // Remove inventory fields delete product.price; diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index b2dd87d48cf..6f257738b07 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -545,6 +545,7 @@ Meteor.publish("Products/grid", (productScrollLimit = 24, productFilters, sort = check(editMode, Match.Maybe(Boolean)); const newSelector = filterProducts(productFilters); +console.log("newSelector", newSelector); if (newSelector === false) { return this.ready(); From 4aebd8665d03606f07d7b0851c4bffc902f869c2 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 11:43:58 -0800 Subject: [PATCH 35/77] refactor: update method name --- imports/plugins/core/catalog/server/methods/catalog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index 5d8a2e26fe2..3e178b3d227 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -52,7 +52,7 @@ export function isBackorder(variants) { } -export async function publishProductToCatalog(productId) { +export async function publishProductsToCatalog(productId) { check(productId, String); let product = Products.findOne({ @@ -116,5 +116,5 @@ export async function publishProductToCatalog(productId) { } Meteor.methods({ - "catalog/publishProduct": publishProductToCatalog + "catalog/publish/products": publishProductsToCatalog }); From ec32b16ddaaac5da0a8a8d791f3b2a692dac32cb Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Fri, 2 Mar 2018 13:56:06 -0600 Subject: [PATCH 36/77] refactor: refactored renderNotice method in customer product grid item component, did some other code clean up --- .../components/customer/productGridItem.js | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGridItem.js b/imports/plugins/included/product-variant/components/customer/productGridItem.js index 4e90e621c97..502aca54c41 100644 --- a/imports/plugins/included/product-variant/components/customer/productGridItem.js +++ b/imports/plugins/included/product-variant/components/customer/productGridItem.js @@ -34,6 +34,16 @@ class ProductGridItem extends Component { } } + // get notice class names + get noticeClassNames() { + const { product: { isSoldOut, isLowQuantity, isBackorder } } = this.props; + return classnames({ + "variant-qty-sold-out": (isSoldOut || (isSoldOut && isBackorder)), + "badge-danger": (isSoldOut && !isBackorder), + "badge-low-inv-warning": (isLowQuantity && !isSoldOut) + }); + } + // get product item class names get productClassNames() { const { position } = this.props; @@ -52,35 +62,28 @@ class ProductGridItem extends Component { // notice renderNotices() { - const { isSoldOut, isLowQuantity, isBackorder } = this.props.product; - let noticeEl; - // TODO: revisit this if statement and jsx + const { product: { isSoldOut, isLowQuantity, isBackorder } } = this.props; + const noticeContent = { classNames: this.noticeClassNames }; + if (isSoldOut) { if (isBackorder) { - noticeEl = ( - - - - ); + noticeContent.defaultValue = "Backorder"; + noticeContent.i18nKey = "productDetail.backOrder"; } else { - noticeEl = ( - - - - ); + noticeContent.defaultValue = "Sold Out!"; + noticeContent.i18nKey = "productDetail.soldOut"; } } else if (isLowQuantity) { - noticeEl = ( - - - - ); + noticeContent.defaultValue = "Limited Supply"; + noticeContent.i18nKey = "productDetail.limitedSupply"; } return (
    - {noticeEl} + + +
    ); From f3a03980b116a751315e9e048a9dfaa69ff4a6cd Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Fri, 2 Mar 2018 14:46:09 -0600 Subject: [PATCH 37/77] style: fixed typo in code commment --- .../product-variant/components/customer/productGridItem.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGridItem.js b/imports/plugins/included/product-variant/components/customer/productGridItem.js index 502aca54c41..a47d30b4804 100644 --- a/imports/plugins/included/product-variant/components/customer/productGridItem.js +++ b/imports/plugins/included/product-variant/components/customer/productGridItem.js @@ -38,6 +38,7 @@ class ProductGridItem extends Component { get noticeClassNames() { const { product: { isSoldOut, isLowQuantity, isBackorder } } = this.props; return classnames({ + "badge": (isSoldOut || isLowQuantity), "variant-qty-sold-out": (isSoldOut || (isSoldOut && isBackorder)), "badge-danger": (isSoldOut && !isBackorder), "badge-low-inv-warning": (isLowQuantity && !isSoldOut) @@ -81,7 +82,7 @@ class ProductGridItem extends Component { return (
    - +
    @@ -108,7 +109,7 @@ class ProductGridItem extends Component { const { product: { media }, position: { weight } } = this.props; // if product is not medium weight - // or the media object is empty exit + // or the media array is empty exit if (weight !== 1 || (!media || media.length === 0)) return; // creating an additional madia array with From 2126496fd8ef851387aea92e3aabba45cf7d3da1 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 12:54:44 -0800 Subject: [PATCH 38/77] feat: publish products after revision publish --- .../client/containers/publishContainer.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/imports/plugins/core/revisions/client/containers/publishContainer.js b/imports/plugins/core/revisions/client/containers/publishContainer.js index 3d2aaf3a300..d4fdc3415dd 100644 --- a/imports/plugins/core/revisions/client/containers/publishContainer.js +++ b/imports/plugins/core/revisions/client/containers/publishContainer.js @@ -12,16 +12,22 @@ import { Reaction, i18next } from "/client/api"; * PublishContainer is a container component connected to Meteor data source. */ class PublishContainer extends Component { - handlePublishClick = (revisions) => { - Meteor.call("catalog/publishProduct", this.props.product._id, (error, result) => { + publishToCatalog(collection, documentIds) { + Meteor.call(`catalog/publish/${collection}`, documentIds, (error, result) => { if (result) { Alerts.toast(i18next("catalog.productPublishSuccess", { defaultValue: "Product published to catalog" }), "success"); - } else { + } else if (error) { Alerts.toast(error.message, "error"); } }); + } - if (Array.isArray(revisions)) { + handlePublishClick = (revisions) => { + const productIds = this.props.documents + .filter((doc) => doc.type === "simple") + .map((doc) => doc._id); + + if (Array.isArray(revisions) && revisions.length) { let documentIds = revisions.map((revision) => { if (revision.parentDocument && revision.documentType !== "product") { return revision.parentDocument; @@ -41,11 +47,17 @@ class PublishContainer extends Component { if (this.props.onPublishSuccess) { this.props.onPublishSuccess(result); + + // Publish to catalog after revisions have been published + this.publishToCatalog("products", productIds); } } else { Alerts.toast(error.message, "error"); } }); + } else { + // Publish to catalog immediately if there are no revisions to publish beforehand + this.publishToCatalog("products", productIds); } } From cc6a8774dc18c94afe570cc948b94a7fc0f5260b Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 12:57:47 -0800 Subject: [PATCH 39/77] feat: support multiple product ids --- .../core/catalog/server/methods/catalog.js | 118 ++++++++++-------- 1 file changed, 65 insertions(+), 53 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index 3e178b3d227..a4ef8eb24dd 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -52,66 +52,78 @@ export function isBackorder(variants) { } -export async function publishProductsToCatalog(productId) { - check(productId, String); - - let product = Products.findOne({ - $or: [ - { _id: productId }, - { ancestors: { $in: [productId] } } - ] - }); +export async function publishProductsToCatalog(productIds) { + check(productIds, Match.OneOf(String, Array)); - if (!product) { - throw new Meteor.error("error", "Cannot publish product"); + let ids = productIds; + if (typeof ids === "string") { + ids = [productIds]; } - if (Array.isArray(product.ancestors) && product.ancestors.length) { - product = Products.findOne({ - _id: product.ancestors[0] + ids.forEach(async (productId) => { + let product = Products.findOne({ + $or: [ + { _id: productId }, + { ancestors: { $in: [productId] } } + ] }); - } - const variants = Products.find({ - ancestors: { - $in: [productId] + if (!product) { + throw new Meteor.error("error", "Cannot publish product"); } - }).fetch(); - - const mediaArray = await Media.find({ - "metadata.productId": productId, - "metadata.toGrid": 1, - "metadata.workflow": { $nin: ["archived", "unpublished"] } - }, { - sort: { "metadata.priority": 1, "uploadedAt": 1 } - }); - const productMedia = mediaArray.map((media) => ({ - thumbnail: `${media.url({ store: "thumbnail" })}`, - small: `${media.url({ store: "small" })}`, - medium: `${media.url({ store: "medium" })}`, - large: `${media.url({ store: "large" })}`, - image: `${media.url({ store: "image" })}` - })); - - product.variants = variants; - product.media = productMedia; - product.type = "simple-product"; - - // Remove inventory fields - delete product.price; - delete product.isSoldOut; - delete product.isLowQuantity; - delete product.isBackorder; - - return CatalogCollection.upsert({ - _id: productId - }, { - $set: product - }, { - multi: true, - upsert: true, - validate: false + if (Array.isArray(product.ancestors) && product.ancestors.length) { + product = Products.findOne({ + _id: product.ancestors[0] + }); + } + + const variants = Products.find({ + ancestors: { + $in: [productId] + } + }).fetch(); + + const mediaArray = await Media.find({ + "metadata.productId": productId, + "metadata.toGrid": 1, + "metadata.workflow": { $nin: ["archived", "unpublished"] } + }, { + sort: { "metadata.priority": 1, "uploadedAt": 1 } + }); + + const productMedia = mediaArray.map((media) => ({ + thumbnail: `${media.url({ store: "thumbnail" })}`, + small: `${media.url({ store: "small" })}`, + medium: `${media.url({ store: "medium" })}`, + large: `${media.url({ store: "large" })}`, + image: `${media.url({ store: "image" })}` + })); + + product.variants = variants; + product.media = productMedia; + product.type = "product-simple"; + + // TODO: Remove these fields in favor of inventory/pricing collection + product.isSoldOut = isSoldOut(variants); + product.isBackorder = isBackorder(variants); + product.isLowQuantity = isLowQuantity(variants); + + // Remove inventory fields + // delete product.price; + // delete product.isSoldOut; + // delete product.isLowQuantity; + // delete product.isBackorder; + + return CatalogCollection.upsert({ + _id: productId + }, { + $set: product + }, { + multi: true, + upsert: true, + validate: false + }); }); } From 1cf72e80f82340685a886f377e736f0be5276429 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 12:58:33 -0800 Subject: [PATCH 40/77] refactor: filter by type `product-simple` --- .../product-variant/containers/productsContainerCustomer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js index 8824bbbd4cb..13e637c2af1 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js +++ b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js @@ -178,7 +178,7 @@ function composer(props, onData) { const productCursor = Catalog.find({ ancestors: [], - type: { $in: ["simple"] }, + type: { $in: ["product-simple"] }, shopId: { $in: activeShopsIds } }, { $sort: sort From 85bb8aff36c6e1e0d8f64a8d10825a426e2006ff Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 12:58:48 -0800 Subject: [PATCH 41/77] refactor: remove log --- server/publications/collections/products.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index 6f257738b07..b2dd87d48cf 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -545,7 +545,6 @@ Meteor.publish("Products/grid", (productScrollLimit = 24, productFilters, sort = check(editMode, Match.Maybe(Boolean)); const newSelector = filterProducts(productFilters); -console.log("newSelector", newSelector); if (newSelector === false) { return this.ready(); From bfd2c6dd2416193fa28f85416b14322b1da869c6 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 13:16:02 -0800 Subject: [PATCH 42/77] refactor: disable publish button if no documents can be published --- .../core/revisions/client/components/publishControls.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/imports/plugins/core/revisions/client/components/publishControls.js b/imports/plugins/core/revisions/client/components/publishControls.js index 0453e6a0d56..8381547b691 100644 --- a/imports/plugins/core/revisions/client/components/publishControls.js +++ b/imports/plugins/core/revisions/client/components/publishControls.js @@ -214,10 +214,13 @@ class PublishControls extends Component { buttonProps.i18nKeyLabel = "toolbar.publishAll"; } + const isDisabled = Array.isArray(this.props.documentIds) && this.props.documentIds.length === 0; + return (
    ); diff --git a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js index 8824bbbd4cb..e7c0b642da2 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js +++ b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js @@ -120,7 +120,7 @@ function composer(props, onData) { const shopIdOrSlug = Reaction.Router.getParam("shopSlug"); const tag = Tags.findOne({ slug }) || Tags.findOne(slug); - const scrollLimit = Session.get("productScrollLimit"); + const scrollLimit = 4; //Session.get("productScrollLimit"); let tags = {}; // this could be shop default implementation needed let shopIds = {}; @@ -229,7 +229,7 @@ function composer(props, onData) { // reactiveProductIds.set(productIds); - canLoadMoreProducts = productCursor.count() >= Session.get("productScrollLimit"); + canLoadMoreProducts = productCursor.count() >= 4; //Session.get("productScrollLimit"); // const isActionViewOpen = Reaction.isActionViewOpen(); // if (isActionViewOpen === false) { From 83d1039a828580f72b345e55e4ae4f32ffd26c22 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 14:02:25 -0800 Subject: [PATCH 44/77] fix: i18n translation call --- .../core/revisions/client/containers/publishContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/core/revisions/client/containers/publishContainer.js b/imports/plugins/core/revisions/client/containers/publishContainer.js index d4fdc3415dd..ac7d600d65a 100644 --- a/imports/plugins/core/revisions/client/containers/publishContainer.js +++ b/imports/plugins/core/revisions/client/containers/publishContainer.js @@ -15,7 +15,7 @@ class PublishContainer extends Component { publishToCatalog(collection, documentIds) { Meteor.call(`catalog/publish/${collection}`, documentIds, (error, result) => { if (result) { - Alerts.toast(i18next("catalog.productPublishSuccess", { defaultValue: "Product published to catalog" }), "success"); + Alerts.toast(i18next.t("catalog.productPublishSuccess", { defaultValue: "Product published to catalog" }), "success"); } else if (error) { Alerts.toast(error.message, "error"); } From dba43e2464cf9ba7e54c5c435060a307825f5935 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 14:04:12 -0800 Subject: [PATCH 45/77] refactor: check permissions on products before publishing --- .../core/catalog/server/methods/catalog.js | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index a4ef8eb24dd..a0cd0cbb12b 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -1,7 +1,7 @@ import { Meteor } from "meteor/meteor"; import { check, Match } from "meteor/check"; import { Products, Catalog as CatalogCollection } from "/lib/collections"; -import { Logger } from "/server/api"; +import { Logger, Reaction } from "/server/api"; import { Media } from "/imports/plugins/core/files/server"; import { ProductRevision as Catalog } from "/imports/plugins/core/revisions/server/hooks"; @@ -60,7 +60,7 @@ export async function publishProductsToCatalog(productIds) { ids = [productIds]; } - ids.forEach(async (productId) => { + return ids.every(async (productId) => { let product = Products.findOne({ $or: [ { _id: productId }, @@ -115,18 +115,54 @@ export async function publishProductsToCatalog(productIds) { // delete product.isLowQuantity; // delete product.isBackorder; - return CatalogCollection.upsert({ + const result = CatalogCollection.upsert({ _id: productId }, { $set: product - }, { - multi: true, - upsert: true, - validate: false }); + + return result && result.numberAffected === 1; }); } Meteor.methods({ - "catalog/publish/products": publishProductsToCatalog + "catalog/publish/products": (productIds) => { + check(productIds, Match.OneOf(String, Array)); + + // Ensure user has createProduct permission for active shop + if (!Reaction.hasPermission("createProduct")) { + throw new Meteor.Error("access-denied", "Access Denied"); + } + + // Convert productIds if it's a string + let ids = productIds; + if (typeof ids === "string") { + ids = [productIds]; + } + + // Find all products + const productsToPublish = Products.find({ + _id: { $in: ids } + }).fetch(); + + if (Array.isArray(productsToPublish)) { + const canUpdatePrimaryShopProducts = Reaction.hasPermission("createProduct", this.userId, Reaction.getPrimaryShopId()); + + const publisableProductIds = productsToPublish + // Only allow users to publish products for shops they permissions to createProductsFor + // If the user can createProducts on the main shop, they can publish products for all shops to the catalog. + .filter((product) => Reaction.hasPermission("createProduct", this.userId, product.shopId) || canUpdatePrimaryShopProducts) + .map((product) => product._id); + + const success = publishProductsToCatalog(publisableProductIds); + + if (!success) { + throw new Meteor.Error("server-error", "Some Products could not be published to the Catalog."); + } + + return true; + } + + return false; + } }); From 9f8708d18c6592f789eb0a8d542dd2ffde3b6258 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 14:11:48 -0800 Subject: [PATCH 46/77] docs: Add jsdoc for `Products/grid` method --- server/publications/collections/products.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index b2dd87d48cf..3f244caeb7e 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -537,7 +537,16 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so }); }); - +/** + * @name Products/grid + * @method + * @memberof Core + * @summary Publication method for a customer facing product grid + * @param {number} productScrollLimit - product find limit + * @param {object} productFilters - filters to be applied to the product find + * @param {object} sort - sorting to be applied to the product find + * @return {MongoCursor} Mongo cursor object of found products + */ Meteor.publish("Products/grid", (productScrollLimit = 24, productFilters, sort = {}, editMode = true) => { check(productScrollLimit, Number); check(productFilters, Match.OneOf(undefined, Object)); From 70abfe86c022ec79a71e72b40b8a42ac421f44f2 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 2 Mar 2018 14:13:23 -0800 Subject: [PATCH 47/77] refactor: remove edit mode param --- .../product-variant/containers/productsContainerCustomer.js | 2 +- server/publications/collections/products.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js index 13e637c2af1..6eea4be7381 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js +++ b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js @@ -163,7 +163,7 @@ function composer(props, onData) { // } const queryParams = Object.assign({}, tags, Reaction.Router.current().query, shopIds); - const productsSubscription = Meteor.subscribe("Products/grid", scrollLimit, queryParams, sort, false); + const productsSubscription = Meteor.subscribe("Products/grid", scrollLimit, queryParams, sort); if (productsSubscription.ready()) { window.prerenderReady = true; diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index 3f244caeb7e..9952a88a9d5 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -547,11 +547,10 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so * @param {object} sort - sorting to be applied to the product find * @return {MongoCursor} Mongo cursor object of found products */ -Meteor.publish("Products/grid", (productScrollLimit = 24, productFilters, sort = {}, editMode = true) => { +Meteor.publish("Products/grid", (productScrollLimit = 24, productFilters, sort = {}) => { check(productScrollLimit, Number); check(productFilters, Match.OneOf(undefined, Object)); check(sort, Match.OneOf(undefined, Object)); - check(editMode, Match.Maybe(Boolean)); const newSelector = filterProducts(productFilters); From 8d5d1a5e695aaec5db26cdcd04efc2ef41c6e02d Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Fri, 2 Mar 2018 17:31:43 -0600 Subject: [PATCH 48/77] refactor: updated customer ProductGrid to display a loading spinner or No Products Found message waiting / no products, WIP loading more products to the grid --- .../components/customer/productGrid.js | 64 ++++++++++--------- .../containers/productsContainerCustomer.js | 10 +-- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGrid.js b/imports/plugins/included/product-variant/components/customer/productGrid.js index 6f169ba7ae0..0a3a142c02b 100644 --- a/imports/plugins/included/product-variant/components/customer/productGrid.js +++ b/imports/plugins/included/product-variant/components/customer/productGrid.js @@ -7,39 +7,45 @@ import { ReactionProduct } from "/lib/api"; class ProductGrid extends Component { static propTypes = { canLoadMoreProducts: PropTypes.bool, - loadMoreProducts: PropTypes.func, - products: PropTypes.array + loadProducts: PropTypes.func, + products: PropTypes.array, + ready: PropTypes.func } - loadMoreProducts = () => { + // events + + // load more products to the grid + loadMoreProducts = (event) => { console.log("loading more products"); - this.props.loadMoreProducts(); + this.props.loadProducts(event); + } + + // render the laoding spinner + renderLoadingSpinner() { + return ; } - // render functions + // render the No Products Found message + renderNotFound() { + return ; + } - renderProductGridItems() { + // render the product grid + renderProductGrid() { const { products } = this.props; const currentTag = ReactionProduct.getTag(); - if (Array.isArray(products)) { - return products.map((product, index) => ( - - )); - } - return ( -
    -
    -

    - -

    -
    +
    +
      + {products.map((product) => ( + + ))} +
    ); } @@ -47,14 +53,14 @@ class ProductGrid extends Component { render() { console.log("this.props", this.props); + const { products, ready } = this.props; + if (!ready()) return this.renderLoadingSpinner(); + if (!Array.isArray(products) || !products.length) return this.renderNotFound(); + return (
    -
    -
      - {this.renderProductGridItems()} -
    - -
    + {this.renderProductGrid()} +
    ); } diff --git a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js index 5d6915d9f5f..7470c9902d4 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js +++ b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js @@ -32,7 +32,7 @@ const reactiveProductIds = new ReactiveVar([], (oldVal, newVal) => JSON.stringif function loadMoreProducts() { let threshold; const target = document.querySelectorAll("#productScrollLimitLoader"); - let scrollContainer = document.querySelectorAll("#container-main"); + let scrollContainer = document.querySelectorAll(".container-main"); if (scrollContainer.length === 0) { scrollContainer = window; } @@ -120,7 +120,7 @@ function composer(props, onData) { const shopIdOrSlug = Reaction.Router.getParam("shopSlug"); const tag = Tags.findOne({ slug }) || Tags.findOne(slug); - const scrollLimit = 4; //Session.get("productScrollLimit"); + const scrollLimit = Session.get("productScrollLimit"); let tags = {}; // this could be shop default implementation needed let shopIds = {}; @@ -229,7 +229,7 @@ function composer(props, onData) { // reactiveProductIds.set(productIds); - canLoadMoreProducts = productCursor.count() >= 4; //Session.get("productScrollLimit"); + canLoadMoreProducts = productCursor.count() >= Session.get("productScrollLimit"); // const isActionViewOpen = Reaction.isActionViewOpen(); // if (isActionViewOpen === false) { @@ -238,10 +238,10 @@ function composer(props, onData) { const products = productCursor.fetch(); onData(null, { canLoadMoreProducts, - products + products, // productMediaById, // products: stateProducts, - // productsSubscription + productsSubscription }); } From 64a8c905d1c6e17d424ba6cf8537b98e437d9c39 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Sun, 4 Mar 2018 10:04:52 -0800 Subject: [PATCH 49/77] feat: add function to publish inventory updates to catalog --- .../core/catalog/server/methods/catalog.js | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index a0cd0cbb12b..381f1e918e0 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -47,7 +47,7 @@ export function isLowQuantity(variants) { * @return {boolean} is backorder allowed or not for a product */ export function isBackorder(variants) { - return variants.every((variant) => variant.inventoryPolicy && variant.inventoryManagement && + return variants.every((variant) => !variant.inventoryPolicy && variant.inventoryManagement && variant.inventoryQuantity === 0); } @@ -100,20 +100,15 @@ export async function publishProductsToCatalog(productIds) { image: `${media.url({ store: "image" })}` })); - product.variants = variants; product.media = productMedia; product.type = "product-simple"; - - // TODO: Remove these fields in favor of inventory/pricing collection product.isSoldOut = isSoldOut(variants); product.isBackorder = isBackorder(variants); product.isLowQuantity = isLowQuantity(variants); - - // Remove inventory fields - // delete product.price; - // delete product.isSoldOut; - // delete product.isLowQuantity; - // delete product.isBackorder; + product.variants = variants.map((variant) => { + const { inventoryQuantity, ...v } = variant; + return v; + }); const result = CatalogCollection.upsert({ _id: productId @@ -125,6 +120,47 @@ export async function publishProductsToCatalog(productIds) { }); } +export function publishProductInventoryAdjustments(productId) { + check(productId, Match.OneOf(String, Array)); + + const catalogProduct = CatalogCollection.findOne({ + _id: productId + }); + + if (!catalogProduct) { + throw new Meteor.error("error", "Cannot publish inventory changes to catalog product"); + } + + const variants = Products.find({ + ancestors: { + $in: [productId] + } + }).fetch(); + + const update = { + isSoldOut: isSoldOut(variants), + isBackorder: isBackorder(variants), + isLowQuantity: isLowQuantity(variants) + }; + + // Only apply changes of one these fields have changed + if ( + update.isSoldOut !== catalogProduct.isSoldOut || + update.isBackorder !== catalogProduct.isBackorder || + update.isLowQuantity !== catalogProduct.isLowQuantity + ) { + const result = CatalogCollection.update({ + _id: productId + }, { + $set: update + }); + + return result; + } + + return false; +} + Meteor.methods({ "catalog/publish/products": (productIds) => { check(productIds, Match.OneOf(String, Array)); From 0c3dc26a7fd66c98fb2a1ad72341cf6f02f68684 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Sun, 4 Mar 2018 10:05:19 -0800 Subject: [PATCH 50/77] feat: publish inventory updates to catalog --- server/methods/core/orders.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/methods/core/orders.js b/server/methods/core/orders.js index 56dd796ea70..d93ebc4dd97 100644 --- a/server/methods/core/orders.js +++ b/server/methods/core/orders.js @@ -7,6 +7,7 @@ import { SSR } from "meteor/meteorhacks:ssr"; import { Orders, Products, Shops, Packages } from "/lib/collections"; import { Logger, Hooks, Reaction } from "/server/api"; import { Media } from "/imports/plugins/core/files/server"; +import { publishProductInventoryAdjustments } from "/imports/plugins/core/catalog/server/methods/catalog"; /** * @name getPrimaryMediaForItem @@ -122,6 +123,9 @@ export function ordersInventoryAdjust(orderId) { type: "variant" } }); + + // Publish inventory updates to the Catalog + publishProductInventoryAdjustments(item.productId); }); } @@ -158,6 +162,9 @@ export function ordersInventoryAdjustByShop(orderId, shopId) { type: "variant" } }); + + // Publish inventory updates to the Catalog + publishProductInventoryAdjustments(item.productId); } }); } @@ -415,6 +422,9 @@ export const methods = { bypassCollection2: true, publish: true }); + + // Publish inventory updates to the Catalog + publishProductInventoryAdjustments(item.productId); } }); } From 7eda33edd97238f08799ff3c0bb63f36e26cc7f1 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Sun, 4 Mar 2018 10:06:20 -0800 Subject: [PATCH 51/77] refactor: don't publish variants to client --- server/publications/collections/products.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index 9952a88a9d5..60761029b4c 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -560,7 +560,10 @@ Meteor.publish("Products/grid", (productScrollLimit = 24, productFilters, sort = const productCursor = Catalog.find(newSelector, { sort, - limit: productScrollLimit + limit: productScrollLimit, + fields: { + variants: 0 + } }); return productCursor; From 91ecd76730a53c61c00cef6229df9fe167ca8f88 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Sun, 4 Mar 2018 10:07:14 -0800 Subject: [PATCH 52/77] docs: remove private --- imports/plugins/core/catalog/server/methods/catalog.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index 381f1e918e0..b022a65ac33 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -7,7 +7,6 @@ import { ProductRevision as Catalog } from "/imports/plugins/core/revisions/serv /** * isSoldOut - * @private * @summary We are stop accepting new orders if product marked as `isSoldOut`. * @param {Array} variants - Array with top-level variants * @return {Boolean} true if summary product quantity is zero. @@ -23,7 +22,6 @@ export function isSoldOut(variants) { /** * isLowQuantity - * @private * @summary If at least one of the variants is less than the threshold, then function returns `true` * @param {Array} variants - array of child variants * @return {boolean} low quantity or not @@ -41,7 +39,6 @@ export function isLowQuantity(variants) { /** * isBackorder - * @private * @description Is products variants is still available to be ordered after summary variants quantity is zero * @param {Array} variants - array with variant objects * @return {boolean} is backorder allowed or not for a product From 1809af7b60d3dc606588830d71e6e6ca5b083956 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Sun, 4 Mar 2018 10:13:47 -0800 Subject: [PATCH 53/77] docs: add jsdoc for catalog publish methods --- .../plugins/core/catalog/server/methods/catalog.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index b022a65ac33..edabdfa8af3 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -48,7 +48,12 @@ export function isBackorder(variants) { variant.inventoryQuantity === 0); } - +/** + * publishProductsToCatalog + * @description Publish one or more products to the Catalog + * @param {string|array} productIds - A string product id or an array of product ids + * @return {boolean} true on successful publish for all documents, false if one ore more fail to publish + */ export async function publishProductsToCatalog(productIds) { check(productIds, Match.OneOf(String, Array)); @@ -117,6 +122,12 @@ export async function publishProductsToCatalog(productIds) { }); } +/** + * publishProductInventoryAdjustments + * @description Publish inventory updates for a single product to the Catalog + * @param {string} productId - A string product id + * @return {boolean} true on success, false on failure + */ export function publishProductInventoryAdjustments(productId) { check(productId, Match.OneOf(String, Array)); From 75b42ac782d941a7c01bae533cedd29dd95ff808 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Mon, 5 Mar 2018 09:30:03 -0800 Subject: [PATCH 54/77] fix: use `function` to fix `this` reference in body --- server/publications/collections/products.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index 60761029b4c..c78718e8c0d 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -547,7 +547,7 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so * @param {object} sort - sorting to be applied to the product find * @return {MongoCursor} Mongo cursor object of found products */ -Meteor.publish("Products/grid", (productScrollLimit = 24, productFilters, sort = {}) => { +Meteor.publish("Products/grid", function (productScrollLimit = 24, productFilters, sort = {}) { check(productScrollLimit, Number); check(productFilters, Match.OneOf(undefined, Object)); check(sort, Match.OneOf(undefined, Object)); From 897ff11605303f912cbf759bee57c3dee4a02ffe Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Mon, 5 Mar 2018 11:27:03 -0800 Subject: [PATCH 55/77] refactor: log errors --- imports/plugins/core/catalog/server/methods/catalog.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index edabdfa8af3..93ee4ffcae1 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -72,6 +72,7 @@ export async function publishProductsToCatalog(productIds) { if (!product) { throw new Meteor.error("error", "Cannot publish product"); + Logger.error("Cannot publish product to catalog"); } if (Array.isArray(product.ancestors) && product.ancestors.length) { @@ -137,6 +138,7 @@ export function publishProductInventoryAdjustments(productId) { if (!catalogProduct) { throw new Meteor.error("error", "Cannot publish inventory changes to catalog product"); + Logger.error("Cannot publish inventory changes to catalog product"); } const variants = Products.find({ @@ -175,6 +177,7 @@ Meteor.methods({ // Ensure user has createProduct permission for active shop if (!Reaction.hasPermission("createProduct")) { + Logger.error("Access Denied"); throw new Meteor.Error("access-denied", "Access Denied"); } @@ -201,6 +204,7 @@ Meteor.methods({ const success = publishProductsToCatalog(publisableProductIds); if (!success) { + Logger.error("Some Products could not be published to the Catalog."); throw new Meteor.Error("server-error", "Some Products could not be published to the Catalog."); } From 0f2cb3e2921bb23fd9b8dfe210b0e5f9728a8e6b Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Mon, 5 Mar 2018 11:28:08 -0800 Subject: [PATCH 56/77] fix: typos with some Meteor.Error constructors --- imports/plugins/core/catalog/server/methods/catalog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index 93ee4ffcae1..9cbf4b9c2aa 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -71,8 +71,8 @@ export async function publishProductsToCatalog(productIds) { }); if (!product) { - throw new Meteor.error("error", "Cannot publish product"); Logger.error("Cannot publish product to catalog"); + throw new Meteor.Error("error", "Cannot publish product to catalog"); } if (Array.isArray(product.ancestors) && product.ancestors.length) { @@ -137,8 +137,8 @@ export function publishProductInventoryAdjustments(productId) { }); if (!catalogProduct) { - throw new Meteor.error("error", "Cannot publish inventory changes to catalog product"); Logger.error("Cannot publish inventory changes to catalog product"); + throw new Meteor.Error("error", "Cannot publish inventory changes to catalog product"); } const variants = Products.find({ From bcfa0cf580f65d91c6bb8d8e2f7796005d192faf Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Mon, 5 Mar 2018 11:41:05 -0800 Subject: [PATCH 57/77] refactor: return false instead of throwing an error Return false instead of throwing an error when attempting to publish a product that may not exist. Instead, return false and allow the client to handle the error appropriately. --- imports/plugins/core/catalog/server/methods/catalog.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index 9cbf4b9c2aa..1ee85157cb2 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -71,8 +71,8 @@ export async function publishProductsToCatalog(productIds) { }); if (!product) { - Logger.error("Cannot publish product to catalog"); - throw new Meteor.Error("error", "Cannot publish product to catalog"); + Logger.info("Cannot publish product to catalog"); + return false; } if (Array.isArray(product.ancestors) && product.ancestors.length) { @@ -137,8 +137,8 @@ export function publishProductInventoryAdjustments(productId) { }); if (!catalogProduct) { - Logger.error("Cannot publish inventory changes to catalog product"); - throw new Meteor.Error("error", "Cannot publish inventory changes to catalog product"); + Logger.info("Cannot publish inventory changes to catalog product"); + return false; } const variants = Products.find({ From 6ebf5727034c33841d68a0282559edfa47a03221 Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Mon, 5 Mar 2018 14:54:23 -0600 Subject: [PATCH 58/77] fix: customer product grid now loads more on scroll, fixed when the not foud and spinner components show --- .../components/customer/productGrid.js | 47 ++++++++--- .../containers/productsContainerCustomer.js | 80 ++++++++++--------- 2 files changed, 75 insertions(+), 52 deletions(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGrid.js b/imports/plugins/included/product-variant/components/customer/productGrid.js index 0a3a142c02b..2361fce1fd0 100644 --- a/imports/plugins/included/product-variant/components/customer/productGrid.js +++ b/imports/plugins/included/product-variant/components/customer/productGrid.js @@ -9,25 +9,51 @@ class ProductGrid extends Component { canLoadMoreProducts: PropTypes.bool, loadProducts: PropTypes.func, products: PropTypes.array, - ready: PropTypes.func + productsSubscription: PropTypes.object } - // events + componentDidMount() { + window.addEventListener("scroll", this.loadMoreProducts); + } + + componentWillUnmount() { + window.removeEventListener("scroll", this.loadMoreProducts); + } // load more products to the grid loadMoreProducts = (event) => { - console.log("loading more products"); - this.props.loadProducts(event); + const { canLoadMoreProducts, loadProducts } = this.props; + const { scrollY, innerHeight } = window; + const { body: { scrollHeight } } = document; + const atBottom = (innerHeight + scrollY === scrollHeight); + + if (canLoadMoreProducts && atBottom) { + loadProducts(event); + } } // render the laoding spinner renderLoadingSpinner() { - return ; + const { productsSubscription: { ready } } = this.props; + // if the products catalog is not ready + // show the loading spinner + if (!ready()) return ; } // render the No Products Found message renderNotFound() { - return ; + const { products, productsSubscription: { ready } } = this.props; + // if the products subscription is ready & the products array is undefined or empty + // show the Not Found message + if (ready() && (!Array.isArray(products) || !products.length)) { + return ( + + ); + } } // render the product grid @@ -51,16 +77,11 @@ class ProductGrid extends Component { } render() { - console.log("this.props", this.props); - - const { products, ready } = this.props; - if (!ready()) return this.renderLoadingSpinner(); - if (!Array.isArray(products) || !products.length) return this.renderNotFound(); - return (
    {this.renderProductGrid()} - + {this.renderLoadingSpinner()} + {this.renderNotFound()}
    ); } diff --git a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js index b5951bccc26..a8a9c693f90 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js +++ b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js @@ -30,25 +30,31 @@ const reactiveProductIds = new ReactiveVar([], (oldVal, newVal) => JSON.stringif * @return {undefined} */ function loadMoreProducts() { - let threshold; - const target = document.querySelectorAll("#productScrollLimitLoader"); - let scrollContainer = document.querySelectorAll(".container-main"); - if (scrollContainer.length === 0) { - scrollContainer = window; - } + // let threshold; + // const target = document.querySelectorAll("#productScrollLimitLoader"); + // let scrollContainer = document.querySelectorAll(".container-main"); + // if (scrollContainer.length === 0) { + // scrollContainer = window; + // } - if (target.length) { - threshold = scrollContainer[0].scrollHeight - scrollContainer[0].scrollTop === scrollContainer[0].clientHeight; + // console.log("target scroll containter", target, scrollContainer); - if (threshold) { - if (!target[0].getAttribute("visible")) { - target[0].setAttribute("productScrollLimit", true); - Session.set("productScrollLimit", Session.get("productScrollLimit") + ITEMS_INCREMENT || 24); - } - } else if (target[0].getAttribute("visible")) { - target[0].setAttribute("visible", false); - } - } + // if (target.length) { + // threshold = scrollContainer[0].scrollHeight - scrollContainer[0].scrollTop === scrollContainer[0].clientHeight; + + // console.log("load more", threshold); + + // if (threshold) { + // if (!target[0].getAttribute("visible")) { + // target[0].setAttribute("productScrollLimit", true); + + // } + // } else if (target[0].getAttribute("visible")) { + // target[0].setAttribute("visible", false); + // } + // } + + Session.set("productScrollLimit", Session.get("productScrollLimit") + ITEMS_INCREMENT || 24); } const wrapComponent = (Comp) => ( @@ -64,34 +70,32 @@ const wrapComponent = (Comp) => ( this.state = { initialLoad: true }; - - this.ready = this.ready.bind(this); - this.loadMoreProducts = this.loadMoreProducts.bind(this); } - ready = () => { - if (this.props.showNotFound === true) { - return false; - } + // ready = () => { + // // if (this.props.showNotFound === true) { + // // return false; + // // } - const isInitialLoad = this.state.initialLoad === true; - const isReady = this.props.productsSubscription.ready(); + // // const isInitialLoad = this.state.initialLoad === true; + // // const isReady = this.props.productsSubscription.ready(); - if (isInitialLoad === false) { - return true; - } + // // if (isInitialLoad === false) { + // // return true; + // // } - if (isReady) { - return true; - } + // // if (isReady) { + // // return true; + // // } - return false; - } + // // return false; + // } + + // ready = () => this.props.productsSubscription.ready(); - loadMoreProducts = () => this.props.canLoadMoreProducts === true + // loadMoreProducts = () => this.props.canLoadMoreProducts === true; - loadProducts = (event) => { - event.preventDefault(); + loadProducts = () => { this.setState({ initialLoad: false }); @@ -102,8 +106,6 @@ const wrapComponent = (Comp) => ( return ( ); From a8f9e22438a91e75b13b898ee1af5d53c90d46a7 Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Mon, 5 Mar 2018 17:58:05 -0600 Subject: [PATCH 59/77] style: cleaning up eslint issues --- .../containers/userOrdersListContainer.js | 2 +- .../containers/completedOrderContainer.js | 2 +- .../orders/client/components/productImage.js | 1 - .../containers/productGridItemsContainer.js | 1 - .../containers/productsContainer.js | 12 +- .../containers/productsContainerAdmin.js | 2 +- .../containers/productsContainerCustomer.js | 140 +----------------- lib/collections/schemas/catalog.js | 1 - 8 files changed, 12 insertions(+), 149 deletions(-) diff --git a/imports/plugins/core/accounts/client/containers/userOrdersListContainer.js b/imports/plugins/core/accounts/client/containers/userOrdersListContainer.js index 46522b86697..d2550e1638f 100644 --- a/imports/plugins/core/accounts/client/containers/userOrdersListContainer.js +++ b/imports/plugins/core/accounts/client/containers/userOrdersListContainer.js @@ -1,4 +1,4 @@ -import { compose, withProps } from "recompose"; +import { compose } from "recompose"; import { Meteor } from "meteor/meteor"; import { Router } from "/client/modules/router/"; import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; diff --git a/imports/plugins/core/checkout/client/containers/completedOrderContainer.js b/imports/plugins/core/checkout/client/containers/completedOrderContainer.js index 9a1912ebec8..0bc87e12998 100644 --- a/imports/plugins/core/checkout/client/containers/completedOrderContainer.js +++ b/imports/plugins/core/checkout/client/containers/completedOrderContainer.js @@ -1,4 +1,4 @@ -import { compose, withProps } from "recompose"; +import { compose } from "recompose"; import { Meteor } from "meteor/meteor"; import { Session } from "meteor/session"; import { Orders } from "/lib/collections"; diff --git a/imports/plugins/core/orders/client/components/productImage.js b/imports/plugins/core/orders/client/components/productImage.js index d021a32f742..7c5e3da8be8 100644 --- a/imports/plugins/core/orders/client/components/productImage.js +++ b/imports/plugins/core/orders/client/components/productImage.js @@ -41,7 +41,6 @@ class ProductImage extends Component { const { displayMedia, item, mode, size } = this.props; const fileRecord = displayMedia(item); - let mediaUrl; if (fileRecord) { diff --git a/imports/plugins/included/product-variant/containers/productGridItemsContainer.js b/imports/plugins/included/product-variant/containers/productGridItemsContainer.js index e4311567379..2cb83595f1f 100644 --- a/imports/plugins/included/product-variant/containers/productGridItemsContainer.js +++ b/imports/plugins/included/product-variant/containers/productGridItemsContainer.js @@ -6,7 +6,6 @@ import { registerComponent } from "@reactioncommerce/reaction-components"; import { Session } from "meteor/session"; import { Reaction } from "/client/api"; import { ReactionProduct } from "/lib/api"; -import { Media } from "/imports/plugins/core/files/client"; import { SortableItem } from "/imports/plugins/core/ui/client/containers"; import ProductGridItems from "../components/productGridItems"; diff --git a/imports/plugins/included/product-variant/containers/productsContainer.js b/imports/plugins/included/product-variant/containers/productsContainer.js index 0a1d6dafdb3..5ce0ce1e184 100644 --- a/imports/plugins/included/product-variant/containers/productsContainer.js +++ b/imports/plugins/included/product-variant/containers/productsContainer.js @@ -1,15 +1,17 @@ -import React, { Component } from "react"; +import React from "react"; import PropTypes from "prop-types"; import { compose } from "recompose"; import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; import { Reaction } from "/client/api"; -import { Products, Tags, Shops } from "/lib/collections"; import ProductsContainerAdmin from "./productsContainerAdmin.js"; import ProductsContainerCustomer from "./productsContainerCustomer.js"; const ProductsContainer = ({ isAdmin }) => { - return (isAdmin) ? : ; + if (isAdmin) { + return ; + } + return ; }; ProductsContainer.propTypes = { @@ -28,6 +30,4 @@ registerComponent("Products", ProductsContainer, [ composeWithTracker(composer) ]); -export default compose( - composeWithTracker(composer) -)(ProductsContainer); +export default compose(composeWithTracker(composer))(ProductsContainer); diff --git a/imports/plugins/included/product-variant/containers/productsContainerAdmin.js b/imports/plugins/included/product-variant/containers/productsContainerAdmin.js index eae3e1b6a66..b410672529d 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerAdmin.js +++ b/imports/plugins/included/product-variant/containers/productsContainerAdmin.js @@ -214,7 +214,7 @@ function composer(props, onData) { }; return { - ...applyProductRevision(product), + ...applyProductRevision(product) // additionalMedia, // primaryMedia }; diff --git a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js index a8a9c693f90..9f8df24fb88 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js +++ b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js @@ -3,60 +3,13 @@ import PropTypes from "prop-types"; import { compose } from "recompose"; import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; -import { ReactiveVar } from "meteor/reactive-var"; import { Session } from "meteor/session"; -import { Tracker } from "meteor/tracker"; import { Reaction } from "/client/api"; import { ITEMS_INCREMENT } from "/client/config/defaults"; import { ReactionProduct } from "/lib/api"; -import { applyProductRevision } from "/lib/api/products"; -import { Catalog, Products, Tags, Shops } from "/lib/collections"; -import { Media } from "/imports/plugins/core/files/client"; +import { Catalog, Tags, Shops } from "/lib/collections"; import ProductsComponent from "../components/customer/productGrid"; -const reactiveProductIds = new ReactiveVar([], (oldVal, newVal) => JSON.stringify(oldVal.sort()) === JSON.stringify(newVal.sort())); - -// Isolated resubscribe to product grid images, only when the list of product IDs changes -// Tracker.autorun(() => { -// Meteor.subscribe("ProductGridMedia", reactiveProductIds.get()); -// }); - - -/** - * loadMoreProducts - * @summary whenever #productScrollLimitLoader becomes visible, retrieve more results - * this basically runs this: - * Session.set('productScrollLimit', Session.get('productScrollLimit') + ITEMS_INCREMENT); - * @return {undefined} - */ -function loadMoreProducts() { - // let threshold; - // const target = document.querySelectorAll("#productScrollLimitLoader"); - // let scrollContainer = document.querySelectorAll(".container-main"); - // if (scrollContainer.length === 0) { - // scrollContainer = window; - // } - - // console.log("target scroll containter", target, scrollContainer); - - // if (target.length) { - // threshold = scrollContainer[0].scrollHeight - scrollContainer[0].scrollTop === scrollContainer[0].clientHeight; - - // console.log("load more", threshold); - - // if (threshold) { - // if (!target[0].getAttribute("visible")) { - // target[0].setAttribute("productScrollLimit", true); - - // } - // } else if (target[0].getAttribute("visible")) { - // target[0].setAttribute("visible", false); - // } - // } - - Session.set("productScrollLimit", Session.get("productScrollLimit") + ITEMS_INCREMENT || 24); -} - const wrapComponent = (Comp) => ( class ProductsContainer extends Component { static propTypes = { @@ -72,34 +25,12 @@ const wrapComponent = (Comp) => ( }; } - // ready = () => { - // // if (this.props.showNotFound === true) { - // // return false; - // // } - - // // const isInitialLoad = this.state.initialLoad === true; - // // const isReady = this.props.productsSubscription.ready(); - - // // if (isInitialLoad === false) { - // // return true; - // // } - - // // if (isReady) { - // // return true; - // // } - - // // return false; - // } - - // ready = () => this.props.productsSubscription.ready(); - - // loadMoreProducts = () => this.props.canLoadMoreProducts === true; - loadProducts = () => { this.setState({ initialLoad: false }); - loadMoreProducts(); + // laod in the next set of products + Session.set("productScrollLimit", Session.get("productScrollLimit") + ITEMS_INCREMENT || 24); } render() { @@ -151,19 +82,6 @@ function composer(props, onData) { createdAt: 1 }; - // TODO: Remove - // const viewAsPref = Reaction.getUserPreferences("reaction-dashboard", "viewAs"); - - // TODO: Remove - // Edit mode is true by default - // let editMode = true; - - // TODO: Remove - // if we have a "viewAs" preference and the preference is not set to "administrator", then edit mode is false - // if (viewAsPref && viewAsPref !== "administrator") { - // editMode = false; - // } - const queryParams = Object.assign({}, tags, Reaction.Router.current().query, shopIds); const productsSubscription = Meteor.subscribe("Products/grid", scrollLimit, queryParams, sort); @@ -186,70 +104,18 @@ function composer(props, onData) { $sort: sort }); - // TODO: Remove - // const sortedProducts = ReactionProduct.sortProducts(productCursor.fetch(), currentTag); - // Session.set("productGrid/products", sortedProducts); - - // TODO: Remove - // const productIds = []; - // // Instantiate an object for use as a map. This object does not inherit prototype or methods from `Object` - // const productMediaById = Object.create(null); - // const stateProducts = sortedProducts.map((product) => { - // productIds.push(product._id); - - // const primaryMedia = Media.findOneLocal({ - // "metadata.productId": product._id, - // "metadata.toGrid": 1, - // "metadata.workflow": { $nin: ["archived", "unpublished"] } - // }, { - // sort: { "metadata.priority": 1, "uploadedAt": 1 } - // }); - - // const variantIds = ReactionProduct.getVariants(product._id).map((variant) => variant._id); - // let additionalMedia = Media.findLocal({ - // "metadata.productId": product._id, - // "metadata.variantId": { $in: variantIds }, - // "metadata.workflow": { $nin: ["archived", "unpublished"] } - // }, { - // limit: 3, - // sort: { "metadata.priority": 1, "uploadedAt": 1 } - // }); - - // if (additionalMedia.length < 2) additionalMedia = null; - - // productMediaById[product._id] = { - // additionalMedia, - // primaryMedia - // }; - - // return { - // // ...applyProductRevision(product), - // // additionalMedia, - // // primaryMedia - // }; - // }); - - // reactiveProductIds.set(productIds); - canLoadMoreProducts = productCursor.count() >= Session.get("productScrollLimit"); - // const isActionViewOpen = Reaction.isActionViewOpen(); - // if (isActionViewOpen === false) { - // Session.set("productGrid/selectedProducts", []); - // } const products = productCursor.fetch(); onData(null, { canLoadMoreProducts, products, - // productMediaById, - // products: stateProducts, productsSubscription }); } registerComponent("ProductsCustomer", ProductsComponent, [ composeWithTracker(composer) - // wrapComponent ]); export default compose( diff --git a/lib/collections/schemas/catalog.js b/lib/collections/schemas/catalog.js index e1c292cb986..c84fc7b9e04 100644 --- a/lib/collections/schemas/catalog.js +++ b/lib/collections/schemas/catalog.js @@ -1,6 +1,5 @@ import { SimpleSchema } from "meteor/aldeed:simple-schema"; -import { Product } from "./products"; export const Catalog = new SimpleSchema({ contentType: { From bccb156cc35dc5d0ff6e4940df4af7121898dd3e Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Mon, 5 Mar 2018 18:36:19 -0800 Subject: [PATCH 60/77] docs: update method jsdoc --- .../core/catalog/server/methods/catalog.js | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index 1ee85157cb2..c8ca1defa77 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -6,8 +6,9 @@ import { Media } from "/imports/plugins/core/files/server"; import { ProductRevision as Catalog } from "/imports/plugins/core/revisions/server/hooks"; /** - * isSoldOut + * @method isSoldOut * @summary We are stop accepting new orders if product marked as `isSoldOut`. + * @memberof Catalog * @param {Array} variants - Array with top-level variants * @return {Boolean} true if summary product quantity is zero. */ @@ -21,8 +22,9 @@ export function isSoldOut(variants) { } /** - * isLowQuantity + * @method isLowQuantity * @summary If at least one of the variants is less than the threshold, then function returns `true` + * @memberof Catalog * @param {Array} variants - array of child variants * @return {boolean} low quantity or not */ @@ -38,8 +40,9 @@ export function isLowQuantity(variants) { } /** - * isBackorder - * @description Is products variants is still available to be ordered after summary variants quantity is zero + * @method isBackorder + * @summary Is products variants is still available to be ordered after summary variants quantity is zero + * @memberof Catalog * @param {Array} variants - array with variant objects * @return {boolean} is backorder allowed or not for a product */ @@ -49,8 +52,9 @@ export function isBackorder(variants) { } /** - * publishProductsToCatalog - * @description Publish one or more products to the Catalog + * @method publishProductsToCatalog + * @summary Publish one or more products to the Catalog + * @memberof Catalog * @param {string|array} productIds - A string product id or an array of product ids * @return {boolean} true on successful publish for all documents, false if one ore more fail to publish */ @@ -124,8 +128,9 @@ export async function publishProductsToCatalog(productIds) { } /** - * publishProductInventoryAdjustments - * @description Publish inventory updates for a single product to the Catalog + * @method publishProductInventoryAdjustments + * @summary Publish inventory updates for a single product to the Catalog + * @memberof Catalog * @param {string} productId - A string product id * @return {boolean} true on success, false on failure */ From 9b9db325a60d16b5eecf526d6ad0b6ce11e29d7c Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Mon, 5 Mar 2018 18:38:49 -0800 Subject: [PATCH 61/77] refactor: remove async declaration --- imports/plugins/core/catalog/server/methods/catalog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index c8ca1defa77..2bf80ac54a6 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -58,7 +58,7 @@ export function isBackorder(variants) { * @param {string|array} productIds - A string product id or an array of product ids * @return {boolean} true on successful publish for all documents, false if one ore more fail to publish */ -export async function publishProductsToCatalog(productIds) { +export function publishProductsToCatalog(productIds) { check(productIds, Match.OneOf(String, Array)); let ids = productIds; From 3355c4887b201d5a37d4ba4b4f78164f18e8d3fd Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Tue, 6 Mar 2018 20:57:53 -0800 Subject: [PATCH 62/77] refactor: split publish into separate function --- .../core/catalog/server/methods/catalog.js | 137 ++++++++++-------- 1 file changed, 78 insertions(+), 59 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index 2bf80ac54a6..bdb39ea486f 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -52,79 +52,98 @@ export function isBackorder(variants) { } /** - * @method publishProductsToCatalog - * @summary Publish one or more products to the Catalog + * @method publishProductToCatalog + * @summary Publish a product to the Catalog * @memberof Catalog - * @param {string|array} productIds - A string product id or an array of product ids - * @return {boolean} true on successful publish for all documents, false if one ore more fail to publish + * @param {string} productId - A string product id + * @return {boolean} true on successful publish, false if publish was unsuccessful */ -export function publishProductsToCatalog(productIds) { - check(productIds, Match.OneOf(String, Array)); +export async function publishProductToCatalog(productId) { + check(productId, String); + + // Find the product by id + let product = Products.findOne({ + $or: [ + { _id: productId }, + { ancestors: { $in: [productId] } } + ] + }); - let ids = productIds; - if (typeof ids === "string") { - ids = [productIds]; + // Stop of a product could not be found + if (!product) { + Logger.info("Cannot publish product to catalog"); + return false; } - return ids.every(async (productId) => { - let product = Products.findOne({ - $or: [ - { _id: productId }, - { ancestors: { $in: [productId] } } - ] + // If the product has ancestors, then find to top product document + if (Array.isArray(product.ancestors) && product.ancestors.length) { + product = Products.findOne({ + _id: product.ancestors[0] }); + } - if (!product) { - Logger.info("Cannot publish product to catalog"); - return false; + // Get variants of the product + const variants = Products.find({ + ancestors: { + $in: [productId] } + }).fetch(); - if (Array.isArray(product.ancestors) && product.ancestors.length) { - product = Products.findOne({ - _id: product.ancestors[0] - }); - } + // Get Media for the product + const mediaArray = await Media.find({ + "metadata.productId": productId, + "metadata.toGrid": 1, + "metadata.workflow": { $nin: ["archived", "unpublished"] } + }, { + sort: { "metadata.priority": 1, "uploadedAt": 1 } + }); - const variants = Products.find({ - ancestors: { - $in: [productId] - } - }).fetch(); + // Denormalize media + const productMedia = mediaArray.map((media) => ({ + thumbnail: `${media.url({ store: "thumbnail" })}`, + small: `${media.url({ store: "small" })}`, + medium: `${media.url({ store: "medium" })}`, + large: `${media.url({ store: "large" })}`, + image: `${media.url({ store: "image" })}` + })); + + // Denormalize product fields + product.media = productMedia; + product.type = "product-simple"; + product.isSoldOut = isSoldOut(variants); + product.isBackorder = isBackorder(variants); + product.isLowQuantity = isLowQuantity(variants); + product.variants = variants.map((variant) => { + const { inventoryQuantity, ...v } = variant; + return v; + }); - const mediaArray = await Media.find({ - "metadata.productId": productId, - "metadata.toGrid": 1, - "metadata.workflow": { $nin: ["archived", "unpublished"] } - }, { - sort: { "metadata.priority": 1, "uploadedAt": 1 } - }); + // Insert/update catalog document + const result = CatalogCollection.upsert({ + _id: productId + }, { + $set: product + }); - const productMedia = mediaArray.map((media) => ({ - thumbnail: `${media.url({ store: "thumbnail" })}`, - small: `${media.url({ store: "small" })}`, - medium: `${media.url({ store: "medium" })}`, - large: `${media.url({ store: "large" })}`, - image: `${media.url({ store: "image" })}` - })); - - product.media = productMedia; - product.type = "product-simple"; - product.isSoldOut = isSoldOut(variants); - product.isBackorder = isBackorder(variants); - product.isLowQuantity = isLowQuantity(variants); - product.variants = variants.map((variant) => { - const { inventoryQuantity, ...v } = variant; - return v; - }); + return result && result.numberAffected === 1; +} - const result = CatalogCollection.upsert({ - _id: productId - }, { - $set: product - }); +/** + * @method publishProductsToCatalog + * @summary Publish one or more products to the Catalog + * @memberof Catalog + * @param {string|array} productIds - A string product id or an array of product ids + * @return {boolean} true on successful publish for all documents, false if one ore more fail to publish + */ +export function publishProductsToCatalog(productIds) { + check(productIds, Match.OneOf(String, Array)); - return result && result.numberAffected === 1; - }); + let ids = productIds; + if (typeof ids === "string") { + ids = [productIds]; + } + + return ids.every(async (productId) => await publishProductToCatalog(productId)); } /** From 49c48cbbf2aafd31176a8975ae02a2e669675fb2 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Tue, 6 Mar 2018 21:01:54 -0800 Subject: [PATCH 63/77] test: add tests for Catalog collection and publication --- .../server/methods/catalog.app-test.js | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 imports/plugins/core/catalog/server/methods/catalog.app-test.js diff --git a/imports/plugins/core/catalog/server/methods/catalog.app-test.js b/imports/plugins/core/catalog/server/methods/catalog.app-test.js new file mode 100644 index 00000000000..002668fd479 --- /dev/null +++ b/imports/plugins/core/catalog/server/methods/catalog.app-test.js @@ -0,0 +1,145 @@ +/* eslint dot-notation: 0 */ +/* eslint prefer-arrow-callback:0 */ +import { Random } from "meteor/random"; +import { expect } from "meteor/practicalmeteor:chai"; +import { sinon } from "meteor/practicalmeteor:sinon"; +import { Roles } from "meteor/alanning:roles"; +import { createActiveShop } from "/server/imports/fixtures/shops"; +import { Reaction } from "/server/api"; +import * as Collections from "/lib/collections"; +import Fixtures from "/server/imports/fixtures"; +import { PublicationCollector } from "meteor/johanbrook:publication-collector"; +import { RevisionApi } from "/imports/plugins/core/revisions/lib/api/revisions"; +import { publishProductToCatalog } from "./catalog"; + +Fixtures(); + +describe("Catalog", function () { + const shopId = Random.id(); + let sandbox; + + beforeEach(function () { + Collections.Shops.remove({}); + createActiveShop({ _id: shopId }); + sandbox = sinon.sandbox.create(); + sandbox.stub(RevisionApi, "isRevisionControlEnabled", () => true); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe("with products", function () { + const priceRangeA = { + range: "1.00 - 12.99", + min: 1.00, + max: 12.99 + }; + + const priceRangeB = { + range: "12.99 - 19.99", + min: 12.99, + max: 19.99 + }; + + beforeEach(function (done) { + this.timeout(10000); + + Collections.Products.direct.remove({}); + Collections.Catalog.remove({}); + + // a product with price range A, and not visible + const id1 = Collections.Products.insert({ + ancestors: [], + title: "My Little Pony", + shopId, + type: "simple", + price: priceRangeA, + isVisible: false, + isLowQuantity: false, + isSoldOut: false, + isBackorder: false + }); + // a product with price range B, and visible + const id2 = Collections.Products.insert({ + ancestors: [], + title: "Shopkins - Peachy", + shopId, + price: priceRangeB, + type: "simple", + isVisible: true, + isLowQuantity: false, + isSoldOut: false, + isBackorder: false + }); + // a product with price range A, and visible + const id3 = Collections.Products.insert({ + ancestors: [], + title: "Fresh Tomatoes", + shopId, + price: priceRangeA, + type: "simple", + isVisible: true, + isLowQuantity: false, + isSoldOut: false, + isBackorder: false + }); + + Promise.all([ + publishProductToCatalog(id1), + publishProductToCatalog(id2), + publishProductToCatalog(id3) + ]).then(() => { + done(); + }); + }); + + describe("Collection", function () { + it("should return 3 products from the Catalog", function () { + const products = Collections.Catalog.find({}).fetch(); + expect(products.length).to.equal(3); + }); + }); + + describe("Publication", function () { + it("should return 2 products from Products/get", function (done) { + const productScrollLimit = 24; + sandbox.stub(Reaction, "getShopId", () => shopId); + sandbox.stub(Roles, "userIsInRole", () => false); + + const collector = new PublicationCollector({ userId: Random.id() }); + let isDone = false; + + collector.collect("Products/grid", productScrollLimit, undefined, {}, (collections) => { + const products = collections.Catalog; + expect(products.length).to.equal(2); + + if (!isDone) { + isDone = true; + done(); + } + }); + }); + + it("should return one product in price.min query", function (done) { + const productScrollLimit = 24; + const filters = { "price.min": "2.00" }; + sandbox.stub(Reaction, "getShopId", () => shopId); + sandbox.stub(Roles, "userIsInRole", () => false); + + const collector = new PublicationCollector({ userId: Random.id() }); + let isDone = false; + + collector.collect("Products/grid", productScrollLimit, filters, {}, (collections) => { + const products = collections.Catalog; + expect(products.length).to.equal(1); + + if (!isDone) { + isDone = true; + done(); + } + }); + }); + }); + }); +}); From c8abf966176bf085d08b99fb6103b87ce795babd Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Wed, 7 Mar 2018 14:13:45 -0800 Subject: [PATCH 64/77] test: remove timeout --- imports/plugins/core/catalog/server/methods/catalog.app-test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.app-test.js b/imports/plugins/core/catalog/server/methods/catalog.app-test.js index 002668fd479..f92ddd747c1 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.app-test.js +++ b/imports/plugins/core/catalog/server/methods/catalog.app-test.js @@ -43,8 +43,6 @@ describe("Catalog", function () { }; beforeEach(function (done) { - this.timeout(10000); - Collections.Products.direct.remove({}); Collections.Catalog.remove({}); From bd80272f5a836a959a990471609e65513e9540e3 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Wed, 7 Mar 2018 14:14:39 -0800 Subject: [PATCH 65/77] test: remove cleanup task to fix broken tests --- imports/plugins/core/catalog/server/methods/catalog.app-test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.app-test.js b/imports/plugins/core/catalog/server/methods/catalog.app-test.js index f92ddd747c1..26b42992ad5 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.app-test.js +++ b/imports/plugins/core/catalog/server/methods/catalog.app-test.js @@ -19,7 +19,6 @@ describe("Catalog", function () { let sandbox; beforeEach(function () { - Collections.Shops.remove({}); createActiveShop({ _id: shopId }); sandbox = sinon.sandbox.create(); sandbox.stub(RevisionApi, "isRevisionControlEnabled", () => true); From 049ab53189759108891f961cf201912478046f20 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Wed, 7 Mar 2018 14:15:55 -0800 Subject: [PATCH 66/77] feat: include metadata in media denormalization --- imports/plugins/core/catalog/server/methods/catalog.js | 1 + 1 file changed, 1 insertion(+) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index bdb39ea486f..ae337ca34fc 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -100,6 +100,7 @@ export async function publishProductToCatalog(productId) { // Denormalize media const productMedia = mediaArray.map((media) => ({ + metadata: media.metadata, thumbnail: `${media.url({ store: "thumbnail" })}`, small: `${media.url({ store: "small" })}`, medium: `${media.url({ store: "medium" })}`, From 450784351c1e9f5e53c5bd533bc61f5f9d59d448 Mon Sep 17 00:00:00 2001 From: Erik Kieckhafer Date: Fri, 9 Mar 2018 10:06:25 -0800 Subject: [PATCH 67/77] ref: change to new simpl-schema --- lib/collections/schemas/catalog.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/collections/schemas/catalog.js b/lib/collections/schemas/catalog.js index c84fc7b9e04..0f87ed614e3 100644 --- a/lib/collections/schemas/catalog.js +++ b/lib/collections/schemas/catalog.js @@ -1,5 +1,4 @@ - -import { SimpleSchema } from "meteor/aldeed:simple-schema"; +import SimpleSchema from "simpl-schema"; export const Catalog = new SimpleSchema({ contentType: { From 7ed94e079befea4f18756f87187567e3752db08d Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Fri, 9 Mar 2018 10:54:14 -0800 Subject: [PATCH 68/77] Fix small grammatical errors. --- imports/plugins/core/catalog/server/methods/catalog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imports/plugins/core/catalog/server/methods/catalog.js b/imports/plugins/core/catalog/server/methods/catalog.js index ae337ca34fc..4075c82e39e 100644 --- a/imports/plugins/core/catalog/server/methods/catalog.js +++ b/imports/plugins/core/catalog/server/methods/catalog.js @@ -7,7 +7,7 @@ import { ProductRevision as Catalog } from "/imports/plugins/core/revisions/serv /** * @method isSoldOut - * @summary We are stop accepting new orders if product marked as `isSoldOut`. + * @summary We are to stop accepting new orders if product is marked as `isSoldOut`. * @memberof Catalog * @param {Array} variants - Array with top-level variants * @return {Boolean} true if summary product quantity is zero. @@ -69,7 +69,7 @@ export async function publishProductToCatalog(productId) { ] }); - // Stop of a product could not be found + // Stop if a product could not be found if (!product) { Logger.info("Cannot publish product to catalog"); return false; From ac88682860ebf559e0da524de23df619014b78b5 Mon Sep 17 00:00:00 2001 From: Erik Kieckhafer Date: Fri, 9 Mar 2018 11:07:38 -0800 Subject: [PATCH 69/77] fix: typo --- .../included/product-variant/components/customer/productGrid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGrid.js b/imports/plugins/included/product-variant/components/customer/productGrid.js index 2361fce1fd0..49ea474e4aa 100644 --- a/imports/plugins/included/product-variant/components/customer/productGrid.js +++ b/imports/plugins/included/product-variant/components/customer/productGrid.js @@ -32,7 +32,7 @@ class ProductGrid extends Component { } } - // render the laoding spinner + // render the loading spinner renderLoadingSpinner() { const { productsSubscription: { ready } } = this.props; // if the products catalog is not ready From 84392afcfe1e9506b0643ffc05ae8cad55de2205 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 9 Mar 2018 11:12:49 -0800 Subject: [PATCH 70/77] refactor: add propType definition for onPublishSuccess --- .../plugins/core/revisions/client/containers/publishContainer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/imports/plugins/core/revisions/client/containers/publishContainer.js b/imports/plugins/core/revisions/client/containers/publishContainer.js index 3f54241edd1..34e357aec95 100644 --- a/imports/plugins/core/revisions/client/containers/publishContainer.js +++ b/imports/plugins/core/revisions/client/containers/publishContainer.js @@ -112,6 +112,7 @@ PublishContainer.propTypes = { isEnabled: PropTypes.bool, isPreview: PropTypes.bool, onAction: PropTypes.func, + onPublishSuccess: PropTypes.func, onVisibilityChange: PropTypes.func, product: PropTypes.object, revisions: PropTypes.arrayOf(PropTypes.object) From 50459ab434e4963cc937718f3008c63d56bb6c90 Mon Sep 17 00:00:00 2001 From: Erik Kieckhafer Date: Fri, 9 Mar 2018 11:26:50 -0800 Subject: [PATCH 71/77] fix: typo --- .../product-variant/containers/productsContainerCustomer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js index 9f8df24fb88..b01bfa566a9 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerCustomer.js +++ b/imports/plugins/included/product-variant/containers/productsContainerCustomer.js @@ -29,7 +29,7 @@ const wrapComponent = (Comp) => ( this.setState({ initialLoad: false }); - // laod in the next set of products + // load in the next set of products Session.set("productScrollLimit", Session.get("productScrollLimit") + ITEMS_INCREMENT || 24); } From e0985fffd565da00604f889ad1692e9ef7e4c10f Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 9 Mar 2018 11:53:00 -0800 Subject: [PATCH 72/77] fix: update and add translation for catalog publish --- imports/plugins/core/catalog/server/i18n/en.json | 1 + .../core/revisions/client/containers/publishContainer.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/imports/plugins/core/catalog/server/i18n/en.json b/imports/plugins/core/catalog/server/i18n/en.json index f500e00acfd..99ed92cdbe5 100644 --- a/imports/plugins/core/catalog/server/i18n/en.json +++ b/imports/plugins/core/catalog/server/i18n/en.json @@ -4,6 +4,7 @@ "translation": { "reaction-catalog": { "admin": { + "catalogProductPublishSuccess": "Product published to catalog", "shortcut": { "catalogLabel": "Catalog", "catalogTitle": "Catalog" diff --git a/imports/plugins/core/revisions/client/containers/publishContainer.js b/imports/plugins/core/revisions/client/containers/publishContainer.js index 34e357aec95..2ce6b398259 100644 --- a/imports/plugins/core/revisions/client/containers/publishContainer.js +++ b/imports/plugins/core/revisions/client/containers/publishContainer.js @@ -15,7 +15,7 @@ class PublishContainer extends Component { publishToCatalog(collection, documentIds) { Meteor.call(`catalog/publish/${collection}`, documentIds, (error, result) => { if (result) { - Alerts.toast(i18next.t("catalog.productPublishSuccess", { defaultValue: "Product published to catalog" }), "success"); + Alerts.toast(i18next.t("admin.catalogProductPublishSuccess", { defaultValue: "Product published to catalog" }), "success"); } else if (error) { Alerts.toast(error.message, "error"); } From b13d6d3f745a11b2e61b25a770e550c08d6a82db Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 9 Mar 2018 11:54:12 -0800 Subject: [PATCH 73/77] refactor: remove commented-out code --- .../product-variant/containers/productsContainerAdmin.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/imports/plugins/included/product-variant/containers/productsContainerAdmin.js b/imports/plugins/included/product-variant/containers/productsContainerAdmin.js index b410672529d..69599ff8a64 100644 --- a/imports/plugins/included/product-variant/containers/productsContainerAdmin.js +++ b/imports/plugins/included/product-variant/containers/productsContainerAdmin.js @@ -213,11 +213,7 @@ function composer(props, onData) { primaryMedia }; - return { - ...applyProductRevision(product) - // additionalMedia, - // primaryMedia - }; + return applyProductRevision(product); }); reactiveProductIds.set(productIds); From 5e92a339bd6e55f64da82d6d92e9a688315813ba Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 9 Mar 2018 11:55:43 -0800 Subject: [PATCH 74/77] refactor: remove commented-out code and add todo --- lib/collections/collections.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/collections/collections.js b/lib/collections/collections.js index c477202d633..8b4b9294dd9 100644 --- a/lib/collections/collections.js +++ b/lib/collections/collections.js @@ -115,13 +115,11 @@ Packages.attachSchema(Schemas.PackageConfig); /** * Catalog Collection + * @todo: Attach a schema to the Catalog collection * @ignore */ export const Catalog = new Mongo.Collection("Catalog"); -// Catalog.attachSchema(Schemas.Catalog); - - /** * Products Collection * @ignore From b70ac407f8b96b04f2076d410ece4d990511b14e Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Fri, 9 Mar 2018 11:59:40 -0800 Subject: [PATCH 75/77] Fix eslint errors --- .../core/revisions/client/containers/publishContainer.js | 1 + .../included/connectors-shopify/server/methods/sync/orders.js | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/imports/plugins/core/revisions/client/containers/publishContainer.js b/imports/plugins/core/revisions/client/containers/publishContainer.js index 3f54241edd1..34e357aec95 100644 --- a/imports/plugins/core/revisions/client/containers/publishContainer.js +++ b/imports/plugins/core/revisions/client/containers/publishContainer.js @@ -112,6 +112,7 @@ PublishContainer.propTypes = { isEnabled: PropTypes.bool, isPreview: PropTypes.bool, onAction: PropTypes.func, + onPublishSuccess: PropTypes.func, onVisibilityChange: PropTypes.func, product: PropTypes.object, revisions: PropTypes.arrayOf(PropTypes.object) diff --git a/imports/plugins/included/connectors-shopify/server/methods/sync/orders.js b/imports/plugins/included/connectors-shopify/server/methods/sync/orders.js index f9ea70c9741..824c84432cb 100644 --- a/imports/plugins/included/connectors-shopify/server/methods/sync/orders.js +++ b/imports/plugins/included/connectors-shopify/server/methods/sync/orders.js @@ -1,8 +1,5 @@ -import { Meteor } from "meteor/meteor"; import { check } from "meteor/check"; -import { Reaction } from "/server/api"; import { Products } from "/lib/collections"; -import { connectorsRoles } from "../../lib/roles"; /** * @file Methods for syncing Shopify orders From 071d965870184da51edd34426f97f8ba8e0f9627 Mon Sep 17 00:00:00 2001 From: Nat Hamilton Date: Mon, 12 Mar 2018 12:51:52 -0500 Subject: [PATCH 76/77] fix: updated filterProducts function to look for the workflow.status active for the shop and not the products, this fixed our failing products test --- server/publications/collections/products.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js index cd5ae342787..e36009222fe 100644 --- a/server/publications/collections/products.js +++ b/server/publications/collections/products.js @@ -86,7 +86,8 @@ function filterProducts(productFilters) { if (shopIdsOrSlugs) { // Get all shopIds associated with the slug or Id const shopIds = Shops.find({ - $or: [{ + "workflow.status": "active", + "$or": [{ _id: { $in: shopIdsOrSlugs } }, { slug: { $in: shopIdsOrSlugs } @@ -112,17 +113,9 @@ function filterProducts(productFilters) { // handle multiple shops if (productFilters.shops) { _.extend(selector, { - $or: [{ - "workflow.status": "active", - "shopId": { - $in: productFilters.shops - } - }, { - "workflow.status": "active", - "slug": { - $in: productFilters.shops - } - }] + shopId: { + $in: productFilters.shops + } }); } From 28e10b07a01680144753531d5e5e4d3ed9687009 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Tue, 13 Mar 2018 15:44:21 -0700 Subject: [PATCH 77/77] fix: broken product grid if price range is undefined --- .../product-variant/components/customer/productGridItem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/included/product-variant/components/customer/productGridItem.js b/imports/plugins/included/product-variant/components/customer/productGridItem.js index a47d30b4804..23c95fa611f 100644 --- a/imports/plugins/included/product-variant/components/customer/productGridItem.js +++ b/imports/plugins/included/product-variant/components/customer/productGridItem.js @@ -145,7 +145,7 @@ class ProductGridItem extends Component { >
    {product.title}
    -
    {formatPriceString(product.price.range)}
    +
    {formatPriceString((product.price && product.price.range) || "")}
    {this.props.isSearch &&
    {product.description}
    }