Skip to content
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/smooth-jobs-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

Fixed bulk gift card issue modal re-rendering issues. After this change modal shouldn't rerender itself.
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
// @ts-strict-ignore
import { IMessage } from "@dashboard/components/messages";
import { DashboardModal } from "@dashboard/components/Modal";
import {
GiftCardBulkCreateInput,
useChannelCurrenciesQuery,
useGiftCardBulkCreateMutation,
} from "@dashboard/graphql";
import { GiftCardBulkCreateInput, useGiftCardBulkCreateMutation } from "@dashboard/graphql";
import useCurrentDate from "@dashboard/hooks/useCurrentDate";
import useNotifier from "@dashboard/hooks/useNotifier";
import { DialogProps } from "@dashboard/types";
Expand All @@ -29,14 +24,13 @@ import {
} from "./types";
import { validateForm } from "./utils";

const GiftCardBulkCreateDialog: React.FC<DialogProps> = ({ onClose, open }) => {
export const GiftCardBulkCreateDialog: React.FC<DialogProps> = ({ onClose, open }) => {
const intl = useIntl();
const notify = useNotifier();
const [formErrors, setFormErrors] = useState<GiftCardBulkCreateFormErrors>(null);
const [formErrors, setFormErrors] = useState<GiftCardBulkCreateFormErrors | null>(null);
const [issuedIds, setIssuedIds] = useState<string[] | null>(null);
const [openIssueSuccessDialog, setOpenIssueSuccessDialog] = useState<boolean>(false);
const onIssueSuccessDialogClose = () => setOpenIssueSuccessDialog(false);
const { loading: loadingChannelCurrencies } = useChannelCurrenciesQuery({});
const [openIssueSuccessDialog, setOpenIssueSuccessDialog] = useState(false);

const currentDate = useCurrentDate();

const getParsedSubmitInputData = (
Expand Down Expand Up @@ -71,8 +65,8 @@ const GiftCardBulkCreateDialog: React.FC<DialogProps> = ({ onClose, open }) => {
notify(getGiftCardCreateOnCompletedMessage(errors, intl, giftCardsBulkIssueSuccessMessage));
setFormErrors(getFormErrors(giftCardBulkCreateErrorKeys, errors));

if (!errors.length) {
setIssuedIds(data?.giftCardBulkCreate?.giftCards?.map(giftCard => giftCard.id));
if (!errors?.length) {
setIssuedIds(data?.giftCardBulkCreate?.giftCards?.map(giftCard => giftCard.id) ?? []);
setOpenIssueSuccessDialog(true);
onClose();
}
Expand Down Expand Up @@ -111,24 +105,20 @@ const GiftCardBulkCreateDialog: React.FC<DialogProps> = ({ onClose, open }) => {
<DashboardModal.Content size="sm">
<DashboardModal.Header>{intl.formatMessage(messages.title)}</DashboardModal.Header>
<ContentWithProgress>
{!loadingChannelCurrencies && (
<GiftCardBulkCreateDialogForm
opts={bulkCreateGiftCardOpts}
onClose={onClose}
formErrors={formErrors}
onSubmit={handleSubmit}
/>
)}
<GiftCardBulkCreateDialogForm
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldnt it incldue prop loading={loadingChannelCurrencies}?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope - it turns out loading was only for GiftCardCreateMoneyInput - now this component has loading inside.

opts={bulkCreateGiftCardOpts}
onClose={onClose}
formErrors={formErrors}
onSubmit={handleSubmit}
/>
</ContentWithProgress>
</DashboardModal.Content>
</DashboardModal>
<GiftCardBulkCreateSuccessDialog
onClose={onIssueSuccessDialogClose}
onClose={() => setOpenIssueSuccessDialog(false)}
open={openIssueSuccessDialog}
idsToExport={issuedIds}
/>
</>
);
};

export default GiftCardBulkCreateDialog;
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import React from "react";
import { useIntl } from "react-intl";

import GiftCardCreateExpirySelect from "../GiftCardCreateDialog/GiftCardCreateExpirySelect";
import GiftCardCreateMoneyInput from "../GiftCardCreateDialog/GiftCardCreateMoneyInput";
import { GiftCardCreateMoneyInput } from "../GiftCardCreateDialog/GiftCardCreateMoneyInput";
import GiftCardCreateRequiresActivationSection from "../GiftCardCreateDialog/GiftCardCreateRequiresActivationSection";
import { giftCardCreateMessages as messages } from "../GiftCardCreateDialog/messages";
import { getGiftCardErrorMessage } from "../GiftCardUpdate/messages";
Expand All @@ -40,7 +40,7 @@ export const initialData: GiftCardBulkCreateFormData = {

interface GiftCardBulkCreateDialogFormProps {
opts: { status: ConfirmButtonTransitionState };
formErrors: GiftCardBulkCreateFormErrors;
formErrors: GiftCardBulkCreateFormErrors | null;
onSubmit: (data: GiftCardBulkCreateFormData) => void;
onClose: () => void;
}
Expand Down
2 changes: 0 additions & 2 deletions src/giftCards/GiftCardBulkCreateDialog/index.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useIntl } from "react-intl";
import GiftCardSendToCustomer from "../components/GiftCardSendToCustomer/GiftCardSendToCustomer";
import { GiftCardCreateCommonFormData } from "../GiftCardBulkCreateDialog/types";
import GiftCardCreateExpirySelect from "./GiftCardCreateExpirySelect";
import GiftCardCreateMoneyInput from "./GiftCardCreateMoneyInput";
import { GiftCardCreateMoneyInput } from "./GiftCardCreateMoneyInput";
import GiftCardCreateRequiresActivationSection from "./GiftCardCreateRequiresActivationSection";
import { giftCardCreateMessages as messages } from "./messages";
import { GiftCardCreateFormCommonProps, GiftCardCreateFormCustomer } from "./types";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// @ts-strict-ignore
import TextWithSelectField from "@dashboard/components/TextWithSelectField";
import { useChannelCurrenciesQuery } from "@dashboard/graphql";
import { ChangeEvent, FormChange } from "@dashboard/hooks/useForm";
import useLocalStorage from "@dashboard/hooks/useLocalStorage";
import { mapSingleValueNodeToChoice } from "@dashboard/utils/maps";
Expand All @@ -13,6 +12,7 @@ import {
GiftCardCreateCommonFormData,
} from "../GiftCardBulkCreateDialog/types";
import { getGiftCardErrorMessage } from "../GiftCardUpdate/messages";
import { useChannelCurrenciesWithCache } from "../hooks/useChannelCurrenciesWithCache";
import { giftCardCreateMessages as messages } from "./messages";

interface GiftCardCreateMoneyInputProps {
Expand All @@ -22,16 +22,15 @@ interface GiftCardCreateMoneyInputProps {
set: (data: Partial<GiftCardCreateCommonFormData>) => void;
}

const GiftCardCreateMoneyInput: React.FC<GiftCardCreateMoneyInputProps> = ({
export const GiftCardCreateMoneyInput: React.FC<GiftCardCreateMoneyInputProps> = ({
errors,
data: { balanceAmount, balanceCurrency },
change,
set,
}) => {
const intl = useIntl();
const { data: channelCurrenciesData } = useChannelCurrenciesQuery({});
const { channelCurrencies } = channelCurrenciesData?.shop ?? {};
const [savedCurrency, setCurrency] = useLocalStorage("giftCardCreateCurrency", undefined);
const { loadingChannelCurrencies, channelCurrencies } = useChannelCurrenciesWithCache();

const getInitialCurrency = React.useCallback(() => {
if (
Expand Down Expand Up @@ -68,7 +67,7 @@ const GiftCardCreateMoneyInput: React.FC<GiftCardCreateMoneyInputProps> = ({

return (
<TextWithSelectField
loading={!channelCurrenciesData?.shop}
loading={loadingChannelCurrencies}
isError={!!errors?.balance}
helperText={getGiftCardErrorMessage(errors?.balance, intl)}
change={handleInputChange}
Expand All @@ -87,5 +86,3 @@ const GiftCardCreateMoneyInput: React.FC<GiftCardCreateMoneyInputProps> = ({
/>
);
};

export default GiftCardCreateMoneyInput;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DashboardModal } from "@dashboard/components/Modal";
import GiftCardListPageDeleteDialog from "@dashboard/giftCards/components/GiftCardDeleteDialog/GiftCardListPageDeleteDialog";
import GiftCardBulkCreateDialog from "@dashboard/giftCards/GiftCardBulkCreateDialog";
import { GiftCardBulkCreateDialog } from "@dashboard/giftCards/GiftCardBulkCreateDialog/GiftCardBulkCreateDialog";
import GiftCardCreateDialogContent from "@dashboard/giftCards/GiftCardCreateDialog";
import GiftCardExportDialogContent from "@dashboard/giftCards/GiftCardExportDialogContent";
import { giftCardListUrl } from "@dashboard/giftCards/urls";
Expand Down
113 changes: 113 additions & 0 deletions src/giftCards/hooks/useChannelCurrenciesWithCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useApolloClient } from "@apollo/client";
import { renderHook } from "@testing-library/react-hooks";

import { useChannelCurrenciesWithCache } from "./useChannelCurrenciesWithCache";

jest.mock("@apollo/client", () => ({
useApolloClient: jest.fn(),
}));

jest.mock("@dashboard/graphql", () => ({
useChannelCurrenciesQuery: jest.fn(),
ChannelCurrenciesDocument: {},
}));

const { useChannelCurrenciesQuery } = jest.requireMock("@dashboard/graphql");

describe("useChannelCurrenciesWithCache", () => {
const mockClient = {
readQuery: jest.fn(),
};

const mockQueryResult = {
data: { shop: { channelCurrencies: ["USD", "EUR"] } },
loading: false,
error: undefined,
};

beforeEach(() => {
jest.clearAllMocks();
(useApolloClient as jest.Mock).mockReturnValue(mockClient);
useChannelCurrenciesQuery.mockReturnValue(mockQueryResult);
});

it("should use cached data when available and skip query", () => {
const cachedData = { shop: { channelCurrencies: ["USD", "EUR", "GBP"] } };

mockClient.readQuery.mockReturnValue(cachedData);

const { result } = renderHook(() => useChannelCurrenciesWithCache());

expect(mockClient.readQuery).toHaveBeenCalledWith({
query: {},
variables: {},
});

expect(useChannelCurrenciesQuery).toHaveBeenCalledWith({
skip: true,
});

expect(result.current.loadingChannelCurrencies).toBe(false);
expect(result.current.channelCurrencies).toEqual(["USD", "EUR", "GBP"]);
});

it("should execute query when no cached data is available", () => {
mockClient.readQuery.mockImplementation(() => {
throw new Error("Cache miss");
});

renderHook(() => useChannelCurrenciesWithCache());

expect(useChannelCurrenciesQuery).toHaveBeenCalledWith({
skip: false,
});
});

it("should handle loading state for gift card forms", () => {
mockClient.readQuery.mockImplementation(() => {
throw new Error("Cache miss");
});

useChannelCurrenciesQuery.mockReturnValue({
...mockQueryResult,
loading: true,
data: undefined,
});

const { result } = renderHook(() => useChannelCurrenciesWithCache());

expect(result.current.loadingChannelCurrencies).toBe(true);
expect(result.current.channelCurrencies).toEqual([]);
});

it("should handle error state appropriately", () => {
mockClient.readQuery.mockImplementation(() => {
throw new Error("Cache miss");
});

const errorResult = {
...mockQueryResult,
loading: false,
error: new Error("Network error"),
data: undefined,
};

useChannelCurrenciesQuery.mockReturnValue(errorResult);

const { result } = renderHook(() => useChannelCurrenciesWithCache());

expect(result.current.loadingChannelCurrencies).toBe(false);
expect(result.current.channelCurrencies).toEqual([]);
});

it("should return query data when cache miss occurs", () => {
mockClient.readQuery.mockImplementation(() => {
throw new Error("Cache miss");
});

const { result } = renderHook(() => useChannelCurrenciesWithCache());

expect(result.current.loadingChannelCurrencies).toBe(false);
expect(result.current.channelCurrencies).toEqual(["USD", "EUR"]);
});
});
46 changes: 46 additions & 0 deletions src/giftCards/hooks/useChannelCurrenciesWithCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useApolloClient } from "@apollo/client";
import {
ChannelCurrenciesDocument,
ChannelCurrenciesQuery,
ChannelCurrenciesQueryVariables,
useChannelCurrenciesQuery,
} from "@dashboard/graphql";
import { useMemo } from "react";

/**
* Custom hook that uses useChannelCurrenciesQuery only if the data isn't already in cache.
* This helps avoid unnecessary network requests when the channel currencies data is already available.
*/
export function useChannelCurrenciesWithCache() {
const client = useApolloClient();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this is not cached with apollo? I mean we can't create a wrapper for every object and appollo supports that

https://www.apollographql.com/docs/react/caching/overview

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I don't know but if I didn't used custom hook useChannelQuery gets triggered - maybe we should use useQuery instead of custom generated hooks?


const cachedData = useMemo(() => {
try {
return client.readQuery<ChannelCurrenciesQuery, ChannelCurrenciesQueryVariables>({
query: ChannelCurrenciesDocument,
variables: {},
});
} catch {
// Query not in cache or cache miss
return null;
}
}, [client]);

const shouldSkip = cachedData !== null;

const queryResult = useChannelCurrenciesQuery({
skip: shouldSkip,
});

if (shouldSkip && cachedData) {
return {
loadingChannelCurrencies: false,
channelCurrencies: cachedData.shop.channelCurrencies,
};
}

return {
loadingChannelCurrencies: queryResult.loading,
channelCurrencies: queryResult.data?.shop.channelCurrencies ?? [],
};
}
Loading
Loading