import React, { useState, useEffect } from 'react'; import { AccessRoleIds, ResourceType } from 'librechat-data-provider'; import { Share2Icon, Users, Link, CopyCheck, UserX, UserCheck } from 'lucide-react'; import { Label, Button, Spinner, Skeleton, OGDialog, OGDialogTitle, OGDialogClose, OGDialogContent, OGDialogTrigger, useToastContext, } from '@librechat/client'; import type { TPrincipal } from 'librechat-data-provider'; import { usePeoplePickerPermissions, useResourcePermissionState, useCopyToClipboard, useLocalize, } from '~/hooks'; import UnifiedPeopleSearch from './PeoplePicker/UnifiedPeopleSearch'; import PeoplePickerAdminSettings from './PeoplePickerAdminSettings'; import PublicSharingToggle from './PublicSharingToggle'; import { SelectedPrincipalsList } from './PeoplePicker'; import { cn } from '~/utils'; export default function GenericGrantAccessDialog({ resourceName, resourceDbId, resourceId, resourceType, onGrantAccess, disabled = false, children, }: { resourceDbId?: string | null; resourceId?: string | null; resourceName?: string; resourceType: ResourceType; onGrantAccess?: (shares: TPrincipal[], isPublic: boolean, publicRole?: AccessRoleIds) => void; disabled?: boolean; children?: React.ReactNode; }) { const localize = useLocalize(); const { showToast } = useToastContext(); const [isModalOpen, setIsModalOpen] = useState(false); const [isCopying, setIsCopying] = useState(false); // Use shared hooks const { hasPeoplePickerAccess, peoplePickerTypeFilter } = usePeoplePickerPermissions(); const { config, permissionsData, isLoadingPermissions, permissionsError, updatePermissionsMutation, currentShares, currentIsPublic, currentPublicRole, isPublic, setIsPublic, publicRole, setPublicRole, } = useResourcePermissionState(resourceType, resourceDbId, isModalOpen); // 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 }); if (!resourceDbId) { return null; } if (!config) { console.error(`Unsupported resource type: ${resourceType}`); return null; } // 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 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 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, removed, public: isPublic, publicAccessRoleId: isPublic ? publicRole : undefined, }, }); if (onGrantAccess) { onGrantAccess(allShares, isPublic, publicRole); } showToast({ message: localize('com_ui_permissions_updated_success'), status: 'success', }); setHasChanges(false); } catch (error) { console.error('Error updating permissions:', error); showToast({ message: localize('com_ui_permissions_failed_update'), status: 'error', }); } }; const handleCancel = () => { // Reset to original state const shares = permissionsData?.principals || []; setAllShares(shares.map((share) => ({ ...share, isExisting: true }))); setDefaultPermissionId(config?.defaultViewerRoleId); setIsPublic(currentIsPublic); setPublicRole(currentPublicRole || config?.defaultViewerRoleId || ''); setHasChanges(false); setIsModalOpen(false); }; // Validation and calculated values const totalCurrentShares = currentShares.length + (currentIsPublic ? 1 : 0); // 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 ) : ( ); return ( {TriggerComponent}
{/* Unified Search and Management Section */}
{/* Search Bar with Default Permission Setting */} {hasPeoplePickerAccess && (

s.idOnTheSource)} /> {/* Unified User/Group List */} {(() => { if (isLoadingPermissions) { return (
); } if (allShares.length === 0 && !hasChanges) { return (
); } return (
{!hasAtLeastOneOwner && hasChanges && (
)} handleRoleChange(id, newRole)} />
); })()}
)}
{/* Public Access Section */} {/* Footer Actions */}
{resourceId && resourceUrl && ( )}
); }