mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
231 lines
8.1 KiB
TypeScript
231 lines
8.1 KiB
TypeScript
import { useState, type ReactNode } from "react";
|
|
import { GripVertical, Plus, Trash2 } from "lucide-react";
|
|
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
import type {
|
|
DropdownOption,
|
|
DropdownSetting,
|
|
} from "@/types/dropdownSettings";
|
|
|
|
export interface ManageDropdownOptionsDialogProps {
|
|
setting: DropdownSetting;
|
|
children: ReactNode;
|
|
}
|
|
|
|
function makeEmptyOption(settingCode: string, idx: number): DropdownOption {
|
|
return {
|
|
id: `${settingCode}-new-${Date.now()}-${idx}`,
|
|
value: "",
|
|
label: "",
|
|
order: idx + 1,
|
|
};
|
|
}
|
|
|
|
export default function ManageDropdownOptionsDialog({
|
|
setting,
|
|
children,
|
|
}: ManageDropdownOptionsDialogProps) {
|
|
const sortedInitial = [...setting.children].sort(
|
|
(a, b) => (a.order ?? 0) - (b.order ?? 0),
|
|
);
|
|
const [options, setOptions] = useState<DropdownOption[]>(sortedInitial);
|
|
|
|
const update = (i: number, patch: Partial<DropdownOption>) =>
|
|
setOptions((prev) =>
|
|
prev.map((o, idx) => (idx === i ? { ...o, ...patch } : o)),
|
|
);
|
|
|
|
const updateMeta = (
|
|
i: number,
|
|
patch: Partial<NonNullable<DropdownOption["meta"]>>,
|
|
) =>
|
|
setOptions((prev) =>
|
|
prev.map((o, idx) =>
|
|
idx === i ? { ...o, meta: { ...(o.meta ?? {}), ...patch } } : o,
|
|
),
|
|
);
|
|
|
|
const remove = (i: number) =>
|
|
setOptions((prev) => prev.filter((_, idx) => idx !== i));
|
|
|
|
const add = () =>
|
|
setOptions((prev) => [
|
|
...prev,
|
|
makeEmptyOption(setting.code, prev.length),
|
|
]);
|
|
|
|
const move = (i: number, dir: -1 | 1) =>
|
|
setOptions((prev) => {
|
|
const next = [...prev];
|
|
const target = i + dir;
|
|
if (target < 0 || target >= next.length) return prev;
|
|
const a = next[i] as DropdownOption;
|
|
const b = next[target] as DropdownOption;
|
|
next[i] = { ...b, order: i + 1 };
|
|
next[target] = { ...a, order: target + 1 };
|
|
return next;
|
|
});
|
|
|
|
return (
|
|
<Dialog>
|
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
|
|
|
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-4xl rounded-3xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-2xl font-bold">
|
|
Manage Options · {setting.label}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Add, edit, reorder, or remove options for{" "}
|
|
<span className="font-mono text-slate-700">{setting.code}</span>.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-3 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-sm text-slate-500">
|
|
{options.length} option{options.length === 1 ? "" : "s"}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={add}
|
|
className="inline-flex items-center gap-1.5 rounded-xl bg-[#33578D] px-3 py-1.5 text-xs font-medium text-white transition hover:bg-[#33578D]/90"
|
|
>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
Add Option
|
|
</button>
|
|
</div>
|
|
|
|
{options.length === 0 ? (
|
|
<div className="rounded-2xl border border-dashed border-slate-200 p-8 text-center text-sm text-slate-500">
|
|
No options yet. Click <span className="font-medium">Add Option</span> to start.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{options.map((opt, i) => (
|
|
<div
|
|
key={opt.id}
|
|
className="grid gap-2 rounded-2xl border border-slate-200 bg-white p-3 md:grid-cols-[auto_1fr_1fr_1fr_auto_auto_auto]"
|
|
>
|
|
<div className="flex items-center gap-1 text-slate-400">
|
|
<GripVertical className="h-4 w-4" />
|
|
<div className="flex flex-col">
|
|
<button
|
|
type="button"
|
|
onClick={() => move(i, -1)}
|
|
aria-label="Move up"
|
|
disabled={i === 0}
|
|
className="text-[10px] leading-none text-slate-400 transition hover:text-[#33578D] disabled:opacity-30"
|
|
>
|
|
▲
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => move(i, 1)}
|
|
aria-label="Move down"
|
|
disabled={i === options.length - 1}
|
|
className="text-[10px] leading-none text-slate-400 transition hover:text-[#33578D] disabled:opacity-30"
|
|
>
|
|
▼
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Label *</Label>
|
|
<Input
|
|
value={opt.label}
|
|
onChange={(e) => update(i, { label: e.target.value })}
|
|
placeholder="Display label"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Value</Label>
|
|
<Input
|
|
value={opt.value}
|
|
onChange={(e) => update(i, { value: e.target.value })}
|
|
placeholder="Stored value"
|
|
className="font-mono"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Note</Label>
|
|
<Input
|
|
value={opt.note ?? ""}
|
|
onChange={(e) => update(i, { note: e.target.value })}
|
|
placeholder="Helper text"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Badge</Label>
|
|
<Input
|
|
value={opt.meta?.badge ?? ""}
|
|
onChange={(e) => updateMeta(i, { badge: e.target.value })}
|
|
placeholder="—"
|
|
className="w-20"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Color</Label>
|
|
<Input
|
|
type="text"
|
|
value={opt.meta?.color ?? ""}
|
|
onChange={(e) => updateMeta(i, { color: e.target.value })}
|
|
placeholder="#…"
|
|
className="w-24 font-mono"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center justify-between gap-2">
|
|
<label className="flex items-center gap-1 text-xs text-slate-600">
|
|
<input
|
|
type="checkbox"
|
|
checked={opt.disabled ?? false}
|
|
onChange={(e) =>
|
|
update(i, { disabled: e.target.checked })
|
|
}
|
|
className="h-3.5 w-3.5 rounded border-slate-300 text-[#33578D] focus:ring-[#33578D]/20"
|
|
/>
|
|
Off
|
|
</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => remove(i)}
|
|
aria-label={`Remove ${opt.label || "option"}`}
|
|
className="rounded-lg p-1 text-red-500 transition hover:bg-red-50"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3 border-t border-slate-100 pt-3">
|
|
<Button variant="outline">Cancel</Button>
|
|
<Button className="bg-[#33578D] text-white hover:bg-[#33578D]/90">
|
|
Save Options
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|