Skip to content

Commit

Permalink
Merge pull request reactioncommerce#4324 from reactioncommerce/feat-4…
Browse files Browse the repository at this point in the history
…319-mikemurray-pdp-catalog

(fix): Use Catalog collection for PDP
  • Loading branch information
spencern authored Jun 21, 2018
2 parents 321c41b + a274261 commit 6dbde4f
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 26 deletions.
53 changes: 40 additions & 13 deletions imports/plugins/core/ui/client/containers/inventoryBadge.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,52 @@
import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components";
import { InventoryBadge } from "../components/badge";
import { Reaction } from "/client/api";
import { ReactionProduct } from "/lib/api";

const composer = (props, onData) => {
const { variant, soldOut } = props;
const { inventoryManagement, inventoryPolicy, lowInventoryWarningThreshold } = variant;
const inventoryQuantity = ReactionProduct.getVariantQuantity(variant);
const {
inventoryManagement,
inventoryPolicy,
lowInventoryWarningThreshold,
isSoldOut,
isBackorder,
isLowQuantity
} = variant;
let label = null;
let i18nKeyLabel = null;
let status = null;
// TODO: update this to use Catalog API.
if (inventoryManagement && !inventoryPolicy && inventoryQuantity === 0) {
status = "info";
label = "Backorder";
i18nKeyLabel = "productDetail.backOrder";
} else if (soldOut) {
status = "danger";
label = "Sold Out!";
i18nKeyLabel = "productDetail.soldOut";
} else if (inventoryManagement) {
if (lowInventoryWarningThreshold >= inventoryQuantity) {

// Admins pull variants from the Products collection
if (Reaction.hasPermission(["createProduct"], Reaction.getShopId())) {
const inventoryQuantity = ReactionProduct.getVariantQuantity(variant);
// Product collection variant
if (inventoryManagement && !inventoryPolicy && inventoryQuantity === 0) {
status = "info";
label = "Backorder";
i18nKeyLabel = "productDetail.backOrder";
} else if (soldOut) {
status = "danger";
label = "Sold Out!";
i18nKeyLabel = "productDetail.soldOut";
} else if (inventoryManagement) {
if (lowInventoryWarningThreshold >= inventoryQuantity) {
status = "warning";
label = "Limited Supply";
i18nKeyLabel = "productDetail.limitedSupply";
}
}
} else if (inventoryManagement) { // Customers pull variants from the Catalog collection
// Catalog item variant
if (isBackorder) {
status = "info";
label = "Backorder";
i18nKeyLabel = "productDetail.backOrder";
} else if (isSoldOut) {
status = "danger";
label = "Sold Out!";
i18nKeyLabel = "productDetail.soldOut";
} else if (isLowQuantity) {
status = "warning";
label = "Limited Supply";
i18nKeyLabel = "productDetail.limitedSupply";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class ChildVariant extends Component {
};
}

componentWillMount() {
componentDidMount() {
this.variantValidation();
}

Expand Down Expand Up @@ -91,7 +91,9 @@ class ChildVariant extends Component {
}

renderValidationButton = () => {
if (this.state.invalidVariant === true) {
if (this.props.isEditable === false) {
return null;
} else if (this.state.invalidVariant === true) {
return (
<Components.Badge
status="danger"
Expand All @@ -102,6 +104,8 @@ class ChildVariant extends Component {
/>
);
}

return null;
}

// checks whether the product variant is validated
Expand Down Expand Up @@ -148,6 +152,7 @@ class ChildVariant extends Component {

ChildVariant.propTypes = {
editButton: PropTypes.node,
isEditable: PropTypes.bool,
isSelected: PropTypes.bool,
media: PropTypes.arrayOf(PropTypes.object),
onClick: PropTypes.func.isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Variant extends Component {
};
}

componentWillMount() {
componentDidMount() {
this.variantValidation();
}

Expand Down Expand Up @@ -56,7 +56,9 @@ class Variant extends Component {
}

renderValidationButton = () => {
if (this.state.selfValidation.isValid === false) {
if (this.props.editable === false) {
return null;
} else if (this.state.selfValidation.isValid === false) {
return (
<Components.Badge
status="danger"
Expand All @@ -65,8 +67,7 @@ class Variant extends Component {
i18nKeyTooltip={"admin.tooltip.validationError"}
/>
);
}
if (this.state.invalidVariant.length) {
} else if (this.state.invalidVariant.length) {
return (
<Components.Badge
status="danger"
Expand All @@ -76,6 +77,8 @@ class Variant extends Component {
/>
);
}

return null;
}

variantValidation = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ class VariantList extends Component {
showsVisibilityButton={true}
>
<Components.ChildVariant
isEditable={this.props.editable}
isSelected={this.props.variantIsSelected(childVariant._id)}
media={media}
onClick={this.handleChildVariantClick}
Expand Down
2 changes: 2 additions & 0 deletions lib/api/products.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ ReactionProduct.setProduct = (currentProductId, currentVariantId) => {
$or: [
{ handle: productId.toLowerCase() }, // Try the handle (slug) lowercased
{ handle: productId }, // Otherwise try the handle (slug) untouched
{ slug: productId.toLowerCase() }, // Try the slug lowercased
{ slug: productId }, // Otherwise try the slug untouched
{ _id: productId }, // try the product id
{ changedHandleWas: productId } // Last attempt: the permalink may have changed.
]
Expand Down
45 changes: 45 additions & 0 deletions server/methods/core/cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,51 @@ Meteor.methods({
}
});

// Customer adding a catalog product in the cart.
if (Reaction.hasPermission("createProduct", Reaction.getShopId()) === false) {
// Fetch the catalog product that should be added to the cart
const { product: catalogProduct } = Collections.Catalog.findOne({
$or: [
{ "product._id": productId },
{ "product.variants._id": variantId },
{ "product.variants.options._id": variantId }
]
});

// Merge the product document and the catalog product document.
// This is to ensure the inventory fields are available for inventory management,
// but also have the most up-to-date title, description, etc for cart and orders if needed.
product = {
...product,
...catalogProduct
};

// Merge the variant document and the catalog variant document.
for (const catalogVariant of catalogProduct.variants) {
// If the catalog variant has options, try to find a match
if (Array.isArray(catalogVariant.options)) {
const catalogVariantOption = catalogVariant.options.find((option) => option === variantId);

if (catalogVariantOption) {
variant = {
...variant,
...catalogVariantOption
};
break;
}
}

// Try to math the top level variant with supplied variant id
if (catalogVariant.variantId === variantId) {
variant = {
...variant,
...catalogVariant
};
break;
}
}
}

// TODO: this lines still needed. We could uncomment them in future if
// decide to not completely remove product data from this method
// const product = Collections.Products.findOne(productId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Reaction } from "/server/api";
import * as Collections from "/lib/collections";
import Fixtures from "/server/imports/fixtures";
import publishProductsToCatalog from "/imports/plugins/core/catalog/server/no-meteor/utils/publishProductsToCatalog";
import publishProductToCatalog from "/imports/plugins/core/catalog/server/no-meteor/utils/publishProductToCatalog";
import collections from "/imports/collections/rawCollections";

Fixtures();
Expand Down Expand Up @@ -382,10 +383,16 @@ describe("Publication", function () {
});

describe("Product", function () {
beforeEach(function () {
Collections.Catalog.remove({});
});

it("should return a product based on an id", function (done) {
const product = Collections.Products.findOne({
isVisible: true
});
Promise.await(publishProductToCatalog(product, collections));

sandbox.stub(Reaction, "getShopId", () => shopId);

collector.collect("Product", product._id, ({ Products }) => {
Expand Down
88 changes: 86 additions & 2 deletions server/publications/collections/product.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
import { Meteor } from "meteor/meteor";
import { check, Match } from "meteor/check";
import { Products, Shops } from "/lib/collections";
import { Catalog, Products, Shops } from "/lib/collections";
import { Logger, Reaction } from "/server/api";

/**
* Flatten variant tree from a Catalog Item Product document
* @param {Object} product A Catalog Item Product document
* @returns {Array} Variant array
*/
function flattenCatalogProductVariants(product) {
const variants = [];

// Un-tree the variant tree
if (Array.isArray(product.variants)) {
// Loop over top-level variants
product.variants.forEach((variant) => {
if (Array.isArray(variant.options)) {
// Loop over variant options
variant.options.forEach((option) => {
variants.push({
ancestors: [
product.productId,
variant.variantId
],
type: "variant",
isVisible: true,
...option
});
});
}

variants.push({
ancestors: [
product.productId
],
type: "variant",
isVisible: true,
...variant
});
});
}

return variants;
}

/**
* product detail publication
* @param {String} productIdOrHandle - productId or handle
Expand Down Expand Up @@ -64,7 +105,50 @@ Meteor.publish("Product", function (productIdOrHandle, shopIdOrSlug) {
selector.isVisible = {
$in: [true, false, undefined]
};

return Products.find(selector);
}

if (!selector.shopId) {
selector.shopId = product.shopId;
}
// Product data for customers visiting the PDP page
const cursor = Catalog.find({
"$or": [{
"product._id": productIdOrHandle
}, {
"product.slug": productIdOrHandle
}],
"product.type": "product-simple",
"product.shopId": selector.shopId,
"product.isVisible": true,
"product.isDeleted": { $in: [null, false] }
});

const handle = cursor.observeChanges({
added: (id, { product: catalogProduct }) => {
this.added("Products", catalogProduct.productId, catalogProduct);
flattenCatalogProductVariants(catalogProduct).forEach((variant) => {
this.added("Products", variant.variantId, variant);
});
},
changed: (id, { product: catalogProduct }) => {
this.changed("Products", catalogProduct.productId, catalogProduct);
flattenCatalogProductVariants(product).forEach((variant) => {
this.changed("Products", variant.variantId, variant);
});
},
removed: (id, { product: catalogProduct }) => {
this.removed("Products", catalogProduct.productId, catalogProduct);
flattenCatalogProductVariants(product).forEach((variant) => {
this.removed("Products", variant.variantId, variant);
});
}
});

this.onStop(() => {
handle.stop();
});

return Products.find(selector);
return this.ready();
});
21 changes: 16 additions & 5 deletions tests/tag/tags.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ beforeAll(async () => {

afterAll(() => tester.stop());

test("get the first 50 tags when neither first or last is in query", async () => {
test("get the first 20 tags when neither first or last is in query", async () => {
let result;
try {
result = await query({ shopId: opaqueShopId });
Expand All @@ -46,9 +46,9 @@ test("get the first 50 tags when neither first or last is in query", async () =>
return;
}

expect(result.tags.nodes.length).toBe(50);
expect(result.tags.nodes.length).toBe(20);
expect(result.tags.totalCount).toBe(55);
expect(result.tags.pageInfo).toEqual({ endCursor: "MTQ5", hasNextPage: true, hasPreviousPage: false, startCursor: "MTAw" });
expect(result.tags.pageInfo).toEqual({ endCursor: "MTE5", hasNextPage: true, hasPreviousPage: false, startCursor: "MTAw" });

try {
result = await query({ shopId: opaqueShopId, after: result.tags.pageInfo.endCursor });
Expand All @@ -57,9 +57,20 @@ test("get the first 50 tags when neither first or last is in query", async () =>
return;
}

expect(result.tags.nodes.length).toBe(5);
expect(result.tags.nodes.length).toBe(20);
expect(result.tags.totalCount).toBe(55);
expect(result.tags.pageInfo).toEqual({ endCursor: "MTU0", hasNextPage: false, hasPreviousPage: true, startCursor: "MTUw" });
expect(result.tags.pageInfo).toEqual({ endCursor: "MTM5", hasNextPage: true, hasPreviousPage: true, startCursor: "MTIw" });

try {
result = await query({ shopId: opaqueShopId, after: result.tags.pageInfo.endCursor });
} catch (error) {
expect(error).toBeUndefined();
return;
}

expect(result.tags.nodes.length).toBe(15);
expect(result.tags.totalCount).toBe(55);
expect(result.tags.pageInfo).toEqual({ endCursor: "MTU0", hasNextPage: false, hasPreviousPage: true, startCursor: "MTQw" });

// Ensure it's also correct when we pass `first: 5` explicitly
try {
Expand Down

0 comments on commit 6dbde4f

Please sign in to comment.