feat: add item duplication with copy-and-edit workflow
Adds POST /api/items/:id/duplicate endpoint, useDuplicateItem hook, and a Duplicate button on ItemCard (collection view only) that opens the new item for editing immediately after creation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -118,4 +118,30 @@ describe("Item Routes", () => {
|
||||
const res = await app.request("/api/items/9999");
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("POST /api/items/:id/duplicate returns 201 with the copy", async () => {
|
||||
const createRes = await app.request("/api/items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: "Tent", categoryId: 1, weightGrams: 1200 }),
|
||||
});
|
||||
const created = await createRes.json();
|
||||
|
||||
const res = await app.request(`/api/items/${created.id}/duplicate`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
const body = await res.json();
|
||||
expect(body.name).toBe("Tent (copy)");
|
||||
expect(body.weightGrams).toBe(1200);
|
||||
expect(body.id).not.toBe(created.id);
|
||||
});
|
||||
|
||||
it("POST /api/items/999/duplicate returns 404", async () => {
|
||||
const res = await app.request("/api/items/999/duplicate", {
|
||||
method: "POST",
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "bun:test";
|
||||
import {
|
||||
createItem,
|
||||
deleteItem,
|
||||
duplicateItem,
|
||||
getAllItems,
|
||||
getItemById,
|
||||
updateItem,
|
||||
@@ -98,6 +99,41 @@ describe("Item Service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("duplicateItem", () => {
|
||||
it("creates a copy with '(copy)' suffix in name", () => {
|
||||
const original = createItem(db, {
|
||||
name: "Tent",
|
||||
weightGrams: 1200,
|
||||
priceCents: 35000,
|
||||
categoryId: 1,
|
||||
notes: "Ultralight",
|
||||
productUrl: "https://example.com/tent",
|
||||
});
|
||||
|
||||
const copy = duplicateItem(db, original?.id);
|
||||
|
||||
expect(copy).toBeDefined();
|
||||
expect(copy?.name).toBe("Tent (copy)");
|
||||
expect(copy?.weightGrams).toBe(1200);
|
||||
expect(copy?.priceCents).toBe(35000);
|
||||
expect(copy?.categoryId).toBe(1);
|
||||
expect(copy?.notes).toBe("Ultralight");
|
||||
expect(copy?.productUrl).toBe("https://example.com/tent");
|
||||
});
|
||||
|
||||
it("copy has a different ID from the original", () => {
|
||||
const original = createItem(db, { name: "Helmet", categoryId: 1 });
|
||||
const copy = duplicateItem(db, original?.id);
|
||||
|
||||
expect(copy?.id).not.toBe(original?.id);
|
||||
});
|
||||
|
||||
it("returns null for non-existent item", () => {
|
||||
const result = duplicateItem(db, 9999);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteItem", () => {
|
||||
it("removes item from DB, returns deleted item", () => {
|
||||
const created = createItem(db, {
|
||||
|
||||
Reference in New Issue
Block a user