Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(refactor): 3662 customer product grid publishing #3876

Merged
merged 89 commits into from Mar 13, 2018
Merged
Show file tree
Hide file tree
Changes from 87 commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
168a2ed
refactor: WIP creating a new product grid publication, updated produc…
nnnnat Feb 28, 2018
61899c9
refactor: add observable to "Products/grid" publication
mikemurray Feb 28, 2018
27ca772
feat: publish product media on product document
mikemurray Feb 28, 2018
3f5232c
feat: add products container for customer grid
mikemurray Feb 28, 2018
216ba0b
refactor: change name of admin product container
mikemurray Feb 28, 2018
f82b6e0
feat: add customer product grid item component
mikemurray Feb 28, 2018
c38266c
feat: add customer product grid component
mikemurray Feb 28, 2018
5fb0f64
refactor: cleaning up code, moved temp product image into renderMedia…
nnnnat Mar 1, 2018
67f9d2c
refactor: updated renderPRoductGridItems to not descruture container …
nnnnat Mar 1, 2018
e51ec1c
Merge branch 'feat-1221-aldeed-npm-cfs' into refactor-3662-nnnnat-pro…
nnnnat Mar 1, 2018
c108861
fix: updated media path inside of the customer grid publication, upda…
nnnnat Mar 1, 2018
0efb321
fix: gridItem using the correct image crop
nnnnat Mar 1, 2018
55fef37
refactor: grid item component clean up, removing unneeded proptypes
nnnnat Mar 1, 2018
197fa61
refactor: cleaning up gridItem component, added pinned and weight cla…
nnnnat Mar 1, 2018
a695ba1
refactoir: custom grid item now has soldOut/backoder notices
nnnnat Mar 1, 2018
86b50bb
refactor: created new productsContainer component to swicth between t…
nnnnat Mar 1, 2018
ca9cb36
refactor: added proptypes to productsContainer, updated producstConta…
nnnnat Mar 1, 2018
46bfb25
refactor: updated product-variant index to import the new ProductsCon…
nnnnat Mar 1, 2018
e601744
refactor: removed drag and drop wrapper component from customer Produ…
nnnnat Mar 1, 2018
9a8dc7c
feat: add catalog schema
mikemurray Mar 1, 2018
f635287
feat: add catalog collection
mikemurray Mar 1, 2018
d4c2e53
feat: fetch products from Catalog collection
mikemurray Mar 1, 2018
a97e79a
feat: always allow the publish button to be clicked
mikemurray Mar 1, 2018
20364fc
feat: add catalog denormalization publish method
mikemurray Mar 1, 2018
8f2365c
feat: publish to catalog on publish click
mikemurray Mar 1, 2018
b35ca6c
refactor: use Catalog collection instead of observables
mikemurray Mar 1, 2018
bb46b62
Merge branch 'refactor-3662-nnnnat-product-publishing' of github.com:…
mikemurray Mar 1, 2018
4efae74
feat: denormalize inventory for catalog product
mikemurray Mar 1, 2018
153d70d
feat: get additional media from a product and it variants
mikemurray Mar 1, 2018
1653cb6
refactor: additional media is now rendering for the customer product …
nnnnat Mar 1, 2018
857f23d
feat: move product filters to a shared function
mikemurray Mar 2, 2018
5ec460f
feat: remove limit on media
mikemurray Mar 2, 2018
cacfbf2
feat: include source image store `image`
mikemurray Mar 2, 2018
57e610f
fix: updated customer product grid item component to only use images …
nnnnat Mar 2, 2018
5818a96
feat: remove inventory and price fields
mikemurray Mar 2, 2018
87ab3de
Merge branch 'refactor-3662-nnnnat-product-publishing' of github.com:…
mikemurray Mar 2, 2018
16fd3f3
feat: set type of product to `simple-product`
mikemurray Mar 2, 2018
4aebd86
refactor: update method name
mikemurray Mar 2, 2018
ec32b16
refactor: refactored renderNotice method in customer product grid ite…
nnnnat Mar 2, 2018
f3a0398
style: fixed typo in code commment
nnnnat Mar 2, 2018
2126496
feat: publish products after revision publish
mikemurray Mar 2, 2018
cc6a877
feat: support multiple product ids
mikemurray Mar 2, 2018
1cf72e8
refactor: filter by type `product-simple`
mikemurray Mar 2, 2018
85bb8af
refactor: remove log
mikemurray Mar 2, 2018
dea0362
Merge branch 'refactor-3662-nnnnat-product-publishing' of github.com:…
mikemurray Mar 2, 2018
bfd2c6d
refactor: disable publish button if no documents can be published
mikemurray Mar 2, 2018
53587b7
fix: WIP loading more products to the customer grid on scroll
nnnnat Mar 2, 2018
ef50942
Merge branch 'refactor-3662-nnnnat-product-publishing' of github.com:…
nnnnat Mar 2, 2018
83d1039
fix: i18n translation call
mikemurray Mar 2, 2018
dba43e2
refactor: check permissions on products before publishing
mikemurray Mar 2, 2018
9f8708d
docs: Add jsdoc for `Products/grid` method
mikemurray Mar 2, 2018
70abfe8
refactor: remove edit mode param
mikemurray Mar 2, 2018
8d5d1a5
refactor: updated customer ProductGrid to display a loading spinner o…
nnnnat Mar 2, 2018
014c009
Merge branch 'refactor-3662-nnnnat-product-publishing' of github.com:…
nnnnat Mar 2, 2018
64a8c90
feat: add function to publish inventory updates to catalog
mikemurray Mar 4, 2018
0c3dc26
feat: publish inventory updates to catalog
mikemurray Mar 4, 2018
7eda33e
refactor: don't publish variants to client
mikemurray Mar 4, 2018
91ecd76
docs: remove private
mikemurray Mar 4, 2018
1809af7
docs: add jsdoc for catalog publish methods
mikemurray Mar 4, 2018
75b42ac
fix: use `function` to fix `this` reference in body
mikemurray Mar 5, 2018
897ff11
refactor: log errors
mikemurray Mar 5, 2018
0f2cb3e
fix: typos with some Meteor.Error constructors
mikemurray Mar 5, 2018
bcfa0cf
refactor: return false instead of throwing an error
mikemurray Mar 5, 2018
6ebf572
fix: customer product grid now loads more on scroll, fixed when the n…
nnnnat Mar 5, 2018
5f37644
Merge branch 'refactor-3662-nnnnat-product-publishing' of github.com:…
nnnnat Mar 5, 2018
a8f9e22
style: cleaning up eslint issues
nnnnat Mar 5, 2018
bccb156
docs: update method jsdoc
mikemurray Mar 6, 2018
9b9db32
refactor: remove async declaration
mikemurray Mar 6, 2018
ef71e6e
Merge branch 'refactor-3662-nnnnat-product-publishing' of github.com:…
mikemurray Mar 6, 2018
3355c48
refactor: split publish into separate function
mikemurray Mar 7, 2018
49c48cb
test: add tests for Catalog collection and publication
mikemurray Mar 7, 2018
c8abf96
test: remove timeout
mikemurray Mar 7, 2018
bd80272
test: remove cleanup task to fix broken tests
mikemurray Mar 7, 2018
049ab53
feat: include metadata in media denormalization
mikemurray Mar 7, 2018
3566522
Merge branch 'feat-1221-aldeed-npm-cfs' into refactor-3662-nnnnat-pro…
mikemurray Mar 9, 2018
4507843
ref: change to new simpl-schema
kieckhafer Mar 9, 2018
7ed94e0
Fix small grammatical errors.
willopez Mar 9, 2018
ac88682
fix: typo
kieckhafer Mar 9, 2018
84392af
refactor: add propType definition for onPublishSuccess
mikemurray Mar 9, 2018
50459ab
fix: typo
kieckhafer Mar 9, 2018
8d32863
Merge branch 'refactor-3662-nnnnat-product-publishing' of https://git…
mikemurray Mar 9, 2018
e0985ff
fix: update and add translation for catalog publish
mikemurray Mar 9, 2018
b13d6d3
refactor: remove commented-out code
mikemurray Mar 9, 2018
5e92a33
refactor: remove commented-out code and add todo
mikemurray Mar 9, 2018
b70ac40
Fix eslint errors
willopez Mar 9, 2018
2cb852f
Merge branch 'refactor-3662-nnnnat-product-publishing' of https://git…
willopez Mar 9, 2018
071d965
fix: updated filterProducts function to look for the workflow.status …
nnnnat Mar 12, 2018
6fd5ef4
Merge branch 'release-1.9.0' into refactor-3662-nnnnat-product-publis…
mikemurray Mar 13, 2018
28e10b0
fix: broken product grid if price range is undefined
mikemurray Mar 13, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions imports/plugins/core/catalog/server/i18n/en.json
Expand Up @@ -4,6 +4,7 @@
"translation": {
"reaction-catalog": {
"admin": {
"catalogProductPublishSuccess": "Product published to catalog",
"shortcut": {
"catalogLabel": "Catalog",
"catalogTitle": "Catalog"
Expand Down
1 change: 1 addition & 0 deletions imports/plugins/core/catalog/server/index.js
@@ -1 +1,2 @@
import "./i18n";
import "./methods/catalog";
142 changes: 142 additions & 0 deletions imports/plugins/core/catalog/server/methods/catalog.app-test.js
@@ -0,0 +1,142 @@
/* 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 () {
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) {
Collections.Products.direct.remove({});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: The direct will no longer be needed after the collections hooks are removed, and this collection hook will need to be removed.

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();
}
});
});
});
});
});
241 changes: 241 additions & 0 deletions imports/plugins/core/catalog/server/methods/catalog.js
@@ -0,0 +1,241 @@
import { Meteor } from "meteor/meteor";
import { check, Match } from "meteor/check";
import { Products, Catalog as CatalogCollection } from "/lib/collections";
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";

/**
* @method 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.
*/
export function isSoldOut(variants) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@method isSoldOut

return variants.every((variant) => {
if (variant.inventoryManagement) {
return Catalog.getVariantQuantity(variant) <= 0;
}
return false;
});
}

/**
* @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
*/
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
if (variant.inventoryManagement && variant.inventoryPolicy && quantity) {
return quantity <= variant.lowInventoryWarningThreshold;
}
return false;
});
}

