From 439c7118450c5f9ee0b541de9014093a42b7d0ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:01:50 +0200 Subject: [PATCH] fix(dashboard, product): product attributes update with a relation update (#13019) * fix(dashboard, product): product attributes update with a relation update * fix: rm log * chore: refactor --- .changeset/polite-falcons-glow.md | 6 + .../__tests__/product/admin/product.spec.ts | 104 ++++++++++++++++++ .../product-attributes-form.tsx | 8 +- .../order/workflows/return/cancel-return.ts | 22 ++-- .../src/product/steps/update-products.ts | 20 ++-- .../src/product/workflows/update-products.ts | 8 +- .../product/src/repositories/product.ts | 23 +++- 7 files changed, 156 insertions(+), 35 deletions(-) create mode 100644 .changeset/polite-falcons-glow.md diff --git a/.changeset/polite-falcons-glow.md b/.changeset/polite-falcons-glow.md new file mode 100644 index 0000000000..bbd6d83ee4 --- /dev/null +++ b/.changeset/polite-falcons-glow.md @@ -0,0 +1,6 @@ +--- +"@medusajs/dashboard": patch +"@medusajs/product": patch +--- + +fix(dashboard, product): update product attributes diff --git a/integration-tests/http/__tests__/product/admin/product.spec.ts b/integration-tests/http/__tests__/product/admin/product.spec.ts index 92f106d283..28e2e4558e 100644 --- a/integration-tests/http/__tests__/product/admin/product.spec.ts +++ b/integration-tests/http/__tests__/product/admin/product.spec.ts @@ -1813,6 +1813,110 @@ medusaIntegrationTestRunner({ ) }) + it("updates products relations and attributes", async () => { + const shortsCategory = ( + await api.post( + "/admin/product-categories", + { name: "Shorts", is_internal: false, is_active: true }, + adminHeaders + ) + ).data.product_category + + const pantsCategory = ( + await api.post( + "/admin/product-categories", + { name: "Pants", is_internal: false, is_active: true }, + adminHeaders + ) + ).data.product_category + + const payload = { + title: "Test an update", + weight: 100, + length: 100, + width: 100, + height: 100, + options: [{ title: "size", values: ["large", "small"] }], + variants: [ + { + options: { size: "large" }, + title: "New variant", + prices: [ + { + currency_code: "usd", + amount: 200, + }, + ], + }, + ], + } + + const createdProduct = ( + await api.post("/admin/products", payload, adminHeaders) + ).data.product + + let updatedProduct = ( + await api.post( + `/admin/products/${createdProduct.id}`, + { weight: 20, length: null }, + adminHeaders + ) + ).data.product + + expect(updatedProduct).toEqual( + expect.objectContaining({ + weight: "20", + length: null, + width: "100", + height: "100", + }) + ) + + updatedProduct = ( + await api.post( + `/admin/products/${createdProduct.id}?fields=+categories.id`, + { categories: [{ id: pantsCategory.id }] }, + adminHeaders + ) + ).data.product + + expect(updatedProduct).toEqual( + expect.objectContaining({ + weight: "20", + length: null, + width: "100", + height: "100", + categories: expect.arrayContaining([ + expect.objectContaining({ + id: pantsCategory.id, + }), + ]), + }) + ) + + updatedProduct = ( + await api.post( + `/admin/products/${createdProduct.id}?fields=+categories.id`, + { weight: null, length: 20, width: 50 }, + adminHeaders + ) + ).data.product + + expect(updatedProduct).toEqual( + expect.objectContaining({ + weight: null, + length: "20", + width: "50", + height: "100", + categories: expect.arrayContaining([ + expect.objectContaining({ + id: pantsCategory.id, + }), + ]), + }) + ) + }) + it("updates a product (update prices, tags, update status, delete collection, delete type, replaces images)", async () => { const payload = { collection_id: null, diff --git a/packages/admin/dashboard/src/routes/products/product-attributes/components/product-attributes-form/product-attributes-form.tsx b/packages/admin/dashboard/src/routes/products/product-attributes/components/product-attributes-form/product-attributes-form.tsx index 87a4d4b36c..e2857f9b27 100644 --- a/packages/admin/dashboard/src/routes/products/product-attributes/components/product-attributes-form/product-attributes-form.tsx +++ b/packages/admin/dashboard/src/routes/products/product-attributes/components/product-attributes-form/product-attributes-form.tsx @@ -68,10 +68,10 @@ export const ProductAttributesForm = ({ const handleSubmit = form.handleSubmit(async (data) => { await mutateAsync( { - weight: data.weight ? data.weight : undefined, - length: data.length ? data.length : undefined, - width: data.width ? data.width : undefined, - height: data.height ? data.height : undefined, + weight: data.weight ? data.weight : null, + length: data.length ? data.length : null, + width: data.width ? data.width : null, + height: data.height ? data.height : null, mid_code: data.mid_code, hs_code: data.hs_code, origin_country: data.origin_country, diff --git a/packages/core/core-flows/src/order/workflows/return/cancel-return.ts b/packages/core/core-flows/src/order/workflows/return/cancel-return.ts index fbcff4e292..7cf2d6e595 100644 --- a/packages/core/core-flows/src/order/workflows/return/cancel-return.ts +++ b/packages/core/core-flows/src/order/workflows/return/cancel-return.ts @@ -32,14 +32,14 @@ export type CancelReturnValidateOrderInput = { * This step validates that a return can be canceled. * If the return is canceled, its fulfillment aren't canceled, * or it has received items, the step will throw an error. - * + * * :::note - * + * * You can retrieve a return details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). - * + * * ::: - * + * * @example * const data = cancelReturnValidateOrder({ * orderReturn: { @@ -53,9 +53,7 @@ export type CancelReturnValidateOrderInput = { */ export const cancelReturnValidateOrder = createStep( "validate-return", - ({ - orderReturn, - }: CancelReturnValidateOrderInput) => { + ({ orderReturn }: CancelReturnValidateOrderInput) => { const orderReturn_ = orderReturn as ReturnDTO & { payment_collections: PaymentCollectionDTO[] fulfillments: FulfillmentDTO[] @@ -92,12 +90,12 @@ export const cancelReturnValidateOrder = createStep( export const cancelReturnWorkflowId = "cancel-return" /** - * This workflow cancels a return. It's used by the + * This workflow cancels a return. It's used by the * [Cancel Return Admin API Route](https://docs.medusajs.com/api/admin#returns_postreturnsidcancel). - * + * * You can use this workflow within your customizations or your own custom workflows, allowing you * to cancel a return in your custom flow. - * + * * @example * const { result } = await cancelReturnWorkflow(container) * .run({ @@ -105,9 +103,9 @@ export const cancelReturnWorkflowId = "cancel-return" * return_id: "return_123", * } * }) - * + * * @summary - * + * * Cancel a return. */ export const cancelReturnWorkflow = createWorkflow( diff --git a/packages/core/core-flows/src/product/steps/update-products.ts b/packages/core/core-flows/src/product/steps/update-products.ts index 599625185f..d498686f37 100644 --- a/packages/core/core-flows/src/product/steps/update-products.ts +++ b/packages/core/core-flows/src/product/steps/update-products.ts @@ -11,9 +11,9 @@ import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" */ export type UpdateProductsStepInput = | { - /** - * The filters to select the products to update. - */ + /** + * The filters to select the products to update. + */ selector: ProductTypes.FilterableProductProps /** * The data to update the products with. @@ -21,19 +21,19 @@ export type UpdateProductsStepInput = update: ProductTypes.UpdateProductDTO } | { - /** - * The data to create or update products. - */ + /** + * The data to create or update products. + */ products: ProductTypes.UpsertProductDTO[] } export const updateProductsStepId = "update-products" /** * This step updates one or more products. - * + * * @example * To update products by their ID: - * + * * ```ts * const data = updateProductsStep({ * products: [ @@ -44,9 +44,9 @@ export const updateProductsStepId = "update-products" * ] * }) * ``` - * + * * To update products matching a filter: - * + * * ```ts * const data = updateProductsStep({ * selector: { diff --git a/packages/core/core-flows/src/product/workflows/update-products.ts b/packages/core/core-flows/src/product/workflows/update-products.ts index 94a37dea50..2259eb9b2f 100644 --- a/packages/core/core-flows/src/product/workflows/update-products.ts +++ b/packages/core/core-flows/src/product/workflows/update-products.ts @@ -343,12 +343,12 @@ export const updateProductsWorkflowId = "update-products" * allows you to update custom data models linked to the products. * * You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around product update. - * + * * :::note - * - * Learn more about adding rules to the product variant's prices in the Pricing Module's + * + * Learn more about adding rules to the product variant's prices in the Pricing Module's * [Price Rules](https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules) documentation. - * + * * ::: * * @example diff --git a/packages/modules/product/src/repositories/product.ts b/packages/modules/product/src/repositories/product.ts index fc1e4ee220..3001ef1271 100644 --- a/packages/modules/product/src/repositories/product.ts +++ b/packages/modules/product/src/repositories/product.ts @@ -8,6 +8,7 @@ import { MedusaError, isPresent, mergeMetadata, + isDefined, } from "@medusajs/framework/utils" import { SqlEntityManager, wrap } from "@mikro-orm/postgresql" @@ -57,10 +58,18 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( height?: string | number width?: string | number }) { - productToUpdate.weight = productToUpdate.weight?.toString() - productToUpdate.length = productToUpdate.length?.toString() - productToUpdate.height = productToUpdate.height?.toString() - productToUpdate.width = productToUpdate.width?.toString() + if (isDefined(productToUpdate.weight)) { + productToUpdate.weight = productToUpdate.weight?.toString() + } + if (isDefined(productToUpdate.length)) { + productToUpdate.length = productToUpdate.length?.toString() + } + if (isDefined(productToUpdate.height)) { + productToUpdate.height = productToUpdate.height?.toString() + } + if (isDefined(productToUpdate.width)) { + productToUpdate.width = productToUpdate.width?.toString() + } } async deepUpdate( @@ -72,6 +81,7 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( context: Context = {} ): Promise[]> { const productIdsToUpdate: string[] = [] + productsToUpdate.forEach((productToUpdate) => { ProductRepository.#correctUpdateDTOTypes(productToUpdate) productIdsToUpdate.push(productToUpdate.id) @@ -151,7 +161,10 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( } if (isPresent(productToUpdate.metadata)) { - productToUpdate.metadata = mergeMetadata(product.metadata ?? {}, productToUpdate.metadata) + productToUpdate.metadata = mergeMetadata( + product.metadata ?? {}, + productToUpdate.metadata + ) } wrappedProduct.assign(productToUpdate)