feat(product,dashboard): Allow re-ordering images (#10187)

* migration

* fix snapshot

* primarykey

* init work on dnd

* progress

* dnd

* undo changes

* undo changes

* undo changes

* undo changes

* fix firefox issue

* lint

* lint

* lint

* add changeset

* undo changes to product module

* set activator node

* init work on service layer

* alternative

* switch to OneToMany

* add tests

* progress

* update migration

* update approach and remove all references to images in product.ts tests

* handle delete images on empty array

* fix config and order type

* update changeset

* rm flag

* export type and fix type in test

* fix type

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Kasper Fabricius Kristensen
2024-11-25 09:03:10 +01:00
committed by GitHub
parent b12408dbd8
commit 1659c9be5d
32 changed files with 1257 additions and 617 deletions

View File

@@ -12,17 +12,18 @@ import {
ProductStatus,
} from "@medusajs/framework/utils"
import {
Image,
Product,
ProductCategory,
ProductCollection,
ProductType,
} from "@models"
import { UpdateProductInput } from "@types"
import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "@medusajs/test-utils"
import { UpdateProductInput } from "@types"
import {
buildProductAndRelationsData,
createCollections,
@@ -1236,6 +1237,154 @@ moduleIntegrationTestRunner<IProductModuleService>({
expect(products).toEqual([])
})
})
describe("images", function () {
it("should create images with correct rank", async () => {
const images = [
{ url: "image-1" },
{ url: "image-2" },
{ url: "image-3" },
]
const [product] = await service.createProducts([
buildProductAndRelationsData({ images }),
])
expect(product.images).toHaveLength(3)
expect(product.images).toEqual([
expect.objectContaining({
url: "image-1",
rank: 0,
}),
expect.objectContaining({
url: "image-2",
rank: 1,
}),
expect.objectContaining({
url: "image-3",
rank: 2,
}),
])
})
it("should update images with correct rank", async () => {
const images = [
{ url: "image-1" },
{ url: "image-2" },
{ url: "image-3" },
]
const [product] = await service.createProducts([
buildProductAndRelationsData({ images }),
])
const reversedImages = [...product.images].reverse()
const updatedProduct = await service.updateProducts(product.id, {
images: reversedImages,
})
expect(updatedProduct.images).toEqual([
expect.objectContaining({
url: "image-3",
rank: 0,
}),
expect.objectContaining({
url: "image-2",
rank: 1,
}),
expect.objectContaining({
url: "image-1",
rank: 2,
}),
])
})
it("should retrieve images in the correct order consistently", async () => {
const images = Array.from({ length: 1000 }, (_, i) => ({
url: `image-${i + 1}`,
}))
const [product] = await service.createProducts([
buildProductAndRelationsData({ images }),
])
const retrievedProduct = await service.retrieveProduct(product.id, {
relations: ["images"],
})
const retrievedProductAgain = await service.retrieveProduct(product.id, {
relations: ["images"],
})
expect(retrievedProduct.images).toEqual(retrievedProductAgain.images)
expect(retrievedProduct.images).toEqual(
Array.from({ length: 1000 }, (_, i) =>
expect.objectContaining({
url: `image-${i + 1}`,
rank: i,
})
)
)
service.listAndCountProducts
// Explicitly verify sequential order
retrievedProduct.images.forEach((img, idx) => {
if (idx > 0) {
expect(img.rank).toBeGreaterThan(retrievedProduct.images[idx - 1].rank)
}
})
})
it("should retrieve images ordered by rank", async () => {
const [product] = await service.createProducts([
buildProductAndRelationsData({}),
])
const manager = MikroOrmWrapper.forkManager()
const images = [
manager.create(Image, {
product_id: product.id,
url: "image-one",
rank: 1,
}),
manager.create(Image, {
product_id: product.id,
url: "image-two",
rank: 0,
}),
manager.create(Image, {
product_id: product.id,
url: "image-three",
rank: 2,
}),
]
await manager.persistAndFlush(images)
const retrievedProduct = await service.retrieveProduct(product.id, {
relations: ["images"],
})
expect(retrievedProduct.images).toEqual([
expect.objectContaining({
url: "image-two",
rank: 0,
}),
expect.objectContaining({
url: "image-one",
rank: 1,
}),
expect.objectContaining({
url: "image-three",
rank: 2,
}),
])
})
})
})
},
})

View File

@@ -1,9 +1,8 @@
import { Image, Product, ProductCategory, ProductCollection } from "@models"
import { Product, ProductCategory, ProductCollection } from "@models"
import {
assignCategoriesToProduct,
buildProductOnlyData,
createCollections,
createImages,
createProductAndTags,
createProductVariants,
} from "../__fixtures__/product"
@@ -15,13 +14,13 @@ import {
ProductStatus,
kebabCase,
} from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import {
ProductCategoryService,
ProductModuleService,
ProductService,
} from "@services"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import {
categoriesData,
productsData,
@@ -215,19 +214,12 @@ moduleIntegrationTestRunner<Service>({
})
describe("create", function () {
let images: Image[] = []
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
images = await createImages(testManager, ["image-1"])
})
it("should create a product", async () => {
const data = buildProductOnlyData({
images,
thumbnail: images[0].url,
})
const data = buildProductOnlyData()
const products = await service.create([data])
@@ -241,25 +233,15 @@ moduleIntegrationTestRunner<Service>({
subtitle: data.subtitle,
is_giftcard: data.is_giftcard,
discountable: data.discountable,
thumbnail: images[0].url,
status: data.status,
images: expect.arrayContaining([
expect.objectContaining({
id: images[0].id,
url: images[0].url,
}),
]),
})
)
})
})
describe("update", function () {
let images: Image[] = []
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
images = await createImages(testManager, ["image-1", "image-2"])
productOne = testManager.create(Product, {
id: "product-1",
@@ -275,8 +257,6 @@ moduleIntegrationTestRunner<Service>({
{
id: productOne.id,
title: "update test 1",
images: images,
thumbnail: images[0].url,
},
]
@@ -284,24 +264,13 @@ moduleIntegrationTestRunner<Service>({
expect(products.length).toEqual(1)
let result = await service.retrieve(productOne.id, {
relations: ["images", "thumbnail"],
})
let result = await service.retrieve(productOne.id)
let serialized = JSON.parse(JSON.stringify(result))
expect(serialized).toEqual(
expect.objectContaining({
id: productOne.id,
title: "update test 1",
thumbnail: images[0].url,
images: [
expect.objectContaining({
url: images[0].url,
}),
expect.objectContaining({
url: images[1].url,
}),
],
})
)
})
@@ -750,19 +719,12 @@ moduleIntegrationTestRunner<Service>({
})
describe("softDelete", function () {
let images: Image[] = []
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
images = await createImages(testManager, ["image-1"])
})
it("should soft delete a product", async () => {
const data = buildProductOnlyData({
images,
thumbnail: images[0].url,
})
const data = buildProductOnlyData()
const products = await service.create([data])
await service.softDelete(products.map((p) => p.id))
@@ -785,19 +747,12 @@ moduleIntegrationTestRunner<Service>({
})
describe("restore", function () {
let images: Image[] = []
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
images = await createImages(testManager, ["image-1"])
})
it("should restore a soft deleted product", async () => {
const data = buildProductOnlyData({
images,
thumbnail: images[0].url,
})
const data = buildProductOnlyData()
const products = await service.create([data])
const product = products[0]