From 2fa6042eef63d47e966a7b6ce06fc97852d264e4 Mon Sep 17 00:00:00 2001
From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com>
Date: Thu, 18 Jun 2026 12:52:08 -0700
Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=82=20feat:=20Toasts=20on=20All=20Save?=
=?UTF-8?q?=20Actions=20(#75)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: success/error toasts on every admin panel save action
Wires click-ui's Toast component into every save mutation in the admin
panel: config editor (per-field, bulk, YAML import), groups (create /
edit / delete), roles (create / edit / delete), users (invite, delete,
role / group assignment, user profile create / delete), system grants
(EditCapabilitiesDialog), and per-field profile mutations
(useProfileMutations).
Uses click-ui's official createToast directly; ClickUIProvider already
mounts the matching ToastProvider, so the global createToast routes
through it. Toasts inherit click-ui brand colours, WCAG contrast,
keyboard accessibility, swipe-to-dismiss, close button, ARIA live
region, and 5s auto-dismiss from the library.
src/utils/toast.ts is a five-line wrapper exposing notifySuccess and
notifyError that delegate to click-ui's createToast. Call sites pass
the affected resource through mutation variables (not closed-over
component state) so the toast renders the correct name even when the
confirm dialog has already cleared its target by the time the response
lands.
Refs AI-1206.
* fix: throw instead of silently no-op when target missing in edit mutations
EditGroupDialog, EditRoleDialog and EditCapabilitiesDialog mutationFns
used to bail with an empty return when their target (group, role,
principal) was missing, which React Query treats as success and which
caused the new onSuccess handlers to fire a success toast and close
the dialog without anything having been persisted.
Throw a localised error in the unavailable case so onError fires
instead, and add a matching guard at each call site so mutate is
never invoked with a missing target in the first place.
* fix: capture submitted name in mutation variables for create/edit toasts
The five create and edit dialogs used to read the resource name from
component state inside onSuccess instead of from the value that was
submitted with the mutation. If the user edited the name field while
the request was in flight (or the dialog reset before the toast
fired), the toast could render an empty or wrong name even though the
server saved the original value.
Pass the submitted name through the mutation variables, use it for
the actual API call, and read it back from the mutation's data or
variables argument in onSuccess so the toast always reflects what
was persisted.
---
src/components/access/CreateGroupDialog.tsx | 38 ++++++---
src/components/access/CreateRoleDialog.tsx | 43 ++++++----
src/components/access/EditGroupDialog.tsx | 50 +++++++----
src/components/access/EditRoleDialog.tsx | 72 ++++++++++------
src/components/access/GroupsTab.tsx | 10 ++-
src/components/access/RolesTab.tsx | 25 ++----
src/components/configuration/ConfigPage.tsx | 70 +++------------
.../grants/EditCapabilitiesDialog.tsx | 19 +++--
src/components/users/CreateUserDialog.tsx | 13 ++-
src/components/users/UserDetailDialog.tsx | 85 +++++++++++++------
src/components/users/UsersPage.tsx | 10 ++-
src/hooks/useProfileMutations.ts | 15 +++-
src/locales/en/translation.json | 20 +++++
src/styles.css | 60 -------------
src/types/config-ui.ts | 6 --
src/utils/index.ts | 1 +
src/utils/toast.ts | 5 ++
17 files changed, 283 insertions(+), 259 deletions(-)
create mode 100644 src/utils/toast.ts
diff --git a/src/components/access/CreateGroupDialog.tsx b/src/components/access/CreateGroupDialog.tsx
index 4c064fd..f383777 100644
--- a/src/components/access/CreateGroupDialog.tsx
+++ b/src/components/access/CreateGroupDialog.tsx
@@ -5,8 +5,8 @@ import type { AdminUserSearchResult } from '@librechat/data-schemas';
import type * as t from '@/types';
import { SelectedMemberList, UserSearchInline } from '@/components/shared';
import { addGroupMemberFn, createGroupFn } from '@/server';
+import { cn, notifySuccess, notifyError } from '@/utils';
import { useLocalize } from '@/hooks';
-import { cn } from '@/utils';
export function CreateGroupDialog({ open, onClose }: t.CreateGroupDialogProps) {
const localize = useLocalize();
@@ -27,20 +27,24 @@ export function CreateGroupDialog({ open, onClose }: t.CreateGroupDialogProps) {
};
const mutation = useMutation({
- mutationFn: async () => {
- const { group } = await createGroupFn({ data: { name, description } });
+ mutationFn: async ({ name: submittedName }: { name: string }) => {
+ const { group } = await createGroupFn({
+ data: { name: submittedName, description },
+ });
for (const user of selectedUsers) {
await addGroupMemberFn({ data: { groupId: group.id, userId: user.id } });
}
+ return { name: submittedName };
},
- onSuccess: () => {
+ onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['groups'] });
queryClient.invalidateQueries({ queryKey: ['groupMembers'] });
queryClient.invalidateQueries({ queryKey: ['availableScopes'] });
queryClient.invalidateQueries({ queryKey: ['groupAssignments'] });
+ notifySuccess(localize('com_toast_group_created', { name: data.name }));
resetAndClose();
},
- onError: (err: Error) => setError(err.message),
+ onError: (err: Error) => notifyError(err.message),
});
const doSubmit = () => {
@@ -50,7 +54,7 @@ export function CreateGroupDialog({ open, onClose }: t.CreateGroupDialogProps) {
setActiveTab('details');
return;
}
- mutation.mutate();
+ mutation.mutate({ name });
};
const handleSubmit = (e: React.FormEvent) => {
@@ -89,14 +93,15 @@ export function CreateGroupDialog({ open, onClose }: t.CreateGroupDialogProps) {
ariaLabel={localize('com_access_create_group')}
>
-
- {localize('com_access_tab_details')}
-
-
- {localize('com_access_tab_members')}
-
+ {localize('com_access_tab_details')}
+ {localize('com_access_tab_members')}
-
+