From f70a62793bec9095abaa75ea574e495cd7cd53c7 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Tue, 5 Aug 2025 23:01:24 +0200 Subject: [PATCH] refactor: remove GenericManagePermissionsDialog and GrantAccessDialog components - Deleted GenericManagePermissionsDialog and GrantAccessDialog components to streamline sharing functionality. - Updated ManagePermissionsDialog to utilize AccessRolesPicker directly. - Introduced UnifiedPeopleSearch for improved people selection experience. - Enhanced PublicSharingToggle with InfoHoverCard for better user guidance. - Adjusted AgentPanel to change error status to warning for duplicate agent versions. - Updated translations to include new keys for search and access management. --- .../Account/DisplayUsernameMessages.tsx | 5 +- .../Balance/AutoRefillSettings.tsx | 5 +- .../SettingsTabs/Balance/TokenCreditsItem.tsx | 5 +- .../Nav/SettingsTabs/Chat/ForkSettings.tsx | 13 +- .../Nav/SettingsTabs/Chat/SaveBadgesState.tsx | 5 +- .../Nav/SettingsTabs/Chat/ShowThinking.tsx | 5 +- .../Nav/SettingsTabs/Commands/Commands.tsx | 6 +- .../Nav/SettingsTabs/DangerButton.tsx | 7 +- .../Nav/SettingsTabs/HoverCardSettings.tsx | 30 -- .../Nav/SettingsTabs/ToggleSwitch.tsx | 20 +- .../components/Sharing/AccessRolesPicker.tsx | 11 +- .../Sharing/GenericGrantAccessDialog.tsx | 267 ++++++++++---- .../GenericManagePermissionsDialog.tsx | 339 ------------------ .../components/Sharing/GrantAccessDialog.tsx | 295 --------------- .../Sharing/ManagePermissionsDialog.tsx | 31 +- .../Sharing/PeoplePicker/SearchPicker.tsx | 6 +- .../PeoplePicker/SelectedPrincipalsList.tsx | 64 +--- .../PeoplePicker/UnifiedPeopleSearch.tsx | 83 +++++ .../components/Sharing/PeoplePicker/index.ts | 1 + .../Sharing/PublicSharingToggle.tsx | 48 +-- client/src/components/Sharing/index.ts | 1 - .../SidePanel/Agents/AgentPanel.tsx | 2 +- client/src/locales/en/translation.json | 4 + client/tsconfig.json | 6 +- 24 files changed, 385 insertions(+), 874 deletions(-) delete mode 100644 client/src/components/Nav/SettingsTabs/HoverCardSettings.tsx delete mode 100644 client/src/components/Sharing/GenericManagePermissionsDialog.tsx delete mode 100644 client/src/components/Sharing/GrantAccessDialog.tsx create mode 100644 client/src/components/Sharing/PeoplePicker/UnifiedPeopleSearch.tsx diff --git a/client/src/components/Nav/SettingsTabs/Account/DisplayUsernameMessages.tsx b/client/src/components/Nav/SettingsTabs/Account/DisplayUsernameMessages.tsx index 011edc4685..bdb3585888 100644 --- a/client/src/components/Nav/SettingsTabs/Account/DisplayUsernameMessages.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/DisplayUsernameMessages.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { useRecoilState } from 'recoil'; -import { Switch, Label } from '@librechat/client'; -import HoverCardSettings from '../HoverCardSettings'; +import { Switch, Label, InfoHoverCard, ESide } from '@librechat/client'; import { useLocalize } from '~/hooks'; import store from '~/store'; @@ -17,7 +16,7 @@ export default function DisplayUsernameMessages() {
- +
= ({ {/* Left Section: Label */}
- +
{/* Right Section: tokenCredits Value */} diff --git a/client/src/components/Nav/SettingsTabs/Balance/TokenCreditsItem.tsx b/client/src/components/Nav/SettingsTabs/Balance/TokenCreditsItem.tsx index d96ecd98ed..3766c988a4 100644 --- a/client/src/components/Nav/SettingsTabs/Balance/TokenCreditsItem.tsx +++ b/client/src/components/Nav/SettingsTabs/Balance/TokenCreditsItem.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { Label } from '@librechat/client'; -import HoverCardSettings from '~/components/Nav/SettingsTabs/HoverCardSettings'; +import { Label, InfoHoverCard, ESide } from '@librechat/client'; import { useLocalize } from '~/hooks'; interface TokenCreditsItemProps { @@ -15,7 +14,7 @@ const TokenCreditsItem: React.FC = ({ tokenCredits }) => {/* Left Section: Label */}
- +
{/* Right Section: tokenCredits Value */} diff --git a/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx b/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx index 465fa1637a..a81d4f4f50 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx @@ -1,7 +1,6 @@ import { useRecoilState } from 'recoil'; -import { Dropdown, Switch } from '@librechat/client'; import { ForkOptions } from 'librechat-data-provider'; -import HoverCardSettings from '../HoverCardSettings'; +import { Dropdown, Switch, InfoHoverCard, ESide } from '@librechat/client'; import { useLocalize } from '~/hooks'; import store from '~/store'; @@ -36,7 +35,10 @@ export const ForkSettings = () => {
{localize('com_ui_fork_change_default')}
- +
{
{localize('com_ui_fork_split_target_setting')}
- +
{localize('com_nav_save_badges_state')}
- +
{localize('com_nav_show_thinking')}
- +
{localize('com_nav_chat_commands')} - +
diff --git a/client/src/components/Nav/SettingsTabs/DangerButton.tsx b/client/src/components/Nav/SettingsTabs/DangerButton.tsx index be29d10cc2..15084e9ba9 100644 --- a/client/src/components/Nav/SettingsTabs/DangerButton.tsx +++ b/client/src/components/Nav/SettingsTabs/DangerButton.tsx @@ -1,9 +1,8 @@ import { forwardRef } from 'react'; -import type { ForwardedRef } from 'react'; import { CheckIcon } from 'lucide-react'; -import { Spinner, DialogButton } from '@librechat/client'; -import HoverCardSettings from './HoverCardSettings'; +import { Spinner, DialogButton, InfoHoverCard, ESide } from '@librechat/client'; import type { TDangerButtonProps } from '~/common'; +import type { ForwardedRef } from 'react'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; @@ -37,7 +36,7 @@ const DangerButton = (props: TDangerButtonProps, ref: ForwardedRef
{localize(infoTextCode)}
- {infoDescriptionCode && } + {infoDescriptionCode && }
)} { - const localize = useLocalize(); - - return ( - - - {' '} - - - -
-

{localize(text)}

-
-
-
-
- ); -}; - -export default HoverCardSettings; diff --git a/client/src/components/Nav/SettingsTabs/ToggleSwitch.tsx b/client/src/components/Nav/SettingsTabs/ToggleSwitch.tsx index 23a14d81b4..64c8062ca7 100644 --- a/client/src/components/Nav/SettingsTabs/ToggleSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/ToggleSwitch.tsx @@ -1,12 +1,14 @@ -import { Switch } from '@librechat/client'; import { RecoilState, useRecoilState } from 'recoil'; -import HoverCardSettings from './HoverCardSettings'; +import { Switch, InfoHoverCard, ESide } from '@librechat/client'; import { useLocalize } from '~/hooks'; +type LocalizeFn = ReturnType; +type LocalizeKey = Parameters[0]; + interface ToggleSwitchProps { stateAtom: RecoilState; - localizationKey: string; - hoverCardText?: string; + localizationKey: LocalizeKey; + hoverCardText?: LocalizeKey; switchId: string; onCheckedChange?: (value: boolean) => void; } @@ -18,21 +20,19 @@ const ToggleSwitch: React.FC = ({ switchId, onCheckedChange, }) => { - const [switchState, setSwitchState] = useRecoilState(stateAtom); + const [switchState, setSwitchState] = useRecoilState(stateAtom); const localize = useLocalize(); const handleCheckedChange = (value: boolean) => { setSwitchState(value); - if (onCheckedChange) { - onCheckedChange(value); - } + onCheckedChange?.(value); }; return (
-
{localize(localizationKey as any)}
- {hoverCardText && } +
{localize(localizationKey)}
+ {hoverCardText && }
-
-
- {localize('com_ui_loading')} -
-
- ); + return ; } const dropdownItems: t.MenuItemProps[] = accessRoles.map((role: AccessRole) => { diff --git a/client/src/components/Sharing/GenericGrantAccessDialog.tsx b/client/src/components/Sharing/GenericGrantAccessDialog.tsx index 9c719472b3..f18ecdcbfb 100644 --- a/client/src/components/Sharing/GenericGrantAccessDialog.tsx +++ b/client/src/components/Sharing/GenericGrantAccessDialog.tsx @@ -1,10 +1,11 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { AccessRoleIds, ResourceType } from 'librechat-data-provider'; -import { Share2Icon, Users, Shield, Link, CopyCheck } from 'lucide-react'; +import { Share2Icon, Users, Link, CopyCheck, UserX, UserCheck } from 'lucide-react'; import { Label, Button, Spinner, + Skeleton, OGDialog, OGDialogTitle, OGDialogClose, @@ -19,10 +20,9 @@ import { useCopyToClipboard, useLocalize, } from '~/hooks'; -import GenericManagePermissionsDialog from './GenericManagePermissionsDialog'; +import UnifiedPeopleSearch from './PeoplePicker/UnifiedPeopleSearch'; import PublicSharingToggle from './PublicSharingToggle'; -import AccessRolesPicker from './AccessRolesPicker'; -import { PeoplePicker } from './PeoplePicker'; +import { SelectedPrincipalsList } from './PeoplePicker'; import { cn } from '~/utils'; export default function GenericGrantAccessDialog({ @@ -51,6 +51,9 @@ export default function GenericGrantAccessDialog({ const { hasPeoplePickerAccess, peoplePickerTypeFilter } = usePeoplePickerPermissions(); const { config, + permissionsData, + isLoadingPermissions, + permissionsError, updatePermissionsMutation, currentShares, currentIsPublic, @@ -61,11 +64,22 @@ export default function GenericGrantAccessDialog({ setPublicRole, } = useResourcePermissionState(resourceType, resourceDbId, isModalOpen); - const [newShares, setNewShares] = useState([]); + // State for unified list of all shares (existing + newly added) + const [allShares, setAllShares] = useState([]); + const [hasChanges, setHasChanges] = useState(false); const [defaultPermissionId, setDefaultPermissionId] = useState( config?.defaultViewerRoleId, ); + // Sync all shares with current shares when modal opens, marking existing vs new + useEffect(() => { + if (permissionsData && isModalOpen) { + const shares = permissionsData.principals || []; + setAllShares(shares.map((share) => ({ ...share, isExisting: true }))); + setHasChanges(false); + } + }, [permissionsData, isModalOpen]); + const resourceUrl = config?.getResourceUrl ? config?.getResourceUrl(resourceId || '') : ''; const copyResourceUrl = useCopyToClipboard({ text: resourceUrl }); @@ -78,21 +92,95 @@ export default function GenericGrantAccessDialog({ return null; } - const handleGrantAccess = async () => { - try { - const sharesToAdd = newShares.map((share) => ({ - ...share, - accessRoleId: defaultPermissionId, - })); + // Handler for adding users from search (immediate add to unified list) + const handleAddFromSearch = (newShares: TPrincipal[]) => { + const sharesToAdd = newShares.filter( + (newShare) => + !allShares.some((existing) => existing.idOnTheSource === newShare.idOnTheSource), + ); - const allShares = [...currentShares, ...sharesToAdd]; + const sharesWithDefaults = sharesToAdd.map((share) => ({ + ...share, + accessRoleId: defaultPermissionId || config?.defaultViewerRoleId, + isExisting: false, // Mark as newly added + })); + + setAllShares((prev) => [...prev, ...sharesWithDefaults]); + setHasChanges(true); + }; + + // Handler for removing individual shares + const handleRemoveShare = (idOnTheSource: string) => { + setAllShares(allShares.filter((s) => s.idOnTheSource !== idOnTheSource)); + setHasChanges(true); + }; + + // Handler for changing individual share permissions + const handleRoleChange = (idOnTheSource: string, newRole: string) => { + setAllShares( + allShares.map((s) => + s.idOnTheSource === idOnTheSource ? { ...s, accessRoleId: newRole as AccessRoleIds } : s, + ), + ); + setHasChanges(true); + }; + + // Handler for revoking all sharing + const handleRevokeAll = () => { + setAllShares([]); + setIsPublic(false); + setHasChanges(true); + }; + + // Handler for public access toggle + const handlePublicToggle = (isPublicValue: boolean) => { + setIsPublic(isPublicValue); + setHasChanges(true); + if (!isPublicValue) { + setPublicRole(config?.defaultViewerRoleId); + } + }; + + // Handler for public role change + const handlePublicRoleChange = (role: string) => { + setPublicRole(role as AccessRoleIds); + setHasChanges(true); + }; + + // Save all changes (unified save handler) + const handleSave = async () => { + if (!allShares.length && !isPublic && !hasChanges) { + return; + } + + try { + // Calculate changes for unified list + const originalSharesMap = new Map( + currentShares.map((share) => [`${share.type}-${share.idOnTheSource}`, share]), + ); + const allSharesMap = new Map( + allShares.map((share) => [`${share.type}-${share.idOnTheSource}`, share]), + ); + + // Find newly added and updated shares + const updated = allShares.filter((share) => { + const key = `${share.type}-${share.idOnTheSource}`; + const original = originalSharesMap.get(key); + return !original || original.accessRoleId !== share.accessRoleId; + }); + + // Find removed shares + const removed = currentShares.filter((share) => { + const key = `${share.type}-${share.idOnTheSource}`; + return !allSharesMap.has(key); + }); await updatePermissionsMutation.mutateAsync({ resourceType, resourceId: resourceDbId, data: { - updated: sharesToAdd, - removed: [], + updated, + removed, public: isPublic, publicAccessRoleId: isPublic ? publicRole : undefined, }, @@ -103,34 +191,48 @@ export default function GenericGrantAccessDialog({ } showToast({ - message: `Access granted successfully to ${newShares.length} ${newShares.length === 1 ? 'person' : 'people'}${isPublic ? ' and made public' : ''}`, + message: localize('com_ui_permissions_updated_success'), status: 'success', }); - setNewShares([]); - setDefaultPermissionId(config?.defaultViewerRoleId); - setIsPublic(false); - setPublicRole(config?.defaultViewerRoleId); + setHasChanges(false); + setIsModalOpen(false); } catch (error) { - console.error('Error granting access:', error); + console.error('Error updating permissions:', error); showToast({ - message: 'Failed to grant access. Please try again.', + message: localize('com_ui_permissions_failed_update'), status: 'error', }); } }; const handleCancel = () => { - setNewShares([]); + // Reset to original state + const shares = permissionsData?.principals || []; + setAllShares(shares.map((share) => ({ ...share, isExisting: true }))); setDefaultPermissionId(config?.defaultViewerRoleId); - setIsPublic(false); - setPublicRole(config?.defaultViewerRoleId); + setIsPublic(currentIsPublic); + setPublicRole(currentPublicRole || config?.defaultViewerRoleId || ''); + setHasChanges(false); setIsModalOpen(false); }; + // Validation and calculated values const totalCurrentShares = currentShares.length + (currentIsPublic ? 1 : 0); - const submitButtonActive = - newShares.length > 0 || isPublic !== currentIsPublic || publicRole !== currentPublicRole; + + // Check if there's at least one owner (user, group, or public with owner role) + const hasAtLeastOneOwner = + allShares.some((share) => share.accessRoleId === config?.defaultOwnerRoleId) || + (isPublic && publicRole === config?.defaultOwnerRoleId); + + // Check if there are any changes to save + const hasPublicChanges = isPublic !== currentIsPublic || publicRole !== currentPublicRole; + const submitButtonActive = hasChanges || hasPublicChanges; + + // Error handling + if (permissionsError) { + return
{localize('com_ui_permissions_failed_load')}
; + } const TriggerComponent = children ? ( children @@ -158,7 +260,6 @@ export default function GenericGrantAccessDialog({ return ( {TriggerComponent} -
@@ -170,47 +271,83 @@ export default function GenericGrantAccessDialog({
- {hasPeoplePickerAccess && ( - <> - + {/* Unified Search and Management Section */} +
+ {/* Search Bar with Default Permission Setting */} + {hasPeoplePickerAccess && ( +
+

+ + {localize('com_ui_user_group_permissions')} ( {allShares.length} ) +

-
-
-
- - -
-
- s.idOnTheSource)} /> + + {/* Unified User/Group List */} + {(() => { + if (isLoadingPermissions) { + return ( +
+ + +
+ ); + } + + if (allShares.length === 0 && !hasChanges) { + return ( +
+ +

+ {localize('com_ui_no_individual_access')} +

+

+ {localize('com_ui_search_above_to_add_people')} +

+
+ ); + } + + return ( +
+ {!hasAtLeastOneOwner && hasChanges && ( +
+
+ + {localize('com_ui_at_least_one_owner_required')} +
+
+ )} + handleRoleChange(id, newRole)} + /> +
+ ); + })()}
- - )} + )} +
+ + {/* Public Access Section */} + + {/* Footer Actions */}
- {hasPeoplePickerAccess && ( - - )} {resourceId && resourceUrl && (
diff --git a/client/src/components/Sharing/GenericManagePermissionsDialog.tsx b/client/src/components/Sharing/GenericManagePermissionsDialog.tsx deleted file mode 100644 index 5d9b26c17b..0000000000 --- a/client/src/components/Sharing/GenericManagePermissionsDialog.tsx +++ /dev/null @@ -1,339 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useGetAccessRolesQuery } from 'librechat-data-provider/react-query'; -import { Settings, Users, Loader, UserCheck, Trash2, Shield } from 'lucide-react'; -import { - Button, - OGDialog, - OGDialogTitle, - OGDialogClose, - OGDialogContent, - OGDialogTrigger, - useToastContext, -} from '@librechat/client'; -import type { TPrincipal, ResourceType, AccessRoleIds } from 'librechat-data-provider'; -import { useResourcePermissionState } from '~/hooks/Sharing'; -import PublicSharingToggle from './PublicSharingToggle'; -import { SelectedPrincipalsList } from './PeoplePicker'; -import { useLocalize } from '~/hooks'; - -export default function GenericManagePermissionsDialog({ - resourceDbId, - resourceName, - resourceType, - onUpdatePermissions, - children, -}: { - resourceDbId: string; - resourceName?: string; - resourceType: ResourceType; - onUpdatePermissions?: ( - shares: TPrincipal[], - isPublic: boolean, - publicRole: AccessRoleIds, - ) => void; - children?: React.ReactNode; -}) { - const localize = useLocalize(); - const { showToast } = useToastContext(); - const [isModalOpen, setIsModalOpen] = useState(false); - const [hasChanges, setHasChanges] = useState(false); - - const { - config, - permissionsData, - isLoadingPermissions, - permissionsError, - updatePermissionsMutation, - currentShares, - currentIsPublic, - currentPublicRole, - isPublic: managedIsPublic, - setIsPublic: setManagedIsPublic, - publicRole: managedPublicRole, - setPublicRole: setManagedPublicRole, - } = useResourcePermissionState(resourceType, resourceDbId, isModalOpen); - - const { data: accessRoles } = useGetAccessRolesQuery(resourceType); - - const [managedShares, setManagedShares] = useState([]); - - useEffect(() => { - if (permissionsData && isModalOpen) { - const shares = permissionsData.principals || []; - setManagedShares(shares); - setHasChanges(false); - } - }, [permissionsData, isModalOpen]); - - if (!resourceDbId) { - return null; - } - - if (!config) { - console.error(`Unsupported resource type: ${resourceType}`); - return null; - } - - if (permissionsError) { - return
{localize('com_ui_permissions_failed_load')}
; - } - - const handleRemoveShare = (idOnTheSource: string) => { - setManagedShares(managedShares.filter((s) => s.idOnTheSource !== idOnTheSource)); - setHasChanges(true); - }; - - const handleRoleChange = (idOnTheSource: string, newRole: string) => { - setManagedShares( - managedShares.map((s) => - s.idOnTheSource === idOnTheSource ? { ...s, accessRoleId: newRole } : s, - ), - ); - setHasChanges(true); - }; - - const handleSaveChanges = async () => { - try { - const originalSharesMap = new Map( - currentShares.map((share) => [`${share.type}-${share.idOnTheSource}`, share]), - ); - const managedSharesMap = new Map( - managedShares.map((share) => [`${share.type}-${share.idOnTheSource}`, share]), - ); - - const updated = managedShares.filter((share) => { - const key = `${share.type}-${share.idOnTheSource}`; - const original = originalSharesMap.get(key); - return !original || original.accessRoleId !== share.accessRoleId; - }); - - const removed = currentShares.filter((share) => { - const key = `${share.type}-${share.idOnTheSource}`; - return !managedSharesMap.has(key); - }); - - await updatePermissionsMutation.mutateAsync({ - resourceType, - resourceId: resourceDbId, - data: { - updated, - removed, - public: managedIsPublic, - publicAccessRoleId: managedIsPublic ? managedPublicRole : undefined, - }, - }); - - if (onUpdatePermissions) { - onUpdatePermissions(managedShares, managedIsPublic, managedPublicRole); - } - - showToast({ - message: localize('com_ui_permissions_updated_success'), - status: 'success', - }); - - setIsModalOpen(false); - } catch (error) { - console.error('Error updating permissions:', error); - showToast({ - message: localize('com_ui_permissions_failed_update'), - status: 'error', - }); - } - }; - - const handleCancel = () => { - setManagedShares(currentShares); - setManagedIsPublic(currentIsPublic); - setManagedPublicRole(currentPublicRole || config?.defaultViewerRoleId || ''); - setIsModalOpen(false); - }; - - const handleRevokeAll = () => { - setManagedShares([]); - setManagedIsPublic(false); - setHasChanges(true); - }; - const handlePublicToggle = (isPublic: boolean) => { - setManagedIsPublic(isPublic); - setHasChanges(true); - if (!isPublic) { - setManagedPublicRole(config?.defaultViewerRoleId); - } - }; - const handlePublicRoleChange = (role: string) => { - setManagedPublicRole(role); - setHasChanges(true); - }; - const totalShares = managedShares.length + (managedIsPublic ? 1 : 0); - const originalTotalShares = currentShares.length + (currentIsPublic ? 1 : 0); - - /** Check if there's at least one owner (user, group, or public with owner role) */ - const hasAtLeastOneOwner = - managedShares.some((share) => share.accessRoleId === config?.defaultOwnerRoleId) || - (managedIsPublic && managedPublicRole === config?.defaultOwnerRoleId); - - let peopleLabel = localize('com_ui_people'); - if (managedShares.length === 1) { - peopleLabel = localize('com_ui_person'); - } - - const buttonAriaLabel = config?.getManageMessage(resourceName); - const dialogTitle = config?.getManageMessage(resourceName); - - let publicSuffix = ''; - if (managedIsPublic) { - publicSuffix = localize('com_ui_and_public'); - } - - const TriggerComponent = children ? ( - children - ) : ( - - ); - - return ( - - {TriggerComponent} - - - -
- - {dialogTitle} -
-
- -
-
-
-
-

- {localize('com_ui_current_access')} -

-

- {(() => { - if (totalShares === 0) { - return localize('com_ui_no_users_groups_access'); - } - return localize('com_ui_shared_with_count', { - 0: managedShares.length, - 1: peopleLabel, - 2: publicSuffix, - }); - })()} -

-
- {(managedShares.length > 0 || managedIsPublic) && ( - - )} -
-
- - {(() => { - if (isLoadingPermissions) { - return ( -
- - - {localize('com_ui_loading_permissions')} - -
- ); - } - - if (managedShares.length > 0) { - return ( -
-

- - {localize('com_ui_user_group_permissions')} ({managedShares.length}) -

- handleRoleChange(id, newRole)} - /> -
- ); - } - - return ( -
- -

- {localize('com_ui_no_individual_access')} -

-
- ); - })()} - -
-

- {localize('com_ui_public_access')} -

- -
- -
- - - - -
- - {hasChanges && ( -
- * {localize('com_ui_unsaved_changes')} -
- )} - - {!hasAtLeastOneOwner && hasChanges && ( -
- * {localize('com_ui_at_least_one_owner_required')} -
- )} -
-
-
- ); -} diff --git a/client/src/components/Sharing/GrantAccessDialog.tsx b/client/src/components/Sharing/GrantAccessDialog.tsx deleted file mode 100644 index beb69265e3..0000000000 --- a/client/src/components/Sharing/GrantAccessDialog.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { Share2Icon, Users, Loader, Shield, Link, CopyCheck } from 'lucide-react'; -import { Permissions, ResourceType, PermissionTypes, AccessRoleIds } from 'librechat-data-provider'; -import { - useGetResourcePermissionsQuery, - useUpdateResourcePermissionsMutation, -} from 'librechat-data-provider/react-query'; -import { - Button, - OGDialog, - OGDialogTitle, - OGDialogClose, - OGDialogContent, - OGDialogTrigger, - useToastContext, -} from '@librechat/client'; -import type { TPrincipal } from 'librechat-data-provider'; -import { useLocalize, useCopyToClipboard, useHasAccess } from '~/hooks'; -import ManagePermissionsDialog from './ManagePermissionsDialog'; -import PublicSharingToggle from './PublicSharingToggle'; -import AccessRolesPicker from './AccessRolesPicker'; -import { cn, removeFocusOutlines } from '~/utils'; -import { PeoplePicker } from './PeoplePicker'; - -export default function GrantAccessDialog({ - agentName, - onGrantAccess, - resourceType = ResourceType.AGENT, - agentDbId, - agentId, -}: { - agentDbId?: string | null; - agentId?: string | null; - agentName?: string; - onGrantAccess?: (shares: TPrincipal[], isPublic: boolean, publicRole: AccessRoleIds) => void; - resourceType?: ResourceType; -}) { - const localize = useLocalize(); - const { showToast } = useToastContext(); - - // Check if user has permission to access people picker - const canViewUsers = useHasAccess({ - permissionType: PermissionTypes.PEOPLE_PICKER, - permission: Permissions.VIEW_USERS, - }); - const canViewGroups = useHasAccess({ - permissionType: PermissionTypes.PEOPLE_PICKER, - permission: Permissions.VIEW_GROUPS, - }); - const hasPeoplePickerAccess = canViewUsers || canViewGroups; - - /** Type filter based on permissions */ - const peoplePickerTypeFilter = useMemo(() => { - if (canViewUsers && canViewGroups) { - return null; // Both types allowed - } else if (canViewUsers) { - return 'user' as const; - } else if (canViewGroups) { - return 'group' as const; - } - return null; - }, [canViewUsers, canViewGroups]); - - const { - data: permissionsData, - // isLoading: isLoadingPermissions, - // error: permissionsError, - } = useGetResourcePermissionsQuery(resourceType, agentDbId!, { - enabled: !!agentDbId, - }); - - const updatePermissionsMutation = useUpdateResourcePermissionsMutation(); - - const [newShares, setNewShares] = useState([]); - const [defaultPermissionId, setDefaultPermissionId] = useState( - AccessRoleIds.AGENT_VIEWER, - ); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isCopying, setIsCopying] = useState(false); - - const agentUrl = `${window.location.origin}/c/new?agent_id=${agentId}`; - const copyAgentUrl = useCopyToClipboard({ text: agentUrl }); - - const currentShares: TPrincipal[] = - permissionsData?.principals?.map((principal) => ({ - type: principal.type, - id: principal.id, - name: principal.name, - email: principal.email, - source: principal.source, - avatar: principal.avatar, - description: principal.description, - accessRoleId: principal.accessRoleId, - })) || []; - - const currentIsPublic = permissionsData?.public ?? false; - const currentPublicRole = permissionsData?.publicAccessRoleId || AccessRoleIds.AGENT_VIEWER; - - const [isPublic, setIsPublic] = useState(false); - const [publicRole, setPublicRole] = useState(AccessRoleIds.AGENT_VIEWER); - - useEffect(() => { - if (permissionsData && isModalOpen) { - setIsPublic(currentIsPublic ?? false); - setPublicRole(currentPublicRole); - } - }, [permissionsData, isModalOpen, currentIsPublic, currentPublicRole]); - - if (!agentDbId) { - return null; - } - - const handleGrantAccess = async () => { - try { - const sharesToAdd = newShares.map((share) => ({ - ...share, - accessRoleId: defaultPermissionId, - })); - - const allShares = [...currentShares, ...sharesToAdd]; - - await updatePermissionsMutation.mutateAsync({ - resourceType, - resourceId: agentDbId, - data: { - updated: sharesToAdd, - removed: [], - public: isPublic, - publicAccessRoleId: isPublic ? publicRole : undefined, - }, - }); - - if (onGrantAccess) { - onGrantAccess(allShares, isPublic, publicRole); - } - - showToast({ - message: `Access granted successfully to ${newShares.length} ${newShares.length === 1 ? 'person' : 'people'}${isPublic ? ' and made public' : ''}`, - status: 'success', - }); - - setNewShares([]); - setDefaultPermissionId(AccessRoleIds.AGENT_VIEWER); - setIsPublic(false); - setPublicRole(AccessRoleIds.AGENT_VIEWER); - setIsModalOpen(false); - } catch (error) { - console.error('Error granting access:', error); - showToast({ - message: 'Failed to grant access. Please try again.', - status: 'error', - }); - } - }; - - const handleCancel = () => { - setNewShares([]); - setDefaultPermissionId(AccessRoleIds.AGENT_VIEWER); - setIsPublic(false); - setPublicRole(AccessRoleIds.AGENT_VIEWER); - setIsModalOpen(false); - }; - - const totalCurrentShares = currentShares.length + (currentIsPublic ? 1 : 0); - const submitButtonActive = - newShares.length > 0 || isPublic !== currentIsPublic || publicRole !== currentPublicRole; - return ( - - - - - - - -
- - {localize('com_ui_share_var', { - 0: - agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent'), - })} -
-
- -
- {hasPeoplePickerAccess && ( - <> - - -
-
-
- - -
-
- -
- - )} - -
-
- {hasPeoplePickerAccess && ( - - )} - {agentId && ( - - )} -
-
- - - - -
-
-
-
-
- ); -} diff --git a/client/src/components/Sharing/ManagePermissionsDialog.tsx b/client/src/components/Sharing/ManagePermissionsDialog.tsx index 490022b7de..7d5f88750a 100644 --- a/client/src/components/Sharing/ManagePermissionsDialog.tsx +++ b/client/src/components/Sharing/ManagePermissionsDialog.tsx @@ -1,14 +1,14 @@ import React, { useState, useEffect } from 'react'; import { AccessRoleIds, ResourceType } from 'librechat-data-provider'; -import { Settings, Users, Loader, UserCheck, Trash2, Shield } from 'lucide-react'; +import { Settings, Users, UserCheck, Trash2, Shield } from 'lucide-react'; import { - useGetAccessRolesQuery, useGetResourcePermissionsQuery, useUpdateResourcePermissionsMutation, } from 'librechat-data-provider/react-query'; import type { TPrincipal } from 'librechat-data-provider'; import { Button, + Spinner, OGDialog, OGDialogTitle, OGDialogClose, @@ -46,10 +46,6 @@ export default function ManagePermissionsDialog({ } = useGetResourcePermissionsQuery(resourceType, agentDbId, { enabled: !!agentDbId, }); - const { - data: accessRoles, - // isLoading, - } = useGetAccessRolesQuery(resourceType); const updatePermissionsMutation = useUpdateResourcePermissionsMutation(); @@ -267,7 +263,7 @@ export default function ManagePermissionsDialog({ if (isLoadingPermissions) { return (
- + {localize('com_ui_loading_permissions')} @@ -285,7 +281,7 @@ export default function ManagePermissionsDialog({ handleRoleChange(id, newRole)} />
@@ -302,17 +298,12 @@ export default function ManagePermissionsDialog({ ); })()} -
-

- {localize('com_ui_public_access')} -

- -
+
@@ -332,7 +323,7 @@ export default function ManagePermissionsDialog({ > {updatePermissionsMutation.isLoading ? (
- + {localize('com_ui_saving')}
) : ( diff --git a/client/src/components/Sharing/PeoplePicker/SearchPicker.tsx b/client/src/components/Sharing/PeoplePicker/SearchPicker.tsx index e4266f4c83..6bbc5613b3 100644 --- a/client/src/components/Sharing/PeoplePicker/SearchPicker.tsx +++ b/client/src/components/Sharing/PeoplePicker/SearchPicker.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; +import { Search } from 'lucide-react'; import debounce from 'lodash/debounce'; -import { Search, X } from 'lucide-react'; import * as Ariakit from '@ariakit/react'; import { Spinner, Skeleton } from '@librechat/client'; import { useLocalize } from '~/hooks'; @@ -78,7 +78,7 @@ export function SearchPicker({
@@ -110,7 +110,7 @@ export function SearchPicker({ value={localQuery} // autoSelect placeholder={placeholder || localize('com_ui_select_options')} - className="m-0 mr-0 w-full rounded-md border-none bg-transparent p-0 py-2 pl-9 pr-3 text-sm leading-tight text-text-primary placeholder-text-secondary placeholder-opacity-100 focus:outline-none focus-visible:outline-none group-focus-within:placeholder-text-primary group-hover:placeholder-text-primary" + className="h-10 w-full rounded-lg bg-transparent pl-10 text-sm leading-tight text-text-primary placeholder-text-secondary placeholder-opacity-100 focus:outline-none focus-visible:outline-none group-focus-within:placeholder-text-primary group-hover:placeholder-text-primary" />
diff --git a/client/src/components/Sharing/PeoplePicker/SelectedPrincipalsList.tsx b/client/src/components/Sharing/PeoplePicker/SelectedPrincipalsList.tsx index 3ff26870ec..ae8fd11892 100644 --- a/client/src/components/Sharing/PeoplePicker/SelectedPrincipalsList.tsx +++ b/client/src/components/Sharing/PeoplePicker/SelectedPrincipalsList.tsx @@ -1,17 +1,17 @@ -import React, { useState, useId } from 'react'; -import * as Ariakit from '@ariakit/react/menu'; -import { Button, DropdownPopup } from '@librechat/client'; -import { Users, X, ExternalLink, ChevronDown } from 'lucide-react'; -import type { TPrincipal, TAccessRole, AccessRoleIds } from 'librechat-data-provider'; -import { getRoleLocalizationKeys } from '~/utils'; +import React from 'react'; +import { Button } from '@librechat/client'; +import { Users, X, ExternalLink } from 'lucide-react'; +import type { TPrincipal, AccessRoleIds } from 'librechat-data-provider'; +import { ResourceType } from 'librechat-data-provider'; import PrincipalAvatar from '../PrincipalAvatar'; +import AccessRolesPicker from '../AccessRolesPicker'; import { useLocalize } from '~/hooks'; interface SelectedPrincipalsListProps { principles: TPrincipal[]; onRemoveHandler: (idOnTheSource: string) => void; onRoleChange?: (idOnTheSource: string, newRoleId: AccessRoleIds) => void; - availableRoles?: Omit[]; + resourceType?: ResourceType; className?: string; } @@ -20,7 +20,7 @@ export default function SelectedPrincipalsList({ onRemoveHandler, className = '', onRoleChange, - availableRoles, + resourceType = ResourceType.AGENT, }: SelectedPrincipalsListProps) { const localize = useLocalize(); @@ -71,12 +71,13 @@ export default function SelectedPrincipalsList({
{!!share.accessRoleId && !!onRoleChange && ( - { onRoleChange?.(share.idOnTheSource!, newRole); }} - availableRoles={availableRoles ?? []} + className="min-w-0" /> )}