From df55dba571813fe27263f0037002a6b6cd67b355 Mon Sep 17 00:00:00 2001 From: Dawid Date: Mon, 22 Aug 2022 14:53:17 +0100 Subject: [PATCH] Update product and page default availability settings (#2165) * Update product and page default settings * Update product and page test snapshots * Update product create errors handling * Fix preselected publication info of product with variant without any channel listing * Update package lock file * Fix page publication state and its date on page view * Update page tests --- locale/defaultMessages.json | 37 ++-- package-lock.json | 12 +- src/channels/utils.ts | 20 +- .../ChannelAvailabilityItemContent.tsx | 53 ++---- .../Channel/messages.ts | 35 ++++ .../VisibilityCard/VisibilityCard.tsx | 68 +++---- src/components/VisibilityCard/messages.ts | 40 ++++ .../PageDetailsPage/PageDetailsPage.tsx | 1 - src/pages/components/PageDetailsPage/form.tsx | 11 +- src/products/views/ProductCreate/handlers.ts | 30 +-- .../__snapshots__/Stories.test.ts.snap | 173 +++++++++--------- src/utils/data/getPublicationData.ts | 2 +- 12 files changed, 268 insertions(+), 214 deletions(-) create mode 100644 src/components/ChannelsAvailabilityCard/Channel/messages.ts create mode 100644 src/components/VisibilityCard/messages.ts diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 4981c4c08..43b01f924 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -303,9 +303,6 @@ "context": "menu item name", "string": "Name" }, - "0cVk9I": { - "string": "Show in product listings" - }, "0dPP8O": { "string": "Order #{orderId} was placed" }, @@ -699,6 +696,10 @@ "context": "gift card export type label", "string": "gift cards" }, + "3oQzWR": { + "context": "product availability", + "string": "Set publication date" + }, "3psvRS": { "context": "attribute values list: slug column header", "string": "Admin" @@ -787,6 +788,10 @@ "context": "tracking number", "string": "Tracking Number: {trackingNumber}" }, + "4VOIum": { + "context": "product availability", + "string": "Enabling this checkbox will remove product from search and category pages. It will be available on collection pages." + }, "4W/CKn": { "context": "modal button", "string": "Upload URL" @@ -976,9 +981,6 @@ "context": "voucher is active from date", "string": "Starts" }, - "5ukAFZ": { - "string": "Disabling this checkbox will remove product from search and category pages. It will be available on collection pages." - }, "5x6yT9": { "context": "weight", "string": "{fromValue} {fromUnit} - {toValue} {toUnit}" @@ -2750,6 +2752,10 @@ "context": "input description", "string": "{unitsLeft} units left" }, + "JmdBa3": { + "context": "product availability publish date", + "string": "Publish on" + }, "JnzDrI": { "context": "discount type", "string": "Fixed Amount" @@ -2769,10 +2775,6 @@ "context": "product type is digital or physical", "string": "Type" }, - "Jt3DwJ": { - "context": "publish on date", - "string": "Publish on" - }, "JtZ71e": { "context": "filtering option", "string": "All Warehouses" @@ -3920,6 +3922,10 @@ "TGX4T1": { "string": "Search Engine Preview" }, + "THpf1b": { + "context": "product availability available date", + "string": "Set available on" + }, "TKmub+": { "string": "This field is required" }, @@ -4001,9 +4007,6 @@ "U2mOqA": { "string": "No vouchers found" }, - "U3BQKA": { - "string": "Set publication date" - }, "U5aVd8": { "context": "product", "string": "Shippable" @@ -4609,10 +4612,6 @@ "context": "app extensions subsection", "string": "Apps" }, - "Y7Vy19": { - "context": "available on date", - "string": "Set available on" - }, "Y9lv8z": { "context": "product unavailability", "string": "Unavailable for purchase" @@ -5848,6 +5847,10 @@ "jWna9Q": { "string": "Content Type Name" }, + "jXf/9p": { + "context": "product availability", + "string": "Hide in product listings" + }, "jZbT0O": { "string": "Add search engine title and description to make this page easier to find" }, diff --git a/package-lock.json b/package-lock.json index 3a050a426..4ca705351 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10091,7 +10091,7 @@ "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", "dev": true }, "buffer-equal-constant-time": { @@ -14970,7 +14970,7 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } } @@ -15453,7 +15453,7 @@ "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, "requires": { "pend": "~1.2.0" @@ -23602,7 +23602,7 @@ "ospath": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", - "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=", "dev": true }, "p-cancelable": { @@ -24155,7 +24155,7 @@ "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", "dev": true }, "performance-now": { @@ -32784,4 +32784,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/channels/utils.ts b/src/channels/utils.ts index 12e6e365a..67260f83f 100644 --- a/src/channels/utils.ts +++ b/src/channels/utils.ts @@ -181,13 +181,13 @@ export const createChannelsData = (data?: ChannelFragment[]): ChannelData[] => costPrice: "", currency: channel.currencyCode, id: channel.id, - isAvailableForPurchase: false, + isAvailableForPurchase: true, variantsIds: [], - isPublished: false, + isPublished: true, name: channel.name, price: "", publicationDate: null, - visibleInListings: false, + visibleInListings: true, })) || []; export const createChannelsDataWithPrice = ( @@ -196,8 +196,8 @@ export const createChannelsDataWithPrice = ( ): ChannelData[] => { if (data && productData?.channelListings) { const dataArr = createChannelsData(data); - const productDataArr = createChannelsDataFromProduct(productData); + return uniqBy([...productDataArr, ...dataArr], obj => obj.id); } return []; @@ -294,6 +294,13 @@ export const createChannelsDataFromProduct = (productData?: ProductFragment) => const variantChannel = productData.variants[0]?.channelListings.find( listing => listing.channel.id === channel.id, ); + // Comparing explicitly to false because `hasVariants` can be undefined + const isSimpleProduct = productData.productType?.hasVariants === false; + const haveVariantsChannelListings = productData.variants.some(variant => + variant.channelListings.some( + listing => listing.channel.id === channel.id, + ), + ); const price = variantChannel?.price; const costPrice = variantChannel?.costPrice; const variantsIds = extractVariantsIdsForChannel( @@ -302,10 +309,13 @@ export const createChannelsDataFromProduct = (productData?: ProductFragment) => ); const soldUnits = variantChannel?.preorderThreshold?.soldUnits; const preorderThreshold = variantChannel?.preorderThreshold?.quantity; + // Published defaults to true if none of variants have set channel listing yet + const isProductPublished = + !isSimpleProduct && !haveVariantsChannelListings ? true : isPublished; return { availableForPurchase, - isPublished, + isPublished: isProductPublished, publicationDate, variantsIds, costPrice: costPrice?.amount.toString() ?? "", diff --git a/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.tsx b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.tsx index 02c2b20eb..cdce35a23 100644 --- a/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.tsx +++ b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.tsx @@ -12,6 +12,8 @@ import { useIntl } from "react-intl"; import { useStyles } from "../styles"; import { ChannelOpts, ChannelsAvailabilityError, Messages } from "../types"; +import { availabilityItemMessages } from "./messages"; + export interface ChannelContentProps { disabled?: boolean; data: ChannelData; @@ -19,6 +21,7 @@ export interface ChannelContentProps { messages: Messages; onChange: (id: string, data: ChannelOpts) => void; } + const ChannelContent: React.FC = ({ data, disabled, @@ -58,20 +61,14 @@ const ChannelContent: React.FC = ({ const todayDateUTC = parsedDate.toISOString().slice(0, 10); const visibleMessage = (date: string) => - intl.formatMessage( - { - id: "UjsI4o", - defaultMessage: "since {date}", - description: "date", - }, - { - date: localizeDate(date), - }, - ); + intl.formatMessage(availabilityItemMessages.sinceDate, { + date: localizeDate(date), + }); const formErrors = getFormErrors( ["availableForPurchaseDate", "publicationDate"], errors, ); + return (
= ({ className={classes.setPublicationDate} onClick={() => setPublicationDate(!isPublicationDate)} > - {intl.formatMessage({ - id: "U3BQKA", - defaultMessage: "Set publication date", - })} + {intl.formatMessage(availabilityItemMessages.setPublicationDate)} {isPublicationDate && ( = ({ = ({

- {intl.formatMessage({ - id: "0cVk9I", - defaultMessage: "Show in product listings", - })} + {intl.formatMessage(availabilityItemMessages.hideInListings)}

- {intl.formatMessage({ - id: "5ukAFZ", - defaultMessage: - "Disabling this checkbox will remove product from search and category pages. It will be available on collection pages.", - })} + {intl.formatMessage( + availabilityItemMessages.hideInListingsDescription, + )} } onChange={e => onChange(id, { ...formData, - visibleInListings: e.target.value, + visibleInListings: !e.target.value, }) } /> diff --git a/src/components/ChannelsAvailabilityCard/Channel/messages.ts b/src/components/ChannelsAvailabilityCard/Channel/messages.ts new file mode 100644 index 000000000..0798f1605 --- /dev/null +++ b/src/components/ChannelsAvailabilityCard/Channel/messages.ts @@ -0,0 +1,35 @@ +import { defineMessages } from "react-intl"; + +export const availabilityItemMessages = defineMessages({ + sinceDate: { + id: "UjsI4o", + defaultMessage: "since {date}", + description: "date", + }, + setPublicationDate: { + id: "3oQzWR", + defaultMessage: "Set publication date", + description: "product availability", + }, + publishOn: { + id: "JmdBa3", + defaultMessage: "Publish on", + description: "product availability publish date", + }, + setAvailableOn: { + id: "THpf1b", + defaultMessage: "Set available on", + description: "product availability available date", + }, + hideInListings: { + id: "jXf/9p", + defaultMessage: "Hide in product listings", + description: "product availability", + }, + hideInListingsDescription: { + id: "4VOIum", + defaultMessage: + "Enabling this checkbox will remove product from search and category pages. It will be available on collection pages.", + description: "product availability", + }, +}); diff --git a/src/components/VisibilityCard/VisibilityCard.tsx b/src/components/VisibilityCard/VisibilityCard.tsx index 88121256a..1ee6bb653 100644 --- a/src/components/VisibilityCard/VisibilityCard.tsx +++ b/src/components/VisibilityCard/VisibilityCard.tsx @@ -15,6 +15,7 @@ import { useIntl } from "react-intl"; import FormSpacer from "../FormSpacer"; import DateVisibilitySelector from "./DateVisibilitySelector"; +import { visibilityCardMessages } from "./messages"; const useStyles = makeStyles( theme => ({ @@ -108,16 +109,9 @@ export const VisibilityCard: React.FC = props => { isAvailable !== undefined && availableForPurchase !== undefined; const visibleMessage = (date: string) => - intl.formatMessage( - { - id: "UjsI4o", - defaultMessage: "since {date}", - description: "date", - }, - { - date: localizeDate(date), - }, - ); + intl.formatMessage(visibilityCardMessages.sinceDate, { + date: localizeDate(date), + }); const handleRadioFieldChange = (type: keyof DateFields) => ( e: ChangeEvent, @@ -136,13 +130,7 @@ export const VisibilityCard: React.FC = props => { return ( - + = props => { /> {!isPublished && ( onChange({ target: { name: "publicationDate", value: null } }) } @@ -189,11 +176,7 @@ export const VisibilityCard: React.FC = props => { = props => { = props => {

- {intl.formatMessage({ - id: "0cVk9I", - defaultMessage: "Show in product listings", - })} + {intl.formatMessage(visibilityCardMessages.hideInListings)}

- {intl.formatMessage({ - id: "5ukAFZ", - defaultMessage: - "Disabling this checkbox will remove product from search and category pages. It will be available on collection pages.", - })} + {intl.formatMessage( + visibilityCardMessages.hideInListingsDescription, + )} } - onChange={onChange} + onChange={event => + onChange({ + ...event, + target: { + ...event.target, + value: !event.target.value, + }, + }) + } /> )} diff --git a/src/components/VisibilityCard/messages.ts b/src/components/VisibilityCard/messages.ts new file mode 100644 index 000000000..3215a2d77 --- /dev/null +++ b/src/components/VisibilityCard/messages.ts @@ -0,0 +1,40 @@ +import { defineMessages } from "react-intl"; + +export const visibilityCardMessages = defineMessages({ + title: { + id: "akXDST", + defaultMessage: "Visibility", + description: "section header", + }, + sinceDate: { + id: "UjsI4o", + defaultMessage: "since {date}", + description: "date", + }, + setPublicationDate: { + id: "3oQzWR", + defaultMessage: "Set publication date", + description: "product availability", + }, + publishOn: { + id: "JmdBa3", + defaultMessage: "Publish on", + description: "product availability publish date", + }, + setAvailableOn: { + id: "THpf1b", + defaultMessage: "Set available on", + description: "product availability available date", + }, + hideInListings: { + id: "jXf/9p", + defaultMessage: "Hide in product listings", + description: "product availability", + }, + hideInListingsDescription: { + id: "4VOIum", + defaultMessage: + "Enabling this checkbox will remove product from search and category pages. It will be available on collection pages.", + description: "product availability", + }, +}); diff --git a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx index db4f27e75..99849380c 100644 --- a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx +++ b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx @@ -212,7 +212,6 @@ const PageDetailsPage: React.FC = ({
- ({ - isPublished: page?.isPublished, +const getInitialFormData = ( + pageExists: boolean, + page?: PageDetailsFragment, +): PageFormData => ({ + isPublished: pageExists ? page?.isPublished : true, metadata: page?.metadata?.map(mapMetadataItemToInput) || [], pageType: null, privateMetadata: page?.privateMetadata?.map(mapMetadataItemToInput) || [], - publicationDate: page?.publicationDate || "", + publicationDate: pageExists ? page?.publicationDate : "", seoDescription: page?.seoDescription || "", seoTitle: page?.seoTitle || "", slug: page?.slug || "", @@ -133,7 +136,7 @@ function usePageForm( const pageExists = page !== null; const { handleChange, triggerChange, data: formData, formId } = useForm( - getInitialFormData(page), + getInitialFormData(pageExists, page), undefined, { confirmLeave: true, diff --git a/src/products/views/ProductCreate/handlers.ts b/src/products/views/ProductCreate/handlers.ts index d18275e7b..72ce9e655 100644 --- a/src/products/views/ProductCreate/handlers.ts +++ b/src/products/views/ProductCreate/handlers.ts @@ -12,12 +12,14 @@ import { AttributeErrorFragment, FileUploadMutation, FileUploadMutationVariables, + ProductChannelListingErrorFragment, ProductChannelListingUpdateMutation, ProductChannelListingUpdateMutationVariables, ProductCreateMutation, ProductCreateMutationVariables, ProductDeleteMutation, ProductDeleteMutationVariables, + ProductErrorFragment, ProductTypeQuery, ProductVariantChannelListingUpdateMutation, ProductVariantChannelListingUpdateMutationVariables, @@ -85,7 +87,12 @@ export function createHandler( }) => Promise>, ) { return async (formData: ProductCreateData) => { - let errors: Array = []; + let errors: Array< + | AttributeErrorFragment + | UploadErrorFragment + | ProductErrorFragment + | ProductChannelListingErrorFragment + > = []; const uploadFilesResult = await handleUploadMultipleFiles( formData.attributesWithNewFileValue, @@ -123,8 +130,9 @@ export function createHandler( }; const result = await productCreate(productVariables); + const productErrors = result.data.productCreate.errors || []; - let hasErrors = errors.length > 0; + errors = [...errors, ...productErrors]; const hasVariants = productType?.hasVariants; const productId = result?.data?.productCreate?.product?.id; @@ -140,12 +148,10 @@ export function createHandler( ), productVariantCreate(getSimpleProductVariables(formData, productId)), ]); - const channelErrors = result[0].data?.productChannelListingUpdate?.errors; - const variantErrors = result[1].data?.productVariantCreate?.errors; - - if ([...(channelErrors || []), ...(variantErrors || [])].length > 0) { - hasErrors = true; - } + const channelErrors = + result[0].data?.productChannelListingUpdate?.errors || []; + const variantErrors = result[1].data?.productVariantCreate?.errors || []; + errors = [...errors, ...channelErrors, ...variantErrors]; const variantId = result[1].data.productVariantCreate.productVariant?.id; if (variantErrors.length === 0 && variantId) { @@ -164,17 +170,17 @@ export function createHandler( const result = await updateChannels( getChannelsVariables(productId, formData.channelListings), ); + const channelErrors = + result.data?.productChannelListingUpdate?.errors || []; - if (result.data?.productChannelListingUpdate?.errors.length > 0) { - hasErrors = true; - } + errors = [...errors, ...channelErrors]; } /* INFO: This is a stop-gap solution, where we delete products that didn't meet all required data in the create form A more robust solution would require merging create and update form into one to persist form state across redirects */ - if (productId && hasErrors) { + if (productId && errors.length > 0) { await productDelete({ variables: { id: productId } }); return { errors }; diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 3d9639a14..949ee38a1 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -3013,12 +3013,13 @@ exports[`Storyshots Channels / Channels with variants availability card default > - + + + + @@ -3051,12 +3062,12 @@ exports[`Storyshots Channels / Channels with variants availability card default

- Show in product listings + Hide in product listings

- Disabling this checkbox will remove product from search and category pages. It will be available on collection pages. + Enabling this checkbox will remove product from search and category pages. It will be available on collection pages. @@ -3361,12 +3372,13 @@ exports[`Storyshots Channels / Channels with variants availability card default > - + + + + @@ -3399,12 +3421,12 @@ exports[`Storyshots Channels / Channels with variants availability card default

- Show in product listings + Hide in product listings

- Disabling this checkbox will remove product from search and category pages. It will be available on collection pages. + Enabling this checkbox will remove product from search and category pages. It will be available on collection pages. @@ -5141,13 +5163,12 @@ exports[`Storyshots Generics / Channels availability card with onChange 1`] = ` > - - - - + @@ -5190,12 +5201,12 @@ exports[`Storyshots Generics / Channels availability card with onChange 1`] = `

- Show in product listings + Hide in product listings

- Disabling this checkbox will remove product from search and category pages. It will be available on collection pages. + Enabling this checkbox will remove product from search and category pages. It will be available on collection pages. @@ -5505,13 +5516,12 @@ exports[`Storyshots Generics / Channels availability card with onChange 1`] = ` > - - - - + @@ -5554,12 +5554,12 @@ exports[`Storyshots Generics / Channels availability card with onChange 1`] = `

- Show in product listings + Hide in product listings

- Disabling this checkbox will remove product from search and category pages. It will be available on collection pages. + Enabling this checkbox will remove product from search and category pages. It will be available on collection pages. @@ -158197,9 +158197,6 @@ exports[`Storyshots Views / Pages / Page details default 1`] = `
-
@@ -159109,9 +159106,6 @@ exports[`Storyshots Views / Pages / Page details form errors 1`] = `
-
@@ -159618,9 +159612,6 @@ exports[`Storyshots Views / Pages / Page details loading 1`] = `
-
diff --git a/src/utils/data/getPublicationData.ts b/src/utils/data/getPublicationData.ts index 74adc06ea..1bfac52ce 100644 --- a/src/utils/data/getPublicationData.ts +++ b/src/utils/data/getPublicationData.ts @@ -8,7 +8,7 @@ function getPublicationData({ isPublished, }: PublicationData): PublicationData { return { - isPublished: !!publicationDate || isPublished, + isPublished, publicationDate: publicationDate || null, }; }