diff --git a/imports/plugins/core/media/register.js b/imports/plugins/core/media/register.js new file mode 100644 index 00000000000..4272c22aea3 --- /dev/null +++ b/imports/plugins/core/media/register.js @@ -0,0 +1,8 @@ +import { Reaction } from "/server/api"; + +Reaction.registerPackage({ + label: "Media", + name: "reaction-media", + icon: "fa fa-picture-o", + autoEnable: true +}); diff --git a/imports/plugins/core/media/server/index.js b/imports/plugins/core/media/server/index.js new file mode 100644 index 00000000000..50e27736252 --- /dev/null +++ b/imports/plugins/core/media/server/index.js @@ -0,0 +1 @@ +import "./methods.js"; diff --git a/imports/plugins/core/media/server/methods.js b/imports/plugins/core/media/server/methods.js new file mode 100644 index 00000000000..dbe9b77daf5 --- /dev/null +++ b/imports/plugins/core/media/server/methods.js @@ -0,0 +1,38 @@ +import { Meteor } from "meteor/meteor"; +import { check, Match } from "meteor/check"; +import { Media } from "/lib/collections"; +import { Reaction } from "/server/api"; + +export function updateMediaMetadata(mediaId, metadata) { + check(mediaId, String); + + Media.update({ _id: mediaId }, { + $set: { + metadata + } + }); +} + +export function updateMediaPosition(mediaIdArray) { + check(mediaIdArray, [String]); + + mediaIdArray.forEach((mediaId, index) => { + Media.update(mediaId, { + $set: { + "metadata.priority": index + } + }); + }); +} + +export function removeMedia(mediaId) { + check(mediaId, String); + + return Media.remove({ _id: mediaId }); +} + +Meteor.methods({ + "media/updateMetadata": updateMediaMetadata, + "media/updatePositions": updateMediaPosition, + "media/remove": removeMedia +}); diff --git a/imports/plugins/core/revisions/client/containers/publishContainer.js b/imports/plugins/core/revisions/client/containers/publishContainer.js index 9578e49a920..39aeff243f9 100644 --- a/imports/plugins/core/revisions/client/containers/publishContainer.js +++ b/imports/plugins/core/revisions/client/containers/publishContainer.js @@ -124,6 +124,16 @@ function composer(props, onData) { parentDocument: { $in: props.documentIds } + }, + { + "documentData.productId": { + $in: props.documentIds + } + }, + { + "documentData.variantId": { + $in: props.documentIds + } } ], "workflow.status": { diff --git a/imports/plugins/core/revisions/server/hooks.js b/imports/plugins/core/revisions/server/hooks.js index 727d36f1664..dd5e76a570b 100644 --- a/imports/plugins/core/revisions/server/hooks.js +++ b/imports/plugins/core/revisions/server/hooks.js @@ -168,7 +168,7 @@ Media.files.before.insert((userid, media) => { documentId: media._id, documentData: revisionMetadata, documentType: "image", - parentDocument: media.metadata.productId, + parentDocument: media.metadata.variantId || media.metadata.productId, changeType: "insert", workflow: { status: "revision/update" @@ -218,7 +218,7 @@ Media.files.before.update((userId, media, fieldNames, modifier) => { documentId: media._id, documentData: convertedMetadata, documentType: "image", - parentDocument: media.metadata.productId, + parentDocument: media.metadata.variantId || media.metadata.productId, changeType: "update", workflow: { status: "revision/update" @@ -249,7 +249,7 @@ Media.files.before.remove((userId, media) => { documentId: media._id, documentData: media.metadata, documentType: "image", - parentDocument: media.metadata.productId, + parentDocument: media.metadata.variantId || media.metadata.productId, changeType: "remove", workflow: { status: "revision/update" diff --git a/imports/plugins/core/ui/client/components/media/media.js b/imports/plugins/core/ui/client/components/media/media.js index 73d26a3d7f6..4c534fa318b 100644 --- a/imports/plugins/core/ui/client/components/media/media.js +++ b/imports/plugins/core/ui/client/components/media/media.js @@ -67,11 +67,20 @@ class MediaItem extends Component { } get source() { - if (typeof this.props.source === "object" && this.props.source) { - return this.props.source.url() || this.defaultSource; + let imageSrc; + + if (this.props.source) { + // Handle FS.File + if (typeof this.props.source.url === "function") { + imageSrc = this.props.source.url(); + } else if (typeof this.props.source.images === "object") { + imageSrc = this.props.source.images.large && this.props.source.images.large.url; + } else { + imageSrc = this.props.source; + } } - return this.props.source || this.defaultSource; + return imageSrc || this.defaultSource; } renderImage() { diff --git a/imports/plugins/core/ui/client/containers/mediaGalleryContainer.js b/imports/plugins/core/ui/client/containers/mediaGalleryContainer.js index 04baf83ae01..e7cafdcdb6e 100644 --- a/imports/plugins/core/ui/client/containers/mediaGalleryContainer.js +++ b/imports/plugins/core/ui/client/containers/mediaGalleryContainer.js @@ -1,11 +1,12 @@ import React, { Component, PropTypes } from "react"; import Measure from "react-measure"; import update from "react/lib/update"; +import { Meteor } from "meteor/meteor"; import { composeWithTracker } from "/lib/api/compose"; import { MediaGallery } from "../components"; import { Reaction } from "/client/api"; import { ReactionProduct } from "/lib/api"; -import { Media, Revisions } from "/lib/collections"; +import { Media } from "/lib/collections"; function uploadHandler(files) { // TODO: It would be cool to move this logic to common ValidatedMethod, but @@ -72,12 +73,23 @@ class MediaGalleryContainer extends Component { }); } + get allowFeaturedMediaHover() { + if (this.state.featuredMedia) { + return true; + } + return false; + } + + get media() { + return (this.state && this.state.media) || this.props.media; + } + handleDrop = (files) => { uploadHandler(files); } handleRemoveMedia = (media) => { - const imageUrl = media.url(); + const imageUrl = media.images && media.images.medium.url; const mediaId = media._id; Alerts.alert({ @@ -88,31 +100,18 @@ class MediaGalleryContainer extends Component { imageHeight: 150 }, (isConfirm) => { if (isConfirm) { - Media.remove({ _id: mediaId }, (error) => { + Meteor.call("media/remove", mediaId, (error) => { if (error) { Alerts.toast(error.reason, "warning", { autoHide: 10000 }); } - - // updateImagePriorities(); }); } // show media as removed (since it will not disappear until changes are published }); } - get allowFeaturedMediaHover() { - if (this.state.featuredMedia) { - return true; - } - return false; - } - - get media() { - return (this.state && this.state.media) || this.props.media; - } - handleMouseEnterMedia = (event, media) => { this.setState({ featuredMedia: media @@ -143,14 +142,14 @@ class MediaGalleryContainer extends Component { }); // Save the updated positions - Meteor.defer(() => { - newMediaOrder.forEach((mediaItem, index) => { - Media.update(mediaItem._id, { - $set: { - "metadata.priority": index - } + const mediaIds = newMediaOrder.map((mediaItem) => mediaItem._id); + + Meteor.call("media/updatePositions", mediaIds, (error) => { + if (error) { + Alerts.toast(error.reason, "warning", { + autoHide: 10000 }); - }); + } }); } @@ -181,54 +180,10 @@ class MediaGalleryContainer extends Component { } } -function fetchMediaRevisions() { - const productId = ReactionProduct.selectedProductId(); - const mediaRevisions = Revisions.find({ - "parentDocument": productId, - "documentType": "image", - "workflow.status": { - $nin: ["revision/published"] - } - }).fetch(); - return mediaRevisions; -} - -// resort the media in -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 -function appendRevisionsToMedia(props, media) { - if (!Reaction.hasPermission(props.permission || ["createProduct"])) { - return media; - } - const mediaRevisions = fetchMediaRevisions(); - 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); -} - function composer(props, onData) { - let media; let editable; const viewAs = Reaction.getUserPreferences("reaction-dashboard", "viewAs", "administrator"); - - if (!props.media) { - // Fetch media based on props - } else { - media = appendRevisionsToMedia(props, props.media); - } + const media = props.media; if (viewAs === "customer") { editable = false; diff --git a/imports/plugins/included/product-detail-simple/client/components/childVariant.js b/imports/plugins/included/product-detail-simple/client/components/childVariant.js index 6405b9e931f..2dbd9b88161 100644 --- a/imports/plugins/included/product-detail-simple/client/components/childVariant.js +++ b/imports/plugins/included/product-detail-simple/client/components/childVariant.js @@ -4,6 +4,15 @@ import { Translation } from "/imports/plugins/core/ui/client/components"; import { MediaItem } from "/imports/plugins/core/ui/client/components"; class ChildVariant extends Component { + static propTypes = { + editButton: PropTypes.node, + isSelected: PropTypes.bool, + onClick: PropTypes.func, + soldOut: PropTypes.bool, + variant: PropTypes.object, + visibilityButton: PropTypes.node + } + handleClick = (event) => { if (this.props.onClick) { this.props.onClick(event, this.props.variant); @@ -11,12 +20,12 @@ class ChildVariant extends Component { } get hasMedia() { - return Array.isArray(this.props.media) && this.props.media.length > 0; + return Array.isArray(this.props.variant.media) && this.props.variant.media.length > 0; } get primaryMediaItem() { if (this.hasMedia) { - return this.props.media[0]; + return this.props.variant.media[0].images.small; } return null; @@ -65,7 +74,7 @@ class ChildVariant extends Component { const media = this.primaryMediaItem; return ( - + ); } @@ -103,15 +112,4 @@ class ChildVariant extends Component { } } -ChildVariant.propTypes = { - editButton: PropTypes.node, - isSelected: PropTypes.bool, - media: PropTypes.arrayOf(PropTypes.object), - onClick: PropTypes.func, - soldOut: PropTypes.bool, - variant: PropTypes.object, - visibilityButton: PropTypes.node -}; - - export default ChildVariant; diff --git a/imports/plugins/included/product-detail-simple/client/components/variantList.js b/imports/plugins/included/product-detail-simple/client/components/variantList.js index b27e11b14b3..979254bb3ef 100644 --- a/imports/plugins/included/product-detail-simple/client/components/variantList.js +++ b/imports/plugins/included/product-detail-simple/client/components/variantList.js @@ -5,6 +5,19 @@ import { Divider, IconButton } from "/imports/plugins/core/ui/client/components" import { ChildVariant } from "./"; class VariantList extends Component { + static propTypes = { + childVariants: PropTypes.arrayOf(PropTypes.object), + displayPrice: PropTypes.func, + editable: PropTypes.bool, + isSoldOut: PropTypes.func, + onCreateVariant: PropTypes.func, + onEditVariant: PropTypes.func, + onMoveVariant: PropTypes.func, + onVariantClick: PropTypes.func, + onVariantVisibiltyToggle: PropTypes.func, + variantIsSelected: PropTypes.func, + variants: PropTypes.arrayOf(PropTypes.object) + } handleVariantEditClick = (event, editButtonProps) => { if (this.props.onEditVariant) { @@ -126,13 +139,6 @@ class VariantList extends Component { if (this.props.childVariants) { childVariants = this.props.childVariants.map((childVariant, index) => { - const media = this.props.childVariantMedia.filter((mediaItem) => { - if (mediaItem.metadata.variantId === childVariant._id) { - return true; - } - return false; - }); - return ( @@ -183,19 +188,4 @@ class VariantList extends Component { } } -VariantList.propTypes = { - childVariantMedia: PropTypes.arrayOf(PropTypes.any), - childVariants: PropTypes.arrayOf(PropTypes.object), - displayPrice: PropTypes.func, - editable: PropTypes.bool, - isSoldOut: PropTypes.func, - onCreateVariant: PropTypes.func, - onEditVariant: PropTypes.func, - onMoveVariant: PropTypes.func, - onVariantClick: PropTypes.func, - onVariantVisibiltyToggle: PropTypes.func, - variantIsSelected: PropTypes.func, - variants: PropTypes.arrayOf(PropTypes.object) -}; - export default VariantList; diff --git a/imports/plugins/included/product-detail-simple/client/containers/productDetailContainer.js b/imports/plugins/included/product-detail-simple/client/containers/productDetailContainer.js index d13c1de090a..eb5ab9255a7 100644 --- a/imports/plugins/included/product-detail-simple/client/containers/productDetailContainer.js +++ b/imports/plugins/included/product-detail-simple/client/containers/productDetailContainer.js @@ -11,6 +11,7 @@ import { ProductDetail, ProductNotFound } from "../components"; import { SocialContainer, VariantListContainer } from "./"; import { MediaGalleryContainer } from "/imports/plugins/core/ui/client/containers"; import { DragDropProvider, TranslationProvider } from "/imports/plugins/core/ui/client/providers"; +import { Subscriptions } from "/client/modules/core/subscriptions" class ProductDetailContainer extends Component { constructor(props) { @@ -267,31 +268,35 @@ function composer(props, onData) { const selectedVariant = ReactionProduct.selectedVariant(); if (selectedVariant) { - // Find the media for the selected variant - mediaArray = Media.find({ - "metadata.variantId": selectedVariant._id - }, { - sort: { - "metadata.priority": 1 - } - }).fetch(); - - // If no media found, broaden the search to include other media from parents - if (Array.isArray(mediaArray) && mediaArray.length === 0 && selectedVariant.ancestors) { - // Loop through ancestors in reverse to find a variant that has media to use - for (const ancestor of selectedVariant.ancestors.reverse()) { - const media = Media.find({ - "metadata.variantId": ancestor - }, { - sort: { - "metadata.priority": 1 + if (selectedVariant.media) { + mediaArray = selectedVariant.media; + } else { + // Find the media for the selected variant + mediaArray = Media.find({ + "metadata.variantId": selectedVariant._id + }, { + sort: { + "metadata.priority": 1 + } + }).fetch(); + + // If no media found, broaden the search to include other media from parents + if (Array.isArray(mediaArray) && mediaArray.length === 0 && selectedVariant.ancestors) { + // Loop through ancestors in reverse to find a variant that has media to use + for (const ancestor of selectedVariant.ancestors.reverse()) { + const media = Media.find({ + "metadata.variantId": ancestor + }, { + sort: { + "metadata.priority": 1 + } + }).fetch(); + + // If we found some media, then stop here + if (Array.isArray(media) && media.length) { + mediaArray = media; + break; } - }).fetch(); - - // If we found some media, then stop here - if (Array.isArray(media) && media.length) { - mediaArray = media; - break; } } } diff --git a/imports/plugins/included/product-detail-simple/client/containers/variantListContainer.js b/imports/plugins/included/product-detail-simple/client/containers/variantListContainer.js index 99258edf01a..0e0ba5c9cff 100644 --- a/imports/plugins/included/product-detail-simple/client/containers/variantListContainer.js +++ b/imports/plugins/included/product-detail-simple/client/containers/variantListContainer.js @@ -187,21 +187,7 @@ class VariantListContainer extends Component { } function composer(props, onData) { - let childVariantMedia = []; const childVariants = getChildVariants(); - - if (Array.isArray(childVariants)) { - childVariantMedia = Media.find({ - "metadata.variantId": { - $in: getVariantIds(childVariants) - } - }, { - sort: { - "metadata.priority": 1 - } - }).fetch(); - } - let editable; if (Reaction.isPreview() === true) { @@ -215,7 +201,6 @@ function composer(props, onData) { variantIsSelected, variantIsInActionView, childVariants, - childVariantMedia, displayPrice: ReactionProduct.getVariantPriceRange, isSoldOut: isSoldOut, editable diff --git a/imports/plugins/included/product-variant/client/templates/products/productGrid/item.html b/imports/plugins/included/product-variant/client/templates/products/productGrid/item.html index 6a814302464..627ce99e572 100644 --- a/imports/plugins/included/product-variant/client/templates/products/productGrid/item.html +++ b/imports/plugins/included/product-variant/client/templates/products/productGrid/item.html @@ -13,7 +13,8 @@
{{#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(); } });