From cd59918b80bef32a98fde7c3c20a83ee11d115e8 Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Fri, 27 Mar 2020 14:46:12 -0700 Subject: [PATCH 1/8] fix: add missing media field to product query Signed-off-by: Will Lopez --- .../client/graphql/fragments/productVariant.js | 7 +++++++ .../client/graphql/fragments/productWithVariants.js | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/imports/plugins/included/product-admin/client/graphql/fragments/productVariant.js b/imports/plugins/included/product-admin/client/graphql/fragments/productVariant.js index 94209d040e..9d6f946dc1 100644 --- a/imports/plugins/included/product-admin/client/graphql/fragments/productVariant.js +++ b/imports/plugins/included/product-admin/client/graphql/fragments/productVariant.js @@ -16,6 +16,13 @@ export default gql` } minOrderQuantity optionTitle + media { + URLs { + original + small + } + priority + } originCountry pricing { compareAtPrice { diff --git a/imports/plugins/included/product-admin/client/graphql/fragments/productWithVariants.js b/imports/plugins/included/product-admin/client/graphql/fragments/productWithVariants.js index f1cdb2c846..2ab23d7806 100644 --- a/imports/plugins/included/product-admin/client/graphql/fragments/productWithVariants.js +++ b/imports/plugins/included/product-admin/client/graphql/fragments/productWithVariants.js @@ -13,6 +13,12 @@ export default gql` key value } + media { + URLs { + small + } + priority + } originCountry pageTitle productType From 27e8e287ff36ccecc16c06191e70992775b37ffb Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Fri, 27 Mar 2020 15:47:03 -0700 Subject: [PATCH 2/8] fix: refetch product media after media is added to products Signed-off-by: Will Lopez --- .../core/ui/client/components/media/mediaUploader.js | 12 ++++++++++-- .../product-admin/client/blocks/ProductMediaForm.js | 3 ++- .../client/graphql/fragments/productVariant.js | 1 + .../client/graphql/fragments/productWithVariants.js | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/imports/plugins/core/ui/client/components/media/mediaUploader.js b/imports/plugins/core/ui/client/components/media/mediaUploader.js index 14aabee2d6..64e1881cfc 100644 --- a/imports/plugins/core/ui/client/components/media/mediaUploader.js +++ b/imports/plugins/core/ui/client/components/media/mediaUploader.js @@ -25,7 +25,7 @@ const createMediaRecordMutation = gql` * @returns {Node} React component */ function MediaUploader(props) { - const { canUploadMultiple, metadata, onError, onFiles, shopId } = props; + const { canUploadMultiple, metadata, onError, onFiles, refetchProduct, shopId } = props; const [isUploading, setIsUploading] = useState(false); const [createMediaRecord] = useMutation(createMediaRecordMutation, { ignoreResults: true }); @@ -61,7 +61,14 @@ function MediaUploader(props) { Promise.all(promises) .then(() => { - setIsUploading(false); + // This is a temporary workaround due to the fact that on the server, + // the sharp library generates product images in an async manner. + // A better solution would be to generate product images synchronously + // or a more robust client side polling solution. + window.setTimeout(async () => { + await refetchProduct(); + setIsUploading(false); + }, 2000); return null; }) .catch((error) => { @@ -108,6 +115,7 @@ MediaUploader.propTypes = { metadata: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), onError: PropTypes.func, onFiles: PropTypes.func, + refetchProduct: PropTypes.func, shopId: PropTypes.string }; diff --git a/imports/plugins/included/product-admin/client/blocks/ProductMediaForm.js b/imports/plugins/included/product-admin/client/blocks/ProductMediaForm.js index fbb16e0a99..66d681fac0 100644 --- a/imports/plugins/included/product-admin/client/blocks/ProductMediaForm.js +++ b/imports/plugins/included/product-admin/client/blocks/ProductMediaForm.js @@ -12,7 +12,7 @@ import useProduct from "../hooks/useProduct"; * @returns {React.Component} React component */ function ProductMediaForm() { - const { product, shopId } = useProduct(); + const { product, refetchProduct, shopId } = useProduct(); if (!product) { return null; @@ -23,6 +23,7 @@ function ProductMediaForm() { Date: Mon, 30 Mar 2020 14:14:56 -0700 Subject: [PATCH 3/8] refactor: add better polling of uploaded product media Signed-off-by: Will Lopez --- .../client/components/media/mediaUploader.js | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/imports/plugins/core/ui/client/components/media/mediaUploader.js b/imports/plugins/core/ui/client/components/media/mediaUploader.js index 64e1881cfc..06f67e824b 100644 --- a/imports/plugins/core/ui/client/components/media/mediaUploader.js +++ b/imports/plugins/core/ui/client/components/media/mediaUploader.js @@ -2,6 +2,7 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; import gql from "graphql-tag"; import { useDropzone } from "react-dropzone"; +import decodeOpaqueId from "@reactioncommerce/api-utils/decodeOpaqueId.js"; import { useMutation } from "@apollo/react-hooks"; import Button from "@reactioncommerce/catalyst/Button"; import LinearProgress from "@material-ui/core/LinearProgress"; @@ -32,7 +33,6 @@ function MediaUploader(props) { const uploadFiles = (acceptedFiles) => { const filesArray = Array.from(acceptedFiles); - setIsUploading(true); const promises = filesArray.map(async (browserFile) => { @@ -59,16 +59,43 @@ function MediaUploader(props) { }); }); + Promise.all(promises) - .then(() => { - // This is a temporary workaround due to the fact that on the server, + .then((responses) => { + // NOTE: This is a temporary workaround due to the fact that on the server, // the sharp library generates product images in an async manner. - // A better solution would be to generate product images synchronously - // or a more robust client side polling solution. - window.setTimeout(async () => { - await refetchProduct(); + // A better solution would be to use subscriptions + const uploadedMediaIds = responses.map((response) => response.data.createMediaRecord.mediaRecord._id); + + // Poll server every half a second to determine if all media has been successfully processed + let isAllMediaProcessed = false; + const timerId = setInterval(async () => { + const { data: { product } } = await refetchProduct(); + const productMedia = []; + product.media.forEach((media) => { + const { id } = decodeOpaqueId(media._id); + productMedia.push({ id, thumbnailUrl: media.URLs.small }); + }); + + isAllMediaProcessed = uploadedMediaIds.every((uploadedMediaId) => { + const mediaItem = productMedia.find((item) => item.id === uploadedMediaId); + + // If a url has been generated, then these media items has been processed successfully. + return mediaItem && mediaItem.thumbnailUrl !== String(null); + }); + + if (isAllMediaProcessed) { + setIsUploading(false); + clearTimeout(timerId); + } + }, 500); + + // Stop polling after 20 seconds + setTimeout(() => { + clearTimeout(timerId); setIsUploading(false); - }, 2000); + }, 20000); + return null; }) .catch((error) => { From b87d1655612fd2ec8c4c17fac4ba2b8f0bc50f99 Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Mon, 30 Mar 2020 14:16:15 -0700 Subject: [PATCH 4/8] refactor: improve removing product media flow Signed-off-by: Will Lopez --- .../client/components/ProductMediaGallery.js | 73 +++++++++++-------- .../client/components/ProductMediaItem.js | 3 + 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/imports/plugins/included/product-admin/client/components/ProductMediaGallery.js b/imports/plugins/included/product-admin/client/components/ProductMediaGallery.js index ebebe4db29..3b474b3b65 100644 --- a/imports/plugins/included/product-admin/client/components/ProductMediaGallery.js +++ b/imports/plugins/included/product-admin/client/components/ProductMediaGallery.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import gql from "graphql-tag"; import PropTypes from "prop-types"; import Logger from "/client/modules/logger"; @@ -10,6 +10,8 @@ import TableHead from "@material-ui/core/TableHead"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import TableRow from "@material-ui/core/TableRow"; +import { useConfirmDialog } from "@reactioncommerce/catalyst"; +import { useSnackbar } from "notistack"; import ProductMediaItem from "./ProductMediaItem"; const archiveMediaRecordMutation = gql` @@ -42,39 +44,48 @@ function ProductMediaGallery(props) { editable, media, productId, + refetchProduct, shopId, variantId } = props; + const { enqueueSnackbar } = useSnackbar(); + const [mediaItemToRemove, setMediaItemToRemove] = useState(null); const [archiveMediaRecord] = useMutation(archiveMediaRecordMutation, { ignoreResults: true }); const [updateMediaRecordPriority] = useMutation(updateMediaRecordPriorityMutation, { ignoreResults: true }); - const handleRemoveMedia = (mediaToRemove) => { - const imageUrl = mediaToRemove.URLs.medium; - Alerts.alert({ - title: "Remove Media?", - type: "warning", - showCancelButton: true, - imageUrl, - imageHeight: 150 - }, async (isConfirm) => { - if (isConfirm) { - archiveMediaRecord({ - variables: { - input: { - mediaRecordId: mediaToRemove._id, - shopId - } - }, - onError(error) { - Logger.error(error); - Alerts.toast("Unable to remove media", "error", { - autoHide: 10000 - }); - } - }); + const handleRemoveMedia = async () => { + await archiveMediaRecord({ + variables: { + input: { + mediaRecordId: mediaItemToRemove._id, + shopId + } + }, + onError(error) { + Logger.error(error); + enqueueSnackbar("Unable to remove media", { variant: "error" }); } }); + + // Re-fetch product data + refetchProduct(); + }; + + const { + openDialog: openRemoveMediaDialog, + ConfirmDialog: RemoveMediaConfirmDialog + } = useConfirmDialog({ + title: "Remove Media", + message: "Are you sure you want to remove this media item?", + onConfirm: () => { + handleRemoveMedia(); + } + }); + + const confirmRemoveMediaItem = (mediaItem) => { + setMediaItemToRemove(mediaItem); + openRemoveMediaDialog(); }; const handleSetMediaPriority = async (mediaRecord, priority) => { @@ -88,14 +99,11 @@ function ProductMediaGallery(props) { }, onError(error) { Logger.error(error); - Alerts.toast("Unable to update media priority", "error", { - autoHide: 10000 - }); + enqueueSnackbar("Unable to update media priority", { variant: "error" }); } }); }; - let count = (Array.isArray(media) && media.length) || 0; const hasMedia = count > 0; @@ -109,7 +117,7 @@ function ProductMediaGallery(props) { }; const onUploadError = (error) => { - Alerts.toast(error.reason || error.message, "error"); + enqueueSnackbar(error.reason || error.message, { variant: "error" }); }; return ( @@ -128,7 +136,7 @@ function ProductMediaGallery(props) { @@ -149,6 +158,7 @@ function ProductMediaGallery(props) { } + ); } @@ -158,6 +168,7 @@ ProductMediaGallery.propTypes = { media: PropTypes.arrayOf(PropTypes.object), onSetMediaPriority: PropTypes.func, productId: PropTypes.string, + refetchProduct: PropTypes.func, shopId: PropTypes.string, variantId: PropTypes.string }; diff --git a/imports/plugins/included/product-admin/client/components/ProductMediaItem.js b/imports/plugins/included/product-admin/client/components/ProductMediaItem.js index 507439adc0..bfc6bb09c2 100644 --- a/imports/plugins/included/product-admin/client/components/ProductMediaItem.js +++ b/imports/plugins/included/product-admin/client/components/ProductMediaItem.js @@ -44,6 +44,9 @@ function ProductMediaItem(props) { let imageSrc = source.URLs[size]; + // If there is no img src, then render nothing + if (imageSrc === String(null)) return null; + if (imageSrc) { imageSrc = `${filesBaseUrl}${imageSrc}`; } else { From 7298066b1ca005082d3150514233e88d0b1ffb1d Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Mon, 30 Mar 2020 14:39:33 -0700 Subject: [PATCH 5/8] refactor: add product refetch function to all product media uploaders Signed-off-by: Will Lopez --- .../included/product-admin/client/blocks/ProductMediaForm.js | 3 +-- .../product-admin/client/components/ProductMediaGallery.js | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/imports/plugins/included/product-admin/client/blocks/ProductMediaForm.js b/imports/plugins/included/product-admin/client/blocks/ProductMediaForm.js index 66d681fac0..fbb16e0a99 100644 --- a/imports/plugins/included/product-admin/client/blocks/ProductMediaForm.js +++ b/imports/plugins/included/product-admin/client/blocks/ProductMediaForm.js @@ -12,7 +12,7 @@ import useProduct from "../hooks/useProduct"; * @returns {React.Component} React component */ function ProductMediaForm() { - const { product, refetchProduct, shopId } = useProduct(); + const { product, shopId } = useProduct(); if (!product) { return null; @@ -23,7 +23,6 @@ function ProductMediaForm() { Date: Tue, 31 Mar 2020 15:33:05 -0700 Subject: [PATCH 6/8] refactor: correctly set priority on media items Signed-off-by: Will Lopez --- .../client/components/ProductMediaItem.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/imports/plugins/included/product-admin/client/components/ProductMediaItem.js b/imports/plugins/included/product-admin/client/components/ProductMediaItem.js index bfc6bb09c2..4d10986caa 100644 --- a/imports/plugins/included/product-admin/client/components/ProductMediaItem.js +++ b/imports/plugins/included/product-admin/client/components/ProductMediaItem.js @@ -21,7 +21,7 @@ const useStyles = makeStyles(() => ({ height: 100 }, priorityField: { - width: 120 + width: 70 } })); @@ -38,10 +38,10 @@ function ProductMediaItem(props) { size, source } = props; + const classes = useStyles(); const [priority, setPriority] = useState(source.priority); - let imageSrc = source.URLs[size]; // If there is no img src, then render nothing @@ -72,9 +72,8 @@ function ProductMediaItem(props) { onChange={(event) => { setPriority(() => { const intValue = parseInt(event.target.value, 10); - return { - priority: isInteger(intValue) ? intValue : null - }; + const newPriority = isInteger(intValue) ? intValue : null; + return newPriority; }); }} /> @@ -86,7 +85,6 @@ function ProductMediaItem(props) { src={imageSrc} /> - { From 8f9bd55ec45b68bfc10a0848f2bfe1f112ed4ba7 Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Tue, 31 Mar 2020 15:33:35 -0700 Subject: [PATCH 7/8] refactor: when deleting a media item, return an optimistic response Signed-off-by: Will Lopez --- .../client/components/ProductMediaGallery.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/imports/plugins/included/product-admin/client/components/ProductMediaGallery.js b/imports/plugins/included/product-admin/client/components/ProductMediaGallery.js index 0f88617faf..899fe202ed 100644 --- a/imports/plugins/included/product-admin/client/components/ProductMediaGallery.js +++ b/imports/plugins/included/product-admin/client/components/ProductMediaGallery.js @@ -63,6 +63,14 @@ function ProductMediaGallery(props) { shopId } }, + optimisticResponse: { + __typename: "Mutation", + archiveMediaRecord: { + id: mediaItemToRemove._id, + __typename: "MediaRecord", + mediaRecord: null + } + }, onError(error) { Logger.error(error); enqueueSnackbar("Unable to remove media", { variant: "error" }); From d93dc302562ceadd685874b72c35938d0bfd463d Mon Sep 17 00:00:00 2001 From: Will Lopez Date: Wed, 1 Apr 2020 12:45:02 -0700 Subject: [PATCH 8/8] fix: uploaded variant and option media not freshing correctly Signed-off-by: Will Lopez --- .../client/components/media/mediaUploader.js | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/imports/plugins/core/ui/client/components/media/mediaUploader.js b/imports/plugins/core/ui/client/components/media/mediaUploader.js index 06f67e824b..ce4a147499 100644 --- a/imports/plugins/core/ui/client/components/media/mediaUploader.js +++ b/imports/plugins/core/ui/client/components/media/mediaUploader.js @@ -8,6 +8,7 @@ import Button from "@reactioncommerce/catalyst/Button"; import LinearProgress from "@material-ui/core/LinearProgress"; import { FileRecord } from "@reactioncommerce/file-collections"; import { registerComponent } from "@reactioncommerce/reaction-components"; +import _ from "lodash"; import { i18next, Logger } from "/client/api"; const createMediaRecordMutation = gql` @@ -67,18 +68,39 @@ function MediaUploader(props) { // A better solution would be to use subscriptions const uploadedMediaIds = responses.map((response) => response.data.createMediaRecord.mediaRecord._id); - // Poll server every half a second to determine if all media has been successfully processed + // Poll server every two seconds to determine if all media has been successfully processed let isAllMediaProcessed = false; const timerId = setInterval(async () => { const { data: { product } } = await refetchProduct(); - const productMedia = []; - product.media.forEach((media) => { + + // Get media for product, variants and options + let allMedia = [product.media]; + if (product.variants) { + product.variants.forEach((variant) => { + // Add variant media if set + if (variant.media) { + allMedia.push(variant.media); + } + + // Add option media if set + if (variant.options) { + variant.options.forEach((option) => { + allMedia.push(option.media); + }); + } + }); + } + + allMedia = _.flatten(allMedia); + + const mediaItems = []; + allMedia.forEach((media) => { const { id } = decodeOpaqueId(media._id); - productMedia.push({ id, thumbnailUrl: media.URLs.small }); + mediaItems.push({ id, thumbnailUrl: media.URLs.small }); }); isAllMediaProcessed = uploadedMediaIds.every((uploadedMediaId) => { - const mediaItem = productMedia.find((item) => item.id === uploadedMediaId); + const mediaItem = mediaItems.find((item) => item.id === uploadedMediaId); // If a url has been generated, then these media items has been processed successfully. return mediaItem && mediaItem.thumbnailUrl !== String(null); @@ -88,13 +110,13 @@ function MediaUploader(props) { setIsUploading(false); clearTimeout(timerId); } - }, 500); + }, 2000); - // Stop polling after 20 seconds + // Stop polling after 30 seconds setTimeout(() => { clearTimeout(timerId); setIsUploading(false); - }, 20000); + }, 30000); return null; })