diff --git a/client/src/components/Sharing/GenericGrantAccessDialog.tsx b/client/src/components/Sharing/GenericGrantAccessDialog.tsx
index 24e9d1ae9b..e28d58a7ba 100644
--- a/client/src/components/Sharing/GenericGrantAccessDialog.tsx
+++ b/client/src/components/Sharing/GenericGrantAccessDialog.tsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { AccessRoleIds, ResourceType } from 'librechat-data-provider';
-import { Share2Icon, Users, Link, CopyCheck, UserX, UserCheck } from 'lucide-react';
+import { Share2Icon, Users, Link, CopyCheck, UserX, UserCheck, AlertCircle } from 'lucide-react';
import {
Label,
Button,
@@ -11,6 +11,7 @@ import {
OGDialogClose,
OGDialogContent,
OGDialogTrigger,
+ TooltipAnchor,
useToastContext,
} from '@librechat/client';
import type { TPrincipal } from 'librechat-data-provider';
@@ -60,7 +61,9 @@ export default function GenericGrantAccessDialog({
config,
permissionsData,
isLoadingPermissions,
+ isFetchingPermissions,
permissionsError,
+ refetchPermissions,
updatePermissionsMutation,
currentShares,
currentIsPublic,
@@ -237,9 +240,35 @@ export default function GenericGrantAccessDialog({
const hasPublicChanges = isPublic !== currentIsPublic || publicRole !== currentPublicRole;
const submitButtonActive = hasChanges || hasPublicChanges;
- // Error handling
+ // On permissions load failure, keep a compact trigger-sized button so the layout holds,
+ // surfacing the error (and a retry on click) through a tooltip.
if (permissionsError) {
- return
{localize('com_ui_permissions_failed_load')}
;
+ return (
+ refetchPermissions()}
+ aria-label={localize('com_ui_permissions_failed_load')}
+ className={cn('h-9', buttonClassName)}
+ >
+
+
+ {isFetchingPermissions ? (
+
+ ) : (
+
+ )}
+
+
+
+ }
+ />
+ );
}
const TriggerComponent = children ? (
diff --git a/client/src/components/Sharing/__tests__/GenericGrantAccessDialog.spec.tsx b/client/src/components/Sharing/__tests__/GenericGrantAccessDialog.spec.tsx
new file mode 100644
index 0000000000..f91667860d
--- /dev/null
+++ b/client/src/components/Sharing/__tests__/GenericGrantAccessDialog.spec.tsx
@@ -0,0 +1,136 @@
+import React from 'react';
+import { ResourceType } from 'librechat-data-provider';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom/extend-expect';
+import GenericGrantAccessDialog from '../GenericGrantAccessDialog';
+
+const mockRefetchPermissions = jest.fn();
+const mockUseResourcePermissionState = jest.fn();
+
+const config = {
+ defaultViewerRoleId: 'viewer',
+ defaultOwnerRoleId: 'owner',
+ getShareMessage: () => 'Share Agent',
+ getResourceUrl: () => 'http://localhost/agent/1',
+ getCopyUrlMessage: () => 'Copied',
+};
+
+const baseState = (overrides: Record = {}) => ({
+ config,
+ permissionsData: { principals: [], public: false },
+ isLoadingPermissions: false,
+ isFetchingPermissions: false,
+ permissionsError: null,
+ refetchPermissions: mockRefetchPermissions,
+ updatePermissionsMutation: { isLoading: false, mutateAsync: jest.fn() },
+ currentShares: [],
+ currentIsPublic: false,
+ currentPublicRole: 'viewer',
+ isPublic: false,
+ setIsPublic: jest.fn(),
+ publicRole: 'viewer',
+ setPublicRole: jest.fn(),
+ ...overrides,
+});
+
+jest.mock('~/hooks', () => ({
+ useLocalize: () => (key: string) => key,
+ useResourcePermissionState: () => mockUseResourcePermissionState(),
+ usePeoplePickerPermissions: () => ({ hasPeoplePickerAccess: true, peoplePickerTypeFilter: '' }),
+ useCanSharePublic: () => true,
+ useCopyToClipboard: () => jest.fn(),
+}));
+
+jest.mock('@librechat/client', () => ({
+ ...jest.requireActual('@librechat/client'),
+ useToastContext: () => ({ showToast: jest.fn() }),
+}));
+
+jest.mock('../PeoplePicker/UnifiedPeopleSearch', () => ({
+ __esModule: true,
+ default: () => ,
+}));
+jest.mock('../PeoplePickerAdminSettings', () => ({
+ __esModule: true,
+ default: () => ,
+}));
+jest.mock('../PublicSharingToggle', () => ({
+ __esModule: true,
+ default: () => ,
+}));
+jest.mock('../PeoplePicker', () => ({
+ __esModule: true,
+ SelectedPrincipalsList: () => ,
+}));
+
+const renderDialog = () =>
+ render(
+ ,
+ );
+
+describe('GenericGrantAccessDialog - permissions load failure', () => {
+ beforeEach(() => {
+ mockUseResourcePermissionState.mockReset();
+ mockRefetchPermissions.mockReset();
+ });
+
+ it('renders a compact alert button (not the share trigger, not raw text) when permissions fail to load', () => {
+ mockUseResourcePermissionState.mockReturnValue(
+ baseState({ permissionsError: new Error('boom'), permissionsData: undefined }),
+ );
+
+ const { container } = renderDialog();
+
+ expect(
+ screen.getByRole('button', { name: 'com_ui_permissions_failed_load' }),
+ ).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'com_ui_share_var' })).not.toBeInTheDocument();
+ expect(container.querySelector('.spinner')).not.toBeInTheDocument();
+ });
+
+ it('retries the permissions fetch when the alert button is clicked', () => {
+ mockUseResourcePermissionState.mockReturnValue(
+ baseState({ permissionsError: new Error('boom'), permissionsData: undefined }),
+ );
+
+ renderDialog();
+
+ fireEvent.click(screen.getByRole('button', { name: 'com_ui_permissions_failed_load' }));
+ expect(mockRefetchPermissions).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows a spinner instead of the alert icon while refetching', () => {
+ mockUseResourcePermissionState.mockReturnValue(
+ baseState({
+ permissionsError: new Error('boom'),
+ permissionsData: undefined,
+ isFetchingPermissions: true,
+ }),
+ );
+
+ const { container } = renderDialog();
+
+ expect(
+ screen.getByRole('button', { name: 'com_ui_permissions_failed_load' }),
+ ).toBeInTheDocument();
+ expect(container.querySelector('.spinner')).toBeInTheDocument();
+ });
+
+ it('renders the normal share trigger and dialog body when permissions load successfully', () => {
+ mockUseResourcePermissionState.mockReturnValue(baseState());
+
+ renderDialog();
+
+ expect(
+ screen.queryByRole('button', { name: 'com_ui_permissions_failed_load' }),
+ ).not.toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole('button', { name: 'com_ui_share_var' }));
+ expect(screen.getByTestId('unified-people-search')).toBeInTheDocument();
+ });
+});
diff --git a/client/src/hooks/Sharing/useResourcePermissionState.ts b/client/src/hooks/Sharing/useResourcePermissionState.ts
index 0a9f8ce386..875fe17832 100644
--- a/client/src/hooks/Sharing/useResourcePermissionState.ts
+++ b/client/src/hooks/Sharing/useResourcePermissionState.ts
@@ -26,7 +26,9 @@ export const useResourcePermissionState = (
const {
data: permissionsData,
isLoading: isLoadingPermissions,
+ isFetching: isFetchingPermissions,
error: permissionsError,
+ refetch: refetchPermissions,
} = useGetResourcePermissionsQuery(resourceType, resourceDbId || '', {
enabled: isValidResourceId,
});
@@ -68,7 +70,9 @@ export const useResourcePermissionState = (
config,
permissionsData,
isLoadingPermissions,
+ isFetchingPermissions,
permissionsError,
+ refetchPermissions,
updatePermissionsMutation,
currentShares,
currentIsPublic,