mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 08:18:19 +00:00
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:
1 parent
8a63e38ecd
commit
2c530f67c3
3 files changed
+31
-73
No files matched your search
@@ -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]",
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user