/**
* @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
*/
export function isBackorder(variants) {
return variants.every((variant) => !variant.inventoryPolicy && variant.inventoryManagement &&
variant.inventoryQuantity === 0);
}

/**
* @method publishProductToCatalog
* @summary Publish a product to the Catalog
* @memberof Catalog
* @param {string} productId - A string product id
* @return {boolean} true on successful publish, false if publish was unsuccessful
*/
export async function publishProductToCatalog(productId) {
check(productId, String);

// Find the product by id
let product = Products.findOne({
$or: [
{ _id: productId },
{ ancestors: { $in: [productId] } }
]
});

// Stop if a product could not be found
if (!product) {
Logger.info("Cannot publish product to catalog");
return false;
}

// 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]
});
}

// Get variants of the product
const variants = Products.find({
ancestors: {
$in: [productId]
}
}).fetch();

// 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 }
});

// 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" })}`,
large: `${media.url({ store: "large" })}`,
image: `${media.url({ store: "image" })}`
}));

// Denormalize product fields
product.media = productMedia;
product.type = "product-simple";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a new type? Why don't we use simple or variant like in other places?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. For the catalog, products of type simple will be product-simple as it's more descriptive and because other types of data may live in the Catalog, not just products.

product.isSoldOut = isSoldOut(variants);
product.isBackorder = isBackorder(variants);
product.isLowQuantity = isLowQuantity(variants);
product.variants = variants.map((variant) => {
const { inventoryQuantity, ...v } = variant;
return v;
});

// Insert/update catalog document
const result = CatalogCollection.upsert({
_id: productId
}, {
$set: product
});

return result && result.numberAffected === 1;
}

/**
* @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));

let ids = productIds;
if (typeof ids === "string") {
ids = [productIds];
}

return ids.every(async (productId) => await publishProductToCatalog(productId));
}

/**
* @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
*/
export function publishProductInventoryAdjustments(productId) {
check(productId, Match.OneOf(String, Array));

const catalogProduct = CatalogCollection.findOne({
_id: productId
});

if (!catalogProduct) {
Logger.info("Cannot publish inventory changes to catalog product");
return false;
}

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));

// Ensure user has createProduct permission for active shop
if (!Reaction.hasPermission("createProduct")) {
Logger.error("Access Denied");
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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to combine the three separate permissions checks in this method?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One call is to check for the admin of the marketplace, the other to check to see if the merchant shop user has permissions.

Marketplace admins can publish/unpublish any product.
Merchant shop owners can only publish their own products.


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) {
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.");
}

return true;
}

return false;
}
});