diff --git a/.changeset/old-feet-remember.md b/.changeset/old-feet-remember.md new file mode 100644 index 00000000000..1ccd1298ad0 --- /dev/null +++ b/.changeset/old-feet-remember.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Setting publication date is now possible when product is set to published. This means Publication and Availability settings now correctly display information when product will become available or be published. diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 7671ec86b1c..49ecc48eee2 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -5059,10 +5059,6 @@ "context": "dialog content", "string": "{counter,plural,one{Are you sure you want to unassign this collection?} other{Are you sure you want to unassign {displayQuantity} collections?}}" }, - "UjsI4o": { - "context": "date", - "string": "since {date}" - }, "Um3g00": { "context": "send to customer selected label", "string": "Send gift card to customer" @@ -6231,6 +6227,10 @@ "context": "subsection header", "string": "Billing Address" }, + "bidZKr": { + "context": "date", + "string": "Since {date}" + }, "bj1U23": { "string": "Are you sure you want to delete {menuName}?" }, diff --git a/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.test.tsx b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.test.tsx new file mode 100644 index 00000000000..49a6af4a4c6 --- /dev/null +++ b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.test.tsx @@ -0,0 +1,125 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; + +import { ChannelAvailabilityItemContent } from "./ChannelAvailabilityItemContent"; + +jest.mock("react-intl", () => ({ + useIntl: jest.fn(() => ({ + formatMessage: jest.fn(x => x.defaultMessage), + })), + defineMessages: jest.fn(x => x), + FormattedMessage: ({ defaultMessage }: { defaultMessage: any }) => <>{defaultMessage}</>, +})); + +jest.mock("@dashboard/hooks/useCurrentDate", () => jest.fn(() => new Date("2024-01-01").getTime())); +jest.mock("@dashboard/hooks/useDateLocalize", () => jest.fn(() => jest.fn(date => date))); + +const mockData = { + id: "123", + name: "Test Channel", + isPublished: true, + publishedAt: "2024-01-01", + visibleInListings: true, + isAvailableForPurchase: true, + availableForPurchaseAt: "2024-01-01", +}; + +const mockMessages = { + visibleLabel: "Visible", + hiddenLabel: "Hidden", + visibleSecondLabel: "Visible since", + hiddenSecondLabel: "Will become visible on", + availableLabel: "Available", + unavailableLabel: "Unavailable", + availableSecondLabel: "Will become available on", + setAvailabilityDateLabel: "Set availability date", +}; + +const defaultProps = { + data: mockData, + errors: [], + messages: mockMessages, + onChange: jest.fn(), +}; + +describe("ChannelAvailabilityItemContent", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("shows publication date field when isPublished is true", () => { + // Arrange & Act + render(<ChannelAvailabilityItemContent {...defaultProps} />); + + // Assert + const publishedRadio = screen.getByLabelText("Visible"); + const publishedDateInput = screen.getByRole("input", { name: "Date" }); + + expect(publishedRadio).toHaveValue("true"); + expect(publishedDateInput).toBeInTheDocument(); + }); + + it("hides publication date field when isPublished is false", () => { + // Arrange & Act + render( + <ChannelAvailabilityItemContent + {...defaultProps} + data={{ ...mockData, isPublished: false }} + />, + ); + + // Assert + const hiddenRadio = screen.getByLabelText("Hidden"); + + expect(hiddenRadio).toBeInTheDocument(); + expect(screen.queryByText("Visible since")).not.toBeInTheDocument(); + }); + + it("shows availability controls when hasAvailableProps is true", () => { + // Arrange & Act + render(<ChannelAvailabilityItemContent {...defaultProps} />); + + // Assert + expect(screen.getByLabelText("Available")).toBeInTheDocument(); + expect(screen.getByLabelText("Unavailable")).toBeInTheDocument(); + }); + + it("hides availability controls when hasAvailableProps is false", () => { + // Arrange & Act + render( + <ChannelAvailabilityItemContent + {...defaultProps} + data={{ + ...mockData, + isAvailableForPurchase: undefined, + availableForPurchaseAt: undefined, + }} + />, + ); + + // Assert + expect(screen.queryByLabelText("Available")).not.toBeInTheDocument(); + expect(screen.queryByLabelText("Unavailable")).not.toBeInTheDocument(); + }); + + it("shows visibility in listings controls when visibleInListings is defined", () => { + // Arrange & Act + render(<ChannelAvailabilityItemContent {...defaultProps} />); + + // Assert + expect(screen.getByTestId("channel:visibleInListings:123")).toBeInTheDocument(); + }); + + it("hides visibility in listings controls when visibleInListings is undefined", () => { + // Arrange & Act + render( + <ChannelAvailabilityItemContent + {...defaultProps} + data={{ ...mockData, visibleInListings: undefined }} + />, + ); + + // Assert + expect(screen.queryByTestId("channel:visibleInListings:123")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.tsx b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.tsx index 1931f97417f..c1ba5bc32bd 100644 --- a/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.tsx +++ b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.tsx @@ -20,6 +20,26 @@ export interface ChannelContentProps { onChange: (id: string, data: ChannelOpts) => void; } +const getFormData = (data: ChannelData): Partial<ChannelOpts> => { + const { + availableForPurchaseAt, + isAvailableForPurchase, + isPublished, + publishedAt, + visibleInListings, + } = data; + + return Object.fromEntries( + Object.entries({ + availableForPurchaseAt, + isAvailableForPurchase, + isPublished, + publishedAt, + visibleInListings, + }).filter(([_, value]) => value !== undefined), + ); +}; + export const ChannelAvailabilityItemContent: React.FC<ChannelContentProps> = ({ data, disabled, @@ -35,24 +55,23 @@ export const ChannelAvailabilityItemContent: React.FC<ChannelContentProps> = ({ visibleInListings, id, } = data; - const formData = { - ...(availableForPurchaseAt !== undefined ? { availableForPurchaseAt } : {}), - ...(isAvailable !== undefined ? { isAvailableForPurchase: isAvailable } : {}), - isPublished, - publishedAt, - ...(visibleInListings !== undefined ? { visibleInListings } : {}), - }; + + const formData = getFormData(data); + const dateNow = useCurrentDate(); const localizeDate = useDateLocalize(); const hasAvailableProps = isAvailable !== undefined && availableForPurchaseAt !== undefined; const [isPublishedAt, setPublishedAt] = useState(!!publishedAt); - const [isAvailableDate, setAvailableDate] = useState(false); + const [isAvailableDate, setAvailableDate] = useState(!!availableForPurchaseAt); const intl = useIntl(); const visibleMessage = (date: string) => intl.formatMessage(availabilityItemMessages.sinceDate, { date: localizeDate(date), }); - const formErrors = getFormErrors(["availableForPurchaseAt", "publishedAt"], errors); + const formErrors = getFormErrors( + ["availableForPurchaseAt", "publishedAt", "availableForPurchaseAt", "publishedAt"], + errors, + ); return ( <Box display="flex" gap={3} paddingTop={3} flexDirection="column"> @@ -62,70 +81,86 @@ export const ChannelAvailabilityItemContent: React.FC<ChannelContentProps> = ({ * Radix issue: https://github.com/radix-ui/primitives/issues/1982 */} <StopPropagation> + {/* Published/not published */} <RadioGroup value={String(isPublished)} onValueChange={value => { onChange(id, { ...formData, isPublished: value === "true", - publishedAt: value === "false" ? null : publishedAt, + publishedAt, }); }} disabled={disabled} display="flex" flexDirection="column" - gap={3} > <RadioGroup.Item id={`${id}-isPublished-true`} value="true" name="isPublished"> - <Box display="flex" alignItems="baseline" gap={2}> - <Text>{messages.visibleLabel}</Text> - {isPublished && publishedAt && Date.parse(publishedAt) < dateNow && ( - <Text size={2} color="default2"> - {messages.visibleSecondLabel || visibleMessage(publishedAt)} - </Text> - )} - </Box> + <Text>{messages.visibleLabel}</Text> </RadioGroup.Item> - <RadioGroup.Item id={`${id}-isPublished-false`} value="false" name="isPublished"> - <Box display="flex" alignItems="baseline" gap={2}> - <Text>{messages.hiddenLabel}</Text> - {publishedAt && !isPublished && Date.parse(publishedAt) >= dateNow && ( - <Text size={2} color="default2"> - {messages.hiddenSecondLabel} - </Text> + {isPublished && publishedAt && Date.parse(publishedAt) < dateNow && ( + <Text size={2} color="default2"> + {messages.visibleSecondLabel || visibleMessage(publishedAt)} + </Text> + )} + + {/* Should be visible all the time, product can be published and this should show since when */} + {isPublished && ( + <Box display="flex" flexDirection="column" alignItems="start" gap={1} marginTop={2}> + <Checkbox + onCheckedChange={(checked: boolean) => { + if (!checked) { + onChange(id, { + ...formData, + publishedAt: null, + }); + } + + setPublishedAt(checked); + }} + checked={isPublishedAt} + > + {intl.formatMessage(availabilityItemMessages.setPublicationDate)} + </Checkbox> + {isPublishedAt && ( + <DateTimeTimezoneField + error={!!formErrors.publishedAt} + helperText={ + formErrors.publishedAt + ? getProductErrorMessage(formErrors.publishedAt, intl) + : "" + } + disabled={disabled} + name={`channel:publicationTime:${id}`} + value={publishedAt || ""} + onChange={dateTime => { + onChange(id, { + ...formData, + publishedAt: dateTime, + }); + }} + fullWidth + /> )} </Box> + )} + + <RadioGroup.Item + id={`${id}-isPublished-false`} + value="false" + name="isPublished" + marginTop={3} + > + <Text>{messages.hiddenLabel}</Text> </RadioGroup.Item> + {publishedAt && !isPublished && Date.parse(publishedAt) >= dateNow && ( + <Text size={2} color="default2"> + {messages.hiddenSecondLabel} + </Text> + )} </RadioGroup> </StopPropagation> - {!isPublished && ( - <Box display="flex" flexDirection="column" alignItems="start" gap={1}> - <Checkbox - onCheckedChange={(checked: boolean) => setPublishedAt(checked)} - checked={isPublishedAt} - > - {intl.formatMessage(availabilityItemMessages.setPublicationDate)} - </Checkbox> - {isPublishedAt && ( - <DateTimeTimezoneField - error={!!formErrors.publishedAt} - helperText={ - formErrors.publishedAt ? getProductErrorMessage(formErrors.publishedAt, intl) : "" - } - disabled={disabled} - name={`channel:publicationTime:${id}`} - value={publishedAt || ""} - onChange={dateTime => - onChange(id, { - ...formData, - publishedAt: dateTime, - }) - } - fullWidth - /> - )} - </Box> - )} + {hasAvailableProps && ( <> <Divider /> @@ -133,44 +168,53 @@ export const ChannelAvailabilityItemContent: React.FC<ChannelContentProps> = ({ disabled={disabled} name={`channel:isAvailableForPurchase:${id}`} value={String(isAvailable)} - onValueChange={value => + onValueChange={value => { onChange(id, { ...formData, - availableForPurchase: value === "false" ? null : availableForPurchaseAt, isAvailableForPurchase: value === "true", - }) - } + }); + }} display="flex" flexDirection="column" - gap={3} > <RadioGroup.Item id={`channel:isAvailableForPurchase:${id}-true`} value="true"> - <Box display="flex" __alignItems="baseline" gap={2}> - <Text>{messages.availableLabel}</Text> - {isAvailable && - availableForPurchaseAt && - Date.parse(availableForPurchaseAt) < dateNow && ( - <Text size={2} color="default2"> - {visibleMessage(availableForPurchaseAt)} - </Text> - )} - </Box> + <Text>{messages.availableLabel}</Text> </RadioGroup.Item> - <RadioGroup.Item id={`channel:isAvailableForPurchase:${id}-false`} value="false"> - <Box display="flex" __alignItems="baseline" gap={2}> - <Text>{messages.unavailableLabel}</Text> - {availableForPurchaseAt && !isAvailable && ( - <Text size={2} color="default2"> - {messages.availableSecondLabel} - </Text> - )} - </Box> + {isAvailable && + availableForPurchaseAt && + Date.parse(availableForPurchaseAt) < dateNow && ( + <Text size={2} color="default2"> + {visibleMessage(availableForPurchaseAt)} + </Text> + )} + + <RadioGroup.Item + id={`channel:isAvailableForPurchase:${id}-false`} + value="false" + marginTop={3} + > + <Text>{messages.unavailableLabel}</Text> </RadioGroup.Item> + {availableForPurchaseAt && !isAvailable && ( + <Text size={2} color="default2"> + {messages.availableSecondLabel} + </Text> + )} </RadioGroup> + {!isAvailable && ( <Box display="flex" gap={1} flexDirection="column" alignItems="start"> <Checkbox - onCheckedChange={(checked: boolean) => setAvailableDate(checked)} + onCheckedChange={(checked: boolean) => { + if (!checked) { + onChange(id, { + ...formData, + availableForPurchaseAt: null, + }); + } + + setAvailableDate(checked); + }} checked={isAvailableDate} > {messages.setAvailabilityDateLabel} @@ -179,14 +223,14 @@ export const ChannelAvailabilityItemContent: React.FC<ChannelContentProps> = ({ <DateTimeTimezoneField error={!!formErrors.availableForPurchaseAt} disabled={disabled} - name={`channel:availableForPurchase:${id}`} + name={`channel:availableForPurchaseAt:${id}`} value={availableForPurchaseAt || ""} - onChange={dateTime => + onChange={dateTime => { onChange(id, { ...formData, - availableForPurchase: dateTime, - }) - } + availableForPurchaseAt: dateTime, + }); + }} fullWidth /> )} @@ -194,12 +238,14 @@ export const ChannelAvailabilityItemContent: React.FC<ChannelContentProps> = ({ )} </> )} + {visibleInListings !== undefined && ( <> <Divider /> <Checkbox name={`channel:visibleInListings:${id}`} id={`channel:visibleInListings:${id}`} + data-test-id={`channel:visibleInListings:${id}`} checked={!visibleInListings} disabled={disabled} onCheckedChange={checked => { diff --git a/src/components/ChannelsAvailabilityCard/Channel/messages.ts b/src/components/ChannelsAvailabilityCard/Channel/messages.ts index 0798f16050d..cae362ae801 100644 --- a/src/components/ChannelsAvailabilityCard/Channel/messages.ts +++ b/src/components/ChannelsAvailabilityCard/Channel/messages.ts @@ -2,8 +2,8 @@ import { defineMessages } from "react-intl"; export const availabilityItemMessages = defineMessages({ sinceDate: { - id: "UjsI4o", - defaultMessage: "since {date}", + id: "bidZKr", + defaultMessage: "Since {date}", description: "date", }, setPublicationDate: { diff --git a/src/components/ChannelsAvailabilityCard/types.tsx b/src/components/ChannelsAvailabilityCard/types.tsx index 45d93ef9612..2d38a65bb91 100644 --- a/src/components/ChannelsAvailabilityCard/types.tsx +++ b/src/components/ChannelsAvailabilityCard/types.tsx @@ -1,15 +1,10 @@ import { CollectionChannelListingErrorFragment, + ProductChannelListingCreateInput, ProductChannelListingErrorFragment, } from "@dashboard/graphql"; -export interface ChannelOpts { - availableForPurchase?: string; - isAvailableForPurchase?: boolean; - isPublished: boolean; - publishedAt: string | null; - visibleInListings?: boolean; -} +export type ChannelOpts = Omit<ProductChannelListingCreateInput, "channelId">; export interface Messages { visibleLabel: string; diff --git a/src/components/VisibilityCard/messages.ts b/src/components/VisibilityCard/messages.ts index 3215a2d77fd..50941e3a1f4 100644 --- a/src/components/VisibilityCard/messages.ts +++ b/src/components/VisibilityCard/messages.ts @@ -7,8 +7,8 @@ export const visibilityCardMessages = defineMessages({ description: "section header", }, sinceDate: { - id: "UjsI4o", - defaultMessage: "since {date}", + id: "bidZKr", + defaultMessage: "Since {date}", description: "date", }, setPublicationDate: { diff --git a/src/products/components/ProductUpdatePage/formChannels.test.ts b/src/products/components/ProductUpdatePage/formChannels.test.ts index e4e1d532f5e..fbbfe4014ee 100644 --- a/src/products/components/ProductUpdatePage/formChannels.test.ts +++ b/src/products/components/ProductUpdatePage/formChannels.test.ts @@ -4,7 +4,7 @@ describe("ProductUpdatePage - fromChannels", () => { describe("updateChannelsInput", () => { const channel = { channelId: "Q2hhbm5lbDox", - availableForPurchaseAt: null, + availableForPurchaseAt: "2020-10-01", __typename: "ProductChannelListing", isPublished: true, publishedAt: "2020-01-01", @@ -25,7 +25,6 @@ describe("ProductUpdatePage - fromChannels", () => { updateChannels: [channel], }; const data = { - availableForPurchase: "2020-10-01", isAvailableForPurchase: true, isPublished: true, publishedAt: "2020-01-01", @@ -40,7 +39,6 @@ describe("ProductUpdatePage - fromChannels", () => { ...channel, isAvailableForPurchase: true, availableForPurchaseAt: "2020-10-01", - availableForPurchase: "2020-10-01", }, ], }); @@ -51,7 +49,6 @@ describe("ProductUpdatePage - fromChannels", () => { updateChannels: [channel], }; const newData = { - availableForPurchase: "2020-10-01", isAvailableForPurchase: false, isPublished: true, publishedAt: "2020-01-01", @@ -65,7 +62,6 @@ describe("ProductUpdatePage - fromChannels", () => { { ...channel, availableForPurchaseAt: "2020-10-01", - availableForPurchase: "2020-10-01", }, ], }); diff --git a/src/products/components/ProductUpdatePage/formChannels.ts b/src/products/components/ProductUpdatePage/formChannels.ts index 847d345cd3e..10d3dae52ec 100644 --- a/src/products/components/ProductUpdatePage/formChannels.ts +++ b/src/products/components/ProductUpdatePage/formChannels.ts @@ -30,7 +30,6 @@ export const updateChannelsInput = ( return { ...listing, ...data, - availableForPurchaseAt: data.availableForPurchase, }; } @@ -51,7 +50,6 @@ export function useProductChannelListingsForm( removeChannels: [], updateChannels: product?.channelListings.map(listing => ({ channelId: listing.channel.id, - availableForPurchaseAt: listing.availableForPurchaseAt, ...listing, })), }); diff --git a/src/products/views/ProductUpdate/handlers/utils.ts b/src/products/views/ProductUpdate/handlers/utils.ts index b3d10e47589..05ceddf58ed 100644 --- a/src/products/views/ProductUpdate/handlers/utils.ts +++ b/src/products/views/ProductUpdate/handlers/utils.ts @@ -117,16 +117,10 @@ export function getProductChannelsUpdateVariables( "isAvailableForPurchase", "isPublished", "visibleInListings", + "availableForPurchaseAt", + "publishedAt", ] as Array<keyof ProductChannelListingAddInput>; - if (!listing.isAvailableForPurchase) { - fieldsToPick.push("availableForPurchaseAt"); - } - - if (!listing.isPublished) { - fieldsToPick.push("publishedAt"); - } - return pick( listing, // Filtering it here so we send only fields defined in input schema