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

Update product handle when title changes #1898

Merged
merged 10 commits into from
Mar 6, 2017
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,16 @@ class PublishContainer extends Component {
const documentIdsSet = new Set(documentIds); // ensures they are unique
documentIds = Array.from(documentIdsSet);
Meteor.call("revisions/publish", documentIds, (error, result) => {
if (result === true) {
if (result && result.status === "success") {
const message = i18next.t("revisions.changedPublished", {
defaultValue: "Changes published successfully"
});

Alerts.toast(message, "success");

if (this.props.onPublishSuccess) {
this.props.onPublishSuccess(result)
}
} else {
const message = i18next.t("revisions.noChangesPublished", {
defaultValue: "There are no changes to publish"
Expand Down
65 changes: 63 additions & 2 deletions imports/plugins/core/revisions/server/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { diff } from "deep-diff";
import { Products, Revisions, Tags, Media } from "/lib/collections";
import { Logger } from "/server/api";
import { RevisionApi } from "../lib/api";
import { getSlug } from "/lib/api";

function convertMetadata(modifierObject) {
const metadata = {};
Expand Down Expand Up @@ -404,6 +405,8 @@ Products.before.update(function (userId, product, fieldNames, modifier, options)
return true;
}

const hasAncestors = Array.isArray(product.ancestors) && product.ancestors.length > 0;

for (const operation in modifier) {
if (Object.hasOwnProperty.call(modifier, operation)) {
if (!revisionModifier[operation]) {
Expand All @@ -425,7 +428,7 @@ Products.before.update(function (userId, product, fieldNames, modifier, options)
revisionModifier.$addToSet = {};
}
revisionModifier.$addToSet[`documentData.${property}`] = modifier.$push[property];
} else if (operation === "$set" && property === "price" && Array.isArray(product.ancestors) && product.ancestors.length) {
} else if (operation === "$set" && property === "price" && hasAncestors) {
Revisions.update(revisionSelector, {
$set: {
"documentData.price": modifier.$set.price
Expand All @@ -436,7 +439,7 @@ Products.before.update(function (userId, product, fieldNames, modifier, options)
const priceRange = ProductRevision.getProductPriceRange(updateId);

Meteor.call("products/updateProductField", updateId, "price", priceRange);
} else if (operation === "$set" && property === "isVisible" && Array.isArray(product.ancestors) && product.ancestors.length) {
} else if (operation === "$set" && property === "isVisible" && hasAncestors) {
Revisions.update(revisionSelector, {
$set: {
"documentData.isVisible": modifier.$set.isVisible
Expand All @@ -447,6 +450,64 @@ Products.before.update(function (userId, product, fieldNames, modifier, options)
const priceRange = ProductRevision.getProductPriceRange(updateId);

Meteor.call("products/updateProductField", updateId, "price", priceRange);
} else if (operation === "$set" && (property === "title" || property === "handle") && hasAncestors === false) {
// Special handling for product title and handle
//
// Summary:
// When a user updates the product title, if the handle matches the product id,
// then update the handle to be a sligified version of the title
//
// This block ensures that the handle is either a custom slug, slug of the title, or
// the _id of the product, but is never blank

// New data
const newValue = modifier.$set[property];
const newTitle = modifier.$set.title;
const newHandle = modifier.$set.handle;

// Current revision data
const documentId = productRevision.documentId;
const slugDocId = getSlug(documentId);
const revisionTitle = productRevision.documentData.title;
const revisionHandle = productRevision.documentData.handle;

// Checks
const hasNewHandle = _.isEmpty(newHandle) === false;
const hasExistingTitle = _.isEmpty(revisionTitle) === false;
const hasNewTitle = _.isEmpty(newTitle) === false;
const hasHandle = _.isEmpty(revisionHandle) === false;
const handleMatchesId = revisionHandle === documentId || revisionHandle === slugDocId || newValue === documentId || newValue === slugDocId;

// Continue to set the title / handle as origionally requested
// Handle will get changed if conditions are met in the below if block
revisionModifier.$set[`documentData.${property}`] = newValue;

if ((handleMatchesId || hasHandle === false) && (hasExistingTitle || hasNewTitle) && hasNewHandle === false) {
// Set the handle to be the slug of the product.title
// when documentId (product._id) matches the handle, then handle is enpty, and a title exists
revisionModifier.$set["documentData.handle"] = getSlug(newTitle || revisionTitle);
} else if (hasHandle === false && hasExistingTitle === false && hasNewHandle === false) {
// If the handle & title is empty, the handle becomes the product id
revisionModifier.$set["documentData.handle"] = documentId;
} else if (hasNewHandle === false && property === "handle") {
// If the handle is empty, the handle becomes the sligified product title, or document id if title does not exist.
// const newTitle = modifier.$set["title"];
revisionModifier.$set["documentData.handle"] = hasExistingTitle ? getSlug(newTitle || revisionTitle) : documentId;
}
} else if (operation === "$unset" && property === "handle" && hasAncestors === false) {
// Special handling for product handle when it is going to be unset
//
// Summary:
// When a user updates the handle to a black string e.g. deltes all text in field in UI and saves,
// the handle will be adjusted so it will not be blank
const newValue = modifier.$unset[property];
const revisionTitle = productRevision.documentData.title;
const hasExistingTitle = _.isEmpty(revisionTitle) === false;

// If the new handle is going to be empty, the handle becomes the sligified product title, or document id if title does not exist.
if (_.isEmpty(newValue)) {
revisionModifier.$set["documentData.handle"] = hasExistingTitle ? getSlug(revisionTitle) : documentId;
}
} else {
// Let everything else through
revisionModifier[operation][`documentData.${property}`] = modifier[operation][property];
Expand Down
8 changes: 7 additions & 1 deletion imports/plugins/core/revisions/server/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,13 @@ Meteor.methods({
}

let updatedDocuments = 0;
const previousDocuments = [];

if (revisions) {
for (const revision of revisions) {
if (!revision.documentType || revision.documentType === "product") {
previousDocuments.push(Products.findOne(revision.documentId));

const res = Products.update({
_id: revision.documentId
}, {
Expand Down Expand Up @@ -164,7 +167,10 @@ Meteor.methods({
}

if (updatedDocuments > 0) {
return true;
return {
status: "success",
previousDocuments
};
}

return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ class ProductAdminContainer extends Component {
if (fieldName === "handle") {
updateValue = Reaction.getSlug(value);
}
Meteor.call("products/updateProductField", productId, fieldName, updateValue);
Meteor.call("products/updateProductField", productId, fieldName, updateValue, (error) => {
if (error) {
Alerts.toast(error.message, "error");
this.forceUpdate();
}
});
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ class ProductField extends Component {
{ e: input, p: { backgroundColor: "#fff" }, o: { duration: 100 } }
]);
});
} else {
this.setState({
value: nextProps.product[this.fieldName]
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ class ProductDetailContainer extends Component {
}

handleProductFieldChange = (productId, fieldName, value) => {
Meteor.call("products/updateProductField", productId, fieldName, value);
Meteor.call("products/updateProductField", productId, fieldName, value, (error) => {
if (error) {
Alerts.toast(error.message, "error");
this.forceUpdate();
}
});
}

handleViewContextChange = (event, value) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { Component, PropTypes } from "react";
import { composeWithTracker } from "/lib/api/compose";
import { Router } from "/client/api";
import { ReactionProduct } from "/lib/api";
import { Tags, Media } from "/lib/collections";
import PublishContainer from "/imports/plugins/core/revisions/client/containers/publishContainer";
Expand All @@ -23,10 +24,27 @@ class ProductPublishContainer extends Component {
}
}

handlePublishSuccess = (result) => {
if (result && result.status === "success" && this.props.product) {
const productDocument = result.previousDocuments.find((product) => this.props.product._id === product._id);

if (this.props.product.handle !== productDocument.handle) {
const newProductPath = Router.pathFor("product", {
hash: {
handle: this.props.product.handle
}
});

window.location.href = newProductPath;
}
}
}

render() {
return (
<PublishContainer
onAction={this.handlePublishActions}
onPublishSuccess={this.handlePublishSuccess}
onVisibilityChange={this.handleVisibilityChange}
{...this.props}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template name="gridContent">
<div class="grid-content">
<a href="{{pathFor 'product' handle=handle}}" data-event-category="grid" data-event-action="product-click" data-event-label="grid product click" data-event-value="{{_id}}">
<a href="{{pdpPath}}" data-event-category="grid" data-event-action="product-click" data-event-label="grid product click" data-event-value="{{_id}}">
<div class="overlay">
<div class="overlay-title">{{title}}</div>
<div class="currency-symbol">{{formatPrice displayPrice}}</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import { Reaction } from "/client/api";

/**
* gridContent helpers
*/

Template.gridContent.helpers({
pdpPath() {
const instance = Template.instance();
const product = instance.data;

if (product) {
let handle = product.handle;

if (product.__published) {
handle = product.__published.handle;
}

return Reaction.Router.pathFor("product", {
hash: {
handle
}
});
}

return "/";
},
displayPrice: function () {
if (this.price && this.price.range) {
return this.price.range;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ Template.productGridItems.events({
"dblclick [data-event-action=productClick]": function (event, template) {
const instance = template;
const product = instance.data;
const handle = product.handle;
const handle = product.__published && product.__published.handle || product.handle;

Reaction.Router.go("product", {
handle: handle
Expand Down
20 changes: 13 additions & 7 deletions server/methods/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -810,13 +810,19 @@ Meteor.methods({
}

// we need to use sync mode here, to return correct error and result to UI
const result = Products.update(_id, {
$set: update
}, {
selector: {
type: type
}
});
let result;

try {
result = Products.update(_id, {
$set: update
}, {
selector: {
type: type
}
});
} catch (e) {
throw new Meteor.Error(e.message);
}

if (typeof result === "number") {
if (type === "variant" && ~toDenormalize.indexOf(field)) {
Expand Down