Attach metadata to the fulfillments within the orders (#3667)

* Metadata for fulfillment

* Metadata for fulfillment

* Trigger deploy

* Fix removing priv metadata

* Remove blinks

* tests for adding, deleteing and updating public and prvate metadata for fullfilled orders (#3684)

---------

Co-authored-by: wojteknowacki <124166231+wojteknowacki@users.noreply.github.com>
Co-authored-by: wojteknowacki <wojciech.nowacki@saleor.io>
This commit is contained in:
Patryk Andrzejewski 2023-05-23 13:51:56 +02:00 committed by GitHub
parent ed8b75a9b3
commit 730c96db88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 693 additions and 143 deletions

View file

@ -9,41 +9,43 @@ import {
ORDERS_SELECTORS,
SHARED_ELEMENTS,
} from "../../elements/";
import { MESSAGES } from "../../fixtures";
import { urlList } from "../../fixtures/urlList";
import { ONE_PERMISSION_USERS } from "../../fixtures/users";
import { MESSAGES, ONE_PERMISSION_USERS, urlList } from "../../fixtures";
import {
createCustomer,
deleteCustomersStartsWith,
} from "../../support/api/requests/Customer";
import {
getOrder,
updateMetadata,
updateOrdersSettings,
} from "../../support/api/requests/Order";
import { getDefaultChannel } from "../../support/api/utils/channelsUtils";
updatePrivateMetadata,
} from "../../support/api/requests/";
import {
createFulfilledOrder,
createOrder,
createReadyToFulfillOrder,
createUnconfirmedOrder,
} from "../../support/api/utils/ordersUtils";
import * as productsUtils from "../../support/api/utils/products/productsUtils";
import {
createShipping,
createUnconfirmedOrder,
deleteShippingStartsWith,
} from "../../support/api/utils/shippingUtils";
import {
getDefaultChannel,
getDefaultTaxClass,
productsUtils,
updateTaxConfigurationForChannel,
} from "../../support/api/utils/taxesUtils";
import { selectChannelInPicker } from "../../support/pages/channelsPage";
import { finalizeDraftOrder } from "../../support/pages/draftOrderPage";
} from "../../support/api/utils/";
import {
addNewProductToOrder,
addPrivateMetadataFieldFulfillmentOrder,
addPublicMetadataFieldFulfillmentOrder,
applyFixedLineDiscountForProduct,
changeQuantityOfProducts,
deletePrivateFulfillmentMetadata,
deleteProductFromGridTableOnIndex,
} from "../../support/pages/ordersOperations";
deletePublicFulfillmentMetadata,
expandPrivateFulfillmentMetadata,
expandPublicFulfillmentMetadata,
finalizeDraftOrder,
selectChannelInPicker,
updatePrivateMetadataFieldFulfillmentOrder,
updatePublicMetadataFieldFulfillmentOrder,
} from "../../support/pages/";
describe("Orders", () => {
const startsWith = "CyOrders-";
@ -59,6 +61,16 @@ describe("Orders", () => {
const shippingPrice = 2;
const variantPrice = 1;
const metadataName = randomName + "- metadata name";
const metadataValue = randomName + "- metadata value";
const privateMetadataName = randomName + "- private metadata name";
const privateMetadataValue = randomName + "- private metadata value";
const updatedMetadataName = metadataName + "- updated metadata name";
const updatedMetadataValue = metadataValue + "- updated metadata value";
const updatedPrivateMetadataName =
privateMetadataName + "- updated private metadata name";
const updatedPrivateMetadataValue =
privateMetadataValue + "- updated private metadata value";
before(() => {
cy.clearSessionData().loginUserViaRequest();
@ -335,4 +347,181 @@ describe("Orders", () => {
});
},
);
it(
"should create metadata and private metadata for fulfilled order . TC: SALEOR_2129",
{ tags: ["@orders", "@allEnv", "@stable"] },
() => {
let order;
cy.addAliasToGraphRequest("UpdateMetadata");
cy.addAliasToGraphRequest("UpdatePrivateMetadata");
createFulfilledOrder({
customerId: customer.id,
channelId: defaultChannel.id,
shippingMethod,
variantsList,
address,
warehouse: warehouse.id,
})
.then(({ order: orderResp }) => {
order = orderResp;
cy.visit(urlList.orders + `${order.id}`);
addPublicMetadataFieldFulfillmentOrder(
0,
metadataName,
metadataValue,
);
addPrivateMetadataFieldFulfillmentOrder(
0,
privateMetadataName,
privateMetadataValue,
);
})
.then(() => {
cy.clickConfirmButton()
.waitForRequestAndCheckIfNoErrors("@UpdateMetadata")
.waitForRequestAndCheckIfNoErrors("@UpdatePrivateMetadata");
cy.confirmationMessageShouldAppear();
getOrder(order.id).then(orderResponse => {
// check to see updated fulfillment metadata and private meta data
cy.wrap(orderResponse.fulfillments[0].metadata[0]).should(
"deep.equal",
{
key: metadataName,
value: metadataValue,
},
);
cy.wrap(orderResponse.fulfillments[0].privateMetadata[0]).should(
"deep.equal",
{
key: privateMetadataName,
value: privateMetadataValue,
},
);
});
});
},
);
it(
"should update metadata and private metadata for fulfilled order . TC: SALEOR_2130",
{ tags: ["@orders", "@allEnv", "@stable"] },
() => {
cy.addAliasToGraphRequest("UpdateMetadata");
cy.addAliasToGraphRequest("UpdatePrivateMetadata");
createFulfilledOrder({
customerId: customer.id,
channelId: defaultChannel.id,
shippingMethod,
variantsList,
address,
warehouse: warehouse.id,
}).then(orderResp => {
getOrder(orderResp.order.id).then(orderDetails => {
updateMetadata(
orderDetails.fulfillments[0].id,
metadataName,
metadataValue,
).then(() => {
updatePrivateMetadata(
orderDetails.fulfillments[0].id,
privateMetadataName,
privateMetadataValue,
)
.then(() => {
cy.visit(urlList.orders + `${orderResp.order.id}`);
updatePublicMetadataFieldFulfillmentOrder(
0,
updatedMetadataName,
updatedMetadataValue,
);
updatePrivateMetadataFieldFulfillmentOrder(
0,
updatedPrivateMetadataName,
updatedPrivateMetadataValue,
);
})
.then(() => {
cy.clickConfirmButton()
.waitForRequestAndCheckIfNoErrors("@UpdateMetadata")
.waitForRequestAndCheckIfNoErrors("@UpdatePrivateMetadata");
cy.confirmationMessageShouldAppear();
getOrder(orderResp.order.id).then(orderResponse => {
// check to see updated fulfillment metadata and private meta data
cy.wrap(orderResponse.fulfillments[0].metadata[0]).should(
"deep.equal",
{
key: updatedMetadataName,
value: updatedMetadataValue,
},
);
cy.wrap(
orderResponse.fulfillments[0].privateMetadata[0],
).should("deep.equal", {
key: updatedPrivateMetadataName,
value: updatedPrivateMetadataValue,
});
});
});
});
});
});
},
);
it(
"should delete metadata and private metadata for fulfilled order . TC: SALEOR_2131",
{ tags: ["@orders", "@allEnv", "@stable"] },
() => {
cy.addAliasToGraphRequest("UpdateMetadata");
cy.addAliasToGraphRequest("UpdatePrivateMetadata");
createFulfilledOrder({
customerId: customer.id,
channelId: defaultChannel.id,
shippingMethod,
variantsList,
address,
warehouse: warehouse.id,
}).then(orderResp => {
getOrder(orderResp.order.id).then(orderDetails => {
updateMetadata(
orderDetails.fulfillments[0].id,
metadataName,
metadataValue,
).then(() => {
updatePrivateMetadata(
orderDetails.fulfillments[0].id,
privateMetadataName,
privateMetadataValue,
)
.then(() => {
cy.visit(urlList.orders + `${orderResp.order.id}`);
expandPublicFulfillmentMetadata(0);
deletePublicFulfillmentMetadata(0, 0);
expandPrivateFulfillmentMetadata(0);
deletePrivateFulfillmentMetadata(0, 0);
})
.then(() => {
cy.clickConfirmButton()
.waitForRequestAndCheckIfNoErrors("@UpdateMetadata")
.waitForRequestAndCheckIfNoErrors("@UpdatePrivateMetadata");
cy.confirmationMessageShouldAppear();
cy.contains(privateMetadataName).should("not.exist");
cy.contains(metadataName).should("not.exist");
getOrder(orderResp.order.id).then(orderResponse => {
// check to see updated fulfillment metadata and private meta data
cy.wrap(orderResponse.fulfillments[0].metadata).should(
"deep.equal",
[],
);
cy.wrap(orderResponse.fulfillments[0].privateMetadata).should(
"deep.equal",
[],
);
});
});
});
});
});
},
);
});

View file

@ -5,5 +5,10 @@ export const METADATA_FORM = {
"[data-test-id='metadata-editor'][data-test-is-private='true']",
addFieldButton: "[data-test-id='add-field']",
nameInput: "[name*='name']",
valueField: "[name*='value']"
valueField: "[name*='value']",
metaExpandButton: "[data-test-id='expand']",
metaDeletedButton: "[data-test-id='delete-field-0']",
privateMetaSection: "[data-test-is-private='true']",
publicMetaSection: "[data-test-is-private='false']",
fulfillmentMetaSection: "[data-test-id='fulfilled-order-section']",
};

View file

@ -155,6 +155,17 @@ export function getOrder(orderId) {
countryArea
phone
}
fulfillments{
id
metadata{
key
value
}
privateMetadata{
key
value
}
}
}
}`;
return cy.sendRequestWithQuery(query).its("body.data.order");

View file

@ -1,6 +1,6 @@
export { createChannel, updateChannelOrderSettings } from "./Channels";
export { createCustomer, deleteCustomersStartsWith } from "./Customer";
export { createDraftOrder, getOrder } from "./Order";
export { createDraftOrder, getOrder, updateOrdersSettings } from "./Order";
export { updateMetadata, updatePrivateMetadata } from "./Metadata";
export { getProductMetadata } from "./storeFront/ProductDetails";
export { activatePlugin, updatePlugin } from "./Plugins";

View file

@ -1,11 +1,17 @@
export { deleteChannelsStartsWith, getDefaultChannel } from "./channelsUtils";
export { createOrder, createReadyToFulfillOrder } from "./ordersUtils";
export { createShipping, deleteShippingStartsWith } from "./shippingUtils";
export {
getDefaultTaxClass,
updateTaxConfigurationForChannel,
} from "./taxesUtils";
export * as productsUtils from "./products/productsUtils";
export {
createFulfilledOrder,
createOrder,
createReadyToFulfillOrder,
createUnconfirmedOrder,
} from "./ordersUtils";
export {
getMailActivationLinkForUser,
getMailActivationLinkForUserAndSubject,

View file

@ -20,6 +20,9 @@ Cypress.Commands.add("clickPrevPagePaginationButton", () =>
Cypress.Commands.add("clickSubmitButton", () =>
cy.get(BUTTON_SELECTORS.submit).click(),
);
Cypress.Commands.add("clickConfirmButton", () =>
cy.get(BUTTON_SELECTORS.confirm).click(),
);
Cypress.Commands.add("createNewOption", (selectSelector, newOption) => {
cy.get(selectSelector).type(newOption);

View file

@ -2,7 +2,7 @@ import { METADATA_FORM } from "../../../elements/shared/metadata/metadata-form";
export const metadataForms = {
private: METADATA_FORM.privateMetadataForm,
public: METADATA_FORM.metadataForm
public: METADATA_FORM.metadataForm,
};
export function addMetadataField({ metadataForm, name, value }) {
@ -17,3 +17,123 @@ export function addMetadataField({ metadataForm, name, value }) {
.find(METADATA_FORM.valueField)
.type(value);
}
export function addPublicMetadataFieldFulfillmentOrder(
fulfillmentIndex,
name,
value,
) {
expandPublicFulfillmentMetadata(fulfillmentIndex);
cy.get('[data-test-id="fulfilled-order-section"]')
.eq(fulfillmentIndex)
.find('[data-test-is-private="false"]')
.find(METADATA_FORM.addFieldButton)
.click();
typePublicFulfillmentMetadataName(name, 0);
typePublicFulfillmentMetadataValue(value, 0);
}
export function updatePublicMetadataFieldFulfillmentOrder(
fulfillmentIndex,
name,
value,
) {
expandPublicFulfillmentMetadata(fulfillmentIndex);
typePublicFulfillmentMetadataName(name, 0);
typePublicFulfillmentMetadataValue(value, 0);
}
export function typePublicFulfillmentMetadataValue(name, valueIndex) {
return cy
.get(METADATA_FORM.publicMetaSection)
.find(METADATA_FORM.valueField)
.eq(valueIndex)
.clear()
.type(name);
}
export function typePrivateFulfillmentMetadataValue(name, valueIndex) {
return cy
.get(METADATA_FORM.privateMetaSection)
.find(METADATA_FORM.valueField)
.eq(valueIndex)
.clear()
.type(name);
}
export function typePublicFulfillmentMetadataName(name, nameIndex) {
return cy
.get(METADATA_FORM.publicMetaSection)
.find(METADATA_FORM.nameInput)
.eq(nameIndex)
.clear()
.type(name);
}
export function typePrivateFulfillmentMetadataName(name, nameIndex) {
return cy
.get(METADATA_FORM.privateMetaSection)
.find(METADATA_FORM.nameInput)
.eq(nameIndex)
.clear()
.type(name);
}
export function expandPublicFulfillmentMetadata(fulfillmentIndex) {
return cy
.get(METADATA_FORM.fulfillmentMetaSection)
.eq(fulfillmentIndex)
.find(METADATA_FORM.publicMetaSection)
.find(METADATA_FORM.metaExpandButton)
.click();
}
export function deletePublicFulfillmentMetadata(
fulfillmentIndex,
metaDataIndex,
) {
return cy
.get(METADATA_FORM.fulfillmentMetaSection)
.eq(fulfillmentIndex)
.find(METADATA_FORM.publicMetaSection)
.find(METADATA_FORM.metaDeletedButton)
.eq(metaDataIndex)
.click();
}
export function deletePrivateFulfillmentMetadata(
fulfillmentIndex,
metaDataIndex,
) {
return cy
.get(METADATA_FORM.fulfillmentMetaSection)
.eq(fulfillmentIndex)
.find(METADATA_FORM.privateMetaSection)
.find(METADATA_FORM.metaDeletedButton)
.eq(metaDataIndex)
.click();
}
export function expandPrivateFulfillmentMetadata(fulfillmentIndex) {
return cy
.get(METADATA_FORM.fulfillmentMetaSection)
.eq(fulfillmentIndex)
.find(METADATA_FORM.privateMetaSection)
.find(METADATA_FORM.metaExpandButton)
.click();
}
export function addPrivateMetadataFieldFulfillmentOrder(
fulfillmentIndex,
name,
value,
) {
expandPrivateFulfillmentMetadata(fulfillmentIndex);
cy.get(METADATA_FORM.fulfillmentMetaSection)
.eq(fulfillmentIndex)
.find(METADATA_FORM.privateMetaSection)
.find(METADATA_FORM.addFieldButton)
.click();
typePrivateFulfillmentMetadataName(name, 0);
typePrivateFulfillmentMetadataValue(value, 0);
}
export function updatePrivateMetadataFieldFulfillmentOrder(
fulfillmentIndex,
name,
value,
) {
expandPrivateFulfillmentMetadata(fulfillmentIndex);
typePrivateFulfillmentMetadataName(name, 0);
typePrivateFulfillmentMetadataValue(value, 0);
}

View file

@ -1,5 +1,24 @@
export * as ordersOperationsHelpers from "./ordersOperations";
export * as transactionsOrderUtils from "./ordersTransactionUtils";
export {
addMetadataField,
addPrivateMetadataFieldFulfillmentOrder,
addPublicMetadataFieldFulfillmentOrder,
deletePrivateFulfillmentMetadata,
deletePublicFulfillmentMetadata,
expandPrivateFulfillmentMetadata,
expandPublicFulfillmentMetadata,
updatePrivateMetadataFieldFulfillmentOrder,
updatePublicMetadataFieldFulfillmentOrder,
} from "./catalog/metadataComponent";
export { selectChannelInPicker } from "./channelsPage";
export { finalizeDraftOrder } from "./draftOrderPage";
export {
addNewProductToOrder,
applyFixedLineDiscountForProduct,
changeQuantityOfProducts,
deleteProductFromGridTableOnIndex,
} from "./ordersOperations";
export { expectWelcomeMessageIncludes } from "./homePage";
export { getDisplayedSelectors } from "./permissionsPage";
export {

View file

@ -2,7 +2,7 @@ import { MetadataInput } from "@dashboard/graphql";
import { ChangeEvent } from "@dashboard/hooks/useForm";
import { removeAtIndex, updateAtIndex } from "@dashboard/utils/lists";
import { Box } from "@saleor/macaw-ui/next";
import React from "react";
import React, { memo } from "react";
import { MetadataCard, MetadataCardProps } from "./MetadataCard";
import { EventDataAction, EventDataField } from "./types";
@ -11,9 +11,24 @@ import { getDataKey, parseEventData } from "./utils";
export interface MetadataProps
extends Omit<MetadataCardProps, "data" | "isPrivate"> {
data: Record<"metadata" | "privateMetadata", MetadataInput[]>;
isLoading?: boolean;
}
export const Metadata: React.FC<MetadataProps> = ({ data, onChange }) => {
const propsCompare = (_, newProps: MetadataProps) => {
/**
If we pass `isLoading` render only when the loading finishes
*/
if (typeof newProps.isLoading !== "undefined") {
return newProps.isLoading;
}
/*
If `isLoading` is not present, keep the old behavior
*/
return false;
};
export const Metadata: React.FC<MetadataProps> = memo(({ data, onChange }) => {
const change = (event: ChangeEvent, isPrivate: boolean) => {
const { action, field, fieldIndex, value } = parseEventData(event);
const key = getDataKey(isPrivate);
@ -66,4 +81,4 @@ export const Metadata: React.FC<MetadataProps> = ({ data, onChange }) => {
/>
</Box>
);
};
}, propsCompare);

View file

@ -19,3 +19,10 @@ export interface MetadataFormData {
metadata: MetadataInput[];
privateMetadata: MetadataInput[];
}
export interface MetadataIdSchema {
[key: string]: {
metadata: MetadataInput[];
privateMetadata: MetadataInput[];
};
}

View file

@ -156,6 +156,7 @@ export const fragmentRefundOrderLine = gql`
export const fulfillmentFragment = gql`
fragment Fulfillment on Fulfillment {
...Metadata
id
lines {
id

View file

@ -1603,6 +1603,7 @@ export const OrderLineFragmentDoc = gql`
${TaxedMoneyFragmentDoc}`;
export const FulfillmentFragmentDoc = gql`
fragment Fulfillment on Fulfillment {
...Metadata
id
lines {
id
@ -1619,7 +1620,8 @@ export const FulfillmentFragmentDoc = gql`
name
}
}
${OrderLineFragmentDoc}`;
${MetadataFragmentDoc}
${OrderLineFragmentDoc}`;
export const InvoiceFragmentDoc = gql`
fragment Invoice on Invoice {
id

File diff suppressed because one or more lines are too long

View file

@ -9,7 +9,7 @@ import { CardSpacer } from "@dashboard/components/CardSpacer";
import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
import { Metadata, MetadataIdSchema } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import {
OrderDetailsFragment,
@ -22,8 +22,6 @@ import { SubmitPromise } from "@dashboard/hooks/useForm";
import useNavigator from "@dashboard/hooks/useNavigator";
import { defaultGraphiQLQuery } from "@dashboard/orders/queries";
import { orderListUrl } from "@dashboard/orders/urls";
import { mapMetadataItemToInput } from "@dashboard/utils/maps";
import useMetadataChangeTrigger from "@dashboard/utils/metadata/useMetadataChangeTrigger";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import React from "react";
import { useIntl } from "react-intl";
@ -41,7 +39,12 @@ import { OrderPaymentOrTransaction } from "../OrderPaymentOrTransaction/OrderPay
import OrderUnfulfilledProductsCard from "../OrderUnfulfilledProductsCard";
import { messages } from "./messages";
import Title from "./Title";
import { filteredConditionalItems, hasAnyItemsReplaceable } from "./utils";
import {
createMetadataHandler,
createOrderMetadataIdSchema,
filteredConditionalItems,
hasAnyItemsReplaceable,
} from "./utils";
export interface OrderDetailsPageProps {
order: OrderDetailsFragment | OrderDetailsFragment;
@ -80,7 +83,7 @@ export interface OrderDetailsPageProps {
onInvoiceSend(invoiceId: string);
onTransactionAction(transactionId: string, actionType: TransactionActionEnum);
onAddManualTransaction();
onSubmit(data: MetadataFormData): SubmitPromise;
onSubmit(data: MetadataIdSchema): SubmitPromise;
}
const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
@ -118,13 +121,6 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
const navigate = useNavigator();
const intl = useIntl();
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler,
resetMetadataChanged,
} = useMetadataChangeTrigger();
const isOrderUnconfirmed = order?.status === OrderStatus.UNCONFIRMED;
const canCancel = order?.status !== OrderStatus.CANCELED;
const canEditAddresses = order?.status !== OrderStatus.CANCELED;
@ -137,24 +133,12 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
line => line.quantityToFulfill > 0,
);
const handleSubmit = async (data: MetadataFormData) => {
const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
const result = await onSubmit({
metadata,
privateMetadata,
});
resetMetadataChanged();
const handleSubmit = async (data: MetadataIdSchema) => {
const result = await onSubmit(data);
return getMutationErrors(result);
};
const initial: MetadataFormData = {
metadata: order?.metadata.map(mapMetadataItemToInput),
privateMetadata: order?.privateMetadata.map(mapMetadataItemToInput),
};
const initial = createOrderMetadataIdSchema(order);
const saveLabel = isOrderUnconfirmed
? { confirm: intl.formatMessage(messages.confirmOrder) }
@ -204,9 +188,18 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
};
return (
<Form confirmLeave initial={initial} onSubmit={handleSubmit}>
{({ change, data, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change);
<Form
confirmLeave
initial={initial}
onSubmit={handleSubmit}
mergeData={false}
>
{({ set, triggerChange, data, submit }) => {
const handleChangeMetadata = createMetadataHandler(
data,
set,
triggerChange,
);
return (
<DetailPageLayout>
@ -248,8 +241,9 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
</>
)}
{order?.fulfillments?.map(fulfillment => (
<React.Fragment key={fulfillment.id}>
<OrderFulfilledProductsCard
dataTestId="fulfilled-order-section"
key={fulfillment.id}
fulfillment={fulfillment}
fulfillmentAllowUnpaid={shop?.fulfillmentAllowUnpaid}
order={order}
@ -262,8 +256,13 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
onOrderFulfillmentApprove={() =>
onFulfillmentApprove(fulfillment.id)
}
>
<Metadata
isLoading={loading}
data={data[fulfillment.id]}
onChange={x => handleChangeMetadata(x, fulfillment.id)}
/>
</React.Fragment>
</OrderFulfilledProductsCard>
))}
<OrderPaymentOrTransaction
order={order}
@ -275,7 +274,11 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
onMarkAsPaid={onMarkAsPaid}
onAddManualTransaction={onAddManualTransaction}
/>
<Metadata data={data} onChange={changeMetadata} />
<Metadata
isLoading={loading}
data={data[order?.id]}
onChange={x => handleChangeMetadata(x, order?.id)}
/>
<OrderHistory
history={order?.events}
orderCurrency={order?.total?.gross.currency}

View file

@ -1,4 +1,11 @@
import { filteredConditionalItems } from "./utils";
import { OrderDetailsFragment } from "@dashboard/graphql";
import { ChangeEvent } from "@dashboard/hooks/useForm";
import {
createMetadataHandler,
createOrderMetadataIdSchema,
filteredConditionalItems,
} from "./utils";
describe("filteredConditionalItems", () => {
it("should return empty [] when no items has shouldExist set to true", () => {
@ -42,3 +49,110 @@ describe("filteredConditionalItems", () => {
]);
});
});
describe("createOrderMetadataIdSchema", () => {
it("returns order and fulfilment metadata", () => {
// Arrange
const order = {
id: "some-order-id",
metadata: [{ key: "mt1", value: "mt1-value" }],
privateMetadata: [{ key: "pmt1", value: "pmt1-value" }],
fulfillments: [
{
id: "some-fulfillment-id",
metadata: [{ key: "fmt1", value: "fmt1-value" }],
privateMetadata: [{ key: "fpmt1", value: "fpmt1-value" }],
},
{
id: "some-fulfillment-id",
metadata: [{ key: "fmt1", value: "fmt1-value" }],
privateMetadata: [{ key: "fpmt1", value: "fpmt1-value" }],
},
],
};
// Act
const metadata = createOrderMetadataIdSchema(order as OrderDetailsFragment);
// Assert
expect(metadata).toEqual({
"some-fulfillment-id": {
metadata: [
{
key: "fmt1",
value: "fmt1-value",
},
],
privateMetadata: [
{
key: "fpmt1",
value: "fpmt1-value",
},
],
},
"some-order-id": {
metadata: [
{
key: "mt1",
value: "mt1-value",
},
],
privateMetadata: [
{
key: "pmt1",
value: "pmt1-value",
},
],
},
});
});
});
describe("createMetadataHandler", () => {
it("handles order metadata change", () => {
// Arrange
const currentData = {
"some-order-id": {
metadata: [{ key: "mt1", value: "mt1-value" }],
privateMetadata: [{ key: "pmt1", value: "pmt1-value" }],
},
"some-fulfillment-id": {
metadata: [{ key: "fmt1", value: "fmt1-value" }],
privateMetadata: [{ key: "fpmt1", value: "fpmt1-value" }],
},
};
const set = jest.fn();
const triggerChange = jest.fn();
const handler = createMetadataHandler(currentData, set, triggerChange);
// Act
handler(
{
target: {
name: "metadata",
value: [{ key: "new-key", value: "new-value" }],
},
} as ChangeEvent,
"some-order-id",
);
// Assert
expect(set).toHaveBeenCalledWith({
"some-order-id": {
metadata: [
{
key: "new-key",
value: "new-value",
},
],
privateMetadata: [
{
key: "pmt1",
value: "pmt1-value",
},
],
},
});
expect(triggerChange).toBeCalled();
});
});

View file

@ -1,4 +1,7 @@
import { MetadataIdSchema } from "@dashboard/components/Metadata";
import { OrderDetailsFragment } from "@dashboard/graphql";
import { ChangeEvent } from "@dashboard/hooks/useForm";
import { mapMetadataItemToInput } from "@dashboard/utils/maps";
import {
getFulfilledFulfillemnts,
@ -29,3 +32,37 @@ export interface ConditionalItem {
export const filteredConditionalItems = (items: ConditionalItem[]) =>
items.filter(({ shouldExist }) => shouldExist).map(({ item }) => item);
export const createOrderMetadataIdSchema = (
order: OrderDetailsFragment,
): MetadataIdSchema => ({
[order?.id]: {
metadata: order?.metadata.map(mapMetadataItemToInput),
privateMetadata: order?.privateMetadata.map(mapMetadataItemToInput),
},
...order?.fulfillments.reduce((p, c) => {
p[c.id] = {
metadata: c?.metadata.map(mapMetadataItemToInput),
privateMetadata: c?.privateMetadata.map(mapMetadataItemToInput),
};
return p;
}, {}),
});
export const createMetadataHandler =
(
currentData: MetadataIdSchema,
set: (newData: Partial<MetadataIdSchema>) => void,
triggerChange: () => void,
) =>
(event: ChangeEvent, objectId: string) => {
const metadataType = event.target.name;
set({
[objectId]: {
...currentData[objectId],
[metadataType]: [...event.target.value],
},
});
triggerChange();
};

View file

@ -1,10 +1,10 @@
import CardSpacer from "@dashboard/components/CardSpacer";
import { FulfillmentStatus, OrderDetailsFragment } from "@dashboard/graphql";
import TrashIcon from "@dashboard/icons/Trash";
import { orderHasTransactions } from "@dashboard/orders/types";
import { mergeRepeatedOrderLines } from "@dashboard/orders/utils/data";
import { Card, CardContent } from "@material-ui/core";
import { CardContent } from "@material-ui/core";
import { IconButton } from "@saleor/macaw-ui";
import { Box, Divider } from "@saleor/macaw-ui/next";
import React from "react";
import OrderCardTitle from "../OrderCardTitle";
@ -20,6 +20,7 @@ interface OrderFulfilledProductsCardProps {
onOrderFulfillmentApprove: () => void;
onOrderFulfillmentCancel: () => void;
onTrackingCodeAdd: () => void;
dataTestId?: string;
}
const statusesToMergeLines = [
@ -43,6 +44,7 @@ const OrderFulfilledProductsCard: React.FC<
onOrderFulfillmentApprove,
onOrderFulfillmentCancel,
onTrackingCodeAdd,
dataTestId,
} = props;
const classes = useStyles(props);
@ -61,8 +63,7 @@ const OrderFulfilledProductsCard: React.FC<
};
return (
<>
<Card>
<Box data-test-id={dataTestId}>
<OrderCardTitle
withStatus
lines={fulfillment?.lines}
@ -71,7 +72,8 @@ const OrderFulfilledProductsCard: React.FC<
warehouseName={fulfillment?.warehouse?.name}
orderNumber={order?.number}
toolbar={
cancelableStatuses.includes(fulfillment?.status) && (
<Box display="flex" alignItems="center" gap={6}>
{cancelableStatuses.includes(fulfillment?.status) && (
<IconButton
variant="secondary"
className={classes.deleteIcon}
@ -80,12 +82,7 @@ const OrderFulfilledProductsCard: React.FC<
>
<TrashIcon />
</IconButton>
)
}
/>
<CardContent>
<OrderDetailsDatagrid lines={getLines()} loading={false} />
<ExtraInfoLines fulfillment={fulfillment} />
)}
<ActionButtons
orderId={order?.id}
status={fulfillment?.status}
@ -96,10 +93,16 @@ const OrderFulfilledProductsCard: React.FC<
onApprove={onOrderFulfillmentApprove}
hasTransactions={orderHasTransactions(order)}
/>
</Box>
}
/>
<CardContent>
<OrderDetailsDatagrid lines={getLines()} loading={false} />
<ExtraInfoLines fulfillment={fulfillment} />
</CardContent>
</Card>
<CardSpacer />
</>
{props.children}
<Divider />
</Box>
);
};

View file

@ -1207,6 +1207,8 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
__typename: "Fulfillment",
fulfillmentOrder: 2,
id: "RnVsZmlsbG1lbnQ6MjQ=",
metadata: [],
privateMetadata: [],
lines: [
{
__typename: "FulfillmentLine",
@ -1331,6 +1333,8 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
__typename: "Fulfillment",
fulfillmentOrder: 1,
id: "RnVsZmlsbG1lbnQ6OQ==",
metadata: [],
privateMetadata: [],
lines: [
{
__typename: "FulfillmentLine",

View file

@ -1,5 +1,5 @@
import { useApolloClient } from "@apollo/client";
import { MetadataFormData } from "@dashboard/components/Metadata";
import { MetadataIdSchema } from "@dashboard/components/Metadata";
import NotFoundPage from "@dashboard/components/NotFoundPage";
import { Task } from "@dashboard/containers/BackgroundTasks/types";
import {
@ -15,6 +15,7 @@ import useBackgroundTask from "@dashboard/hooks/useBackgroundTask";
import useNavigator from "@dashboard/hooks/useNavigator";
import useNotifier from "@dashboard/hooks/useNotifier";
import { commonMessages } from "@dashboard/intl";
import { createOrderMetadataIdSchema } from "@dashboard/orders/components/OrderDetailsPage/utils";
import getOrderErrorMessage from "@dashboard/utils/errors/order";
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@dashboard/utils/handlers/metadataUpdateHandler";
@ -83,21 +84,27 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
const isOrderUnconfirmed = order?.status === OrderStatus.UNCONFIRMED;
const isOrderDraft = order?.status === OrderStatus.DRAFT;
const handleSubmit = async (data: MetadataFormData) => {
const handleSubmit = async (data: MetadataIdSchema) => {
if (order?.status === OrderStatus.UNCONFIRMED) {
await orderConfirm({ variables: { id: order?.id } });
}
const initial = createOrderMetadataIdSchema(order);
const metadataPromises = Object.entries(initial).map(([id, metaEntry]) => {
const update = createMetadataUpdateHandler(
order,
{ ...metaEntry, id },
() => Promise.resolve([]),
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables }),
);
const result = await update(data);
return update(data[id]);
});
if (result.length === 0) {
const result = await Promise.all(metadataPromises);
const errors = result.reduce((p, c) => p.concat(c), []);
if (errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges),
@ -182,6 +189,7 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
<OrderNormalDetails
id={id}
params={params}
loading={loading}
data={data}
orderAddNote={orderAddNote}
orderInvoiceRequest={orderInvoiceRequest}

View file

@ -64,6 +64,7 @@ interface OrderNormalDetailsProps {
id: string;
params: OrderUrlQueryParams;
data: OrderDetailsQueryResult["data"];
loading: boolean;
orderAddNote: any;
orderInvoiceRequest: any;
handleSubmit: any;
@ -104,6 +105,7 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
id,
params,
data,
loading,
orderAddNote,
orderInvoiceRequest,
handleSubmit,
@ -187,7 +189,9 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
<OrderDetailsPage
onOrderReturn={() => navigate(orderReturnUrl(id))}
loading={
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
loading ||
updateMetadataOpts.loading ||
updatePrivateMetadataOpts.loading
}
errors={errors}
onNoteAdd={variables =>

View file

@ -52,11 +52,10 @@ function createMetadataUpdateHandler<TData extends MetadataFormData, TError>(
if (data.metadata && hasMetadataChanged) {
const initialKeys = initial.metadata.map(m => m.key);
const modifiedKeys = data.metadata.map(m => m.key);
const keyDiff = arrayDiff(initialKeys, modifiedKeys);
const metadataInput = filterMetadataArray(data.metadata);
if (metadataInput.length) {
if (metadataInput.length || keyDiff.removed.length) {
const updateMetaResult = await updateMetadata({
id: initial.id,
input: metadataInput,
@ -81,7 +80,7 @@ function createMetadataUpdateHandler<TData extends MetadataFormData, TError>(
const keyDiff = arrayDiff(initialKeys, modifiedKeys);
const privateMetadataInput = filterMetadataArray(data.privateMetadata);
if (privateMetadataInput.length) {
if (privateMetadataInput.length || keyDiff.removed.length) {
const updatePrivateMetaResult = await updatePrivateMetadata({
id: initial.id,
input: privateMetadataInput,