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:
committed by
GitHub
parent
b12408dbd8
commit
1659c9be5d
@@ -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,
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user