fix: allow replacing a model's only file in the edit dialog

Saving deleted removed files before uploading new ones, so swapping a model's
only file failed with "Cannot remove the only plate of a print". New files are
now uploaded first, then removed ones deleted, and a replacement that got a
" (2)" suffix while the old file existed is renamed back to its own name.
This commit is contained in:
Tautvydas Derzinskas committed 2026-09-27 20:54:07 +02:00
1 parent 32710f25e7
commit fc3f3efd48
2 files changed
+39 -9

No files matched your search

+19
View File
@@ -273,6 +273,25 @@ describe("plate add/remove/reorder/rename", () => {
res = await auth(request(app).delete(`/api/print/${printId}/plates/${lastPlateId}`));
expect(res.status).toBe(409);
});
// The Edit dialog's order for swapping a model's only file for an edited copy of the same name.
it("replaces the only plate with a same-named file: upload, delete the old one, rename back", async () => {
let res = await auth(request(app).get(`/api/print/${printId}`));
const [original] = res.body.plates;
res = await auth(request(app).post(`/api/print/${printId}/plates`)).attach(
"files",
tmpFile(original.filename, "solid replacement endsolid"),
);
const added = res.body.print.plates.find((p: any) => p.id !== original.id);
expect(added.filename).not.toBe(original.filename); // suffixed while the old one exists
res = await auth(request(app).delete(`/api/print/${printId}/plates/${original.id}`));
expect(res.status).toBe(200);
res = await auth(request(app).post(`/api/print/${printId}/plate/${added.id}/rename`)).send({ filename: original.filename });
expect(res.status).toBe(200);
expect(res.body.print.plates).toEqual([expect.objectContaining({ id: added.id, filename: original.filename })]);
});
});
describe("category CRUD + cycle rejection", () => {
@@ -247,26 +247,37 @@ export default function EditModelModal({ print, onClose, onUnauthorized, onUpdat
latest = reorderRes.print ?? latest;
}
// Model files (plates): same delete -> upload -> reorder sequence. Always at least one
// plate stays staged locally (removePlateItem refuses to drop the last one), matching the
// backend's own "can't remove the only plate" rule.
const keptPlateIds = new Set(plateItems.filter((p) => p.kind === "existing").map((p) => p.id));
for (const original of print.plates) {
if (keptPlateIds.has(original.id)) continue;
const res = await printsApi.deletePlate(print.id, original.id);
latest = res.print ?? latest;
}
// Model files (plates): upload -> delete -> rename -> reorder. Uploading first is what lets
// a model's only file be swapped for a new one in a single save -- deleting first would
// briefly leave it with none, which the backend refuses ("can't remove the only plate").
const newPlateFiles = plateItems.filter((p) => p.kind === "new").map((p) => p.file);
let newPlateIdsInOrder: string[] = [];
if (newPlateFiles.length) {
const beforeIds = new Set(latest.plates.map((p) => p.id));
const uploadRes = await printsApi.addPlates(print.id, newPlateFiles);
latest = uploadRes.print;
// The backend appends them in upload order, so this lines up with newPlateFiles.
newPlateIdsInOrder = latest.plates
.filter((p) => !beforeIds.has(p.id))
.toSorted((a, b) => a.position - b.position)
.map((p) => p.id);
}
const keptPlateIds = new Set(plateItems.filter((p) => p.kind === "existing").map((p) => p.id));
for (const original of print.plates) {
if (keptPlateIds.has(original.id)) continue;
const res = await printsApi.deletePlate(print.id, original.id);
latest = res.print ?? latest;
}
// A replacement usually has the same name as the file it replaces, and while that one still
// existed the backend gave it a " (2)" suffix. Now it's gone, ask for the name as uploaded:
// the backend keeps the suffix if another of the model's files still has that name.
for (const [index, plateId] of newPlateIdsInOrder.entries()) {
const wanted = newPlateFiles[index].name;
const current = latest.plates.find((p) => p.id === plateId);
if (!current || current.filename === wanted) continue;
const res = await printsApi.renamePlate(print.id, plateId, wanted);
latest = res.print ?? latest;
}
{
let nextNew = 0;
const finalOrder = plateItems.map((p) => (p.kind === "existing" ? p.id : newPlateIdsInOrder[nextNew++]));