{{#with media}}
-
+ {{!--
--}}
+
{{else}}
{{/with}}
@@ -27,7 +28,7 @@
{{#if isMediumWeight}}
{{#each additionalMedia}}
-
+
{{/each}}
{{#unless isVisible}}
diff --git a/imports/plugins/included/product-variant/client/templates/products/productGrid/item.js b/imports/plugins/included/product-variant/client/templates/products/productGrid/item.js
index 8e03f7d1b6c..f2936cb9949 100644
--- a/imports/plugins/included/product-variant/client/templates/products/productGrid/item.js
+++ b/imports/plugins/included/product-variant/client/templates/products/productGrid/item.js
@@ -92,26 +92,18 @@ Template.productGridItems.helpers({
};
},
media: function () {
- const media = Media.findOne({
- "metadata.productId": this._id,
- "metadata.toGrid": 1
- }, {
- sort: { "metadata.priority": 1, "uploadedAt": 1 }
- });
+ if (Array.isArray(this.media) && this.media.length) {
+ return this.media[0].images.medium;
+ }
- return media instanceof FS.File ? media : false;
+ return false;
},
additionalMedia: function () {
- const mediaArray = Media.find({
- "metadata.productId": this._id,
- "metadata.priority": {
- $gt: 0
- },
- "metadata.toGrid": 1
- }, { limit: 3 });
-
- if (mediaArray.count() > 1) {
- return mediaArray;
+ if (Array.isArray(this.media)) {
+ return this.media
+ .filter(mediaItem => mediaItem.metadata.priority > 0)
+ .map(mediaItem => mediaItem.images.small)
+ .slice(0, 3);
}
return false;
diff --git a/lib/api/media.js b/lib/api/media.js
new file mode 100644
index 00000000000..e3f9dab7d3b
--- /dev/null
+++ b/lib/api/media.js
@@ -0,0 +1,96 @@
+import { each } from "lodash";
+import { Media, Revisions } from "/lib/collections";
+
+export function getProductMedia({ productId, getRevisions }) {
+ const selector = {
+ "$or": [
+ { "metadata.productId": productId },
+ { "metadata.variantId": productId }
+ ],
+ "metadata.workflow": {
+ $nin: ["archived"]
+ }
+ };
+
+ if (getRevisions !== true) {
+ selector["metadata.workflow"] = {
+ $in: [null, "published"]
+ };
+ }
+
+ const productMedia = Media.find(selector, {
+ sort: { "metadata.priority": 1, "uploadedAt": 1 }
+ }).map((mediaItem) => {
+ const images = {};
+
+ each(mediaItem.copies, (copy, store) => {
+ images[store] = {
+ name: copy.name,
+ type: copy.type,
+ size: copy.size,
+ url: mediaItem.url({ store })
+ };
+ });
+
+ return {
+ _id: mediaItem._id,
+ name: mediaItem.original.name,
+ images,
+ metadata: {
+ productId: mediaItem.metadata.productId,
+ variantId: mediaItem.metadata.variantId,
+ shopId: mediaItem.metadata.shopId,
+ priority: mediaItem.metadata.priority,
+ toGrid: mediaItem.metadata.toGrid
+ }
+ };
+ });
+
+ if (getRevisions) {
+ return appendRevisionsToMedia(productId, productMedia);
+ }
+
+ return productMedia;
+}
+
+export function fetchMediaRevisions(productId) {
+ const mediaRevisions = Revisions.find({
+ "parentDocument": productId,
+ "documentType": "image",
+ "workflow.status": {
+ $nin: ["revision/published"]
+ }
+ }).fetch();
+
+ return mediaRevisions;
+}
+
+// resort the media in
+export function sortMedia(media) {
+ const sortedMedia = _.sortBy(media, function (m) {
+ return m.metadata.priority;
+ });
+
+ return sortedMedia;
+}
+
+// Search through revisions and if we find one for the image, stick it on the object
+export function appendRevisionsToMedia(productId, media) {
+ const mediaRevisions = fetchMediaRevisions(productId);
+ const newMedia = [];
+
+ for (const image of media) {
+ image.revision = undefined;
+
+ for (const revision of mediaRevisions) {
+ if (revision.documentId === image._id) {
+ image.revision = revision;
+ image.metadata.priority = revision.documentData.priority;
+ }
+ }
+
+ newMedia.push(image);
+ }
+
+ return sortMedia(newMedia);
+}
diff --git a/server/publications/collections/media.js b/server/publications/collections/media.js
index 8dc2daeb2f2..c82e7135f3a 100644
--- a/server/publications/collections/media.js
+++ b/server/publications/collections/media.js
@@ -70,11 +70,9 @@ Meteor.publish("Media", function (shops) {
this.onStop(() => {
revisionHandle.stop();
});
+
+ return this.ready();
}
- return Media.find(selector, {
- sort: {
- "metadata.priority": 1
- }
- });
+ return this.ready();
});
diff --git a/server/publications/collections/product.js b/server/publications/collections/product.js
index 022abf1d61d..717750987a4 100644
--- a/server/publications/collections/product.js
+++ b/server/publications/collections/product.js
@@ -1,6 +1,62 @@
-import { Products, Revisions } from "/lib/collections";
+import { Media, Products, Revisions } from "/lib/collections";
import { Logger, Reaction } from "/server/api";
import { RevisionApi } from "/imports/plugins/core/revisions/lib/api/revisions";
+import { getProductMedia } from "/lib/api/media";
+
+export function fetchRevisions(documentId) {
+ return Revisions.find({
+ "documentId": documentId,
+ "workflow.status": {
+ $nin: [
+ "revision/published"
+ ]
+ }
+ }).fetch();
+}
+
+export function fetchAllProductData(productId) {
+ const product = Products.findOne(productId);
+
+ product.__revisions = fetchRevisions(product._id);
+ product.media = getProductMedia({ productId: product._id, getRevisions: true });
+
+ return product;
+}
+
+export function fetchPublicProductData(productId) {
+ const product = Products.findOne(productId);
+
+ product.media = getProductMedia({ productId: product._id });
+
+ return product;
+}
+
+export function createMediaSelector() {
+ let selector;
+
+ const shopId = Reaction.getShopId();
+ if (!shopId) {
+ return false;
+ }
+
+ if (shopId) {
+ selector = {
+ "metadata.shopId": shopId
+ };
+ }
+
+ // Product editors can see both published and unpublished images
+ if (!Reaction.hasPermission(["createProduct"], this.userId)) {
+ selector["metadata.workflow"] = {
+ $in: [null, "published"]
+ };
+ } else {
+ // but no one gets to see archived images
+ selector["metadata.workflow"] = {
+ $nin: ["archived"]
+ };
+ }
+}
/**
* product detail publication
@@ -21,11 +77,11 @@ Meteor.publish("Product", function (productId) {
}
let selector = {};
+
selector.isVisible = true;
selector.isDeleted = { $in: [null, false] };
- if (Roles.userIsInRole(this.userId, ["owner", "admin", "createProduct"],
- shop._id)) {
+ if (Roles.userIsInRole(this.userId, ["owner", "admin", "createProduct"], shop._id)) {
selector.isVisible = {
$in: [true, false]
};
@@ -48,7 +104,7 @@ Meteor.publish("Product", function (productId) {
}
}
- // Selector for hih?
+ // Selector for product?
selector = {
isVisible: true,
isDeleted: { $in: [null, false] },
@@ -81,7 +137,9 @@ Meteor.publish("Product", function (productId) {
]
}
}).fetch();
+
fields.__revisions = revisions;
+ fields.media = getProductMedia({ productId: id, getRevisions: true });
this.added("Products", id, fields);
},
@@ -96,6 +154,8 @@ Meteor.publish("Product", function (productId) {
}).fetch();
fields.__revisions = revisions;
+ fields.media = getProductMedia({ productId: id, getRevisions: true });
+
this.changed("Products", id, fields);
},
removed: (id) => {
@@ -112,18 +172,23 @@ Meteor.publish("Product", function (productId) {
}).observe({
added: (revision) => {
let product;
+
if (!revision.parentDocument) {
product = Products.findOne(revision.documentId);
} else {
product = Products.findOne(revision.parentDocument);
}
+
if (product) {
- this.added("Products", product._id, product);
+ product.media = getProductMedia({ productId: product._id, getRevisions: true });
+
+ this.changed("Products", product._id, product);
this.added("Revisions", revision._id, revision);
}
},
changed: (revision) => {
let product;
+
if (!revision.parentDocument) {
product = Products.findOne(revision.documentId);
} else {
@@ -132,17 +197,21 @@ Meteor.publish("Product", function (productId) {
if (product) {
product.__revisions = [revision];
+ product.media = getProductMedia({ productId: product._id, getRevisions: true });
+
this.changed("Products", product._id, product);
this.changed("Revisions", revision._id, revision);
}
},
removed: (revision) => {
let product;
+
if (!revision.parentDocument) {
product = Products.findOne(revision.documentId);
} else {
product = Products.findOne(revision.parentDocument);
}
+
if (product) {
product.__revisions = [];
this.changed("Products", product._id, product);
@@ -151,9 +220,45 @@ Meteor.publish("Product", function (productId) {
}
});
+ const mediaHandle = Media.files.find({
+ "$or": [
+ { "metadata.productId": _id },
+ { "metadata.variantId": _id }
+ ],
+ "metadata.workflow": {
+ $nin: ["archived"]
+ }
+ }).observe({
+ added: (media) => {
+ if (media.metadata && media.metadata.variantId) {
+ const product = fetchAllProductData(media.metadata.variantId);
+ this.changed("Products", media.metadata.variantId, product);
+ }
+
+ this.added("Media", media._id, media);
+ },
+ changed: (media) => {
+ if (media.metadata && media.metadata.variantId) {
+ const product = fetchAllProductData(media.metadata.variantId);
+ this.changed("Products", media.metadata.variantId, product);
+ }
+
+ this.changed("Media", media._id, media);
+ },
+ removed: (media) => {
+ if (media.metadata && media.metadata.variantId) {
+ const product = fetchAllProductData(media.metadata.variantId);
+ this.changed("Products", media.metadata.variantId, product);
+ }
+
+ this.removed("Media", media._id, media);
+ }
+ });
+
this.onStop(() => {
handle.stop();
handle2.stop();
+ mediaHandle.stop();
});
return this.ready();
@@ -164,5 +269,60 @@ Meteor.publish("Product", function (productId) {
}
// Everyone else gets the standard, visibile products and variants
- return Products.find(selector);
+ const handle = Products.find(selector).observe({
+ added: (product) => {
+ product.media = getProductMedia({ productId: product._id });
+
+ this.added("Products", product._id, product);
+ },
+ changed: (product) => {
+ product.media = getProductMedia({ productId: product._id });
+
+ this.changed("Products", product._id, product);
+ },
+ removed: (product) => {
+ this.removed("Products", product._id, product);
+ }
+ });
+
+ const mediaHandle = Media.files.find({
+ "$or": [
+ { "metadata.productId": _id },
+ { "metadata.variantId": _id }
+ ],
+ "metadata.workflow": "published"
+ }).observe({
+ added: (media) => {
+ if (media.metadata && media.metadata.variantId) {
+ const product = fetchPublicProductData(media.metadata.variantId);
+ this.changed("Products", media.metadata.variantId, product);
+ }
+
+ this.added("Media", media._id, media);
+ },
+ changed: (media) => {
+ if (media.metadata && media.metadata.variantId) {
+ const product = fetchPublicProductData(media.metadata.variantId);
+ this.changed("Products", media.metadata.variantId, product);
+ }
+
+ this.changed("Media", media._id, media);
+ },
+ removed: (media) => {
+ if (media.metadata && media.metadata.variantId) {
+ const product = fetchPublicProductData(media.metadata.variantId);
+ this.changed("Products", media.metadata.variantId, product);
+ }
+
+ this.removed("Media", media._id, media);
+ }
+ });
+
+
+ this.onStop(() => {
+ handle.stop();
+ mediaHandle.stop();
+ });
+
+ return this.ready();
});
diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js
index 3dce1e564fa..124aee9be26 100644
--- a/server/publications/collections/products.js
+++ b/server/publications/collections/products.js
@@ -1,6 +1,8 @@
-import { Products, Revisions } from "/lib/collections";
+import { Media, Products, Revisions } from "/lib/collections";
import { Reaction, Logger } from "/server/api";
import { RevisionApi } from "/imports/plugins/core/revisions/lib/api/revisions";
+import { getProductMedia } from "/lib/api/media";
+import { fetchPublicProductData } from "./product";
//
// define search filters as a schema so we can validate
@@ -286,7 +288,9 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so
]
}
}).fetch();
+
fields.__revisions = revisions;
+ fields.media = getProductMedia({ productId: id, getRevisions: true });
this.added("Products", id, fields);
},
@@ -304,6 +308,8 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so
}).fetch();
fields.__revisions = revisions;
+ fields.media = getProductMedia({ productId: id, getRevisions: true });
+
this.changed("Products", id, fields);
},
removed: (id) => {
@@ -327,6 +333,8 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so
}
if (product) {
+ product.media = getProductMedia({ productId: product._id, getRevisions: true });
+
this.added("Products", product._id, product);
this.added("Revisions", revision._id, revision);
}
@@ -340,6 +348,8 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so
}
if (product) {
product.__revisions = [revision];
+ product.media = getProductMedia({ productId: product._id, getRevisions: true });
+
this.changed("Products", product._id, product);
this.changed("Revisions", revision._id, revision);
}
@@ -354,6 +364,8 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so
}
if (product) {
product.__revisions = [];
+ product.media = getProductMedia({ productId: product._id, getRevisions: true });
+
this.changed("Products", product._id, product);
this.removed("Revisions", revision._id, revision);
}
@@ -393,6 +405,7 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so
// Re-configure selector to pick either Variants of one of the top-level products, or the top-level products in the filter
_.extend(newSelector, {
$or: [
+ { _id: { $in: productIds } },
{
ancestors: {
$in: productIds
@@ -405,12 +418,69 @@ Meteor.publish("Products", function (productScrollLimit = 24, productFilters, so
]
});
}
- // Returning Complete product tree for top level products to avoid sold out warning.
- return Products.find(
- {
- $or: [ { _id: { $in: productIds } },
- { ancestors: { $in: productIds } }
- ]
- });
+
+ const productCursor = Products.find(newSelector, {
+ sort: sort,
+ limit: productScrollLimit
+ });
+
+ const foundProducts = productCursor.fetch();
+ const updatedProductIds = foundProducts.map(product => product._id);
+
+ const handle = productCursor.observe({
+ added: (product) => {
+ product.media = getProductMedia({ productId: product._id });
+
+ this.added("Products", product._id, product);
+ },
+ changed: (product) => {
+ product.media = getProductMedia({ productId: product._id });
+
+ this.changed("Products", product._id, product);
+ },
+ removed: (product) => {
+ this.removed("Products", product._id, product);
+ }
+ });
+
+ const mediaHandle = Media.files.find({
+ "$or": [
+ { "metadata.productId": { $in: updatedProductIds } },
+ { "metadata.variantId": { $in: updatedProductIds } }
+ ],
+ "metadata.workflow": "published"
+ }).observe({
+ added: (media) => {
+ if (media.metadata && media.metadata.variantId) {
+ const product = fetchPublicProductData(media.metadata.variantId);
+ this.changed("Products", media.metadata.variantId, product);
+ }
+
+ this.added("Media", media._id, media);
+ },
+ changed: (media) => {
+ if (media.metadata && media.metadata.variantId) {
+ const product = fetchPublicProductData(media.metadata.variantId);
+ this.changed("Products", media.metadata.variantId, product);
+ }
+
+ this.changed("Media", media._id, media);
+ },
+ removed: (media) => {
+ if (media.metadata && media.metadata.variantId) {
+ const product = fetchPublicProductData(media.metadata.variantId);
+ this.changed("Products", media.metadata.variantId, product);
+ }
+
+ this.removed("Media", media._id, media);
+ }
+ });
+
+ this.onStop(() => {
+ handle.stop();
+ mediaHandle.stop();
+ });
+
+ return this.ready();
}
});