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/bright-warehouses-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

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.
3 changes: 3 additions & 0 deletions locale/defaultMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -11162,6 +11162,9 @@
"context": "product available for purchase date",
"string": "Will become available on {date}"
},
"zo5aT3": {
"string": "Warehouse Metadata"
},
"zoUlyR": {
"string": "Go to product types"
},
Expand Down
6 changes: 6 additions & 0 deletions src/fragments/warehouses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,11 @@ export const warehouseDetailsFragment = gql`
...Address
}
email
metadata {
...MetadataItem
}
privateMetadata {
...MetadataItem
}
}
`;
9 changes: 8 additions & 1 deletion src/graphql/hooks.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3693,9 +3693,16 @@ export const WarehouseDetailsFragmentDoc = gql`
...Address
}
email
metadata {
...MetadataItem
}
privateMetadata {
...MetadataItem
}
}
${WarehouseWithShippingFragmentDoc}
${AddressFragmentDoc}`;
${AddressFragmentDoc}
${MetadataItemFragmentDoc}`;
export const WebhookDetailsFragmentDoc = gql`
fragment WebhookDetails on Webhook {
...Webhook
Expand Down
8 changes: 4 additions & 4 deletions src/graphql/types.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10570,7 +10570,7 @@ export type WarehouseFragment = { __typename: 'Warehouse', id: string, name: str

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

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 } }> } };
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 } }> } };

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

Expand Down Expand Up @@ -12724,15 +12724,15 @@ export type WarehouseCreateMutationVariables = Exact<{
}>;


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 };
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 };

export type WarehouseUpdateMutationVariables = Exact<{
id: Scalars['ID'];
input: WarehouseUpdateInput;
}>;


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 };
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 };

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


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 };
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 };

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

Expand Down
4 changes: 4 additions & 0 deletions src/ripples/allRipples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { rippleProductAvailabilityDiagnostics } from "@dashboard/products/ripple
import { rippleVariantGenerator } from "@dashboard/products/ripples/variantGenerator";
import { rippleIntroducedRipples } from "@dashboard/ripples/ripples/introducedRipples";
import { Ripple } from "@dashboard/ripples/types";
import { rippleWarehouseMetadata } from "@dashboard/warehouses/ripples/warehouseMetadata";

