From 25c08e727917abd829e07176dc645f8bc4fc81bc Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 18 May 2026 21:57:44 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Create=20label=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 9 ++ src/components/CreateEditOrgLabelForm.tsx | 7 -- src/components/CreateOrgLabelDialog.tsx | 54 +++++++--- src/components/OrgLabelForm.tsx | 125 ++++++++++++++++++++++ src/components/OrgLabelsTable.tsx | 49 ++++++--- src/components/labels-selector.tsx | 2 +- 6 files changed, 208 insertions(+), 38 deletions(-) delete mode 100644 src/components/CreateEditOrgLabelForm.tsx create mode 100644 src/components/OrgLabelForm.tsx diff --git a/messages/en-US.json b/messages/en-US.json index e42662968..da5c8a4a5 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -256,6 +256,15 @@ "resourceDelete": "Delete Resource", "resourceDeleteConfirm": "Confirm Delete Resource", "labelDelete": "Delete Label", + "labelAdd": "Add Label", + "labelCreateSuccessMessage": "Label Created Successfully", + "labelEditSuccess": "Label Modified Successfully", + "labelNameField": "Label Name", + "labelColorField": "Label Color", + "labelPlaceholder": "Ex: homelab", + "labelCreate": "Create Label", + "createLabelDialogTitle": "Create Label", + "createLabelDialogDescription": "Create a new label that can be attached to this organization", "labelDeleteConfirm": "Confirm Delete Label", "labelErrorDelete": "Failed to delete label", "labelMessageRemove": "This action is permanent. All sites, resources, and clients tagged with this label will be untagged.", diff --git a/src/components/CreateEditOrgLabelForm.tsx b/src/components/CreateEditOrgLabelForm.tsx deleted file mode 100644 index 117131aca..000000000 --- a/src/components/CreateEditOrgLabelForm.tsx +++ /dev/null @@ -1,7 +0,0 @@ -"use client"; - -export type CreateEditOrgLabelProps = {}; - -export function CreateEditOrgLabel({}: CreateEditOrgLabelProps) { - return <>; -} diff --git a/src/components/CreateOrgLabelDialog.tsx b/src/components/CreateOrgLabelDialog.tsx index f06f979ca..d91a075c0 100644 --- a/src/components/CreateOrgLabelDialog.tsx +++ b/src/components/CreateOrgLabelDialog.tsx @@ -1,9 +1,12 @@ "use client"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { createApiClient } from "@app/lib/api"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; +import type { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; -import { useState, useTransition } from "react"; +import { useTransition } from "react"; import { Credenza, CredenzaBody, @@ -14,6 +17,7 @@ import { CredenzaHeader, CredenzaTitle } from "./Credenza"; +import { OrgLabelForm } from "./OrgLabelForm"; import { Button } from "./ui/button"; export type CreateOrgLabelDialogProps = { @@ -33,21 +37,45 @@ export function CreateOrgLabelDialog({ const api = createApiClient(useEnvContext()); const [isSubmitting, startTransition] = useTransition(); + async function createOrgLabel(data: { name: string; color: string }) { + try { + const res = await api.post< + AxiosResponse + >(`/org/${orgId}/labels`, data); + + if (res.status === 201) { + setOpen(false); + onSuccess?.(); + + toast({ + title: t("success"), + description: t("labelCreateSuccessMessage") + }); + } + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e, t("errorOccurred")), + variant: "destructive" + }); + } + } + return ( - + - - {t("createInternalResourceDialogCreateClientResource")} - + {t("createLabelDialogTitle")} - {t( - "createInternalResourceDialogCreateClientResourceDescription" - )} + {t("createLabelDialogDescription")} - <> + { + startTransition(async () => createOrgLabel(data)); + }} + /> @@ -56,16 +84,16 @@ export function CreateOrgLabelDialog({ onClick={() => setOpen(false)} disabled={isSubmitting} > - {t("createInternalResourceDialogCancel")} + {t("cancel")} diff --git a/src/components/OrgLabelForm.tsx b/src/components/OrgLabelForm.tsx new file mode 100644 index 000000000..492c80e95 --- /dev/null +++ b/src/components/OrgLabelForm.tsx @@ -0,0 +1,125 @@ +"use client"; + +import z from "zod"; +import { Input } from "./ui/input"; +import { useTranslations } from "use-intl"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "./ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "./ui/select"; +import { LABEL_COLORS } from "./labels-selector"; + +const labelFormSchema = z.object({ + name: z.string().nonempty(), + color: z + .string() + .regex(/^#?([0-9a-f]{6}|[0-9a-f]{3})$/i) + .nonempty() +}); + +export type LabelFormData = z.infer; + +export type OrgLabelFormProps = { + onSubmit: (data: LabelFormData) => void; +}; + +export function OrgLabelForm({ onSubmit }: OrgLabelFormProps) { + const t = useTranslations(); + + const colorValues = Object.values(LABEL_COLORS); + const randomColor = + colorValues[Math.floor(Math.random() * colorValues.length)]; + + const form = useForm({ + resolver: zodResolver(labelFormSchema), + defaultValues: { + name: "", + color: randomColor + } + }); + + return ( +
+ { + if (await form.trigger()) { + onSubmit(form.getValues()); + } + }} + > + ( + + {t("labelNameField")} + + + + + + )} + /> + + ( + + {t("labelColorField")} + + + + )} + /> + + + ); +} diff --git a/src/components/OrgLabelsTable.tsx b/src/components/OrgLabelsTable.tsx index bcb6f59ea..5fd90f28d 100644 --- a/src/components/OrgLabelsTable.tsx +++ b/src/components/OrgLabelsTable.tsx @@ -32,6 +32,7 @@ import { LabelBadge } from "./label-badge"; import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { cn } from "@app/lib/cn"; import ConfirmDeleteDialog from "./ConfirmDeleteDialog"; +import { CreateOrgLabelDialog } from "./CreateOrgLabelDialog"; export type LabelRow = { labelId: number; @@ -62,6 +63,8 @@ export default function OrgLabelsTable({ const [selectedLabel, setSelectedLabel] = useState(null); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isRefreshing, startTransition] = useTransition(); @@ -171,27 +174,39 @@ export default function OrgLabelsTable({ return ( <> {selectedLabel && ( - { - setIsDeleteModalOpen(val); - setSelectedLabel(null); - }} - dialog={ -
-

{t("labelQuestionRemove")}

-

{t("labelMessageRemove")}

-
- } - buttonText={t("labelDeleteConfirm")} - onConfirm={async () => deleteLabel(selectedLabel)} - string={selectedLabel.name} - title={t("labelDelete")} - /> + <> + { + setIsDeleteModalOpen(val); + setSelectedLabel(null); + }} + dialog={ +
+

{t("labelQuestionRemove")}

+

{t("labelMessageRemove")}

+
+ } + buttonText={t("labelDeleteConfirm")} + onConfirm={async () => deleteLabel(selectedLabel)} + string={selectedLabel.name} + title={t("labelDelete")} + /> + )} + + startTransition(() => router.refresh())} + /> + setIsCreateModalOpen(true)} tableId="org-labels-table" searchPlaceholder={t("labelSearch")} pagination={pagination} diff --git a/src/components/labels-selector.tsx b/src/components/labels-selector.tsx index 1f7714d07..94b8925b0 100644 --- a/src/components/labels-selector.tsx +++ b/src/components/labels-selector.tsx @@ -38,7 +38,7 @@ export type LabelsSelectorProps = { toggleLabel: (newlabel: SelectedLabel, action: "detach" | "attach") => void; }; -const LABEL_COLORS = { +export const LABEL_COLORS = { red: "#ff6467", green: "#05df72", blue: "#51a2ff",