mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
🔗 fix: Surface Share Permissions Load Error as Alert Button With Tooltip (#13833)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
This commit is contained in:
parent
a468becf8c
commit
2fcba914f7
3 changed files with 172 additions and 3 deletions
|
|
@ -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 <div className="text-sm text-red-600">{localize('com_ui_permissions_failed_load')}</div>;
|
||||
return (
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_permissions_failed_load')}
|
||||
render={
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => refetchPermissions()}
|
||||
aria-label={localize('com_ui_permissions_failed_load')}
|
||||
className={cn('h-9', buttonClassName)}
|
||||
>
|
||||
<div className="flex min-w-[32px] items-center justify-center text-red-500">
|
||||
<span className="flex h-6 w-6 items-center justify-center">
|
||||
{isFetchingPermissions ? (
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
<AlertCircle className="icon-md h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const TriggerComponent = children ? (
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> = {}) => ({
|
||||
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: () => <div data-testid="unified-people-search" />,
|
||||
}));
|
||||
jest.mock('../PeoplePickerAdminSettings', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="admin-settings" />,
|
||||
}));
|
||||
jest.mock('../PublicSharingToggle', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="public-toggle" />,
|
||||
}));
|
||||
jest.mock('../PeoplePicker', () => ({
|
||||
__esModule: true,
|
||||
SelectedPrincipalsList: () => <div data-testid="principals-list" />,
|
||||
}));
|
||||
|
||||
const renderDialog = () =>
|
||||
render(
|
||||
<GenericGrantAccessDialog
|
||||
resourceDbId="agent-db-1"
|
||||
resourceId="agent-1"
|
||||
resourceName="Test Agent"
|
||||
resourceType={ResourceType.AGENT}
|
||||
/>,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue