Skip to content

Commit 1a8279b

Browse files
feat(warehouse): Add metadata edition modal (#6284)
* feat(warehouse): Add metadata edition modal * fix: i18n and changeset * feat: add test --------- Co-authored-by: Jonatan Witoszek <jonatanwitoszek@gmail.com>
1 parent 6f61b4f commit 1a8279b

13 files changed

Lines changed: 256 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"saleor-dashboard": patch
3+
---
4+
5+
Added metadata editing dialog for warehouses, allowing users to manage both public and private metadata through a dedicated modal accessible from the warehouse details page.

locale/defaultMessages.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11162,6 +11162,9 @@
1116211162
"context": "product available for purchase date",
1116311163
"string": "Will become available on {date}"
1116411164
},
11165+
"zo5aT3": {
11166+
"string": "Warehouse Metadata"
11167+
},
1116511168
"zoUlyR": {
1116611169
"string": "Go to product types"
1116711170
},

src/fragments/warehouses.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,11 @@ export const warehouseDetailsFragment = gql`
2929
...Address
3030
}
3131
email
32+
metadata {
33+
...MetadataItem
34+
}
35+
privateMetadata {
36+
...MetadataItem
37+
}
3238
}
3339
`;

src/graphql/hooks.generated.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3693,9 +3693,16 @@ export const WarehouseDetailsFragmentDoc = gql`
36933693
...Address
36943694
}
36953695
email
3696+
metadata {
3697+
...MetadataItem
3698+
}
3699+
privateMetadata {
3700+
...MetadataItem
3701+
}
36963702
}
36973703
${WarehouseWithShippingFragmentDoc}
3698-
${AddressFragmentDoc}`;
3704+
${AddressFragmentDoc}
3705+
${MetadataItemFragmentDoc}`;
36993706
export const WebhookDetailsFragmentDoc = gql`
37003707
fragment WebhookDetails on Webhook {
37013708
...Webhook

src/graphql/types.generated.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10570,7 +10570,7 @@ export type WarehouseFragment = { __typename: 'Warehouse', id: string, name: str
1057010570

1057110571
export type WarehouseWithShippingFragment = { __typename: 'Warehouse', id: string, name: string, shippingZones: { __typename: 'ShippingZoneCountableConnection', edges: Array<{ __typename: 'ShippingZoneCountableEdge', node: { __typename: 'ShippingZone', id: string, name: string } }> } };
1057210572

10573-
export type WarehouseDetailsFragment = { __typename: 'Warehouse', isPrivate: boolean, clickAndCollectOption: WarehouseClickAndCollectOptionEnum, email: string, id: string, name: string, address: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } }, shippingZones: { __typename: 'ShippingZoneCountableConnection', edges: Array<{ __typename: 'ShippingZoneCountableEdge', node: { __typename: 'ShippingZone', id: string, name: string } }> } };
10573+
export type WarehouseDetailsFragment = { __typename: 'Warehouse', isPrivate: boolean, clickAndCollectOption: WarehouseClickAndCollectOptionEnum, email: string, id: string, name: string, address: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } }, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, shippingZones: { __typename: 'ShippingZoneCountableConnection', edges: Array<{ __typename: 'ShippingZoneCountableEdge', node: { __typename: 'ShippingZone', id: string, name: string } }> } };
1057410574

1057510575
export type WebhookFragment = { __typename: 'Webhook', id: string, name: string | null, isActive: boolean, app: { __typename: 'App', id: string, name: string | null } };
1057610576

@@ -12724,15 +12724,15 @@ export type WarehouseCreateMutationVariables = Exact<{
1272412724
}>;
1272512725

1272612726

12727-
export type WarehouseCreateMutation = { __typename: 'Mutation', createWarehouse: { __typename: 'WarehouseCreate', errors: Array<{ __typename: 'WarehouseError', code: WarehouseErrorCode, field: string | null, message: string | null }>, warehouse: { __typename: 'Warehouse', isPrivate: boolean, clickAndCollectOption: WarehouseClickAndCollectOptionEnum, email: string, id: string, name: string, address: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } }, shippingZones: { __typename: 'ShippingZoneCountableConnection', edges: Array<{ __typename: 'ShippingZoneCountableEdge', node: { __typename: 'ShippingZone', id: string, name: string } }> } } | null } | null };
12727+
export type WarehouseCreateMutation = { __typename: 'Mutation', createWarehouse: { __typename: 'WarehouseCreate', errors: Array<{ __typename: 'WarehouseError', code: WarehouseErrorCode, field: string | null, message: string | null }>, warehouse: { __typename: 'Warehouse', isPrivate: boolean, clickAndCollectOption: WarehouseClickAndCollectOptionEnum, email: string, id: string, name: string, address: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } }, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, shippingZones: { __typename: 'ShippingZoneCountableConnection', edges: Array<{ __typename: 'ShippingZoneCountableEdge', node: { __typename: 'ShippingZone', id: string, name: string } }> } } | null } | null };
1272812728

1272912729
export type WarehouseUpdateMutationVariables = Exact<{
1273012730
id: Scalars['ID'];
1273112731
input: WarehouseUpdateInput;
1273212732
}>;
1273312733

1273412734

12735-
export type WarehouseUpdateMutation = { __typename: 'Mutation', updateWarehouse: { __typename: 'WarehouseUpdate', errors: Array<{ __typename: 'WarehouseError', code: WarehouseErrorCode, field: string | null, message: string | null }>, warehouse: { __typename: 'Warehouse', isPrivate: boolean, clickAndCollectOption: WarehouseClickAndCollectOptionEnum, email: string, id: string, name: string, address: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } }, shippingZones: { __typename: 'ShippingZoneCountableConnection', edges: Array<{ __typename: 'ShippingZoneCountableEdge', node: { __typename: 'ShippingZone', id: string, name: string } }> } } | null } | null };
12735+
export type WarehouseUpdateMutation = { __typename: 'Mutation', updateWarehouse: { __typename: 'WarehouseUpdate', errors: Array<{ __typename: 'WarehouseError', code: WarehouseErrorCode, field: string | null, message: string | null }>, warehouse: { __typename: 'Warehouse', isPrivate: boolean, clickAndCollectOption: WarehouseClickAndCollectOptionEnum, email: string, id: string, name: string, address: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } }, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, shippingZones: { __typename: 'ShippingZoneCountableConnection', edges: Array<{ __typename: 'ShippingZoneCountableEdge', node: { __typename: 'ShippingZone', id: string, name: string } }> } } | null } | null };
1273612736

1273712737
export type WarehouseListQueryVariables = Exact<{
1273812738
first?: InputMaybe<Scalars['Int']>;
@@ -12751,7 +12751,7 @@ export type WarehouseDetailsQueryVariables = Exact<{
1275112751
}>;
1275212752

1275312753

12754-
export type WarehouseDetailsQuery = { __typename: 'Query', warehouse: { __typename: 'Warehouse', isPrivate: boolean, clickAndCollectOption: WarehouseClickAndCollectOptionEnum, email: string, id: string, name: string, address: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } }, shippingZones: { __typename: 'ShippingZoneCountableConnection', edges: Array<{ __typename: 'ShippingZoneCountableEdge', node: { __typename: 'ShippingZone', id: string, name: string } }> } } | null };
12754+
export type WarehouseDetailsQuery = { __typename: 'Query', warehouse: { __typename: 'Warehouse', isPrivate: boolean, clickAndCollectOption: WarehouseClickAndCollectOptionEnum, email: string, id: string, name: string, address: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } }, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, shippingZones: { __typename: 'ShippingZoneCountableConnection', edges: Array<{ __typename: 'ShippingZoneCountableEdge', node: { __typename: 'ShippingZone', id: string, name: string } }> } } | null };
1275512755

1275612756
export type WarehousesCountQueryVariables = Exact<{ [key: string]: never; }>;
1275712757

src/ripples/allRipples.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { rippleProductAvailabilityDiagnostics } from "@dashboard/products/ripple
88
import { rippleVariantGenerator } from "@dashboard/products/ripples/variantGenerator";
99
import { rippleIntroducedRipples } from "@dashboard/ripples/ripples/introducedRipples";
1010
import { Ripple } from "@dashboard/ripples/types";
11+
import { rippleWarehouseMetadata } from "@dashboard/warehouses/ripples/warehouseMetadata";
1112

1213
export const allRipples: Ripple[] = [
1314
// ... register ripples here
@@ -21,6 +22,9 @@ export const allRipples: Ripple[] = [
2122
rippleOrderMetadata,
2223
rippleRefreshedOrderSections,
2324

25+
// Warehouses
26+
rippleWarehouseMetadata,
27+
2428
// Products
2529
rippleProductAvailabilityDiagnostics,
2630
rippleVariantGenerator,

src/warehouses/components/WarehouseDetailsPage/WarehouseDetailsPage.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import CardSpacer from "@dashboard/components/CardSpacer";
55
import CompanyAddressInput from "@dashboard/components/CompanyAddressInput";
66
import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton";
77
import Form from "@dashboard/components/Form";
8+
import { iconSize, iconStrokeWidth } from "@dashboard/components/icons";
89
import { DetailPageLayout } from "@dashboard/components/Layouts";
910
import { Savebar } from "@dashboard/components/Savebar";
1011
import { AddressTypeInput } from "@dashboard/customers/types";
@@ -19,9 +20,13 @@ import { useBackLinkWithState } from "@dashboard/hooks/useBackLinkWithState";
1920
import { SubmitPromise } from "@dashboard/hooks/useForm";
2021
import useNavigator from "@dashboard/hooks/useNavigator";
2122
import useStateFromProps from "@dashboard/hooks/useStateFromProps";
23+
import { Ripple } from "@dashboard/ripples/components/Ripple";
2224
import createSingleAutocompleteSelectHandler from "@dashboard/utils/handlers/singleAutocompleteSelectChangeHandler";
2325
import { mapCountriesToChoices, mapEdgesToItems } from "@dashboard/utils/maps";
26+
import { rippleWarehouseMetadata } from "@dashboard/warehouses/ripples/warehouseMetadata";
2427
import { warehouseListPath } from "@dashboard/warehouses/urls";
28+
import { Box, Button } from "@saleor/macaw-ui-next";
29+
import { Code } from "lucide-react";
2530
import { useIntl } from "react-intl";
2631

2732
import WarehouseInfo from "../WarehouseInfo";
@@ -40,6 +45,7 @@ interface WarehouseDetailsPageProps {
4045
saveButtonBarState: ConfirmButtonTransitionState;
4146
warehouse: WarehouseDetailsFragment | undefined;
4247
onDelete: () => void;
48+
onShowMetadata: () => void;
4349
onSubmit: (data: WarehouseDetailsPageFormData) => SubmitPromise;
4450
}
4551

@@ -50,6 +56,7 @@ const WarehouseDetailsPage = ({
5056
saveButtonBarState,
5157
warehouse,
5258
onDelete,
59+
onShowMetadata,
5360
onSubmit,
5461
}: WarehouseDetailsPageProps) => {
5562
const intl = useIntl();
@@ -91,7 +98,20 @@ const WarehouseDetailsPage = ({
9198

9299
return (
93100
<DetailPageLayout>
94-
<TopNav href={warehouseListBackLink} title={warehouse?.name} />
101+
<TopNav href={warehouseListBackLink} title={warehouse?.name}>
102+
<Box position="relative">
103+
<Button
104+
variant="secondary"
105+
icon={<Code size={iconSize.medium} strokeWidth={iconStrokeWidth} />}
106+
onClick={onShowMetadata}
107+
data-test-id="show-warehouse-metadata"
108+
title="Edit warehouse metadata"
109+
/>
110+
<Box position="absolute" __top="-4px" __right="-4px">
111+
<Ripple model={rippleWarehouseMetadata} />
112+
</Box>
113+
</Box>
114+
</TopNav>
95115
<DetailPageLayout.Content>
96116
<WarehouseInfo data={data} disabled={disabled} errors={errors} onChange={change} />
97117
<CardSpacer />
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { WarehouseDetailsFragment } from "@dashboard/graphql";
2+
import { warehouse } from "@dashboard/warehouses/fixtures";
3+
import { fireEvent, render, screen, within } from "@testing-library/react";
4+
5+
import { WarehouseMetadataDialog } from "./WarehouseMetadataDialog";
6+
7+
const mockOnSubmit = jest.fn();
8+
9+
jest.mock("@dashboard/components/MetadataDialog/useHandleMetadataSubmit", () => ({
10+
useHandleMetadataSubmit: jest.fn(() => ({
11+
onSubmit: mockOnSubmit,
12+
lastSubmittedData: undefined,
13+
submitInProgress: false,
14+
})),
15+
}));
16+
17+
const mockWarehouse: WarehouseDetailsFragment = {
18+
...warehouse,
19+
metadata: [{ key: "test-key", value: "test-value", __typename: "MetadataItem" }],
20+
privateMetadata: [{ key: "private-key", value: "private-value", __typename: "MetadataItem" }],
21+
};
22+
23+
describe("WarehouseMetadataDialog", () => {
24+
const onCloseMock = jest.fn();
25+
26+
beforeEach(() => {
27+
jest.clearAllMocks();
28+
});
29+
30+
it("renders dialog with correct title when open", () => {
31+
// Arrange & Act
32+
render(<WarehouseMetadataDialog open={true} onClose={onCloseMock} warehouse={mockWarehouse} />);
33+
34+
// Assert
35+
expect(screen.getByText("Warehouse Metadata")).toBeInTheDocument();
36+
});
37+
38+
it("does not render when open is false", () => {
39+
// Arrange & Act
40+
render(
41+
<WarehouseMetadataDialog open={false} onClose={onCloseMock} warehouse={mockWarehouse} />,
42+
);
43+
44+
// Assert
45+
expect(screen.queryByText("Warehouse Metadata")).not.toBeInTheDocument();
46+
});
47+
48+
it("closes when user clicks close button", () => {
49+
// Arrange
50+
render(<WarehouseMetadataDialog open={true} onClose={onCloseMock} warehouse={mockWarehouse} />);
51+
52+
// Act
53+
fireEvent.click(screen.getByTestId("back"));
54+
55+
// Assert
56+
expect(onCloseMock).toHaveBeenCalled();
57+
});
58+
59+
it("renders with undefined warehouse", () => {
60+
// Arrange & Act
61+
render(<WarehouseMetadataDialog open={true} onClose={onCloseMock} warehouse={undefined} />);
62+
63+
// Assert
64+
expect(screen.getByText("Warehouse Metadata")).toBeInTheDocument();
65+
});
66+
67+
it("displays metadata when section is expanded", () => {
68+
// Arrange
69+
render(<WarehouseMetadataDialog open={true} onClose={onCloseMock} warehouse={mockWarehouse} />);
70+
71+
const metadataEditors = screen.getAllByTestId("metadata-editor");
72+
const publicMetadataEditor = metadataEditors.find(
73+
editor => editor.getAttribute("data-test-is-private") === "false",
74+
)!;
75+
76+
// Act - expand metadata section
77+
const expandButton = within(publicMetadataEditor).getByTestId("expand");
78+
79+
fireEvent.click(expandButton);
80+
81+
// Assert - check metadata values after expansion
82+
expect(within(publicMetadataEditor).getByDisplayValue("test-key")).toBeInTheDocument();
83+
expect(within(publicMetadataEditor).getByDisplayValue("test-value")).toBeInTheDocument();
84+
});
85+
86+
it("displays private metadata when section is expanded", () => {
87+
// Arrange
88+
render(<WarehouseMetadataDialog open={true} onClose={onCloseMock} warehouse={mockWarehouse} />);
89+
90+
const metadataEditors = screen.getAllByTestId("metadata-editor");
91+
const privateMetadataEditor = metadataEditors.find(
92+
editor => editor.getAttribute("data-test-is-private") === "true",
93+
)!;
94+
95+
// Act - expand private metadata section
96+
const expandButton = within(privateMetadataEditor).getByTestId("expand");
97+
98+
fireEvent.click(expandButton);
99+
100+
// Assert - check private metadata values after expansion
101+
expect(within(privateMetadataEditor).getByDisplayValue("private-key")).toBeInTheDocument();
102+
expect(within(privateMetadataEditor).getByDisplayValue("private-value")).toBeInTheDocument();
103+
});
104+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { MetadataDialog } from "@dashboard/components/MetadataDialog/MetadataDialog";
2+
import { useHandleMetadataSubmit } from "@dashboard/components/MetadataDialog/useHandleMetadataSubmit";
3+
import { useMetadataForm } from "@dashboard/components/MetadataDialog/useMetadataForm";
4+
import { mapFieldArrayToMetadataInput } from "@dashboard/components/MetadataDialog/validation";
5+
import { WarehouseDetailsDocument, WarehouseDetailsFragment } from "@dashboard/graphql";
6+
import { useEffect } from "react";
7+
import { useIntl } from "react-intl";
8+
9+
interface WarehouseMetadataDialogProps {
10+
open: boolean;
11+
onClose: () => void;
12+
warehouse: WarehouseDetailsFragment | undefined;
13+
}
14+
15+
export const WarehouseMetadataDialog = ({
16+
onClose,
17+
open,
18+
warehouse,
19+
}: WarehouseMetadataDialogProps) => {
20+
const intl = useIntl();
21+
const { onSubmit, lastSubmittedData, submitInProgress } = useHandleMetadataSubmit({
22+
initialData: warehouse,
23+
onClose,
24+
refetchDocument: WarehouseDetailsDocument,
25+
});
26+
27+
const {
28+
metadataFields,
29+
privateMetadataFields,
30+
metadataErrors,
31+
privateMetadataErrors,
32+
reset,
33+
formIsDirty,
34+
handleChange,
35+
formData,
36+
} = useMetadataForm({
37+
graphqlData: warehouse,
38+
submitInProgress,
39+
lastSubmittedData,
40+
});
41+
42+
useEffect(() => {
43+
if (!open) {
44+
reset();
45+
}
46+
}, [open, reset]);
47+
48+
return (
49+
<MetadataDialog
50+
open={open}
51+
onClose={onClose}
52+
onSave={async () => {
53+
await onSubmit(formData);
54+
}}
55+
title={intl.formatMessage({
56+
defaultMessage: "Warehouse Metadata",
57+
id: "zo5aT3",
58+
})}
59+
data={{
60+
metadata: mapFieldArrayToMetadataInput(metadataFields),
61+
privateMetadata: mapFieldArrayToMetadataInput(privateMetadataFields),
62+
}}
63+
onChange={handleChange}
64+
loading={submitInProgress}
65+
errors={{
66+
metadata: metadataErrors.length ? metadataErrors.join(", ") : undefined,
67+
privateMetadata: privateMetadataErrors.length
68+
? privateMetadataErrors.join(", ")
69+
: undefined,
70+
}}
71+
formIsDirty={formIsDirty}
72+
/>
73+
);
74+
};

src/warehouses/fixtures.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export const warehouse: WarehouseDetailsFragment = {
6666
clickAndCollectOption: WarehouseClickAndCollectOptionEnum.DISABLED,
6767
address,
6868
email,
69+
metadata: [],
70+
privateMetadata: [],
6971
};
7072

7173
export const warehouseForPickup: WarehouseDetailsFragment = {
@@ -74,4 +76,6 @@ export const warehouseForPickup: WarehouseDetailsFragment = {
7476
clickAndCollectOption: WarehouseClickAndCollectOptionEnum.ALL,
7577
address,
7678
email,
79+
metadata: [],
80+
privateMetadata: [],
7781
};

0 commit comments

Comments
 (0)