Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix swatch attribute preview when selecting image #5226

Merged
merged 8 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -1,4 +1,4 @@
import { ADDRESS } from "@data/addresses";

Check failure on line 1 in playwright/tests/orders.spec.ts

View workflow job for this annotation

GitHub Actions / run-tests (1/2)

[e2e] › orders.spec.ts:36:5 › TC: SALEOR_28 Create basic order @e2e @order

1) [e2e] › orders.spec.ts:36:5 › TC: SALEOR_28 Create basic order @e2e @order ──────────────────── Test timeout of 30000ms exceeded.
import { ORDERS, PRODUCTS } from "@data/e2eTestData";
import { AddressesListPage } from "@pages/addressesListPage";
import { AddressDialog } from "@pages/dialogs/addressDialog";
Expand All @@ -8,8 +8,8 @@
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
Loading