mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-04 23:30:19 +01:00
🧬 feat: Allow Agent Editors to Duplicate Agents (#12041)
* feat: allow editors to duplicate agents * fix: Update permissions for duplicating agents and enhance visibility in AgentFooter - Changed required permission for duplicating agents from VIEW to EDIT in the API route. - Updated AgentFooter component to display the duplicate button for admins and users with EDIT permission, improving access control. - Added tests to ensure the duplicate button visibility logic works correctly based on user roles and permissions. * test: Update AgentFooter tests to reflect permission changes - Adjusted tests in AgentFooter.spec.tsx to verify UI behavior based on user permissions. - Updated expectations for the visibility of the grant access dialog and duplicate button, ensuring they align with the new permission logic. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
d3622844ad
commit
14bcab60b3
5 changed files with 92 additions and 5 deletions
|
|
@ -117,7 +117,7 @@ router.post(
|
|||
'/:id/duplicate',
|
||||
checkAgentCreate,
|
||||
canAccessAgentResource({
|
||||
requiredPermission: PermissionBits.VIEW,
|
||||
requiredPermission: PermissionBits.EDIT,
|
||||
resourceIdParam: 'id',
|
||||
}),
|
||||
v1.duplicateAgent,
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export default function AgentFooter({
|
|||
useResourcePermissions(ResourceType.REMOTE_AGENT, agent?._id || '');
|
||||
|
||||
const canShareThisAgent = hasPermission(PermissionBits.SHARE);
|
||||
const canEditThisAgent = hasPermission(PermissionBits.EDIT);
|
||||
const canDeleteThisAgent = hasPermission(PermissionBits.DELETE);
|
||||
const canShareRemoteAgent = hasRemoteAgentPermission(PermissionBits.SHARE);
|
||||
const isSaving = createMutation.isLoading || updateMutation.isLoading || isAvatarUploading;
|
||||
|
|
@ -118,7 +119,8 @@ export default function AgentFooter({
|
|||
</button>
|
||||
</GenericGrantAccessDialog>
|
||||
)}
|
||||
{agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />}
|
||||
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN || canEditThisAgent) &&
|
||||
!permissionsLoading && <DuplicateAgent agent_id={agent_id} />}
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
className="btn btn-primary focus:shadow-outline flex h-9 w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export default function DeleteButton({
|
|||
size="sm"
|
||||
variant="outline"
|
||||
aria-label={localize('com_ui_delete_agent')}
|
||||
title={localize('com_ui_delete_agent')}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2 text-red-500">
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export default function DuplicateAgent({ agent_id }: { agent_id: string }) {
|
|||
size="sm"
|
||||
variant="outline"
|
||||
aria-label={localize('com_ui_duplicate_agent')}
|
||||
title={localize('com_ui_duplicate_agent')}
|
||||
type="button"
|
||||
onClick={handleDuplicate}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -362,9 +362,15 @@ describe('AgentFooter', () => {
|
|||
}
|
||||
return undefined;
|
||||
});
|
||||
mockUseHasAccess.mockReturnValue(true);
|
||||
mockUseResourcePermissions.mockReturnValue({
|
||||
hasPermission: () => false,
|
||||
isLoading: false,
|
||||
permissionBits: 0,
|
||||
});
|
||||
render(<AgentFooter {...defaultProps} />);
|
||||
expect(screen.queryByTestId('grant-access-dialog-agent')).toBeInTheDocument(); // Still shows because hasAccess is true
|
||||
expect(screen.queryByTestId('duplicate-agent')).not.toBeInTheDocument(); // Should not show for different author
|
||||
expect(screen.queryByTestId('grant-access-dialog-agent')).not.toBeInTheDocument(); // No share permission
|
||||
expect(screen.queryByTestId('duplicate-button')).not.toBeInTheDocument(); // No edit permission
|
||||
});
|
||||
|
||||
test('adjusts UI based on permissions', () => {
|
||||
|
|
@ -420,7 +426,84 @@ describe('AgentFooter', () => {
|
|||
render(<AgentFooter {...defaultProps} />);
|
||||
expect(screen.queryByTestId('delete-button')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('grant-access-dialog-agent')).not.toBeInTheDocument();
|
||||
// Duplicate button should still show as it doesn't depend on permissions loading
|
||||
expect(screen.queryByTestId('duplicate-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows duplicate button for non-owner with EDIT permission', () => {
|
||||
mockUseAuthContext.mockReturnValue(createAuthContext(mockUsers.different));
|
||||
mockUseWatch.mockImplementation(({ name }) => {
|
||||
if (name === 'agent') {
|
||||
return {
|
||||
_id: 'agent-db-123',
|
||||
name: 'Test Agent',
|
||||
author: 'user-123',
|
||||
projectIds: ['project-1'],
|
||||
isCollaborative: false,
|
||||
};
|
||||
}
|
||||
if (name === 'id') {
|
||||
return 'agent-123';
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
mockUseResourcePermissions.mockReturnValue({
|
||||
hasPermission: (bit: number) => bit === 2,
|
||||
isLoading: false,
|
||||
permissionBits: 2,
|
||||
});
|
||||
render(<AgentFooter {...defaultProps} />);
|
||||
expect(screen.getByTestId('duplicate-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('hides duplicate button for non-owner with only VIEW permission', () => {
|
||||
mockUseAuthContext.mockReturnValue(createAuthContext(mockUsers.different));
|
||||
mockUseWatch.mockImplementation(({ name }) => {
|
||||
if (name === 'agent') {
|
||||
return {
|
||||
_id: 'agent-db-123',
|
||||
name: 'Test Agent',
|
||||
author: 'user-123',
|
||||
projectIds: ['project-1'],
|
||||
isCollaborative: false,
|
||||
};
|
||||
}
|
||||
if (name === 'id') {
|
||||
return 'agent-123';
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
mockUseResourcePermissions.mockReturnValue({
|
||||
hasPermission: () => false,
|
||||
isLoading: false,
|
||||
permissionBits: 1,
|
||||
});
|
||||
render(<AgentFooter {...defaultProps} />);
|
||||
expect(screen.queryByTestId('duplicate-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows duplicate button for admin who is not the author', () => {
|
||||
mockUseAuthContext.mockReturnValue(createAuthContext(mockUsers.admin));
|
||||
mockUseWatch.mockImplementation(({ name }) => {
|
||||
if (name === 'agent') {
|
||||
return {
|
||||
_id: 'agent-db-123',
|
||||
name: 'Test Agent',
|
||||
author: 'user-123',
|
||||
projectIds: ['project-1'],
|
||||
isCollaborative: false,
|
||||
};
|
||||
}
|
||||
if (name === 'id') {
|
||||
return 'agent-123';
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
mockUseResourcePermissions.mockReturnValue({
|
||||
hasPermission: () => false,
|
||||
isLoading: false,
|
||||
permissionBits: 0,
|
||||
});
|
||||
render(<AgentFooter {...defaultProps} />);
|
||||
expect(screen.getByTestId('duplicate-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue