fix(ui): agent delete dialog + channel managers cleanup + combobox iOS

- agents-page: ConfirmDeleteDialog with type-to-confirm, expanded warning
- channel-managers-tab: simplify add form (remove manual name/username
  fields, auto-fill from contact search), Loader2 spinner on refresh
- combobox: text-base md:text-sm to prevent iOS Safari auto-zoom
This commit is contained in:
viettranx committed 2026-03-19 13:35:57 +07:00
1 parent 8a63e38ecd
commit 2c530f67c3
3 files changed
+31 -73

No files matched your search

+1 -1
View File
@@ -147,7 +147,7 @@ export function Combobox({
onFocus={() => options.length > 0 && setOpen(true)}
placeholder={placeholder}
className={cn(
"border-input placeholder:text-muted-foreground dark:bg-input/30 h-9 w-full rounded-md border bg-transparent px-3 py-1 pr-8 text-sm shadow-xs outline-none transition-[color,box-shadow]",
"border-input placeholder:text-muted-foreground dark:bg-input/30 h-9 w-full rounded-md border bg-transparent px-3 py-1 pr-8 text-base md:text-sm shadow-xs outline-none transition-[color,box-shadow]",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
)}
/>
+9 -10
View File
@@ -10,7 +10,7 @@ import { CardSkeleton } from "@/components/shared/loading-skeleton";
import { useDeferredLoading } from "@/hooks/use-deferred-loading";
import { Button } from "@/components/ui/button";
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { ConfirmDialog } from "@/components/shared/confirm-dialog";
import { ConfirmDeleteDialog } from "@/components/shared/confirm-delete-dialog";
import {
Select,
SelectContent,
@@ -38,7 +38,7 @@ export function AgentsPage() {
const [viewMode, setViewMode] = useState<"card" | "list">("card");
const [ownerFilter, setOwnerFilter] = useState<string | undefined>();
const [createOpen, setCreateOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
const [deleteTarget, setDeleteTarget] = useState<{ id: string; name: string } | null>(null);
const [summoningAgent, setSummoningAgent] = useState<{ id: string; name: string } | null>(null);
// Collect unique owner IDs for filter + contact resolution
@@ -192,7 +192,7 @@ export function AgentsPage() {
agent={agent}
onClick={() => handleClick(agent)}
onResummon={() => handleResummon(agent)}
onDelete={() => setDeleteTarget(agent.id)}
onDelete={() => setDeleteTarget({ id: agent.id, name: agent.display_name || agent.agent_key })}
/>
))}
</div>
@@ -205,7 +205,7 @@ export function AgentsPage() {
ownerName={resolveOwnerName(agent.owner_id)}
onClick={() => handleClick(agent)}
onResummon={() => handleResummon(agent)}
onDelete={() => setDeleteTarget(agent.id)}
onDelete={() => setDeleteTarget({ id: agent.id, name: agent.display_name || agent.agent_key })}
/>
))}
</div>
@@ -238,16 +238,15 @@ export function AgentsPage() {
}}
/>
<ConfirmDialog
<ConfirmDeleteDialog
open={!!deleteTarget}
onOpenChange={() => setDeleteTarget(null)}
onOpenChange={(open) => !open && setDeleteTarget(null)}
title={t("delete.title")}
description={t("delete.description")}
confirmLabel={t("delete.confirmLabel")}
variant="destructive"
description={t("delete.deleteWarning")}
confirmValue={deleteTarget?.name ?? ""}
onConfirm={async () => {
if (deleteTarget) {
await deleteAgent(deleteTarget);
await deleteAgent(deleteTarget.id);
setDeleteTarget(null);
}
}}
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Plus, Trash2, RefreshCw, Users, ChevronDown, ChevronRight } from "lucide-react";
import { Plus, Trash2, Loader2, RefreshCw, Users, ChevronDown, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -37,19 +37,12 @@ function InlineAddForm({ groupId, showGroupField, listContacts, onAdd }: InlineA
const { t } = useTranslation("channels");
const [formGroupId, setFormGroupId] = useState("");
const [userId, setUserId] = useState("");
const [displayName, setDisplayName] = useState("");
const [username, setUsername] = useState("");
const [adding, setAdding] = useState(false);
const [error, setError] = useState("");
const { options: contactOptions, searchContacts, getContact, clearOptions } = useContactPicker(listContacts);
const handleUserIdChange = (val: string) => {
setUserId(val);
const contact = getContact(val);
if (contact) {
setDisplayName(contact.display_name ?? "");
setUsername(contact.username ?? "");
}
searchContacts(val);
};
@@ -63,10 +56,12 @@ function InlineAddForm({ groupId, showGroupField, listContacts, onAdd }: InlineA
setAdding(true);
setError("");
try {
await onAdd(gid, uid, displayName.trim(), username.trim());
// Auto-fill display name and username from selected contact
const contact = getContact(uid);
const displayName = contact?.display_name ?? "";
const username = contact?.username ?? "";
await onAdd(gid, uid, displayName, username);
setUserId("");
setDisplayName("");
setUsername("");
clearOptions();
if (!groupId) setFormGroupId("");
} catch (err) {
@@ -81,89 +76,50 @@ function InlineAddForm({ groupId, showGroupField, listContacts, onAdd }: InlineA
<fieldset className="rounded-md border p-4 space-y-3">
<legend className="px-1 text-sm font-medium">{t("detail.managers.addForm.title")}</legend>
<p className="text-xs text-muted-foreground">{t("detail.managers.addForm.hint")}</p>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="grid gap-1.5">
<div className="flex flex-wrap items-end gap-2">
<div className="grid gap-1.5 flex-1 min-w-[180px]">
<Label className="text-xs">{t("detail.managers.addForm.groupId")}</Label>
<Input
value={formGroupId}
onChange={(e) => setFormGroupId(e.target.value)}
placeholder={t("detail.managers.addForm.groupIdPlaceholder")}
className="text-sm"
className="text-base md:text-sm"
/>
</div>
<div className="grid gap-1.5">
<div className="grid gap-1.5 flex-1 min-w-[180px]">
<Label className="text-xs">{t("detail.managers.addForm.userId")}</Label>
<Combobox
value={userId}
onChange={handleUserIdChange}
options={contactOptions}
placeholder={t("detail.managers.addForm.userIdPlaceholder")}
className="text-sm"
/>
</div>
<div className="grid gap-1.5">
<Label className="text-xs">{t("detail.managers.addForm.displayName")}</Label>
<Input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder={t("detail.managers.addForm.optional")}
className="text-sm"
/>
</div>
<div className="grid gap-1.5">
<Label className="text-xs">{t("detail.managers.addForm.username")}</Label>
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder={t("detail.managers.addForm.usernameWithout")}
className="text-sm"
/>
</div>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<div className="flex justify-end">
<Button
onClick={handleSubmit}
disabled={adding || !formGroupId.trim() || !userId.trim()}
size="sm"
className="gap-1"
className="h-9 gap-1 shrink-0"
>
<Plus className="h-3.5 w-3.5" />
{adding ? t("detail.managers.addForm.adding") : t("detail.managers.addForm.addManager")}
{adding ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
{t("detail.managers.addForm.addManager")}
</Button>
</div>
{error && <p className="text-sm text-destructive mt-1">{error}</p>}
</fieldset>
);
}
return (
<div className="flex items-end gap-2">
<div className="grid gap-1 flex-1">
<div className="grid gap-1 flex-1 min-w-[140px]">
<Label className="text-xs text-muted-foreground">{t("detail.managers.addForm.userId")}</Label>
<Combobox
value={userId}
onChange={handleUserIdChange}
options={contactOptions}
placeholder={t("detail.managers.addForm.userIdPlaceholder")}
className="h-8 text-sm"
/>
</div>
<div className="grid gap-1 flex-1">
<Label className="text-xs text-muted-foreground">{t("detail.managers.addForm.displayName")}</Label>
<Input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder={t("detail.managers.addForm.optional")}
className="h-8 text-sm"
/>
</div>
<div className="grid gap-1 flex-1">
<Label className="text-xs text-muted-foreground">{t("detail.managers.addForm.username")}</Label>
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder={t("detail.managers.addForm.optional")}
className="h-8 text-sm"
className="h-8"
/>
</div>
<Button
@@ -172,7 +128,7 @@ function InlineAddForm({ groupId, showGroupField, listContacts, onAdd }: InlineA
onClick={handleSubmit}
disabled={adding || !userId.trim()}
>
<Plus className="h-3.5 w-3.5" />
{adding ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
{t("detail.managers.addForm.add")}
</Button>
{error && <p className="text-xs text-destructive mt-1">{error}</p>}
@@ -277,7 +233,10 @@ export function ChannelManagersTab({
)}
</h3>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={refreshGroups} disabled={loadingGroups}>
<RefreshCw className={"h-3.5 w-3.5" + (loadingGroups ? " animate-spin" : "")} />
{loadingGroups
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
: <RefreshCw className="h-3.5 w-3.5" />
}
</Button>
</div>