diff --git a/messages/en-US.json b/messages/en-US.json index f377c653d..ccc20c13a 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -292,6 +292,8 @@ "labelDelete": "Delete Label", "labelAdd": "Add Label", "labelCreateSuccessMessage": "Label Created Successfully", + "labelDuplicateError": "Duplicate Label", + "labelDuplicateErrorDescription": "A label with this name already exists.", "labelEditSuccessMessage": "Label Modified Successfully", "labelNameField": "Label Name", "labelColorField": "Label Color", diff --git a/server/private/routers/labels/createOrgLabel.ts b/server/private/routers/labels/createOrgLabel.ts index 074a96207..c856eecf4 100644 --- a/server/private/routers/labels/createOrgLabel.ts +++ b/server/private/routers/labels/createOrgLabel.ts @@ -22,7 +22,7 @@ import response from "@server/lib/response"; import logger from "@server/logger"; import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; import HttpCode from "@server/types/HttpCode"; -import { and, eq } from "drizzle-orm"; +import { and, eq, sql } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -107,6 +107,26 @@ export async function createOrgLabel( } } + const [existingLabel] = await db + .select({ labelId: labels.labelId }) + .from(labels) + .where( + and( + eq(labels.orgId, orgId), + sql`LOWER(${labels.name}) = ${name.toLowerCase()}` + ) + ) + .limit(1); + + if (existingLabel) { + return next( + createHttpError( + HttpCode.CONFLICT, + "A label with this name already exists" + ) + ); + } + const label = await db.transaction(async (tx) => { const [label] = await tx .insert(labels) diff --git a/server/private/routers/labels/updateOrgLabel.ts b/server/private/routers/labels/updateOrgLabel.ts index eb5f5177a..134a01f02 100644 --- a/server/private/routers/labels/updateOrgLabel.ts +++ b/server/private/routers/labels/updateOrgLabel.ts @@ -16,7 +16,7 @@ import response from "@server/lib/response"; import logger from "@server/logger"; import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; import HttpCode from "@server/types/HttpCode"; -import { and, eq } from "drizzle-orm"; +import { and, eq, ne, sql } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -74,6 +74,29 @@ export async function updateOrgLabel( const { name, color } = parsedBody.data; + if (name && name.toLowerCase() !== existing.name.toLowerCase()) { + const [duplicateLabel] = await db + .select({ labelId: labels.labelId }) + .from(labels) + .where( + and( + eq(labels.orgId, orgId), + ne(labels.labelId, labelId), + sql`LOWER(${labels.name}) = ${name.toLowerCase()}` + ) + ) + .limit(1); + + if (duplicateLabel) { + return next( + createHttpError( + HttpCode.CONFLICT, + "A label with this name already exists" + ) + ); + } + } + const [label] = await db .update(labels) .set({ diff --git a/src/components/CreateOrgLabelDialog.tsx b/src/components/CreateOrgLabelDialog.tsx index 9e1ab12f5..047e6289f 100644 --- a/src/components/CreateOrgLabelDialog.tsx +++ b/src/components/CreateOrgLabelDialog.tsx @@ -52,12 +52,20 @@ export function CreateOrgLabelDialog({ description: t("labelCreateSuccessMessage") }); } - } catch (e) { - toast({ - title: t("error"), - description: formatAxiosError(e, t("errorOccurred")), - variant: "destructive" - }); + } catch (e: any) { + if (e.response?.status === 409) { + toast({ + title: t("labelDuplicateError"), + description: t("labelDuplicateErrorDescription"), + variant: "destructive" + }); + } else { + toast({ + title: t("error"), + description: formatAxiosError(e, t("errorOccurred")), + variant: "destructive" + }); + } } } diff --git a/src/components/EditOrgLabelDialog.tsx b/src/components/EditOrgLabelDialog.tsx index 98891cd38..23bd6f88c 100644 --- a/src/components/EditOrgLabelDialog.tsx +++ b/src/components/EditOrgLabelDialog.tsx @@ -58,12 +58,20 @@ export function EditOrgLabelDialog({ description: t("labelEditSuccessMessage") }); } - } catch (e) { - toast({ - title: t("error"), - description: formatAxiosError(e, t("errorOccurred")), - variant: "destructive" - }); + } catch (e: any) { + if (e.response?.status === 409) { + toast({ + title: t("labelDuplicateError"), + description: t("labelDuplicateErrorDescription"), + variant: "destructive" + }); + } else { + toast({ + title: t("error"), + description: formatAxiosError(e, t("errorOccurred")), + variant: "destructive" + }); + } } } diff --git a/src/components/labels-selector.tsx b/src/components/labels-selector.tsx index 788d9fe7d..5cf7ce944 100644 --- a/src/components/labels-selector.tsx +++ b/src/components/labels-selector.tsx @@ -112,12 +112,20 @@ export function LabelsSelector({ }, "attach" ); - } catch (e) { - toast({ - title: t("error"), - description: formatAxiosError(e, t("errorOccurred")), - variant: "destructive" - }); + } catch (e: any) { + if (e.response?.status === 409) { + toast({ + title: t("labelDuplicateError"), + description: t("labelDuplicateErrorDescription"), + variant: "destructive" + }); + } else { + toast({ + title: t("error"), + description: formatAxiosError(e, t("errorOccurred")), + variant: "destructive" + }); + } } setlabelsSearchQuery(""); }