export const allRipples: Ripple[] = [
// ... register ripples here
Expand All @@ -21,6 +22,9 @@ export const allRipples: Ripple[] = [
rippleOrderMetadata,
rippleRefreshedOrderSections,

// Warehouses
rippleWarehouseMetadata,

// Products
rippleProductAvailabilityDiagnostics,
rippleVariantGenerator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import CardSpacer from "@dashboard/components/CardSpacer";
import CompanyAddressInput from "@dashboard/components/CompanyAddressInput";
import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton";
import Form from "@dashboard/components/Form";
import { iconSize, iconStrokeWidth } from "@dashboard/components/icons";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import { Savebar } from "@dashboard/components/Savebar";
import { AddressTypeInput } from "@dashboard/customers/types";
Expand All @@ -19,9 +20,13 @@ import { useBackLinkWithState } from "@dashboard/hooks/useBackLinkWithState";
import { SubmitPromise } from "@dashboard/hooks/useForm";
import useNavigator from "@dashboard/hooks/useNavigator";
import useStateFromProps from "@dashboard/hooks/useStateFromProps";
import { Ripple } from "@dashboard/ripples/components/Ripple";
import createSingleAutocompleteSelectHandler from "@dashboard/utils/handlers/singleAutocompleteSelectChangeHandler";
import { mapCountriesToChoices, mapEdgesToItems } from "@dashboard/utils/maps";
import { rippleWarehouseMetadata } from "@dashboard/warehouses/ripples/warehouseMetadata";
import { warehouseListPath } from "@dashboard/warehouses/urls";
import { Box, Button } from "@saleor/macaw-ui-next";
import { Code } from "lucide-react";
import { useIntl } from "react-intl";

import WarehouseInfo from "../WarehouseInfo";
Expand All @@ -40,6 +45,7 @@ interface WarehouseDetailsPageProps {
saveButtonBarState: ConfirmButtonTransitionState;
warehouse: WarehouseDetailsFragment | undefined;
onDelete: () => void;
onShowMetadata: () => void;
onSubmit: (data: WarehouseDetailsPageFormData) => SubmitPromise;
}

Expand All @@ -50,6 +56,7 @@ const WarehouseDetailsPage = ({
saveButtonBarState,
warehouse,
onDelete,
onShowMetadata,
onSubmit,
}: WarehouseDetailsPageProps) => {
const intl = useIntl();
Expand Down Expand Up @@ -91,7 +98,20 @@ const WarehouseDetailsPage = ({

return (
<DetailPageLayout>
<TopNav href={warehouseListBackLink} title={warehouse?.name} />
<TopNav href={warehouseListBackLink} title={warehouse?.name}>
<Box position="relative">
<Button
variant="secondary"
icon={<Code size={iconSize.medium} strokeWidth={iconStrokeWidth} />}
onClick={onShowMetadata}
data-test-id="show-warehouse-metadata"
title="Edit warehouse metadata"
/>
<Box position="absolute" __top="-4px" __right="-4px">
<Ripple model={rippleWarehouseMetadata} />
</Box>
</Box>
</TopNav>
<DetailPageLayout.Content>
<WarehouseInfo data={data} disabled={disabled} errors={errors} onChange={change} />
<CardSpacer />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { WarehouseDetailsFragment } from "@dashboard/graphql";
import { warehouse } from "@dashboard/warehouses/fixtures";
import { fireEvent, render, screen, within } from "@testing-library/react";

import { WarehouseMetadataDialog } from "./WarehouseMetadataDialog";

const mockOnSubmit = jest.fn();

jest.mock("@dashboard/components/MetadataDialog/useHandleMetadataSubmit", () => ({
useHandleMetadataSubmit: jest.fn(() => ({
onSubmit: mockOnSubmit,
lastSubmittedData: undefined,
submitInProgress: false,
})),
}));

const mockWarehouse: WarehouseDetailsFragment = {
...warehouse,
metadata: [{ key: "test-key", value: "test-value", __typename: "MetadataItem" }],
privateMetadata: [{ key: "private-key", value: "private-value", __typename: "MetadataItem" }],
};

describe("WarehouseMetadataDialog", () => {
const onCloseMock = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

it("renders dialog with correct title when open", () => {
// Arrange & Act
render(<WarehouseMetadataDialog open={true} onClose={onCloseMock} warehouse={mockWarehouse} />);

// Assert
expect(screen.getByText("Warehouse Metadata")).toBeInTheDocument();
});

it("does not render when open is false", () => {
// Arrange & Act
render(
<WarehouseMetadataDialog open={false} onClose={onCloseMock} warehouse={mockWarehouse} />,
);

// Assert
expect(screen.queryByText("Warehouse Metadata")).not.toBeInTheDocument();
});

it("closes when user clicks close button", () => {
// Arrange
render(<WarehouseMetadataDialog open={true} onClose={onCloseMock} warehouse={mockWarehouse} />);

// Act
fireEvent.click(screen.getByTestId("back"));

// Assert
expect(onCloseMock).toHaveBeenCalled();
});

it("renders with undefined warehouse", () => {
// Arrange & Act
render(<WarehouseMetadataDialog open={true} onClose={onCloseMock} warehouse={undefined} />);

// Assert
expect(screen.getByText("Warehouse Metadata")).toBeInTheDocument();
});

it("displays metadata when section is expanded", () => {
// Arrange
render(<WarehouseMetadataDialog open={true} onClose={onCloseMock} warehouse={mockWarehouse} />);

const metadataEditors = screen.getAllByTestId("metadata-editor");
const publicMetadataEditor = metadataEditors.find(
editor => editor.getAttribute("data-test-is-private") === "false",
)!;

// Act - expand metadata section
const expandButton = within(publicMetadataEditor).getByTestId("expand");

fireEvent.click(expandButton);

// Assert - check metadata values after expansion
expect(within(publicMetadataEditor).getByDisplayValue("test-key")).toBeInTheDocument();
expect(within(publicMetadataEditor).getByDisplayValue("test-value")).toBeInTheDocument();
});

it("displays private metadata when section is expanded", () => {
// Arrange
render(<WarehouseMetadataDialog open={true} onClose={onCloseMock} warehouse={mockWarehouse} />);

const metadataEditors = screen.getAllByTestId("metadata-editor");
const privateMetadataEditor = metadataEditors.find(
editor => editor.getAttribute("data-test-is-private") === "true",
)!;

// Act - expand private metadata section
const expandButton = within(privateMetadataEditor).getByTestId("expand");

fireEvent.click(expandButton);

// Assert - check private metadata values after expansion
expect(within(privateMetadataEditor).getByDisplayValue("private-key")).toBeInTheDocument();
expect(within(privateMetadataEditor).getByDisplayValue("private-value")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { MetadataDialog } from "@dashboard/components/MetadataDialog/MetadataDialog";
import { useHandleMetadataSubmit } from "@dashboard/components/MetadataDialog/useHandleMetadataSubmit";
import { useMetadataForm } from "@dashboard/components/MetadataDialog/useMetadataForm";
import { mapFieldArrayToMetadataInput } from "@dashboard/components/MetadataDialog/validation";
import { WarehouseDetailsDocument, WarehouseDetailsFragment } from "@dashboard/graphql";
import { useEffect } from "react";
import { useIntl } from "react-intl";

interface WarehouseMetadataDialogProps {
open: boolean;
onClose: () => void;
warehouse: WarehouseDetailsFragment | undefined;
}

export const WarehouseMetadataDialog = ({
onClose,
open,
warehouse,
}: WarehouseMetadataDialogProps) => {
const intl = useIntl();
const { onSubmit, lastSubmittedData, submitInProgress } = useHandleMetadataSubmit({
initialData: warehouse,
onClose,
refetchDocument: WarehouseDetailsDocument,
});

const {
metadataFields,
privateMetadataFields,
metadataErrors,
privateMetadataErrors,
reset,
formIsDirty,
handleChange,
formData,
} = useMetadataForm({
graphqlData: warehouse,
submitInProgress,
lastSubmittedData,
});

useEffect(() => {
if (!open) {
reset();
}
}, [open, reset]);

return (
<MetadataDialog
open={open}
onClose={onClose}
onSave={async () => {
await onSubmit(formData);
}}
title={intl.formatMessage({
defaultMessage: "Warehouse Metadata",
id: "zo5aT3",
})}
data={{
metadata: mapFieldArrayToMetadataInput(metadataFields),
privateMetadata: mapFieldArrayToMetadataInput(privateMetadataFields),
}}
onChange={handleChange}
loading={submitInProgress}
errors={{
metadata: metadataErrors.length ? metadataErrors.join(", ") : undefined,
privateMetadata: privateMetadataErrors.length
? privateMetadataErrors.join(", ")
: undefined,
}}
formIsDirty={formIsDirty}
/>
);
};
4 changes: 4 additions & 0 deletions src/warehouses/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export const warehouse: WarehouseDetailsFragment = {
clickAndCollectOption: WarehouseClickAndCollectOptionEnum.DISABLED,
address,
email,
metadata: [],
privateMetadata: [],
};

export const warehouseForPickup: WarehouseDetailsFragment = {
Expand All @@ -74,4 +76,6 @@ export const warehouseForPickup: WarehouseDetailsFragment = {
clickAndCollectOption: WarehouseClickAndCollectOptionEnum.ALL,
address,
email,
metadata: [],
privateMetadata: [],
};
15 changes: 15 additions & 0 deletions src/warehouses/ripples/warehouseMetadata.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Ripple } from "@dashboard/ripples/types";

export const rippleWarehouseMetadata: Ripple = {
type: "feature",
ID: "warehouse-metadata",
TTL_seconds: 60 * 60 * 24 * 3, // 3 days
content: {
oneLiner: "Warehouse metadata editing",
contextual:
"Warehouse metadata can now be edited via this button which opens a dedicated dialog",
global:
"You can now edit warehouse metadata through a dedicated dialog, allowing you to manage both public and private metadata for your warehouses.",
},
dateAdded: new Date("2026-01-27"),
};
Loading
Loading