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, ORDERS_SELECTORS,
SHARED_ELEMENTS, SHARED_ELEMENTS,
} from "../../elements/"; } from "../../elements/";
import { MESSAGES } from "../../fixtures"; import { MESSAGES, ONE_PERMISSION_USERS, urlList } from "../../fixtures";
import { urlList } from "../../fixtures/urlList";
import { ONE_PERMISSION_USERS } from "../../fixtures/users";
import { import {
createCustomer, createCustomer,
deleteCustomersStartsWith, deleteCustomersStartsWith,
} from "../../support/api/requests/Customer";
import {
getOrder, getOrder,
updateMetadata,
updateOrdersSettings, updateOrdersSettings,
} from "../../support/api/requests/Order"; updatePrivateMetadata,
import { getDefaultChannel } from "../../support/api/utils/channelsUtils"; } from "../../support/api/requests/";
import { import {
createFulfilledOrder, createFulfilledOrder,
createOrder, createOrder,
createReadyToFulfillOrder, createReadyToFulfillOrder,
createUnconfirmedOrder,
} from "../../support/api/utils/ordersUtils";
import * as productsUtils from "../../support/api/utils/products/productsUtils";
import {
createShipping, createShipping,
createUnconfirmedOrder,
deleteShippingStartsWith, deleteShippingStartsWith,
} from "../../support/api/utils/shippingUtils"; getDefaultChannel,
import {
getDefaultTaxClass, getDefaultTaxClass,
productsUtils,
updateTaxConfigurationForChannel, updateTaxConfigurationForChannel,
} from "../../support/api/utils/taxesUtils"; } from "../../support/api/utils/";
import { selectChannelInPicker } from "../../support/pages/channelsPage";
import { finalizeDraftOrder } from "../../support/pages/draftOrderPage";
import { import {
addNewProductToOrder, addNewProductToOrder,
addPrivateMetadataFieldFulfillmentOrder,
addPublicMetadataFieldFulfillmentOrder,
applyFixedLineDiscountForProduct, applyFixedLineDiscountForProduct,
changeQuantityOfProducts, changeQuantityOfProducts,
deletePrivateFulfillmentMetadata,
deleteProductFromGridTableOnIndex, deleteProductFromGridTableOnIndex,
} from "../../support/pages/ordersOperations"; deletePublicFulfillmentMetadata,
expandPrivateFulfillmentMetadata,
expandPublicFulfillmentMetadata,
finalizeDraftOrder,
selectChannelInPicker,
updatePrivateMetadataFieldFulfillmentOrder,
updatePublicMetadataFieldFulfillmentOrder,
} from "../../support/pages/";
describe("Orders", () => { describe("Orders", () => {
const startsWith = "CyOrders-"; const startsWith = "CyOrders-";
@ -59,6 +61,16 @@ describe("Orders", () => {
const shippingPrice = 2; const shippingPrice = 2;
const variantPrice = 1; 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(() => { before(() => {
cy.clearSessionData().loginUserViaRequest(); 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']", "[data-test-id='metadata-editor'][data-test-is-private='true']",
addFieldButton: "[data-test-id='add-field']", addFieldButton: "[data-test-id='add-field']",
nameInput: "[name*='name']", 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 countryArea
phone phone
} }
fulfillments{
id
metadata{
key
value
}
privateMetadata{
key
value
}
}
} }
}`; }`;
return cy.sendRequestWithQuery(query).its("body.data.order"); return cy.sendRequestWithQuery(query).its("body.data.order");

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import { METADATA_FORM } from "../../../elements/shared/metadata/metadata-form";
export const metadataForms = { export const metadataForms = {
private: METADATA_FORM.privateMetadataForm, private: METADATA_FORM.privateMetadataForm,
public: METADATA_FORM.metadataForm public: METADATA_FORM.metadataForm,
}; };
export function addMetadataField({ metadataForm, name, value }) { export function addMetadataField({ metadataForm, name, value }) {
@ -17,3 +17,123 @@ export function addMetadataField({ metadataForm, name, value }) {
.find(METADATA_FORM.valueField) .find(METADATA_FORM.valueField)
.type(value); .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 ordersOperationsHelpers from "./ordersOperations";
export * as transactionsOrderUtils from "./ordersTransactionUtils"; 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 { expectWelcomeMessageIncludes } from "./homePage";
export { getDisplayedSelectors } from "./permissionsPage"; export { getDisplayedSelectors } from "./permissionsPage";
export { export {

View file

@ -2,7 +2,7 @@ import { MetadataInput } from "@dashboard/graphql";
import { ChangeEvent } from "@dashboard/hooks/useForm"; import { ChangeEvent } from "@dashboard/hooks/useForm";
import { removeAtIndex, updateAtIndex } from "@dashboard/utils/lists"; import { removeAtIndex, updateAtIndex } from "@dashboard/utils/lists";
import { Box } from "@saleor/macaw-ui/next"; import { Box } from "@saleor/macaw-ui/next";
import React from "react"; import React, { memo } from "react";
import { MetadataCard, MetadataCardProps } from "./MetadataCard"; import { MetadataCard, MetadataCardProps } from "./MetadataCard";
import { EventDataAction, EventDataField } from "./types"; import { EventDataAction, EventDataField } from "./types";
@ -11,9 +11,24 @@ import { getDataKey, parseEventData } from "./utils";
export interface MetadataProps export interface MetadataProps
extends Omit<MetadataCardProps, "data" | "isPrivate"> { extends Omit<MetadataCardProps, "data" | "isPrivate"> {
data: Record<"metadata" | "privateMetadata", MetadataInput[]>; 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 change = (event: ChangeEvent, isPrivate: boolean) => {
const { action, field, fieldIndex, value } = parseEventData(event); const { action, field, fieldIndex, value } = parseEventData(event);
const key = getDataKey(isPrivate); const key = getDataKey(isPrivate);
@ -66,4 +81,4 @@ export const Metadata: React.FC<MetadataProps> = ({ data, onChange }) => {
/> />
</Box> </Box>
); );
}; }, propsCompare);

View file

@ -19,3 +19,10 @@ export interface MetadataFormData {
metadata: MetadataInput[]; metadata: MetadataInput[];
privateMetadata: 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` export const fulfillmentFragment = gql`
fragment Fulfillment on Fulfillment { fragment Fulfillment on Fulfillment {
...Metadata
id id
lines { lines {
id id

View file

@ -1603,6 +1603,7 @@ export const OrderLineFragmentDoc = gql`
${TaxedMoneyFragmentDoc}`; ${TaxedMoneyFragmentDoc}`;
export const FulfillmentFragmentDoc = gql` export const FulfillmentFragmentDoc = gql`
fragment Fulfillment on Fulfillment { fragment Fulfillment on Fulfillment {
...Metadata
id id
lines { lines {
id id
@ -1619,7 +1620,8 @@ export const FulfillmentFragmentDoc = gql`
name name
} }
} }
${OrderLineFragmentDoc}`; ${MetadataFragmentDoc}
${OrderLineFragmentDoc}`;
export const InvoiceFragmentDoc = gql` export const InvoiceFragmentDoc = gql`
fragment Invoice on Invoice { fragment Invoice on Invoice {
id 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 { useDevModeContext } from "@dashboard/components/DevModePanel/hooks";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import { import {
OrderDetailsFragment, OrderDetailsFragment,
@ -22,8 +22,6 @@ import { SubmitPromise } from "@dashboard/hooks/useForm";
import useNavigator from "@dashboard/hooks/useNavigator"; import useNavigator from "@dashboard/hooks/useNavigator";
import { defaultGraphiQLQuery } from "@dashboard/orders/queries"; import { defaultGraphiQLQuery } from "@dashboard/orders/queries";
import { orderListUrl } from "@dashboard/orders/urls"; 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 { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -41,7 +39,12 @@ import { OrderPaymentOrTransaction } from "../OrderPaymentOrTransaction/OrderPay
import OrderUnfulfilledProductsCard from "../OrderUnfulfilledProductsCard"; import OrderUnfulfilledProductsCard from "../OrderUnfulfilledProductsCard";
import { messages } from "./messages"; import { messages } from "./messages";
import Title from "./Title"; import Title from "./Title";
import { filteredConditionalItems, hasAnyItemsReplaceable } from "./utils"; import {
createMetadataHandler,
createOrderMetadataIdSchema,
filteredConditionalItems,
hasAnyItemsReplaceable,
} from "./utils";
export interface OrderDetailsPageProps { export interface OrderDetailsPageProps {
order: OrderDetailsFragment | OrderDetailsFragment; order: OrderDetailsFragment | OrderDetailsFragment;
@ -80,7 +83,7 @@ export interface OrderDetailsPageProps {
onInvoiceSend(invoiceId: string); onInvoiceSend(invoiceId: string);
onTransactionAction(transactionId: string, actionType: TransactionActionEnum); onTransactionAction(transactionId: string, actionType: TransactionActionEnum);
onAddManualTransaction(); onAddManualTransaction();
onSubmit(data: MetadataFormData): SubmitPromise; onSubmit(data: MetadataIdSchema): SubmitPromise;
} }
const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => { const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
@ -118,13 +121,6 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
const navigate = useNavigator(); const navigate = useNavigator();
const intl = useIntl(); const intl = useIntl();
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler,
resetMetadataChanged,
} = useMetadataChangeTrigger();
const isOrderUnconfirmed = order?.status === OrderStatus.UNCONFIRMED; const isOrderUnconfirmed = order?.status === OrderStatus.UNCONFIRMED;
const canCancel = order?.status !== OrderStatus.CANCELED; const canCancel = order?.status !== OrderStatus.CANCELED;
const canEditAddresses = order?.status !== OrderStatus.CANCELED; const canEditAddresses = order?.status !== OrderStatus.CANCELED;
@ -137,24 +133,12 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
line => line.quantityToFulfill > 0, line => line.quantityToFulfill > 0,
); );
const handleSubmit = async (data: MetadataFormData) => { const handleSubmit = async (data: MetadataIdSchema) => {
const metadata = isMetadataModified ? data.metadata : undefined; const result = await onSubmit(data);
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
const result = await onSubmit({
metadata,
privateMetadata,
});
resetMetadataChanged();
return getMutationErrors(result); return getMutationErrors(result);
}; };
const initial: MetadataFormData = { const initial = createOrderMetadataIdSchema(order);
metadata: order?.metadata.map(mapMetadataItemToInput),
privateMetadata: order?.privateMetadata.map(mapMetadataItemToInput),
};
const saveLabel = isOrderUnconfirmed const saveLabel = isOrderUnconfirmed
? { confirm: intl.formatMessage(messages.confirmOrder) } ? { confirm: intl.formatMessage(messages.confirmOrder) }
@ -204,9 +188,18 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
}; };
return ( return (
<Form confirmLeave initial={initial} onSubmit={handleSubmit}> <Form
{({ change, data, submit }) => { confirmLeave
const changeMetadata = makeMetadataChangeHandler(change); initial={initial}
onSubmit={handleSubmit}
mergeData={false}
>
{({ set, triggerChange, data, submit }) => {
const handleChangeMetadata = createMetadataHandler(
data,
set,
triggerChange,
);
return ( return (
<DetailPageLayout> <DetailPageLayout>
@ -248,22 +241,28 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
</> </>
)} )}
{order?.fulfillments?.map(fulfillment => ( {order?.fulfillments?.map(fulfillment => (
<React.Fragment key={fulfillment.id}> <OrderFulfilledProductsCard
<OrderFulfilledProductsCard dataTestId="fulfilled-order-section"
fulfillment={fulfillment} key={fulfillment.id}
fulfillmentAllowUnpaid={shop?.fulfillmentAllowUnpaid} fulfillment={fulfillment}
order={order} fulfillmentAllowUnpaid={shop?.fulfillmentAllowUnpaid}
onOrderFulfillmentCancel={() => order={order}
onFulfillmentCancel(fulfillment.id) onOrderFulfillmentCancel={() =>
} onFulfillmentCancel(fulfillment.id)
onTrackingCodeAdd={() => }
onFulfillmentTrackingNumberUpdate(fulfillment.id) onTrackingCodeAdd={() =>
} onFulfillmentTrackingNumberUpdate(fulfillment.id)
onOrderFulfillmentApprove={() => }
onFulfillmentApprove(fulfillment.id) onOrderFulfillmentApprove={() =>
} onFulfillmentApprove(fulfillment.id)
}
>
<Metadata
isLoading={loading}
data={data[fulfillment.id]}
onChange={x => handleChangeMetadata(x, fulfillment.id)}
/> />
</React.Fragment> </OrderFulfilledProductsCard>
))} ))}
<OrderPaymentOrTransaction <OrderPaymentOrTransaction
order={order} order={order}
@ -275,7 +274,11 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
onMarkAsPaid={onMarkAsPaid} onMarkAsPaid={onMarkAsPaid}
onAddManualTransaction={onAddManualTransaction} onAddManualTransaction={onAddManualTransaction}
/> />
<Metadata data={data} onChange={changeMetadata} /> <Metadata
isLoading={loading}
data={data[order?.id]}
onChange={x => handleChangeMetadata(x, order?.id)}
/>
<OrderHistory <OrderHistory
history={order?.events} history={order?.events}
orderCurrency={order?.total?.gross.currency} 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", () => { describe("filteredConditionalItems", () => {
it("should return empty [] when no items has shouldExist set to true", () => { 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 { OrderDetailsFragment } from "@dashboard/graphql";
import { ChangeEvent } from "@dashboard/hooks/useForm";
import { mapMetadataItemToInput } from "@dashboard/utils/maps";
import { import {
getFulfilledFulfillemnts, getFulfilledFulfillemnts,
@ -29,3 +32,37 @@ export interface ConditionalItem {
export const filteredConditionalItems = (items: ConditionalItem[]) => export const filteredConditionalItems = (items: ConditionalItem[]) =>
items.filter(({ shouldExist }) => shouldExist).map(({ item }) => item); 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 { FulfillmentStatus, OrderDetailsFragment } from "@dashboard/graphql";
import TrashIcon from "@dashboard/icons/Trash"; import TrashIcon from "@dashboard/icons/Trash";
import { orderHasTransactions } from "@dashboard/orders/types"; import { orderHasTransactions } from "@dashboard/orders/types";
import { mergeRepeatedOrderLines } from "@dashboard/orders/utils/data"; 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 { IconButton } from "@saleor/macaw-ui";
import { Box, Divider } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import OrderCardTitle from "../OrderCardTitle"; import OrderCardTitle from "../OrderCardTitle";
@ -20,6 +20,7 @@ interface OrderFulfilledProductsCardProps {
onOrderFulfillmentApprove: () => void; onOrderFulfillmentApprove: () => void;
onOrderFulfillmentCancel: () => void; onOrderFulfillmentCancel: () => void;
onTrackingCodeAdd: () => void; onTrackingCodeAdd: () => void;
dataTestId?: string;
} }
const statusesToMergeLines = [ const statusesToMergeLines = [
@ -43,6 +44,7 @@ const OrderFulfilledProductsCard: React.FC<
onOrderFulfillmentApprove, onOrderFulfillmentApprove,
onOrderFulfillmentCancel, onOrderFulfillmentCancel,
onTrackingCodeAdd, onTrackingCodeAdd,
dataTestId,
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
@ -61,17 +63,17 @@ const OrderFulfilledProductsCard: React.FC<
}; };
return ( return (
<> <Box data-test-id={dataTestId}>
<Card> <OrderCardTitle
<OrderCardTitle withStatus
withStatus lines={fulfillment?.lines}
lines={fulfillment?.lines} fulfillmentOrder={fulfillment?.fulfillmentOrder}
fulfillmentOrder={fulfillment?.fulfillmentOrder} status={fulfillment?.status}
status={fulfillment?.status} warehouseName={fulfillment?.warehouse?.name}
warehouseName={fulfillment?.warehouse?.name} orderNumber={order?.number}
orderNumber={order?.number} toolbar={
toolbar={ <Box display="flex" alignItems="center" gap={6}>
cancelableStatuses.includes(fulfillment?.status) && ( {cancelableStatuses.includes(fulfillment?.status) && (
<IconButton <IconButton
variant="secondary" variant="secondary"
className={classes.deleteIcon} className={classes.deleteIcon}
@ -80,26 +82,27 @@ const OrderFulfilledProductsCard: React.FC<
> >
<TrashIcon /> <TrashIcon />
</IconButton> </IconButton>
) )}
} <ActionButtons
/> orderId={order?.id}
<CardContent> status={fulfillment?.status}
<OrderDetailsDatagrid lines={getLines()} loading={false} /> trackingNumber={fulfillment?.trackingNumber}
<ExtraInfoLines fulfillment={fulfillment} /> orderIsPaid={order?.isPaid}
<ActionButtons fulfillmentAllowUnpaid={fulfillmentAllowUnpaid}
orderId={order?.id} onTrackingCodeAdd={onTrackingCodeAdd}
status={fulfillment?.status} onApprove={onOrderFulfillmentApprove}
trackingNumber={fulfillment?.trackingNumber} hasTransactions={orderHasTransactions(order)}
orderIsPaid={order?.isPaid} />
fulfillmentAllowUnpaid={fulfillmentAllowUnpaid} </Box>
onTrackingCodeAdd={onTrackingCodeAdd} }
onApprove={onOrderFulfillmentApprove} />
hasTransactions={orderHasTransactions(order)} <CardContent>
/> <OrderDetailsDatagrid lines={getLines()} loading={false} />
</CardContent> <ExtraInfoLines fulfillment={fulfillment} />
</Card> </CardContent>
<CardSpacer /> {props.children}
</> <Divider />
</Box>
); );
}; };

View file

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

View file

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

View file

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

View file

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