From 173562654b3ef183c57b4dd4c69422ed5194e112 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 14 May 2026 21:09:48 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20delete=20org=20label=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + server/auth/actions.ts | 1 + server/private/routers/external.ts | 8 + .../private/routers/labels/deleteOrgLabel.ts | 72 ++++++ server/private/routers/labels/index.ts | 1 + .../settings/(private)/labels/page.tsx | 49 +++- src/components/OrgLabelsTable.tsx | 230 ++++++++++++++++++ src/components/labels-selector.tsx | 21 +- 8 files changed, 369 insertions(+), 14 deletions(-) create mode 100644 server/private/routers/labels/deleteOrgLabel.ts diff --git a/messages/en-US.json b/messages/en-US.json index 751c0e746..07482bf80 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1141,6 +1141,7 @@ "idpErrorNotFound": "IdP not found", "inviteInvalid": "Invalid Invite", "labels": "Labels", + "orgLabelsDescription": "Manage labels in this organization.", "addLabels": "Add labels", "siteLabelsTab": "Labels", "siteLabelsDescription": "Manage labels associated with this site.", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 969f9e4ae..bba2265fb 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -151,6 +151,7 @@ export enum ActionsEnum { listOrgLabels = "listOrgLabels", createOrgLabel = "createOrgLabel", updateOrgLabel = "updateOrgLabel", + deleteOrgLabel = "deleteOrgLabel", attachLabelToItem = "attachLabelToItem", detachLabelFromItem = "detachLabelFromItem", getAlertRule = "getAlertRule", diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 5e20f6db6..8745dffdf 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -757,6 +757,14 @@ authenticated.patch( labels.updateOrgLabel ); +authenticated.delete( + "/org/:orgId/label/:labelId", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.deleteOrgLabel), + labels.deleteOrgLabel +); + authenticated.put( "/org/:orgId/label/:labelId/attach", verifyValidLicense, diff --git a/server/private/routers/labels/deleteOrgLabel.ts b/server/private/routers/labels/deleteOrgLabel.ts new file mode 100644 index 000000000..f091c910a --- /dev/null +++ b/server/private/routers/labels/deleteOrgLabel.ts @@ -0,0 +1,72 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ +import { db, labels } from "@server/db"; +import response from "@server/lib/response"; +import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; +import { and, eq } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; + +const paramsSchema = z.strictObject({ + orgId: z.string().nonempty(), + labelId: z.string().transform(Number).pipe(z.int().positive()) +}); + +export async function deleteOrgLabel( + req: Request, + res: Response, + next: NextFunction +) { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId, labelId } = parsedParams.data; + + const [existing] = await db + .select() + .from(labels) + .where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId))); + + if (!existing) { + return next(createHttpError(HttpCode.NOT_FOUND, "Label not found")); + } + + await db + .delete(labels) + .where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId))); + + return response(res, { + data: null, + success: true, + error: false, + message: "Label deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/labels/index.ts b/server/private/routers/labels/index.ts index ffec1229b..d988d8e38 100644 --- a/server/private/routers/labels/index.ts +++ b/server/private/routers/labels/index.ts @@ -16,3 +16,4 @@ export * from "./createOrgLabel"; export * from "./updateOrgLabel"; export * from "./attachLabelToItem"; export * from "./detachLabelFromItem"; +export * from "./deleteOrgLabel"; diff --git a/src/app/[orgId]/settings/(private)/labels/page.tsx b/src/app/[orgId]/settings/(private)/labels/page.tsx index 54bffb3ff..806c60325 100644 --- a/src/app/[orgId]/settings/(private)/labels/page.tsx +++ b/src/app/[orgId]/settings/(private)/labels/page.tsx @@ -1,7 +1,14 @@ +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { ListOrgLabelsResponse } from "@server/routers/labels/types"; +import { AxiosResponse } from "axios"; +import OrgLabelsTable from "@app/components/OrgLabelsTable"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import type { Metadata } from "next"; +import { getTranslations } from "next-intl/server"; export const metadata: Metadata = { - title: "Enterprise Licenses" + title: "Labels" }; type Props = { @@ -14,7 +21,43 @@ export const dynamic = "force-dynamic"; export default async function LabelsPage({ params, searchParams }: Props) { const { orgId } = await params; - const sp = await searchParams; + const searchParamsObj = new URLSearchParams(await searchParams); - return <>; + let labels: ListOrgLabelsResponse["labels"] = []; + let pagination: ListOrgLabelsResponse["pagination"] = { + total: 0, + page: 1, + pageSize: 20 + }; + + try { + const res = await internal.get>( + `/org/${orgId}/labels?${searchParamsObj.toString()}`, + await authCookieHeader() + ); + const responseData = res.data.data; + labels = responseData.labels; + pagination = responseData.pagination; + } catch (e) {} + + const t = await getTranslations(); + + return ( + <> + + + + + ); } diff --git a/src/components/OrgLabelsTable.tsx b/src/components/OrgLabelsTable.tsx index bce9a91fe..bc06b9101 100644 --- a/src/components/OrgLabelsTable.tsx +++ b/src/components/OrgLabelsTable.tsx @@ -1 +1,231 @@ "use client"; + +import { Button } from "@app/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useNavigationContext } from "@app/hooks/useNavigationContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { type PaginationState } from "@tanstack/react-table"; +import { + ArrowDown01Icon, + ArrowUp10Icon, + ChevronsUpDownIcon, + MoreHorizontal, + PencilIcon, + PencilLineIcon +} from "lucide-react"; +import { useTranslations } from "next-intl"; +import { usePathname, useRouter } from "next/navigation"; +import { useActionState, useMemo, useState, useTransition } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { + ControlledDataTable, + type ExtendedColumnDef +} from "./ui/controlled-data-table"; +import { LabelBadge } from "./label-badge"; +import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; +import { cn } from "@app/lib/cn"; +import ConfirmDeleteDialog from "./ConfirmDeleteDialog"; + +export type LabelRow = { + labelId: number; + name: string; + color: string; +}; + +type OrgLabelsTableProps = { + labels: LabelRow[]; + pagination: PaginationState; + orgId: string; + rowCount: number; +}; + +export default function OrgLabelsTable({ + labels, + orgId, + pagination, + rowCount +}: OrgLabelsTableProps) { + const router = useRouter(); + const pathname = usePathname(); + const { + navigate: filter, + isNavigating: isFiltering, + searchParams + } = useNavigationContext(); + + const [selectedLabel, setSelectedLabel] = useState(null); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + const [isRefreshing, startRefreshTransition] = useTransition(); + + const api = createApiClient(useEnvContext()); + const t = useTranslations(); + + function refreshData() { + startRefreshTransition(async () => { + try { + router.refresh(); + } catch { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } + }); + } + + function toggleSort(column: string) { + const newSearch = getNextSortOrder(column, searchParams); + filter({ searchParams: newSearch }); + } + + const handlePaginationChange = (newPage: PaginationState) => { + searchParams.set("page", (newPage.pageIndex + 1).toString()); + searchParams.set("pageSize", newPage.pageSize.toString()); + filter({ searchParams }); + }; + + const handleSearchChange = useDebouncedCallback((query: string) => { + searchParams.set("query", query); + searchParams.delete("page"); + filter({ searchParams }); + }, 300); + + const columns = useMemo[]>( + () => [ + { + accessorKey: "name", + enableHiding: false, + header: () => { + const nameOrder = getSortDirection("name", searchParams); + const Icon = + nameOrder === "asc" + ? ArrowDown01Icon + : nameOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; + return ( + + ); + }, + cell: ({ row }) => + }, + { + accessorKey: "actions", + enableHiding: false, + header: () => { + return {t("actions")}; + }, + cell: ({ row }) => ( + + + + + + {t("edit")} + {}}> + + {t("delete")} + + + + + ) + } + ], + [searchParams, t] + ); + + async function deleteLabel() { + // ... + } + + return ( + <> + {selectedLabel && ( + { + setIsDeleteModalOpen(val); + setSelectedLabel(null); + }} + dialog={ +
+

{t("resourceQuestionRemove")}

+

{t("resourceMessageRemove")}

+
+ } + buttonText={t("resourceDeleteConfirm")} + onConfirm={async () => {}} + string={selectedLabel.name} + title={t("resourceDelete")} + /> + )} + + + ); +} + +type EditLabelCellProps = { + label: LabelRow; +}; + +function EditLabelCell({ label }: EditLabelCellProps) { + const t = useTranslations(); + + return ( +
+
+ + {label.name} + + {/* */} +
+ ); +} diff --git a/src/components/labels-selector.tsx b/src/components/labels-selector.tsx index 64a80b26a..1f7714d07 100644 --- a/src/components/labels-selector.tsx +++ b/src/components/labels-selector.tsx @@ -1,6 +1,15 @@ +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { orgQueries } from "@app/lib/queries"; +import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; import { useQuery } from "@tanstack/react-query"; -import { useActionState, useMemo, useState, useTransition } from "react"; +import type { AxiosResponse } from "axios"; +import { useTranslations } from "next-intl"; +import { useActionState, useMemo, useState } from "react"; +import { useDebounce } from "use-debounce"; +import { Button } from "./ui/button"; +import { Checkbox } from "./ui/checkbox"; import { Command, CommandEmpty, @@ -9,11 +18,6 @@ import { CommandItem, CommandList } from "./ui/command"; -import { Checkbox } from "./ui/checkbox"; -import { useTranslations } from "next-intl"; -import { useDebounce } from "use-debounce"; -import { type Selectedsite, SiteOnlineStatus } from "./site-selector"; -import { Button } from "./ui/button"; import { Select, SelectContent, @@ -21,11 +25,6 @@ import { SelectTrigger, SelectValue } from "./ui/select"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; -import type { AxiosResponse } from "axios"; -import { toast } from "@app/hooks/useToast"; export type SelectedLabel = { name: string;