From 95d0da25a0381d43a10d0b78f39df42b729c0de4 Mon Sep 17 00:00:00 2001 From: ChristoferMendes Date: Fri, 26 Sep 2025 17:13:28 -0300 Subject: [PATCH] feat(notifications): introduce KeyValueInput component for managing key-value pairs in notifications --- .../notifications/key-value-input.tsx | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 apps/dokploy/components/dashboard/settings/notifications/key-value-input.tsx diff --git a/apps/dokploy/components/dashboard/settings/notifications/key-value-input.tsx b/apps/dokploy/components/dashboard/settings/notifications/key-value-input.tsx new file mode 100644 index 000000000..b8d275e6c --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/notifications/key-value-input.tsx @@ -0,0 +1,162 @@ +import { Plus, Trash2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +interface KeyValuePair { + key: string; + value: string; + enabled: boolean; +} + +interface KeyValueInputProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + label: string; + description?: string; +} + +const createEmptyPair = (): KeyValuePair => ({ + key: "", + value: "", + enabled: true, +}); + +const parseJsonToPairs = (jsonString: string): KeyValuePair[] => { + try { + const parsed = JSON.parse(jsonString); + const pairs = Object.entries(parsed).map(([key, val]) => ({ + key, + value: String(val), + enabled: true, + })); + return pairs.length > 0 ? pairs : [createEmptyPair()]; + } catch { + return [createEmptyPair()]; + } +}; + +const convertPairsToJson = (pairs: KeyValuePair[]): string => { + const enabledPairs = pairs.filter((pair) => pair.enabled && pair.key.trim()); + + if (enabledPairs.length === 0) { + return ""; + } + + const obj = enabledPairs.reduce((acc, pair) => { + acc[pair.key.trim()] = pair.value; + return acc; + }, {} as Record); + + return JSON.stringify(obj, null, 2); +}; + +export const KeyValueInput = ({ + value, + onChange, + label, + description, +}: KeyValueInputProps) => { + const [pairs, setPairs] = useState([]); + + useEffect(() => { + const newPairs = value ? parseJsonToPairs(value) : [createEmptyPair()]; + setPairs(newPairs); + }, [value]); + + const syncPairsWithParent = (newPairs: KeyValuePair[]) => { + setPairs(newPairs); + onChange(convertPairsToJson(newPairs)); + }; + + const addPair = () => { + syncPairsWithParent([...pairs, createEmptyPair()]); + }; + + const removePair = (index: number) => { + const filteredPairs = pairs.filter((_, i) => i !== index); + syncPairsWithParent(filteredPairs); + }; + + const updatePair = ( + index: number, + field: keyof KeyValuePair, + newValue: string | boolean + ) => { + const updatedPairs = pairs.map((pair, i) => + i === index ? { ...pair, [field]: newValue } : pair + ); + syncPairsWithParent(updatedPairs); + }; + + return ( +
+
+ + {description && ( +

{description}

+ )} +
+ +
+ {pairs.map((pair, index) => ( +
+
+ + updatePair(index, "enabled", checked) + } + className="mr-2" + /> +
+
+ updatePair(index, "key", e.target.value)} + className="text-sm" + disabled={!pair.enabled} + /> +
+
+ updatePair(index, "value", e.target.value)} + className="text-sm" + disabled={!pair.enabled} + /> +
+ +
+ ))} +
+ + +
+ ); +};