Skip to content

Commit

Permalink
Revision control for images (#1555)
Browse files Browse the repository at this point in the history
* Add media hook. Rename file to "hooks'"

* Only return published images for non-admin users

* Only set product images as unpublished

* Capture revision record and show Publish button when image changes are made

* Adding images deferred until published

* Removing images deferred until published

* Capturing and deferring other changes to metadata (priority really)

* Combine revisions when making multiple changes before publishing

* Properly handle non-product revisions in applyProductRevision so that product doesn't get wiped.

* Find the product correctly based on revision type

* Fix regression where images weren't being published because revision data was wrong

* Setting priceRange should be in an `else` statement

* If you add an image and then delete it before publishing it, just remove it.

* Update revisions with media

* Show visual indicator on image if there are pending revisions

* Discard drafts of image revisions

* Sometimes rendering the "edited" marker

* Reset revisions so that removals take effect

* Set priority to revision priority

* Fix issue where changing order wouldn't stick

* Remove logging

* Change lost in merge

* Another change from merge
  • Loading branch information
brent-hoover authored and kieckhafer committed Nov 10, 2016
1 parent bca6752 commit 86f52d2
Show file tree
Hide file tree
Showing 14 changed files with 389 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ class PublishControls extends Component {
get hasChanges() {
// Verify we even have any revision at all
if (this.hasRevisions) {
// Loop through all revisions to determin if they have changes
// Loop through all revisions to determine if they have changes
const diffHasActualChanges = this.props.revisions.map((revision) => {
// We probably do have chnages to publish
// Note: Sometimes "updatedAt" will cause false positives, but just incase, lets
// enable the publish button anyway.
if (Array.isArray(revision.diff) && revision.diff.length) {
if (Array.isArray(revision.diff) && revision.diff.length || revision.documentType !== "product") {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ import { i18next } from "/client/api";
class PublishContainer extends Component {
handlePublishClick = (revisions) => {
if (Array.isArray(revisions)) {
const documentIds = revisions.map((revision) => {
let documentIds = revisions.map((revision) => {
if (revision.parentDocument && revision.documentType !== "product") {
return revision.parentDocument;
}
return revision.documentId;
});

const documentIdsSet = new Set(documentIds); // ensures they are unique
documentIds = Array.from(documentIdsSet);
Meteor.call("revisions/publish", documentIds, (error, result) => {
if (result === true) {
const message = i18next.t("revisions.changedPublished", {
Expand Down Expand Up @@ -105,6 +110,11 @@ function composer(props, onData) {
"documentData.ancestors": {
$in: props.documentIds
}
},
{
parentDocument: {
$in: props.documentIds
}
}
],
"workflow.status": {
Expand All @@ -113,7 +123,6 @@ function composer(props, onData) {
]
}
}).fetch();

onData(null, {
isEnabled: isRevisionControlEnabled(),
documentIds: props.documentIds,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,127 @@
import { Products, Revisions, Tags } from "/lib/collections";
import { Logger } from "/server/api";
import _ from "lodash";
import { diff } from "deep-diff";
import { Products, Revisions, Tags, Media } from "/lib/collections";
import { Logger } from "/server/api";
import { RevisionApi } from "../lib/api";


function convertMetadata(modifierObject) {
const metadata = {};
for (const prop in modifierObject) {
if (modifierObject.hasOwnProperty(prop)) {
if (prop.indexOf("metadata") !== -1) {
const splitName = _.split(prop, ".")[1];
metadata[splitName] = modifierObject[prop];
}
}
}
return metadata;
}

Media.files.before.insert((userid, media) => {
if (RevisionApi.isRevisionControlEnabled() === false) {
return true;
}
if (media.metadata.productId) {
const revisionMetadata = Object.assign({}, media.metadata);
revisionMetadata.workflow = "published";
Revisions.insert({
documentId: media._id,
documentData: revisionMetadata,
documentType: "image",
parentDocument: media.metadata.productId,
changeType: "insert",
workflow: {
status: "revision/update"
}
});
media.metadata.workflow = "unpublished";
} else {
media.metadata.workflow = "published";
}
return true;
});

Media.files.before.update((userId, media, fieldNames, modifier) => {
if (RevisionApi.isRevisionControlEnabled() === false) {
return true;
}
// if it's not metadata ignore it, as LOTS of othing things change on this record
if (!_.includes(fieldNames, "metadata")) {
return true;
}

if (media.metadata.productId) {
const convertedModifier = convertMetadata(modifier.$set);
const convertedMetadata = Object.assign({}, media.metadata, convertedModifier);
const existingRevision = Revisions.findOne({
"documentId": media._id,
"workflow.status": {
$nin: [
"revision/published"
]
}
});
if (existingRevision) {
const updatedMetadata = Object.assign({}, existingRevision.documentData, convertedMetadata);
// Special case where if we have both added and reordered images before publishing we don't want to overwrite
// the workflow status since it would be "unpublished"
if (existingRevision.documentData.workflow === "published" || existingRevision.changeType === "insert") {
updatedMetadata.workflow = "published";
}
Revisions.update({_id: existingRevision._id}, {
$set: {
documentData: updatedMetadata
}
});
} else {
Revisions.insert({
documentId: media._id,
documentData: convertedMetadata,
documentType: "image",
parentDocument: media.metadata.productId,
changeType: "update",
workflow: {
status: "revision/update"
}
});
}

return false; // prevent actual update of image. This also stops other hooks from running :/
}
// for non-product images, just ignore and keep on moving
return true;
});

Media.files.before.remove((userId, media) => {
if (RevisionApi.isRevisionControlEnabled() === false) {
return true;
}

// if the media is unpublished, then go ahead and just delete it
if (media.metadata.workflow && media.metadata.workflow === "unpublished") {
Revisions.remove({
documentId: media._id
});
return true;
}
if (media.metadata.productId) {
Revisions.insert({
documentId: media._id,
documentData: media.metadata,
documentType: "image",
parentDocument: media.metadata.productId,
changeType: "remove",
workflow: {
status: "revision/update"
}
});
return false; // prevent actual deletion of image. This also stops other hooks from running :/
}
return true;
});


Products.before.insert((userId, product) => {
if (RevisionApi.isRevisionControlEnabled() === false) {
return true;
Expand Down Expand Up @@ -243,13 +362,21 @@ Revisions.after.update(function (userId, revision) {
if (RevisionApi.isRevisionControlEnabled() === false) {
return true;
}
let differences;

// Make diff
const product = Products.findOne({
_id: revision.documentId
});

const differences = diff(product, revision.documentData);
if (!revision.documentType || revision.documentType === "product") {
// Make diff
const product = Products.findOne({
_id: revision.documentId
});
differences = diff(product, revision.documentData);
}

if (revision.documentType && revision.documentType === "image") {
const image = Media.findOne(revision.documentId);
differences = diff(image.metadata, revision.documentData);
}

Revisions.direct.update({
_id: revision._id
Expand Down
2 changes: 1 addition & 1 deletion imports/plugins/core/revisions/server/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import "./startup";
import "./hooks";
import "./methods";
70 changes: 60 additions & 10 deletions imports/plugins/core/revisions/server/methods.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Products, Revisions, Packages } from "/lib/collections";
import { Meteor } from "meteor/meteor";
import { check, Match } from "meteor/check";
import { Products, Media, Revisions, Packages } from "/lib/collections";
import { Logger } from "/server/api";

export function updateSettings(settings) {
check(settings, Object);
Expand Down Expand Up @@ -41,6 +42,11 @@ export function discardDrafts(documentIds) {
"documentData.ancestors": {
$in: documentIdArray
}
},
{
parentDocument: {
$in: documentIds
}
}
]
};
Expand Down Expand Up @@ -76,6 +82,11 @@ Meteor.methods({
"documentData.ancestors": {
$in: documentIds
}
},
{
parentDocument: {
$in: documentIds
}
}
]
}).fetch();
Expand All @@ -101,15 +112,54 @@ Meteor.methods({

if (revisions) {
for (const revision of revisions) {
const res = Products.update({
_id: revision.documentId
}, {
$set: revision.documentData
}, {
publish: true
});

updatedDocuments += res;
if (!revision.documentType || revision.documentType === "product") {
const res = Products.update({
_id: revision.documentId
}, {
$set: revision.documentData
}, {
publish: true
});
updatedDocuments += res;
} else if (revision.documentType === "image") {
if (revision.changeType === "insert") {
const res = Media.files.direct.update({
_id: revision.documentId
}, {
$set: {
metadata: revision.documentData
}
});
updatedDocuments += res;
} else if (revision.changeType === "remove") {
const res = Media.files.direct.update({
_id: revision.documentId
}, {
$set: {
"metadata.workflow": "archived"
}
});
updatedDocuments += res;
} else if (revision.changeType === "update") {
const res = Media.files.direct.update({
_id: revision.documentId
}, {
$set: {
metadata: revision.documentData
}
});
updatedDocuments += res;
Logger.debug(`setting metadata for ${revision.documentId} to ${JSON.stringify(revision.documentData, null, 4)}`);
}
// mark revision published whether we are publishing the image or not
Revisions.direct.update({
_id: revision._id
}, {
$set: {
"workflow.status": "revision/published"
}
});
}
}
}

Expand Down
16 changes: 16 additions & 0 deletions imports/plugins/core/ui/client/components/media/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ class MediaItem extends Component {
}
}

renderRevision() {
if (this.props.revision) {
if (this.props.revision.changeType === "remove") {
return (
<IconButton icon="fa fa-pencil-remove" />
);
}
return (
<IconButton icon="fa fa-pencil-square-o" />
);
}
}

renderControls() {
if (this.props.editable) {
return (
Expand All @@ -33,6 +46,7 @@ class MediaItem extends Component {
icon="fa fa-times"
onClick={this.handleRemoveMedia}
/>
{this.renderRevision()}
</div>
);
}
Expand Down Expand Up @@ -93,9 +107,11 @@ MediaItem.propTypes = {
connectDropTarget: PropTypes.func,
defaultSource: PropTypes.string,
editable: PropTypes.bool,
metadata: PropTypes.object,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
onRemoveMedia: PropTypes.func,
revision: PropTypes.object,
source: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class MediaGallery extends Component {
editable={this.props.editable}
index={index}
key={index}
revision={this.featuredMedia.revision}
metadata={this.featuredMedia.metadata}
onMouseEnter={this.props.onMouseEnterMedia}
onMouseLeave={this.props.onMouseLeaveMedia}
Expand All @@ -66,6 +67,7 @@ class MediaGallery extends Component {
editable={this.props.editable}
index={index}
key={index}
revision={media.revision}
metadata={media.metadata}
onMouseEnter={this.props.onMouseEnterMedia}
onMouseLeave={this.props.onMouseLeaveMedia}
Expand Down
Loading

0 comments on commit 86f52d2

Please sign in to comment.