fix(custom-metrics): say "name taken" only for a name clash

The form read every 409 as "a metric with this name already exists".
Now that it keeps one idempotency key across taps, a second tap while
the first create is still running also answers 409, and the form told
the person their name was taken. The create and rename routes send
`customMetric.name_taken` with the clash, and the form matches the code.
This commit is contained in:
Marc-André Bombeck committed 2026-11-12 12:52:39 +01:00
1 parent 671245c782
commit 706af61cdd
6 files changed
+14 -3

No files matched your search

@@ -225,6 +225,7 @@ describe("PATCH /api/custom-metrics/[id]", () => {
const res = await PATCH(patchReq({ name: "Other" }), params("cm-1"));
expect(res.status).toBe(409);
expect((await res.json()).meta.errorCode).toBe("customMetric.name_taken");
expect(prisma.customMetric.update).not.toHaveBeenCalled();
});
});
+3 -1
View File
@@ -111,7 +111,9 @@ export const PATCH = apiHandler(
select: { id: true },
});
if (clash) {
return apiError("A custom metric with this name already exists", 409);
return apiError("A custom metric with this name already exists", 409, {
errorCode: "customMetric.name_taken",
});
}
}
@@ -134,6 +134,9 @@ describe("POST /api/custom-metrics", () => {
const res = await POST(postReq({ name: "Grip strength", unit: "kg" }));
expect(res.status).toBe(409);
// The form names the clash by this code: a 409 without it can be the
// first save still running under the same key.
expect((await res.json()).meta.errorCode).toBe("customMetric.name_taken");
expect(prisma.customMetric.create).not.toHaveBeenCalled();
});
+3 -1
View File
@@ -100,7 +100,9 @@ async function postCustomMetric(request: NextRequest) {
select: { id: true, deletedAt: true },
});
if (existing && existing.deletedAt === null) {
return apiError("A custom metric with this name already exists", 409);
return apiError("A custom metric with this name already exists", 409, {
errorCode: "customMetric.name_taken",
});
}
// Field-by-field assignment — never spread `parsed.data`.
@@ -132,8 +132,10 @@ export function CustomMetricForm({
);
onSuccess?.(saved);
} catch (err) {
// By its code: a 409 can also be the first save still running.
setError(
err instanceof ApiError && err.status === 409
err instanceof ApiError &&
err.meta?.errorCode === "customMetric.name_taken"
? t("customMetrics.form.nameTaken")
: localizedApiError(err, t, "customMetrics.form.saveError"),
);
+1
View File
@@ -140,6 +140,7 @@ export const ERROR_CODE_CATALOGUE: Readonly<Record<string, readonly string[]>> =
"coach-prefs.body.invalid_shape",
],
consent: ["consent.ai.confirmRequired", "consent.ai.required"],
customMetric: ["customMetric.name_taken"],
cycle: [
"cycle.bulk.invalid",
"cycle.bulk.too_large",