diff --git a/.changeset/curly-wolves-happen.md b/.changeset/curly-wolves-happen.md new file mode 100644 index 00000000000..25ddca47c03 --- /dev/null +++ b/.changeset/curly-wolves-happen.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Now, swatch presents the preview of the selected image. diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 3664a9e3cdf..45ebfda1abb 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -6699,6 +6699,10 @@ "context": "export items to csv file, choice field label", "string": "Export information for:" }, + "g8lXTL": { + "context": "swatch attribute", + "string": "Swatch" + }, "g9Mb+U": { "context": "change warehouse dialog title", "string": "Change warehouse" @@ -6795,10 +6799,6 @@ "gvOzOl": { "string": "Page Title" }, - "gx4wCT": { - "context": "swatch attribute type", - "string": "Swatch" - }, "gx6b6x": { "context": "search shortcut", "string": "Search" @@ -9573,6 +9573,10 @@ "ztQgD8": { "string": "No attributes found" }, + "ztvvcm": { + "context": "swatch attribute type", + "string": "Swatch type" + }, "zxs6G3": { "string": "Manage how you ship out orders" }, diff --git a/playwright/tests/orders.spec.ts b/playwright/tests/orders.spec.ts index afab77c557f..5f6842425fd 100644 --- a/playwright/tests/orders.spec.ts +++ b/playwright/tests/orders.spec.ts @@ -8,8 +8,8 @@ import { FulfillmentPage } from "@pages/fulfillmentPage"; import { OrdersPage } from "@pages/ordersPage"; import { RefundPage } from "@pages/refundPage"; import { expect } from "@playwright/test"; -import { test } from "utils/testWithPermission"; import * as faker from "faker"; +import { test } from "utils/testWithPermission"; test.use({ permissionName: "admin" }); diff --git a/src/attributes/components/AttributeDetails/messages.tsx b/src/attributes/components/AttributeDetails/messages.tsx index 9db9debca88..671c721d992 100644 --- a/src/attributes/components/AttributeDetails/messages.tsx +++ b/src/attributes/components/AttributeDetails/messages.tsx @@ -107,8 +107,13 @@ export const inputTypeMessages = defineMessages({ description: "date time attribute type", }, swatch: { - id: "gx4wCT", + id: "g8lXTL", defaultMessage: "Swatch", + description: "swatch attribute", + }, + swatchType: { + id: "ztvvcm", + defaultMessage: "Swatch type", description: "swatch attribute type", }, }); diff --git a/src/attributes/components/AttributeSwatchField/AttributeSwatchField.tsx b/src/attributes/components/AttributeSwatchField/AttributeSwatchField.tsx index 9abc76de314..139b152251b 100644 --- a/src/attributes/components/AttributeSwatchField/AttributeSwatchField.tsx +++ b/src/attributes/components/AttributeSwatchField/AttributeSwatchField.tsx @@ -2,17 +2,15 @@ import { inputTypeMessages } from "@dashboard/attributes/components/AttributeDet import { AttributeValueEditDialogFormData } from "@dashboard/attributes/utils/data"; import { ColorPicker, ColorPickerProps } from "@dashboard/components/ColorPicker"; import FileUploadField from "@dashboard/components/FileUploadField"; -import { RadioGroupField } from "@dashboard/components/RadioGroupField"; -import VerticalSpacer from "@dashboard/components/VerticalSpacer"; -import { useFileUploadMutation } from "@dashboard/graphql"; +import { SimpleRadioGroupField } from "@dashboard/components/SimpleRadioGroupField"; import { UseFormResult } from "@dashboard/hooks/useForm"; -import useNotifier from "@dashboard/hooks/useNotifier"; -import { errorMessages } from "@dashboard/intl"; +import { Box, Skeleton } from "@saleor/macaw-ui-next"; import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { swatchFieldMessages } from "./messages"; -import { useStyles } from "./styles"; +import { useColorProcessing } from "./useColorProcessing"; +import { useFileProcessing } from "./useFileProcessing"; type AttributeSwatchFieldProps = Pick< UseFormResult, @@ -25,47 +23,16 @@ const AttributeSwatchField: React.FC< AttributeSwatchFieldProps > = ({ set, ...props }) => { const { data } = props; - const notify = useNotifier(); - const intl = useIntl(); const { formatMessage } = useIntl(); - const classes = useStyles(); - const [processing, setProcessing] = useState(false); - const [uploadFile] = useFileUploadMutation({}); const [type, setType] = useState(data.fileUrl ? "image" : "picker"); - const handleColorChange = (hex: string) => - set({ value: hex, fileUrl: undefined, contentType: undefined }); - const handleFileUpload = async (file: File) => { - setProcessing(true); - - const { data } = await uploadFile({ variables: { file } }); - - if (data?.fileUpload?.errors?.length) { - notify({ - status: "error", - title: intl.formatMessage(errorMessages.imgageUploadErrorTitle), - text: intl.formatMessage(errorMessages.imageUploadErrorText), - }); - } else { - set({ - fileUrl: data?.fileUpload?.uploadedFile?.url, - contentType: data?.fileUpload?.uploadedFile?.contentType ?? "", - value: undefined, - }); - } - - setProcessing(false); - }; - const handleFileDelete = () => - set({ - fileUrl: undefined, - contentType: undefined, - value: undefined, - }); + const { handleFileUpload, handleFileDelete, handleOnload, processing } = useFileProcessing({ + set, + }); + const { handleColorChange } = useColorProcessing({ set }); return ( <> - - } + label={} name="swatch" value={type} onChange={event => setType(event.target.value)} + display="flex" + paddingTop={3} + gap={4} data-test-id="swatch-radio" /> - {type === "image" ? ( - <> - - - {data.fileUrl && ( -
- )} - - ) : ( - - )} + + {type === "image" ? ( + <> + + + + + {data.fileUrl && ( + + )} + {processing && } + + + ) : ( + + )} + ); }; diff --git a/src/attributes/components/AttributeSwatchField/useColorProcessing.ts b/src/attributes/components/AttributeSwatchField/useColorProcessing.ts new file mode 100644 index 00000000000..12c5844ab68 --- /dev/null +++ b/src/attributes/components/AttributeSwatchField/useColorProcessing.ts @@ -0,0 +1,12 @@ +import { AttributeValueEditDialogFormData } from "@dashboard/attributes/utils/data"; + +export const useColorProcessing = ({ + set, +}: { + set: (data: Partial) => void; +}) => { + const handleColorChange = (hex: string) => + set({ value: hex, fileUrl: undefined, contentType: undefined }); + + return { handleColorChange }; +}; diff --git a/src/attributes/components/AttributeSwatchField/useFileProcessing.test.tsx b/src/attributes/components/AttributeSwatchField/useFileProcessing.test.tsx new file mode 100644 index 00000000000..cdef38731c8 --- /dev/null +++ b/src/attributes/components/AttributeSwatchField/useFileProcessing.test.tsx @@ -0,0 +1,106 @@ +import { useFileUploadMutation } from "@dashboard/graphql"; +import { act, renderHook } from "@testing-library/react-hooks"; +import React from "react"; + +import { useFileProcessing } from "./useFileProcessing"; + +jest.mock("@dashboard/graphql", () => ({ + useFileUploadMutation: jest.fn(), +})); + +jest.mock("react-intl", () => ({ + useIntl: jest.fn(() => ({ + formatMessage: jest.fn(x => x.defaultMessage), + })), + defineMessages: (x: unknown) => x, + FormattedMessage: ({ defaultMessage }: { defaultMessage: string }) => <>{defaultMessage}, +})); + +jest.mock("@dashboard/intl", () => ({ + errorMessages: { + imgageUploadErrorTitle: "Image upload error title", + imageUploadErrorText: "Image upload error text", + }, +})); + +jest.mock("@dashboard/hooks/useNotifier", () => () => jest.fn()); + +describe("useFileProcessing", () => { + const mockUploadFile = jest.fn(); + const setMock = jest.fn(); + + beforeEach(() => { + (useFileUploadMutation as jest.Mock).mockReturnValue([mockUploadFile]); + jest.clearAllMocks(); + }); + + it("should handle file upload successfully", async () => { + // Arrange + const { result } = renderHook(() => useFileProcessing({ set: setMock })); + const file = new File(["dummy content"], "example.png", { type: "image/png" }); + + mockUploadFile.mockResolvedValueOnce({ + data: { + fileUpload: { + errors: [], + uploadedFile: { + url: "http://example.com/example.png", + contentType: "image/png", + }, + }, + }, + }); + + // Act + await act(() => result.current.handleFileUpload(file)); + await act(() => result.current.handleOnload()); + + expect(mockUploadFile).toHaveBeenCalledWith({ variables: { file } }); + expect(setMock).toHaveBeenCalledWith({ + fileUrl: "http://example.com/example.png", + contentType: "image/png", + value: undefined, + }); + expect(result.current.processing).toBe(false); + }); + + it("should handle file upload error", async () => { + // Arrange + const { result } = renderHook(() => useFileProcessing({ set: setMock })); + const file = new File(["dummy content"], "example.png", { type: "image/png" }); + + mockUploadFile.mockResolvedValueOnce({ + data: { + fileUpload: { + errors: [{ message: "Upload error" }], + }, + }, + }); + + // Act + await act(() => result.current.handleFileUpload(file)); + await act(() => result.current.handleOnload()); + + // Assert + expect(mockUploadFile).toHaveBeenCalledWith({ variables: { file } }); + expect(setMock).not.toHaveBeenCalled(); + expect(result.current.processing).toBe(false); + }); + + it("should handle file deletion", () => { + // Arrange + const { result } = renderHook(() => useFileProcessing({ set: setMock })); + + // Act + act(() => { + result.current.handleFileDelete(); + }); + + // Assert + expect(setMock).toHaveBeenCalledWith({ + fileUrl: undefined, + contentType: undefined, + value: undefined, + }); + }); +}); diff --git a/src/attributes/components/AttributeSwatchField/useFileProcessing.ts b/src/attributes/components/AttributeSwatchField/useFileProcessing.ts new file mode 100644 index 00000000000..e993ff9f2ac --- /dev/null +++ b/src/attributes/components/AttributeSwatchField/useFileProcessing.ts @@ -0,0 +1,57 @@ +import { AttributeValueEditDialogFormData } from "@dashboard/attributes/utils/data"; +import { useFileUploadMutation } from "@dashboard/graphql"; +import useNotifier from "@dashboard/hooks/useNotifier"; +import { errorMessages } from "@dashboard/intl"; +import { useState } from "react"; +import { useIntl } from "react-intl"; + +export const useFileProcessing = ({ + set, +}: { + set: (data: Partial) => void; +}) => { + const notify = useNotifier(); + const intl = useIntl(); + const [processing, setProcessing] = useState(false); + + const [uploadFile] = useFileUploadMutation({}); + + const handleFileUpload = async (file: File) => { + setProcessing(true); + + const { data } = await uploadFile({ variables: { file } }); + + if (data?.fileUpload?.errors?.length) { + notify({ + status: "error", + title: intl.formatMessage(errorMessages.imgageUploadErrorTitle), + text: intl.formatMessage(errorMessages.imageUploadErrorText), + }); + } else { + set({ + fileUrl: data?.fileUpload?.uploadedFile?.url, + contentType: data?.fileUpload?.uploadedFile?.contentType ?? "", + value: undefined, + }); + } + }; + + const handleFileDelete = () => { + set({ + fileUrl: undefined, + contentType: undefined, + value: undefined, + }); + }; + + const handleOnload = () => { + setProcessing(false); + }; + + return { + processing, + handleFileUpload, + handleFileDelete, + handleOnload, + }; +}; diff --git a/src/attributes/components/AttributeValues/AttributeValues.tsx b/src/attributes/components/AttributeValues/AttributeValues.tsx index 4dfe97fa4e7..1cf890ea2f2 100644 --- a/src/attributes/components/AttributeValues/AttributeValues.tsx +++ b/src/attributes/components/AttributeValues/AttributeValues.tsx @@ -13,7 +13,7 @@ import { renderCollection, stopPropagation } from "@dashboard/misc"; import { ListProps, PaginateListProps, RelayToFlat, ReorderAction } from "@dashboard/types"; import { TableCell, TableFooter, TableHead } from "@material-ui/core"; import { IconButton, makeStyles } from "@saleor/macaw-ui"; -import { Skeleton, TrashBinIcon } from "@saleor/macaw-ui-next"; +import { Box, Skeleton, TrashBinIcon } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -173,11 +173,23 @@ const AttributeValues: React.FC = ({ > {isSwatch && ( -
+ {value?.file ? ( + + ) : ( +
+ )} )} diff --git a/src/categories/components/CategoryListPage/CategoryListPage.tsx b/src/categories/components/CategoryListPage/CategoryListPage.tsx index d2aa25e11c5..b9dab8953a2 100644 --- a/src/categories/components/CategoryListPage/CategoryListPage.tsx +++ b/src/categories/components/CategoryListPage/CategoryListPage.tsx @@ -11,6 +11,7 @@ import { PageListProps, SearchPageProps, SortPage, TabPageProps } from "@dashboa import { Box, Button, ChevronRightIcon } from "@saleor/macaw-ui-next"; import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; +import { Link } from "react-router-dom"; import { CategoryListDatagrid } from "../CategoryListDatagrid"; import { messages } from "./messages"; @@ -77,9 +78,11 @@ export const CategoryListPage: React.FC = ({ /> - + + +