Skip to content

Commit

Permalink
Fix swatch attribute preview when selecting image (#5226)
Browse files Browse the repository at this point in the history
* Handle swatch preview

* Handle swatch preview

* Handle swatch preview

* Translations

* Translations

* Translations

* Fix list

* Fix list
  • Loading branch information
andrzejewsky authored Oct 23, 2024
1 parent dc54164 commit e269b46
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 82 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-wolves-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

Now, swatch presents the preview of the selected image.
12 changes: 8 additions & 4 deletions locale/defaultMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -6795,10 +6799,6 @@
"gvOzOl": {
"string": "Page Title"
},
"gx4wCT": {
"context": "swatch attribute type",
"string": "Swatch"
},
"gx6b6x": {
"context": "search shortcut",
"string": "Search"
Expand Down Expand Up @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion playwright/tests/orders.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });

Expand Down
7 changes: 6 additions & 1 deletion src/attributes/components/AttributeDetails/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = Pick<
UseFormResult<T>,
Expand All @@ -25,47 +23,16 @@ const AttributeSwatchField: React.FC<
AttributeSwatchFieldProps<AttributeValueEditDialogFormData>
> = ({ 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<SwatchType>(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 (
<>
<VerticalSpacer spacing={2} />
<RadioGroupField
<SimpleRadioGroupField
choices={[
{
label: formatMessage(swatchFieldMessages.picker),
Expand All @@ -76,36 +43,56 @@ const AttributeSwatchField: React.FC<
value: "image",
},
]}
variant="inline"
label={<FormattedMessage {...inputTypeMessages.swatch} />}
label={<FormattedMessage {...inputTypeMessages.swatchType} />}
name="swatch"
value={type}
onChange={event => setType(event.target.value)}
display="flex"
paddingTop={3}
gap={4}
data-test-id="swatch-radio"
/>
{type === "image" ? (
<>
<FileUploadField
disabled={processing}
loading={processing}
file={{ label: "", value: "", file: undefined }}
onFileUpload={handleFileUpload}
onFileDelete={handleFileDelete}
inputProps={{
accept: "image/*",
}}
/>

{data.fileUrl && (
<div
className={classes.filePreview}
style={{ backgroundImage: `url(${data.fileUrl})` }}
/>
)}
</>
) : (
<ColorPicker {...(props as ColorPickerProps)} onColorChange={handleColorChange} />
)}
<Box __height={280} overflow="hidden">
{type === "image" ? (
<>
<Box paddingBottom={4}>
<FileUploadField
disabled={processing}
loading={processing}
file={{ label: "", value: "", file: undefined }}
onFileUpload={handleFileUpload}
onFileDelete={handleFileDelete}
inputProps={{
accept: "image/*",
}}
/>
</Box>
<Box
width="100%"
marginX="auto"
position="relative"
display="flex"
justifyContent="center"
alignItems="center"
>
{data.fileUrl && (
<Box
display={processing ? "none" : "block"}
as="img"
src={data.fileUrl}
__width="216px"
__height="216px"
objectFit="cover"
onLoad={handleOnload}
/>
)}
{processing && <Skeleton __width="216px" __height="216px" />}
</Box>
</>
) : (
<ColorPicker {...(props as ColorPickerProps)} onColorChange={handleColorChange} />
)}
</Box>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AttributeValueEditDialogFormData } from "@dashboard/attributes/utils/data";

export const useColorProcessing = ({
set,
}: {
set: (data: Partial<AttributeValueEditDialogFormData>) => void;
}) => {
const handleColorChange = (hex: string) =>
set({ value: hex, fileUrl: undefined, contentType: undefined });

return { handleColorChange };
};
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
Original file line number Diff line number Diff line change
@@ -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<AttributeValueEditDialogFormData>) => 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,
};
};
Loading

0 comments on commit e269b46

Please sign in to comment.