fix(ui): add loading spinners and toast feedback to create/update modals

- TeamMembersDialog: Loader2 spinner on add/remove, toast on success/error
- TeamCreateDialog: Loader2 spinner on create button
- TeamSettingsTab: Loader2 spinner on save button (was text-only)
- ProviderFormDialog: Loader2 spinner on save/create button
- Add i18n keys for member add/remove toast (en/vi/zh)
This commit is contained in:
viettranx committed 2026-03-17 10:53:07 +07:00
1 parent 1b876231a1
commit 0a78902f9b
7 files changed
+50 -11

No files matched your search

+4
View File
@@ -42,6 +42,10 @@
"searchAgents": "Search agents...",
"adding": "Adding...",
"add": "Add",
"added": "Member added",
"failedAdd": "Failed to add member",
"removed": "Member removed",
"failedRemove": "Failed to remove member",
"removeMember": "Remove member",
"columns": {
"agent": "Agent",
+4
View File
@@ -42,6 +42,10 @@
"searchAgents": "Tìm kiếm agent...",
"adding": "Đang thêm...",
"add": "Thêm",
"added": "Đã thêm thành viên",
"failedAdd": "Thêm thành viên thất bại",
"removed": "Đã xóa thành viên",
"failedRemove": "Xóa thành viên thất bại",
"removeMember": "Xóa thành viên",
"columns": {
"agent": "Agent",
+4
View File
@@ -42,6 +42,10 @@
"searchAgents": "搜索Agent...",
"adding": "添加中...",
"add": "添加",
"added": "成员已添加",
"failedAdd": "添加成员失败",
"removed": "成员已移除",
"failedRemove": "移除成员失败",
"removeMember": "移除成员",
"columns": {
"agent": "Agent",
@@ -26,6 +26,7 @@ import { PROVIDER_TYPES } from "@/constants/providers";
import { OAuthSection } from "./provider-oauth-section";
import { CLISection } from "./provider-cli-section";
import { ACPSection } from "./provider-acp-section";
import { Loader2 } from "lucide-react";
interface ProviderFormDialogProps {
open: boolean;
@@ -293,7 +294,9 @@ export function ProviderFormDialog({ open, onOpenChange, provider, onSubmit, exi
<Button
onClick={handleSubmit}
disabled={!name.trim() || !isValidSlug(name) || !providerType || loading}
className="gap-1"
>
{loading && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
{loading
? (isEdit ? t("form.saving") : t("form.creating"))
: isEdit ? t("form.save") : t("form.create")}
@@ -5,9 +5,10 @@ import {
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { Bot, UserPlus, X } from "lucide-react";
import { Bot, UserPlus, X, Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useAgents } from "@/pages/agents/hooks/use-agents";
import { toast } from "@/stores/use-toast-store";
import type { TeamMemberData } from "@/types/team";
interface TeamMembersDialogProps {
@@ -67,6 +68,8 @@ export function TeamMembersDialog({
[members],
);
const [removing, setRemoving] = useState<string | null>(null);
const handleAdd = async () => {
if (!selected) return;
setAdding(true);
@@ -74,8 +77,24 @@ export function TeamMembersDialog({
await onAddMember(selected, "member");
setSelected("");
setShowAdd(false);
} catch { /* handled upstream */ }
finally { setAdding(false); }
toast.success(t("members.added"));
} catch {
toast.error(t("members.failedAdd"));
} finally {
setAdding(false);
}
};
const handleRemove = async (agentId: string) => {
setRemoving(agentId);
try {
await onRemoveMember(agentId);
toast.success(t("members.removed"));
} catch {
toast.error(t("members.failedRemove"));
} finally {
setRemoving(null);
}
};
return (
@@ -104,7 +123,8 @@ export function TeamMembersDialog({
placeholder={t("members.searchAgents")}
/>
</div>
<Button size="sm" className="h-9 shrink-0" disabled={!selected || adding} onClick={handleAdd}>
<Button size="sm" className="h-9 shrink-0 gap-1" disabled={!selected || adding} onClick={handleAdd}>
{adding && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
{t("members.add")}
</Button>
</div>
@@ -139,10 +159,11 @@ export function TeamMembersDialog({
</div>
{m.role !== "lead" && (
<button
className="mt-0.5 shrink-0 cursor-pointer text-muted-foreground opacity-0 transition-opacity hover:text-destructive group-hover:opacity-100"
onClick={() => onRemoveMember(m.agent_id)}
className="mt-0.5 shrink-0 cursor-pointer text-muted-foreground opacity-0 transition-opacity hover:text-destructive group-hover:opacity-100 disabled:opacity-50"
onClick={() => handleRemove(m.agent_id)}
disabled={removing === m.agent_id}
>
<X className="h-4 w-4" />
{removing === m.agent_id ? <Loader2 className="h-4 w-4 animate-spin" /> : <X className="h-4 w-4" />}
</button>
)}
</div>
@@ -11,7 +11,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Combobox } from "@/components/ui/combobox";
import { Badge } from "@/components/ui/badge";
import { X, Info } from "lucide-react";
import { X, Info, Loader2 } from "lucide-react";
import {
Tooltip,
TooltipContent,
@@ -199,7 +199,8 @@ export function TeamCreateDialog({ open, onOpenChange, onCreate }: TeamCreateDia
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
{t("create.cancel")}
</Button>
<Button onClick={handleCreate} disabled={!name.trim() || !leadOptions.some((o) => o.value === lead) || loading}>
<Button onClick={handleCreate} disabled={!name.trim() || !leadOptions.some((o) => o.value === lead) || loading} className="gap-1">
{loading && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
{loading ? t("create.creating") : t("create.create")}
</Button>
</DialogFooter>
+4 -2
View File
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Combobox } from "@/components/ui/combobox";
import { X, Save, Check, Bell, ShieldAlert, Clock, Info, FolderLock, FolderSync, Zap, Bot } from "lucide-react";
import { X, Save, Check, Bell, ShieldAlert, Clock, Info, FolderLock, FolderSync, Zap, Bot, Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CHANNEL_TYPES } from "@/constants/channels";
import type { TeamData, TeamAccessSettings, EscalationMode, EscalationAction } from "@/types/team";
@@ -449,7 +449,9 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={saving} className="gap-2">
{saving ? (
t("settings.saving")
<>
<Loader2 className="h-4 w-4 animate-spin" /> {t("settings.saving")}
</>
) : saved ? (
<>
<Check className="h-4 w-4" /> {t("settings.saved")}