diff --git a/bun.lock b/bun.lock index 5cb0e1e..6bcdebd 100644 --- a/bun.lock +++ b/bun.lock @@ -71,6 +71,7 @@ }, "overrides": { "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-dismissable-layer": "1.1.11", "@tanstack/devtools": "0.11.0", "@tanstack/devtools-ui": "0.5.1", "@tanstack/devtools-vite": "0.3.12", @@ -346,7 +347,7 @@ "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-menu": "2.1.2", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA=="], @@ -392,7 +393,7 @@ "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], @@ -1704,15 +1705,9 @@ "@radix-ui/react-context-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], - "@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], - "@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - "@radix-ui/react-dismissable-layer/@radix-ui/primitive": ["@radix-ui/primitive@1.1.0", "", {}, "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="], - - "@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw=="], - - "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], + "@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-dropdown-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.0", "", {}, "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="], @@ -1876,8 +1871,6 @@ "@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A=="], - "@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.0", "", { "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig=="], - "@radix-ui/react-tooltip/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], "@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.0", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-context": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg=="], @@ -1896,6 +1889,8 @@ "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-use-escape-keydown/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="], @@ -2014,14 +2009,8 @@ "@radix-ui/react-context-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], - "@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], - - "@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], - "@radix-ui/react-dialog/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], - "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], "@radix-ui/react-hover-card/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.0", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw=="], diff --git a/package.json b/package.json index a15c01d..7d10528 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ }, "overrides": { "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-dismissable-layer": "1.1.11", "@tanstack/devtools": "0.11.0", "@tanstack/devtools-ui": "0.5.1", "@tanstack/devtools-vite": "0.3.12", 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')} - +
- +
u.id)} diff --git a/src/components/access/CreateRoleDialog.tsx b/src/components/access/CreateRoleDialog.tsx index 9234fdc..836f353 100644 --- a/src/components/access/CreateRoleDialog.tsx +++ b/src/components/access/CreateRoleDialog.tsx @@ -6,9 +6,9 @@ import type * as t from '@/types'; import { addRoleMemberFn, createRoleFn, updateRolePermissionsFn } from '@/server'; import { SelectedMemberList, UserSearchInline } from '@/components/shared'; import { RolePermissionsPanel } from './RolePermissionsPanel'; +import { cn, notifySuccess, notifyError } from '@/utils'; import { defaultPermissions } from '@/constants'; import { useLocalize } from '@/hooks'; -import { cn } from '@/utils'; export function CreateRoleDialog({ open, onClose }: t.CreateRoleDialogProps) { const localize = useLocalize(); @@ -31,21 +31,23 @@ export function CreateRoleDialog({ open, onClose }: t.CreateRoleDialogProps) { }; const mutation = useMutation({ - mutationFn: async () => { - const { role } = await createRoleFn({ data: { name, description } }); + mutationFn: async ({ name: submittedName }: { name: string }) => { + const { role } = await createRoleFn({ data: { name: submittedName, description } }); await updateRolePermissionsFn({ data: { id: role.id, permissions } }); for (const user of selectedUsers) { await addRoleMemberFn({ data: { roleId: role.id, userId: user.id } }); } + return { name: submittedName }; }, - onSuccess: () => { + onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['roles'] }); queryClient.invalidateQueries({ queryKey: ['roleMembers'] }); queryClient.invalidateQueries({ queryKey: ['availableScopes'] }); queryClient.invalidateQueries({ queryKey: ['roleAssignments'] }); + notifySuccess(localize('com_toast_role_created', { name: data.name })); resetAndClose(); }, - onError: (err: Error) => setError(err.message), + onError: (err: Error) => notifyError(err.message), }); const doSubmit = () => { @@ -55,7 +57,7 @@ export function CreateRoleDialog({ open, onClose }: t.CreateRoleDialogProps) { setActiveTab('details'); return; } - mutation.mutate(); + mutation.mutate({ name }); }; const handleSubmit = (e: React.FormEvent) => { @@ -94,17 +96,18 @@ export function CreateRoleDialog({ open, onClose }: t.CreateRoleDialogProps) { ariaLabel={localize('com_access_create_role')} > - - {localize('com_access_tab_details')} - + {localize('com_access_tab_details')} {localize('com_access_tab_permissions')} - - {localize('com_access_tab_members')} - + {localize('com_access_tab_members')} - +
- +
- +
u.id)} diff --git a/src/components/access/EditGroupDialog.tsx b/src/components/access/EditGroupDialog.tsx index e7fb923..8c0d6cf 100644 --- a/src/components/access/EditGroupDialog.tsx +++ b/src/components/access/EditGroupDialog.tsx @@ -18,8 +18,8 @@ import { TrashButton, UserSearchInline, } from '@/components/shared'; +import { cn, notifySuccess, notifyError } from '@/utils'; import { useLocalize } from '@/hooks'; -import { cn } from '@/utils'; type EditGroupTab = 'details' | 'members'; @@ -73,10 +73,10 @@ export function EditGroupDialog({ group, canManage, onClose }: t.EditGroupDialog }; const mutation = useMutation({ - mutationFn: async () => { - if (!group) return; + mutationFn: async ({ name: submittedName }: { name: string }) => { + if (!group) throw new Error(localize('com_access_group_unavailable')); if (detailsDirty) { - await updateGroupFn({ data: { id: group.id, name, description } }); + await updateGroupFn({ data: { id: group.id, name: submittedName, description } }); } const memberResults = await Promise.allSettled([ ...pendingAdditions.map((user) => @@ -95,24 +95,27 @@ export function EditGroupDialog({ group, canManage, onClose }: t.EditGroupDialog parts.push(localize('com_access_member_ops_failed', { count: failures.length })); throw new Error(parts.join(', ')); } + return { name: submittedName }; }, - onSuccess: () => { + onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['groups'] }); queryClient.invalidateQueries({ queryKey: ['groupAssignments'] }); queryClient.invalidateQueries({ queryKey: ['groupMembers', group?.id] }); + notifySuccess(localize('com_toast_group_updated', { name: data.name })); onClose(); }, - onError: (err: Error) => setError(err.message), + onError: (err: Error) => notifyError(err.message), }); const doSubmit = () => { setError(''); + if (!group) return; if (!name.trim()) { setError(localize('com_access_name_required')); setActiveTab('details'); return; } - mutation.mutate(); + mutation.mutate({ name }); }; const handleSubmit = (e: React.FormEvent) => { @@ -140,14 +143,15 @@ export function EditGroupDialog({ group, canManage, onClose }: t.EditGroupDialog ariaLabel={localize('com_access_edit_group')} > - - {localize('com_access_tab_details')} - - - {localize('com_access_tab_members')} - + {localize('com_access_tab_details')} + {localize('com_access_tab_members')} - +
- +
{canManage && ( ({ id: m.userId, name: m.name, email: m.email, avatarUrl: m.avatarUrl }))} + users={pendingRemovals.map((m) => ({ + id: m.userId, + name: m.name, + email: m.email, + avatarUrl: m.avatarUrl, + }))} onRemove={unstageRemoval} disabled={mutation.isPending} /> @@ -254,7 +268,9 @@ export function EditGroupDialog({ group, canManage, onClose }: t.EditGroupDialog
diff --git a/src/components/access/EditRoleDialog.tsx b/src/components/access/EditRoleDialog.tsx index 841b238..891c1ac 100644 --- a/src/components/access/EditRoleDialog.tsx +++ b/src/components/access/EditRoleDialog.tsx @@ -21,8 +21,8 @@ import { UserSearchInline, } from '@/components/shared'; import { RolePermissionsPanel } from './RolePermissionsPanel'; +import { cn, notifySuccess, notifyError } from '@/utils'; import { useLocalize } from '@/hooks'; -import { cn } from '@/utils'; type EditRoleTab = 'details' | 'permissions' | 'members'; @@ -91,11 +91,17 @@ export function EditRoleDialog({ role, canManage, onClose }: t.EditRoleDialogPro }; const updateMutation = useMutation({ - mutationFn: async (): Promise => { - if (!role) return ''; + mutationFn: async ({ + name: submittedName, + }: { + name: string; + }): Promise<{ roleId: string; name: string }> => { + if (!role) throw new Error(localize('com_access_role_unavailable')); let roleId = role.id; if (detailsDirty) { - const result = await updateRoleFn({ data: { id: role.id, name, description } }); + const result = await updateRoleFn({ + data: { id: role.id, name: submittedName, description }, + }); roleId = result.role.id; } if (permissionsDirty && permissions) { @@ -113,9 +119,7 @@ export function EditRoleDialog({ role, canManage, onClose }: t.EditRoleDialogPro } } const memberResults = await Promise.allSettled([ - ...pendingAdditions.map((user) => - addRoleMemberFn({ data: { roleId, userId: user.id } }), - ), + ...pendingAdditions.map((user) => addRoleMemberFn({ data: { roleId, userId: user.id } })), ...pendingRemovals.map((member) => removeRoleMemberFn({ data: { roleId, userId: member.userId } }), ), @@ -130,32 +134,34 @@ export function EditRoleDialog({ role, canManage, onClose }: t.EditRoleDialogPro parts.push(localize('com_access_member_ops_failed', { count: failures.length })); throw new Error(parts.join(', ')); } - return roleId; + return { roleId, name: submittedName }; }, - onSuccess: (newRoleId) => { + onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['roles'] }); queryClient.invalidateQueries({ queryKey: ['role', role?.id] }); - if (newRoleId !== role?.id) { - queryClient.invalidateQueries({ queryKey: ['role', newRoleId] }); + if (data.roleId !== role?.id) { + queryClient.invalidateQueries({ queryKey: ['role', data.roleId] }); } queryClient.invalidateQueries({ queryKey: ['roleAssignments'] }); queryClient.invalidateQueries({ queryKey: ['roleMembers', role?.id] }); - if (newRoleId !== role?.id) { - queryClient.invalidateQueries({ queryKey: ['roleMembers', newRoleId] }); + if (data.roleId !== role?.id) { + queryClient.invalidateQueries({ queryKey: ['roleMembers', data.roleId] }); } + notifySuccess(localize('com_toast_role_updated', { name: data.name })); onClose(); }, - onError: (err: Error) => setError(err.message), + onError: (err: Error) => notifyError(err.message), }); const doSubmit = () => { setError(''); + if (!role) return; if (!name.trim()) { setError(localize('com_access_name_required')); setActiveTab('details'); return; } - updateMutation.mutate(); + updateMutation.mutate({ name }); }; const handleSubmit = (e: React.FormEvent) => { @@ -183,17 +189,18 @@ export function EditRoleDialog({ role, canManage, onClose }: t.EditRoleDialogPro ariaLabel={localize('com_access_edit_role')} > - - {localize('com_access_tab_details')} - + {localize('com_access_tab_details')} {localize('com_access_tab_permissions')} - - {localize('com_access_tab_members')} - + {localize('com_access_tab_members')} - +
{role?.isSystemRole && ( @@ -239,7 +246,12 @@ export function EditRoleDialog({ role, canManage, onClose }: t.EditRoleDialogPro
- +
{(() => { if (roleDetail.isLoading) { @@ -269,7 +281,12 @@ export function EditRoleDialog({ role, canManage, onClose }: t.EditRoleDialogPro })()}
- +
{canManage && ( ({ id: m.userId, name: m.name, email: m.email, avatarUrl: m.avatarUrl }))} + users={pendingRemovals.map((m) => ({ + id: m.userId, + name: m.name, + email: m.email, + avatarUrl: m.avatarUrl, + }))} onRemove={unstageRemoval} disabled={updateMutation.isPending} /> diff --git a/src/components/access/GroupsTab.tsx b/src/components/access/GroupsTab.tsx index 4829ade..2819f3c 100644 --- a/src/components/access/GroupsTab.tsx +++ b/src/components/access/GroupsTab.tsx @@ -11,11 +11,11 @@ import { TrashButton, } from '@/components/shared'; import { deleteGroupFn, groupsQueryOptions, GROUPS_PAGE_SIZE } from '@/server'; +import { cn, notifySuccess, notifyError } from '@/utils'; import { useCapabilities, useLocalize } from '@/hooks'; import { EditGroupDialog } from './EditGroupDialog'; import { SystemCapabilities } from '@/constants'; import { ConfirmDialog } from './ConfirmDialog'; -import { cn } from '@/utils'; export function GroupsTab({ onCreateGroup }: t.GroupsTabProps) { const localize = useLocalize(); @@ -52,17 +52,19 @@ export function GroupsTab({ onCreateGroup }: t.GroupsTabProps) { const totalPages = Math.ceil(total / GROUPS_PAGE_SIZE); const deleteMutation = useMutation({ - mutationFn: (id: string) => deleteGroupFn({ data: { id } }), - onSuccess: () => { + mutationFn: (group: AdminGroup) => deleteGroupFn({ data: { id: group.id } }), + onSuccess: (_data, group) => { queryClient.invalidateQueries({ queryKey: ['groups'] }); queryClient.invalidateQueries({ queryKey: ['availableScopes'] }); queryClient.invalidateQueries({ queryKey: ['groupAssignments'] }); queryClient.invalidateQueries({ queryKey: ['groupMembers'] }); + notifySuccess(localize('com_toast_group_deleted', { name: group.name })); setDeleteTarget(null); if (groups.length === 1) { setPage((prev) => (prev > 1 ? prev - 1 : prev)); } }, + onError: (err: Error) => notifyError(err.message), }); if (isLoading && !data) { @@ -151,7 +153,7 @@ export function GroupsTab({ onCreateGroup }: t.GroupsTabProps) { confirmLabel={localize('com_ui_delete')} saving={deleteMutation.isPending} onConfirm={() => { - if (deleteTarget) deleteMutation.mutate(deleteTarget.id); + if (deleteTarget) deleteMutation.mutate(deleteTarget); }} onCancel={() => setDeleteTarget(null)} /> diff --git a/src/components/access/RolesTab.tsx b/src/components/access/RolesTab.tsx index bf945dd..50d53a1 100644 --- a/src/components/access/RolesTab.tsx +++ b/src/components/access/RolesTab.tsx @@ -11,6 +11,7 @@ import { } from '@/components/shared'; import { deleteRoleFn, allRolesQueryOptions, ROLES_PAGE_SIZE } from '@/server'; import { useCapabilities, useLocalize } from '@/hooks'; +import { notifySuccess, notifyError } from '@/utils'; import { EditRoleDialog } from './EditRoleDialog'; import { SystemCapabilities } from '@/constants'; import { ConfirmDialog } from './ConfirmDialog'; @@ -22,7 +23,6 @@ export function RolesTab({ onCreateRole }: t.RolesTabProps) { const canManage = hasCapability(SystemCapabilities.MANAGE_ROLES); const [editTarget, setEditTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); - const [deleteError, setDeleteError] = useState(''); const [search, setSearch] = useState(''); const [page, setPage] = useState(1); @@ -48,19 +48,19 @@ export function RolesTab({ onCreateRole }: t.RolesTabProps) { }; const deleteMutation = useMutation({ - mutationFn: (id: string) => deleteRoleFn({ data: { id } }), - onSuccess: () => { + mutationFn: (role: t.Role) => deleteRoleFn({ data: { id: role.id } }), + onSuccess: (_data, role) => { queryClient.invalidateQueries({ queryKey: ['roles'] }); queryClient.invalidateQueries({ queryKey: ['availableScopes'] }); queryClient.invalidateQueries({ queryKey: ['roleAssignments'] }); queryClient.invalidateQueries({ queryKey: ['roleMembers'] }); + notifySuccess(localize('com_toast_role_deleted', { name: role.name })); setDeleteTarget(null); - setDeleteError(''); if (paged.length === 1) { setPage((prev) => (prev > 1 ? prev - 1 : prev)); } }, - onError: (err: Error) => setDeleteError(err.message), + onError: (err: Error) => notifyError(err.message), }); if (isLoading) { @@ -96,11 +96,7 @@ export function RolesTab({ onCreateRole }: t.RolesTabProps) { {paged.length === 0 ? ( ) : (
@@ -157,15 +153,10 @@ export function RolesTab({ onCreateRole }: t.RolesTabProps) { description={localize('com_access_delete_role_desc', { name: deleteTarget?.name ?? '' })} confirmLabel={localize('com_ui_delete')} saving={deleteMutation.isPending} - error={deleteError} onConfirm={() => { - setDeleteError(''); - if (deleteTarget) deleteMutation.mutate(deleteTarget.id); - }} - onCancel={() => { - setDeleteTarget(null); - setDeleteError(''); + if (deleteTarget) deleteMutation.mutate(deleteTarget); }} + onCancel={() => setDeleteTarget(null)} />
); diff --git a/src/components/configuration/ConfigPage.tsx b/src/components/configuration/ConfigPage.tsx index ccd9ed1..ea0a966 100644 --- a/src/components/configuration/ConfigPage.tsx +++ b/src/components/configuration/ConfigPage.tsx @@ -22,13 +22,16 @@ import { unflattenObject, serializeKVPairs, deepSerializeKVPairs, - cn, normalizeImportConfig, hasConfigCapability, getTabsWithPermission, + notifySuccess, + notifyError, } from '@/utils'; import { useLocalize, useHighlightRef, useActiveSection, useCapabilities } from '@/hooks'; import { CONFIG_TABS, OTHER_TAB, SECTION_META, HIDDEN_SECTIONS } from './configMeta'; +import { mergeIndexedArrayEdits, partitionScopeResetPaths } from './utils'; +import { validateMcpCrossField } from './sections/McpServersRenderer'; import { ScopeSelector, ScopeTriggerButton } from './ScopeSelector'; import { ConfigTableOfContents } from './ConfigTableOfContents'; import { ConfirmSaveDialog } from './ConfirmSaveDialog'; @@ -36,8 +39,6 @@ import { StickyActionBar } from '@/components/shared'; import { ConfigTabContent } from './ConfigTabContent'; import { ImportYamlDialog } from './ImportYamlDialog'; import { ContentToolbar } from './ContentToolbar'; -import { validateMcpCrossField } from './sections/McpServersRenderer'; -import { mergeIndexedArrayEdits, partitionScopeResetPaths } from './utils'; import { SystemCapabilities } from '@/constants'; import { ConfigTabBar } from './ConfigTabBar'; import { InfoBanner } from './InfoBanner'; @@ -193,18 +194,6 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi const dismissTimer = useRef | undefined>(undefined); useEffect(() => () => clearTimeout(dismissTimer.current), []); - const [toast, setToast] = useState(null); - const toastTimer = useRef | undefined>(undefined); - useEffect(() => () => clearTimeout(toastTimer.current), []); - - const showToast = useCallback((state: t.ToastState, autoHideMs?: number) => { - setToast(state); - clearTimeout(toastTimer.current); - if (autoHideMs) { - toastTimer.current = setTimeout(() => setToast(null), autoHideMs); - } - }, []); - const [showConfiguredOnly, setShowConfiguredOnly] = useState(false); const [scopeSelectorOpen, setScopeSelectorOpen] = useState(false); @@ -477,8 +466,8 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi setConfirmSaveOpen(false); setSaving(false); setSaveError(null); - showToast({ type: 'saved' }, 3000); - }, [showToast]); + notifySuccess(localize('com_config_saved')); + }, [localize]); const invalidateAndResetBase = useCallback(() => { queryClient.invalidateQueries({ queryKey: ['baseConfig'] }); @@ -494,8 +483,7 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi const importMutation = useMutation({ mutationFn: (config: Record) => importBaseConfigFn({ data: { config } }), - onMutate: () => showToast({ type: 'saving' }), - onError: (err: Error) => showToast({ type: 'error', message: err.message }, 5000), + onError: (err: Error) => notifyError(err.message), onSuccess: invalidateAndResetBase, }); @@ -544,7 +532,7 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi field: missingField, }); setSaveError(message); - showToast({ type: 'error', message }, 5000); + notifyError(message); return; } } @@ -568,7 +556,6 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi setSaving(true); setSaveError(null); - showToast({ type: 'saving' }); try { /** Resets must land before saves so a delete-then-recreate at the same path (e.g. MCP entry replaced with different fields) wipes stale fields first and the new leaf PATCHes don't race against the DELETE. */ @@ -629,7 +616,7 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi const message = err instanceof Error ? err.message : String(err); setSaving(false); setSaveError(message); - showToast({ type: 'error', message }, 5000); + notifyError(message); } }, [ touchedPaths, @@ -640,7 +627,6 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi configValues, baseConfigData, localize, - showToast, editingScope, invalidateAndResetScope, invalidateAndResetBase, @@ -718,7 +704,7 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi const normalized = normalizeImportConfig(appConfig); if (isEditingScope && editingScope) { handleImportAsProfile(normalized, editingScope).catch((err: Error) => { - showToast({ type: 'error', message: err.message }, 5000); + notifyError(err.message); }); } else { importMutation.mutate(normalized, { onSuccess: () => showImportSuccess() }); @@ -954,7 +940,7 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi showConfiguredOnly={showConfiguredOnly} isEditingScope={isEditingScope} baseRecordKeys={baseRecordKeys} - onValidationError={(message) => showToast({ type: 'error', message }, 5000)} + onValidationError={(message) => notifyError(message)} />
@@ -978,38 +964,6 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi /> )} - {toast && - createPortal( -
- {toast.type === 'saving' && ( - <> - - {localize('com_config_saving')} - - )} - {toast.type === 'saved' && ( - <> - - {localize('com_config_saved')} - - )} - {toast.type === 'error' && ( - <> - - {toast.message} - - )} -
, - document.body, - )} - showToast({ type: 'error', message: msg }, 5000)} + onError={(msg) => notifyError(msg)} /> { const record: Record = {}; @@ -51,8 +51,10 @@ export function EditCapabilitiesDialog({ }, [open, isLoading, grants]); const saveMutation = useMutation({ - mutationFn: async () => { - if (!principalType || !principalId) return; + mutationFn: async (vars: { name: string }) => { + if (!principalType || !principalId) { + throw new Error(localize('com_cap_principal_unavailable')); + } const toGrant: string[] = []; const toRevoke: string[] = []; for (const [cap, enabled] of Object.entries(capabilities)) { @@ -66,22 +68,25 @@ export function EditCapabilitiesDialog({ for (const cap of toRevoke) { await revokeCapabilityFn({ data: { ...shared, capability: cap } }); } + return vars; }, - onSuccess: () => { + onSuccess: (_data, vars) => { queryClient.invalidateQueries({ queryKey: ['systemGrants'] }); queryClient.invalidateQueries({ queryKey: ['effectiveCapabilities'] }); queryClient.invalidateQueries({ queryKey: ['auditLog'] }); + notifySuccess(localize('com_toast_capabilities_saved', { name: vars.name })); onClose(); }, - onError: (err: Error) => setError(err.message), + onError: (err: Error) => notifyError(err.message), }); const hasChanges = Object.keys(capabilities).some((cap) => capabilities[cap] !== baseline[cap]); const handleSave = useCallback(() => { setError(''); - saveMutation.mutate(); - }, [saveMutation]); + if (!principalType || !principalId) return; + saveMutation.mutate({ name: principalName }); + }, [saveMutation, principalType, principalId, principalName]); const dialogTitle = principalType ? `${localize('com_cap_edit_title', { name: principalName })}` diff --git a/src/components/users/CreateUserDialog.tsx b/src/components/users/CreateUserDialog.tsx index 87ec8cc..9a2e79a 100644 --- a/src/components/users/CreateUserDialog.tsx +++ b/src/components/users/CreateUserDialog.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { SystemRoles } from 'librechat-data-provider'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import type * as t from '@/types'; +import { notifySuccess, notifyError } from '@/utils'; import { FormDialog } from '@/components/shared'; import { createUserFn } from '@/server'; import { useLocalize } from '@/hooks'; @@ -15,12 +16,16 @@ export function CreateUserDialog({ open, onClose }: t.CreateUserDialogProps) { const [error, setError] = useState(''); const mutation = useMutation({ - mutationFn: () => createUserFn({ data: { name, email, role } }), - onSuccess: () => { + mutationFn: async ({ name: submittedName }: { name: string }) => { + await createUserFn({ data: { name: submittedName, email, role } }); + return { name: submittedName }; + }, + onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['users'] }); + notifySuccess(localize('com_toast_user_invited', { name: data.name })); resetAndClose(); }, - onError: (err: Error) => setError(err.message), + onError: (err: Error) => notifyError(err.message), }); const resetAndClose = () => { @@ -41,7 +46,7 @@ export function CreateUserDialog({ open, onClose }: t.CreateUserDialogProps) { setError(localize('com_users_email_required')); return; } - mutation.mutate(); + mutation.mutate({ name }); }; return ( diff --git a/src/components/users/UserDetailDialog.tsx b/src/components/users/UserDetailDialog.tsx index 8c0ac21..c218491 100644 --- a/src/components/users/UserDetailDialog.tsx +++ b/src/components/users/UserDetailDialog.tsx @@ -18,9 +18,9 @@ import { allRolesQueryOptions, } from '@/server'; import { Avatar, TrashButton } from '@/components/shared'; +import { cn, notifySuccess, notifyError } from '@/utils'; import { ConfirmDialog } from '@/components/access'; import { useLocalize } from '@/hooks'; -import { cn } from '@/utils'; const CONFIRM_TITLE_KEYS: Record = { role: 'com_users_remove_role_title', @@ -89,57 +89,73 @@ export function UserDetailDialog({ queryClient.invalidateQueries({ queryKey: ['availableScopes'] }); }; + const handleError = (err: Error) => notifyError(err.message); + const addRoleMutation = useMutation({ - mutationFn: (roleId: string) => { + mutationFn: (role: { id: string; name: string }) => { if (!userId) throw new Error('No user selected'); - return addRoleMemberFn({ data: { roleId, userId } }); + return addRoleMemberFn({ data: { roleId: role.id, userId } }); + }, + onSuccess: (_data, role) => { + invalidateAll(); + notifySuccess(localize('com_toast_role_assigned', { name: role.name })); }, - onSuccess: invalidateAll, + onError: handleError, }); const removeRoleMutation = useMutation({ - mutationFn: (roleId: string) => { + mutationFn: (role: { id: string; name: string }) => { if (!userId) throw new Error('No user selected'); - return removeRoleMemberFn({ data: { roleId, userId } }); + return removeRoleMemberFn({ data: { roleId: role.id, userId } }); }, - onSuccess: () => { + onSuccess: (_data, role) => { invalidateAll(); + notifySuccess(localize('com_toast_role_unassigned', { name: role.name })); setRemoveTarget(null); }, + onError: handleError, }); const addGroupMutation = useMutation({ - mutationFn: (groupId: string) => { + mutationFn: (group: { id: string; name: string }) => { if (!userId) throw new Error('No user selected'); - return addGroupMemberFn({ data: { groupId, userId } }); + return addGroupMemberFn({ data: { groupId: group.id, userId } }); + }, + onSuccess: (_data, group) => { + invalidateAll(); + notifySuccess(localize('com_toast_group_assigned', { name: group.name })); }, - onSuccess: invalidateAll, + onError: handleError, }); const removeGroupMutation = useMutation({ - mutationFn: (groupId: string) => { + mutationFn: (group: { id: string; name: string }) => { if (!userId) throw new Error('No user selected'); - return removeGroupMemberFn({ data: { groupId, userId } }); + return removeGroupMemberFn({ data: { groupId: group.id, userId } }); }, - onSuccess: () => { + onSuccess: (_data, group) => { invalidateAll(); + notifySuccess(localize('com_toast_group_unassigned', { name: group.name })); setRemoveTarget(null); }, + onError: handleError, }); const createProfileMutation = useMutation({ - mutationFn: () => { - if (!userId) throw new Error('No user selected'); - return createScopeFn({ + mutationFn: (vars: { userId: string; name: string }) => + createScopeFn({ data: { principalType: PrincipalType.USER, - name: userName, + name: vars.name, priority: 100, - principalId: userId, + principalId: vars.userId, }, - }); + }), + onSuccess: (_data, vars) => { + invalidateAll(); + notifySuccess(localize('com_toast_user_profile_created', { name: vars.name })); }, - onSuccess: invalidateAll, + onError: handleError, }); const deleteProfileMutation = useMutation({ @@ -149,8 +165,10 @@ export function UserDetailDialog({ }), onSuccess: () => { invalidateAll(); + notifySuccess(localize('com_toast_user_profile_deleted')); setRemoveTarget(null); }, + onError: handleError, }); const busy = @@ -166,9 +184,13 @@ export function UserDetailDialog({ const handleConfirmRemove = () => { if (!removeTarget) return; - if (removeTarget.kind === 'role') removeRoleMutation.mutate(removeTarget.ref.id); - else if (removeTarget.kind === 'group') removeGroupMutation.mutate(removeTarget.ref.id); - else if (removeTarget.kind === 'profile') deleteProfileMutation.mutate(removeTarget.scope); + if (removeTarget.kind === 'role') { + removeRoleMutation.mutate({ id: removeTarget.ref.id, name: removeTarget.ref.name }); + } else if (removeTarget.kind === 'group') { + removeGroupMutation.mutate({ id: removeTarget.ref.id, name: removeTarget.ref.name }); + } else if (removeTarget.kind === 'profile') { + deleteProfileMutation.mutate(removeTarget.scope); + } }; const confirmTitle = removeTarget ? localize(CONFIRM_TITLE_KEYS[removeTarget.kind]) : ''; @@ -240,9 +262,20 @@ export function UserDetailDialog({ canManageRoles={canManageRoles} canManageGroups={canManageGroups} canAssignConfigs={canAssignConfigs} - onAddRole={(id) => addRoleMutation.mutate(id)} - onAddGroup={(id) => addGroupMutation.mutate(id)} - onCreateUserProfile={() => createProfileMutation.mutate()} + onAddRole={(id) => { + if (addRoleMutation.isPending) return; + const role = availableRoles.find((r) => r.id === id); + if (role) addRoleMutation.mutate({ id: role.id, name: role.name }); + }} + onAddGroup={(id) => { + if (addGroupMutation.isPending) return; + const group = availableGroups.find((g) => g.id === id); + if (group) addGroupMutation.mutate({ id: group.id, name: group.name }); + }} + onCreateUserProfile={() => { + if (createProfileMutation.isPending || !userId) return; + createProfileMutation.mutate({ userId, name: userName }); + }} onDone={() => setView('main')} /> )} diff --git a/src/components/users/UsersPage.tsx b/src/components/users/UsersPage.tsx index e3ea904..fa1158f 100644 --- a/src/components/users/UsersPage.tsx +++ b/src/components/users/UsersPage.tsx @@ -20,11 +20,11 @@ import { SearchInput, } from '@/components/shared'; import { useAnnouncement, useCapabilities, useLocalize } from '@/hooks'; +import { cn, notifySuccess, notifyError } from '@/utils'; import { CreateUserDialog } from './CreateUserDialog'; import { UserDetailDialog } from './UserDetailDialog'; import { ConfirmDialog } from '@/components/access'; import { SystemCapabilities } from '@/constants'; -import { cn } from '@/utils'; const ROLE_FILTER_LABELS: Record = { all: 'com_ui_all', @@ -61,16 +61,18 @@ export function UsersPage() { }, [allScopes]); const deleteMutation = useMutation({ - mutationFn: (id: string) => deleteUserFn({ data: { id } }), - onSuccess: () => { + mutationFn: (user: TUser) => deleteUserFn({ data: { id: user.id } }), + onSuccess: (_data, user) => { queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['roleAssignments'] }); queryClient.invalidateQueries({ queryKey: ['groupAssignments'] }); queryClient.invalidateQueries({ queryKey: ['roleMembers'] }); queryClient.invalidateQueries({ queryKey: ['groupMembers'] }); queryClient.invalidateQueries({ queryKey: ['availableScopes'] }); + notifySuccess(localize('com_toast_user_deleted', { name: user.name })); setDeleteTarget(null); }, + onError: (err: Error) => notifyError(err.message), }); const applyFilters = (list: TUser[], q: string, role: t.RoleFilter) => @@ -221,7 +223,7 @@ export function UsersPage() { confirmLabel={localize('com_ui_delete')} saving={deleteMutation.isPending} onConfirm={() => { - if (deleteTarget) deleteMutation.mutate(deleteTarget.id); + if (deleteTarget) deleteMutation.mutate(deleteTarget); }} onCancel={() => setDeleteTarget(null)} /> diff --git a/src/hooks/useProfileMutations.ts b/src/hooks/useProfileMutations.ts index 38e8b54..a81508d 100644 --- a/src/hooks/useProfileMutations.ts +++ b/src/hooks/useProfileMutations.ts @@ -3,12 +3,15 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import type { PrincipalType } from 'librechat-data-provider'; import type * as t from '@/types'; import { removeFieldProfileValueFn, saveFieldProfileValueFn } from '@/server'; +import { notifySuccess, notifyError } from '@/utils'; +import { useLocalize } from './useLocalize'; export function useProfileMutations({ fieldPath, onProfileChange, }: t.UseProfileMutationsOptions): t.UseProfileMutationsReturn { const queryClient = useQueryClient(); + const localize = useLocalize(); const invalidate = useCallback(() => { queryClient.invalidateQueries({ queryKey: ['profileMap'] }); @@ -20,13 +23,21 @@ export function useProfileMutations({ const saveMutation = useMutation({ mutationFn: (params: { principalType: PrincipalType; principalId: string; value: unknown }) => saveFieldProfileValueFn({ data: { fieldPath, ...params } }), - onSuccess: () => invalidate(), + onSuccess: () => { + invalidate(); + notifySuccess(localize('com_toast_profile_value_saved')); + }, + onError: (err: Error) => notifyError(err.message), }); const removeMutation = useMutation({ mutationFn: (params: { principalType: PrincipalType; principalId: string }) => removeFieldProfileValueFn({ data: { fieldPath, ...params } }), - onSuccess: () => invalidate(), + onSuccess: () => { + invalidate(); + notifySuccess(localize('com_toast_profile_value_removed')); + }, + onError: (err: Error) => notifyError(err.message), }); return { diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 3f95eaa..7f1540c 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -109,6 +109,23 @@ "com_config_saving": "Saving...", "com_config_saved": "Changes saved", "com_config_save_error": "Failed to save", + "com_toast_group_created": "Group \"{{name}}\" created", + "com_toast_group_updated": "Group \"{{name}}\" updated", + "com_toast_group_deleted": "Group \"{{name}}\" deleted", + "com_toast_role_created": "Role \"{{name}}\" created", + "com_toast_role_updated": "Role \"{{name}}\" updated", + "com_toast_role_deleted": "Role \"{{name}}\" deleted", + "com_toast_user_invited": "User \"{{name}}\" invited", + "com_toast_user_deleted": "User \"{{name}}\" deleted", + "com_toast_role_assigned": "Added to role \"{{name}}\"", + "com_toast_role_unassigned": "Removed from role \"{{name}}\"", + "com_toast_group_assigned": "Added to group \"{{name}}\"", + "com_toast_group_unassigned": "Removed from group \"{{name}}\"", + "com_toast_user_profile_created": "User profile created for \"{{name}}\"", + "com_toast_user_profile_deleted": "User profile deleted", + "com_toast_capabilities_saved": "Capabilities for \"{{name}}\" saved", + "com_toast_profile_value_saved": "Profile value saved", + "com_toast_profile_value_removed": "Profile value removed", "com_config_import_yaml": "Import YAML", "com_config_import_yaml_title": "Import configuration", "com_config_import_yaml_desc": "Upload or paste a librechat.yaml file to populate configuration values across the UI.", @@ -882,6 +899,8 @@ "com_access_member_ops_failed_other": "{{count}} member operations failed", "com_access_details_saved_permissions_failed": "Details saved, but permissions failed: {{error}}", "com_access_name_required": "Name is required", + "com_access_group_unavailable": "Group is no longer available", + "com_access_role_unavailable": "Role is no longer available", "com_access_col_name": "Name", "com_access_group_name_placeholder": "e.g. Engineering", "com_access_group_desc_placeholder": "Optional description...", @@ -1025,6 +1044,7 @@ "com_access_denied_title": "Access denied", "com_access_denied_description": "You don't have permission to view this page. Contact your administrator.", "com_cap_no_permission": "You need {{cap}} to perform this action", + "com_cap_principal_unavailable": "Principal is no longer available", "com_a11y_cap_filter_changed": "Showing {{count}} principals", "com_nav_audit_log": "Audit log", "com_audit_title": "Audit log", diff --git a/src/styles.css b/src/styles.css index 38ad3fe..88b7c25 100644 --- a/src/styles.css +++ b/src/styles.css @@ -797,66 +797,6 @@ input[type='number'].config-input::-webkit-outer-spin-button { transition: opacity 150ms ease; } -/* ── Toast notification ──────────────────────────────────────── */ - -.config-toast { - position: fixed; - bottom: 1.5rem; - right: 1.5rem; - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.625rem 1rem; - border-radius: 0.5rem; - font-size: 0.8125rem; - font-weight: 500; - box-shadow: 0 8px 30px var(--cui-color-shadow); - z-index: var(--z-toast); - animation: toast-in 200ms ease-out; - pointer-events: none; -} - -.config-toast-info { - background: var(--cui-color-accent-info); - color: var(--cui-color-text-on-accent); -} - -.config-toast-success { - background: var(--cui-color-accent-success); - color: var(--cui-color-text-on-accent); -} - -.config-toast-error { - background: var(--cui-color-accent-danger); - color: var(--cui-color-text-on-accent); -} - -.config-toast-spinner { - width: 14px; - height: 14px; - border: 2px solid color-mix(in srgb, var(--cui-color-text-on-accent) 30%, transparent); - border-top-color: var(--cui-color-text-on-accent); - border-radius: 50%; - animation: toast-spin 600ms linear infinite; -} - -@keyframes toast-in { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes toast-spin { - to { - transform: rotate(360deg); - } -} - .modal-frost[role='dialog'] { border-radius: 12px !important; box-shadow: 0 16px 70px 0 var(--cui-color-shadow) !important; diff --git a/src/types/config-ui.ts b/src/types/config-ui.ts index 1503e02..782a263 100644 --- a/src/types/config-ui.ts +++ b/src/types/config-ui.ts @@ -48,12 +48,6 @@ export interface ConfigPageProps { initialScope?: string; } -export type ToastState = - | { type: 'saving' } - | { type: 'saved' } - | { type: 'error'; message: string } - | null; - export interface ConfigTabBarProps { tabs: ConfigTab[]; activeTab: string; diff --git a/src/utils/index.ts b/src/utils/index.ts index 449e2b1..4f887f9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,3 +2,4 @@ export * from './capabilities'; export * from './cn'; export * from './format'; export * from './interfacePermissions'; +export * from './toast'; diff --git a/src/utils/toast.ts b/src/utils/toast.ts new file mode 100644 index 0000000..4283532 --- /dev/null +++ b/src/utils/toast.ts @@ -0,0 +1,5 @@ +import { createToast } from '@clickhouse/click-ui'; + +export const notifySuccess = (title: string): void => createToast({ type: 'success', title }); + +export const notifyError = (title: string): void => createToast({ type: 'danger', title });