🔧 refactor: Organize Sharing/Agent Components and Improve Type Safety

refactor: organize Sharing/Agent components, improve type safety for resource types and access role ids, rename enums to PascalCase

refactor: organize Sharing/Agent components, improve type safety for resource types and access role ids

chore: move sharing related components to dedicated "Sharing" directory

chore: remove PublicSharingToggle component and update index exports

chore: move non-sidepanel agent components to `~/components/Agents`

chore: move AgentCategoryDisplay component with tests

chore: remove commented out code

refactor: change PERMISSION_BITS from const to enum for better type safety

refactor: reorganize imports in GenericGrantAccessDialog and update index exports for hooks

refactor: update type definitions to use ACCESS_ROLE_IDS for improved type safety

refactor: remove unused canAccessPromptResource middleware and related code

refactor: remove unused prompt access roles from createAccessRoleMethods

refactor: update resourceType in AclEntry type definition to remove unused 'prompt' value

refactor: introduce ResourceType enum and update resourceType usage across data provider files for improved type safety

refactor: update resourceType usage to ResourceType enum across sharing and permissions components for improved type safety

refactor: standardize resourceType usage to ResourceType enum across agent and prompt models, permissions controller, and middleware for enhanced type safety

refactor: update resourceType references from PROMPT_GROUP to PROMPTGROUP for consistency across models, middleware, and components

refactor: standardize access role IDs and resource type usage across agent, file, and prompt models for improved type safety and consistency

chore: add typedefs for TUpdateResourcePermissionsRequest and TUpdateResourcePermissionsResponse to enhance type definitions

chore: move SearchPicker to PeoplePicker dir

refactor: implement debouncing for query changes in SearchPicker for improved performance

chore: fix typing, import order for agent admin settings

fix: agent admin settings, prevent agent form submission

refactor: rename `ACCESS_ROLE_IDS` to `AccessRoleIds`

refactor: replace PermissionBits with PERMISSION_BITS

refactor: replace PERMISSION_BITS with PermissionBits
This commit is contained in:
Danny Avila 2025-07-28 17:52:36 -04:00
parent ae732b2ebc
commit 81b32e400a
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
96 changed files with 781 additions and 798 deletions

View file

@ -1,10 +1,7 @@
import React from 'react';
import type t from 'librechat-data-provider';
import useLocalize from '~/hooks/useLocalize';
import { renderAgentAvatar, getContactDisplayName } from '~/utils/agents';
import { cn } from '~/utils';
import { cn, renderAgentAvatar, getContactDisplayName } from '~/utils';
interface AgentCardProps {
agent: t.Agent; // The agent data to display

View file

@ -6,7 +6,7 @@ import {
QueryKeys,
Constants,
EModelEndpoint,
PERMISSION_BITS,
PermissionBits,
LocalStorageKeys,
AgentListResponse,
} from 'librechat-data-provider';
@ -45,7 +45,7 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
*/
const handleStartChat = () => {
if (agent) {
const keys = [QueryKeys.agents, { requiredPermission: PERMISSION_BITS.EDIT }];
const keys = [QueryKeys.agents, { requiredPermission: PermissionBits.EDIT }];
const listResp = queryClient.getQueryData<AgentListResponse>(keys);
if (listResp != null) {
if (!listResp.data.some((a) => a.id === agent.id)) {

View file

@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { Button, Spinner } from '@librechat/client';
import { PERMISSION_BITS } from 'librechat-data-provider';
import { PermissionBits } from 'librechat-data-provider';
import type t from 'librechat-data-provider';
import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents';
import { useAgentCategories, useLocalize } from '~/hooks';
@ -33,7 +33,7 @@ const AgentGrid: React.FC<AgentGridProps> = ({ category, searchQuery, onSelectAg
limit: number;
promoted?: 0 | 1;
} = {
requiredPermission: PERMISSION_BITS.VIEW, // View permission for marketplace viewing
requiredPermission: PermissionBits.VIEW, // View permission for marketplace viewing
limit: 6,
};

View file

@ -7,7 +7,7 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from '@librechat/client';
import { PERMISSION_BITS } from 'librechat-data-provider';
import { PermissionBits } from 'librechat-data-provider';
import type { TPromptGroup } from 'librechat-data-provider';
import { useLocalize, useSubmitMessage, useCustomLink, useResourcePermissions } from '~/hooks';
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
@ -35,7 +35,7 @@ function ChatGroupItem({
// Check permissions for the promptGroup
const { hasPermission } = useResourcePermissions('promptGroup', group._id || '');
const canEdit = hasPermission(PERMISSION_BITS.EDIT);
const canEdit = hasPermission(PermissionBits.EDIT);
const onCardClick: React.MouseEventHandler<HTMLButtonElement> = () => {
const text = group.productionPrompt?.prompt;

View file

@ -1,7 +1,7 @@
import { memo, useState, useRef, useMemo, useCallback, KeyboardEvent } from 'react';
import { EarthIcon, Pen } from 'lucide-react';
import { useNavigate, useParams } from 'react-router-dom';
import { PERMISSION_BITS, type TPromptGroup } from 'librechat-data-provider';
import { PermissionBits, type TPromptGroup } from 'librechat-data-provider';
import {
Input,
Label,
@ -30,8 +30,8 @@ function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps
const [nameInputValue, setNameInputValue] = useState(group.name);
const { hasPermission } = useResourcePermissions('promptGroup', group._id || '');
const canEdit = hasPermission(PERMISSION_BITS.EDIT);
const canDelete = hasPermission(PERMISSION_BITS.DELETE);
const canEdit = hasPermission(PermissionBits.EDIT);
const canDelete = hasPermission(PermissionBits.DELETE);
const isGlobalGroup = useMemo(
() => instanceProjectId && group.projectIds?.includes(instanceProjectId),

View file

@ -6,7 +6,7 @@ import { Menu, Rocket } from 'lucide-react';
import { useForm, FormProvider } from 'react-hook-form';
import { useParams, useOutletContext } from 'react-router-dom';
import { Button, Skeleton, useToastContext } from '@librechat/client';
import { Permissions, PermissionTypes, PERMISSION_BITS } from 'librechat-data-provider';
import { Permissions, PermissionTypes, PermissionBits } from 'librechat-data-provider';
import type { TCreatePrompt, TPrompt, TPromptGroup } from 'librechat-data-provider';
import {
useGetPrompts,
@ -186,8 +186,8 @@ const PromptForm = () => {
group?._id || '',
);
const canEdit = hasPermission(PERMISSION_BITS.EDIT);
const canView = hasPermission(PERMISSION_BITS.VIEW);
const canEdit = hasPermission(PermissionBits.EDIT);
const canView = hasPermission(PermissionBits.VIEW);
const methods = useForm({
defaultValues: {

View file

@ -3,8 +3,9 @@ import { Share2Icon } from 'lucide-react';
import {
SystemRoles,
Permissions,
ResourceType,
PermissionTypes,
PERMISSION_BITS,
PermissionBits,
} from 'librechat-data-provider';
import { Button } from '@librechat/client';
import type { TPromptGroup } from 'librechat-data-provider';
@ -25,7 +26,7 @@ const SharePrompt = React.memo(
// The query will be disabled if groupId is empty
const groupId = group?._id || '';
const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions(
'promptGroup',
ResourceType.PROMPTGROUP,
groupId,
);
@ -34,7 +35,7 @@ const SharePrompt = React.memo(
return null;
}
const canShareThisPrompt = hasPermission(PERMISSION_BITS.SHARE);
const canShareThisPrompt = hasPermission(PermissionBits.SHARE);
const shouldShowShareButton =
(group.author === user?.id || user?.role === SystemRoles.ADMIN || canShareThisPrompt) &&
@ -49,7 +50,7 @@ const SharePrompt = React.memo(
<GenericGrantAccessDialog
resourceDbId={groupId}
resourceName={group.name}
resourceType="promptGroup"
resourceType={ResourceType.PROMPTGROUP}
disabled={disabled}
>
<Button

View file

@ -2,7 +2,7 @@ import React from 'react';
import * as Ariakit from '@ariakit/react';
import { ChevronDown } from 'lucide-react';
import { DropdownPopup } from '@librechat/client';
import { ACCESS_ROLE_IDS } from 'librechat-data-provider';
import { AccessRoleIds, ResourceType } from 'librechat-data-provider';
import { useGetAccessRolesQuery } from 'librechat-data-provider/react-query';
import type { AccessRole } from 'librechat-data-provider';
import type * as t from '~/common';
@ -10,15 +10,15 @@ import { cn, getRoleLocalizationKeys } from '~/utils';
import { useLocalize } from '~/hooks';
interface AccessRolesPickerProps {
resourceType?: string;
selectedRoleId?: ACCESS_ROLE_IDS;
onRoleChange: (roleId: ACCESS_ROLE_IDS) => void;
resourceType?: ResourceType;
selectedRoleId?: AccessRoleIds;
onRoleChange: (roleId: AccessRoleIds) => void;
className?: string;
}
export default function AccessRolesPicker({
resourceType = 'agent',
selectedRoleId = ACCESS_ROLE_IDS.AGENT_VIEWER,
resourceType = ResourceType.AGENT,
selectedRoleId = AccessRoleIds.AGENT_VIEWER,
onRoleChange,
className = '',
}: AccessRolesPickerProps) {
@ -27,7 +27,7 @@ export default function AccessRolesPicker({
const { data: accessRoles, isLoading: rolesLoading } = useGetAccessRolesQuery(resourceType);
/** Helper function to get localized role name and description */
const getLocalizedRoleInfo = (roleId: ACCESS_ROLE_IDS) => {
const getLocalizedRoleInfo = (roleId: AccessRoleIds) => {
const keys = getRoleLocalizationKeys(roleId);
return {
name: localize(keys.name),

View file

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { AccessRoleIds, ResourceType } from 'librechat-data-provider';
import { Share2Icon, Users, Loader, Shield, Link, CopyCheck } from 'lucide-react';
import {
Button,
@ -10,13 +11,17 @@ import {
useToastContext,
} from '@librechat/client';
import type { TPrincipal } from 'librechat-data-provider';
import { useLocalize, useCopyToClipboard } from '~/hooks';
import { usePeoplePickerPermissions, useResourcePermissionState } from '~/hooks/Sharing';
import {
usePeoplePickerPermissions,
useResourcePermissionState,
useCopyToClipboard,
useLocalize,
} from '~/hooks';
import GenericManagePermissionsDialog from './GenericManagePermissionsDialog';
import PeoplePicker from '../SidePanel/Agents/Sharing/PeoplePicker/PeoplePicker';
import AccessRolesPicker from '../SidePanel/Agents/Sharing/AccessRolesPicker';
import PublicSharingToggle from './PublicSharingToggle';
import AccessRolesPicker from './AccessRolesPicker';
import { cn, removeFocusOutlines } from '~/utils';
import { PeoplePicker } from './PeoplePicker';
export default function GenericGrantAccessDialog({
resourceName,
@ -30,8 +35,8 @@ export default function GenericGrantAccessDialog({
resourceDbId?: string | null;
resourceId?: string | null;
resourceName?: string;
resourceType: string;
onGrantAccess?: (shares: TPrincipal[], isPublic: boolean, publicRole: string) => void;
resourceType: ResourceType;
onGrantAccess?: (shares: TPrincipal[], isPublic: boolean, publicRole: AccessRoleIds) => void;
disabled?: boolean;
children?: React.ReactNode;
}) {
@ -55,8 +60,8 @@ export default function GenericGrantAccessDialog({
} = useResourcePermissionState(resourceType, resourceDbId, isModalOpen);
const [newShares, setNewShares] = useState<TPrincipal[]>([]);
const [defaultPermissionId, setDefaultPermissionId] = useState<string>(
config?.defaultViewerRoleId ?? '',
const [defaultPermissionId, setDefaultPermissionId] = useState<AccessRoleIds | undefined>(
config?.defaultViewerRoleId,
);
const resourceUrl = config?.getResourceUrl ? config?.getResourceUrl(resourceId || '') : '';

View file

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { TPrincipal } from 'librechat-data-provider';
import { Settings, Users, Loader, UserCheck, Trash2, Shield } from 'lucide-react';
import { useGetAccessRolesQuery } from 'librechat-data-provider/react-query';
import { Settings, Users, Loader, UserCheck, Trash2, Shield } from 'lucide-react';
import {
Button,
OGDialog,
@ -11,9 +10,10 @@ import {
OGDialogTrigger,
useToastContext,
} from '@librechat/client';
import SelectedPrincipalsList from '../SidePanel/Agents/Sharing/PeoplePicker/SelectedPrincipalsList';
import type { TPrincipal, ResourceType, AccessRoleIds } from 'librechat-data-provider';
import { useResourcePermissionState } from '~/hooks/Sharing';
import PublicSharingToggle from './PublicSharingToggle';
import { SelectedPrincipalsList } from './PeoplePicker';
import { cn, removeFocusOutlines } from '~/utils';
import { useLocalize } from '~/hooks';
@ -26,8 +26,12 @@ export default function GenericManagePermissionsDialog({
}: {
resourceDbId: string;
resourceName?: string;
resourceType: string;
onUpdatePermissions?: (shares: TPrincipal[], isPublic: boolean, publicRole: string) => void;
resourceType: ResourceType;
onUpdatePermissions?: (
shares: TPrincipal[],
isPublic: boolean,
publicRole: AccessRoleIds,
) => void;
children?: React.ReactNode;
}) {
const localize = useLocalize();

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect, useMemo } from 'react';
import { ACCESS_ROLE_IDS, PermissionTypes, Permissions } from 'librechat-data-provider';
import { Share2Icon, Users, Loader, Shield, Link, CopyCheck } from 'lucide-react';
import { Permissions, ResourceType, PermissionTypes, AccessRoleIds } from 'librechat-data-provider';
import {
useGetResourcePermissionsQuery,
useUpdateResourcePermissionsMutation,
@ -18,22 +18,22 @@ import type { TPrincipal } from 'librechat-data-provider';
import { useLocalize, useCopyToClipboard, useHasAccess } from '~/hooks';
import ManagePermissionsDialog from './ManagePermissionsDialog';
import PublicSharingToggle from './PublicSharingToggle';
import PeoplePicker from './PeoplePicker/PeoplePicker';
import AccessRolesPicker from './AccessRolesPicker';
import { cn, removeFocusOutlines } from '~/utils';
import { PeoplePicker } from './PeoplePicker';
export default function GrantAccessDialog({
agentName,
onGrantAccess,
resourceType = 'agent',
resourceType = ResourceType.AGENT,
agentDbId,
agentId,
}: {
agentDbId?: string | null;
agentId?: string | null;
agentName?: string;
onGrantAccess?: (shares: TPrincipal[], isPublic: boolean, publicRole: string) => void;
resourceType?: string;
onGrantAccess?: (shares: TPrincipal[], isPublic: boolean, publicRole: AccessRoleIds) => void;
resourceType?: ResourceType;
}) {
const localize = useLocalize();
const { showToast } = useToastContext();
@ -73,7 +73,7 @@ export default function GrantAccessDialog({
const [newShares, setNewShares] = useState<TPrincipal[]>([]);
const [defaultPermissionId, setDefaultPermissionId] = useState<string>(
ACCESS_ROLE_IDS.AGENT_VIEWER,
AccessRoleIds.AGENT_VIEWER,
);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isCopying, setIsCopying] = useState(false);
@ -94,10 +94,10 @@ export default function GrantAccessDialog({
})) || [];
const currentIsPublic = permissionsData?.public ?? false;
const currentPublicRole = permissionsData?.publicAccessRoleId || ACCESS_ROLE_IDS.AGENT_VIEWER;
const currentPublicRole = permissionsData?.publicAccessRoleId || AccessRoleIds.AGENT_VIEWER;
const [isPublic, setIsPublic] = useState(false);
const [publicRole, setPublicRole] = useState<string>(ACCESS_ROLE_IDS.AGENT_VIEWER);
const [publicRole, setPublicRole] = useState<AccessRoleIds>(AccessRoleIds.AGENT_VIEWER);
useEffect(() => {
if (permissionsData && isModalOpen) {
@ -140,9 +140,9 @@ export default function GrantAccessDialog({
});
setNewShares([]);
setDefaultPermissionId(ACCESS_ROLE_IDS.AGENT_VIEWER);
setDefaultPermissionId(AccessRoleIds.AGENT_VIEWER);
setIsPublic(false);
setPublicRole(ACCESS_ROLE_IDS.AGENT_VIEWER);
setPublicRole(AccessRoleIds.AGENT_VIEWER);
setIsModalOpen(false);
} catch (error) {
console.error('Error granting access:', error);
@ -155,9 +155,9 @@ export default function GrantAccessDialog({
const handleCancel = () => {
setNewShares([]);
setDefaultPermissionId(ACCESS_ROLE_IDS.AGENT_VIEWER);
setDefaultPermissionId(AccessRoleIds.AGENT_VIEWER);
setIsPublic(false);
setPublicRole(ACCESS_ROLE_IDS.AGENT_VIEWER);
setPublicRole(AccessRoleIds.AGENT_VIEWER);
setIsModalOpen(false);
};

View file

@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react';
import { ACCESS_ROLE_IDS, TPrincipal } from 'librechat-data-provider';
import { AccessRoleIds, ResourceType } from 'librechat-data-provider';
import { Settings, Users, Loader, 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,
OGDialog,
@ -15,21 +16,25 @@ import {
OGDialogTrigger,
useToastContext,
} from '@librechat/client';
import SelectedPrincipalsList from './PeoplePicker/SelectedPrincipalsList';
import { SelectedPrincipalsList } from './PeoplePicker';
import PublicSharingToggle from './PublicSharingToggle';
import { cn, removeFocusOutlines } from '~/utils';
import { useLocalize } from '~/hooks';
export default function ManagePermissionsDialog({
agentDbId,
agentName,
resourceType = 'agent',
resourceType = ResourceType.AGENT,
agentDbId,
onUpdatePermissions,
}: {
agentDbId: string;
agentName?: string;
resourceType?: string;
onUpdatePermissions?: (shares: TPrincipal[], isPublic: boolean, publicRole: string) => void;
resourceType?: ResourceType;
onUpdatePermissions?: (
shares: TPrincipal[],
isPublic: boolean,
publicRole: AccessRoleIds,
) => void;
}) {
const localize = useLocalize();
const { showToast } = useToastContext();
@ -50,20 +55,22 @@ export default function ManagePermissionsDialog({
const [managedShares, setManagedShares] = useState<TPrincipal[]>([]);
const [managedIsPublic, setManagedIsPublic] = useState(false);
const [managedPublicRole, setManagedPublicRole] = useState<string>(ACCESS_ROLE_IDS.AGENT_VIEWER);
const [managedPublicRole, setManagedPublicRole] = useState<AccessRoleIds>(
AccessRoleIds.AGENT_VIEWER,
);
const [isModalOpen, setIsModalOpen] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const currentShares: TPrincipal[] = permissionsData?.principals || [];
const isPublic = permissionsData?.public || false;
const publicRole = permissionsData?.publicAccessRoleId || ACCESS_ROLE_IDS.AGENT_VIEWER;
const publicRole = permissionsData?.publicAccessRoleId || AccessRoleIds.AGENT_VIEWER;
useEffect(() => {
if (permissionsData) {
const shares = permissionsData.principals || [];
const isPublicValue = permissionsData.public || false;
const publicRoleValue = permissionsData.publicAccessRoleId || ACCESS_ROLE_IDS.AGENT_VIEWER;
const publicRoleValue = permissionsData.publicAccessRoleId || AccessRoleIds.AGENT_VIEWER;
setManagedShares(shares);
setManagedIsPublic(isPublicValue);
@ -85,7 +92,7 @@ export default function ManagePermissionsDialog({
setHasChanges(true);
};
const handleRoleChange = (idOnTheSource: string, newRole: string) => {
const handleRoleChange = (idOnTheSource: string, newRole: AccessRoleIds) => {
setManagedShares(
managedShares.map((s) =>
s.idOnTheSource === idOnTheSource ? { ...s, accessRoleId: newRole } : s,
@ -160,10 +167,10 @@ export default function ManagePermissionsDialog({
setManagedIsPublic(isPublic);
setHasChanges(true);
if (!isPublic) {
setManagedPublicRole(ACCESS_ROLE_IDS.AGENT_VIEWER);
setManagedPublicRole(AccessRoleIds.AGENT_VIEWER);
}
};
const handlePublicRoleChange = (role: string) => {
const handlePublicRoleChange = (role: AccessRoleIds) => {
setManagedPublicRole(role);
setHasChanges(true);
};
@ -172,8 +179,8 @@ export default function ManagePermissionsDialog({
/** Check if there's at least one owner (user, group, or public with owner role) */
const hasAtLeastOneOwner =
managedShares.some((share) => share.accessRoleId === ACCESS_ROLE_IDS.AGENT_OWNER) ||
(managedIsPublic && managedPublicRole === ACCESS_ROLE_IDS.AGENT_OWNER);
managedShares.some((share) => share.accessRoleId === AccessRoleIds.AGENT_OWNER) ||
(managedIsPublic && managedPublicRole === AccessRoleIds.AGENT_OWNER);
let peopleLabel = localize('com_ui_people');
if (managedShares.length === 1) {

View file

@ -3,7 +3,7 @@ import type { TPrincipal, PrincipalSearchParams } from 'librechat-data-provider'
import { useSearchPrincipalsQuery } from 'librechat-data-provider/react-query';
import PeoplePickerSearchItem from './PeoplePickerSearchItem';
import SelectedPrincipalsList from './SelectedPrincipalsList';
import { SearchPicker } from '~/components/ui/SearchPicker';
import { SearchPicker } from './SearchPicker';
import { useLocalize } from '~/hooks';
interface PeoplePickerProps {

View file

@ -1,4 +1,5 @@
import * as React from 'react';
import debounce from 'lodash/debounce';
import { Search, X } from 'lucide-react';
import * as Ariakit from '@ariakit/react';
import { Spinner, Skeleton } from '@librechat/client';
@ -36,10 +37,31 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
const localize = useLocalize();
const [_open, setOpen] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
const [localQuery, setLocalQuery] = React.useState(query);
const combobox = Ariakit.useComboboxStore({
resetValueOnHide,
});
React.useEffect(() => {
setLocalQuery(query);
}, [query]);
const debouncedOnQueryChange = React.useMemo(
() =>
debounce((value: string) => {
onQueryChange(value);
}, 500),
[onQueryChange],
);
React.useEffect(() => {
return () => {
debouncedOnQueryChange.cancel();
};
}, [debouncedOnQueryChange]);
const onPickHandler = (option: TOption) => {
setLocalQuery('');
onQueryChange('');
onPick(option);
setOpen(false);
@ -47,9 +69,11 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
inputRef.current.focus();
}
};
const showClearIcon = query.trim().length > 0;
const showClearIcon = localQuery.trim().length > 0;
const clearText = () => {
setLocalQuery('');
onQueryChange('');
debouncedOnQueryChange.cancel();
if (inputRef.current) {
inputRef.current.focus();
}
@ -77,7 +101,9 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
if (e.key === 'Escape' && combobox.getState().open) {
e.preventDefault();
e.stopPropagation();
setLocalQuery('');
onQueryChange('');
debouncedOnQueryChange.cancel();
setOpen(false);
}
}}
@ -85,9 +111,11 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
setValueOnClick={false}
setValueOnChange={false}
onChange={(e) => {
onQueryChange(e.target.value);
const value = e.target.value;
setLocalQuery(value);
debouncedOnQueryChange(value);
}}
value={query}
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"
@ -115,7 +143,7 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
open={
isLoading ||
options.length > 0 ||
(query.trim().length >= minQueryLengthForNoResults && !isLoading)
(localQuery.trim().length >= minQueryLengthForNoResults && !isLoading)
}
store={combobox}
unmountOnHide
@ -162,7 +190,7 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
));
}
if (query.trim().length >= minQueryLengthForNoResults) {
if (localQuery.trim().length >= minQueryLengthForNoResults) {
return (
<div
className={cn(

View file

@ -2,7 +2,7 @@ import React, { useState, useId } from 'react';
import * as Menu from '@ariakit/react/menu';
import { Button, DropdownPopup } from '@librechat/client';
import { Users, X, ExternalLink, ChevronDown } from 'lucide-react';
import type { TPrincipal, TAccessRole, ACCESS_ROLE_IDS } from 'librechat-data-provider';
import type { TPrincipal, TAccessRole, AccessRoleIds } from 'librechat-data-provider';
import { getRoleLocalizationKeys } from '~/utils';
import PrincipalAvatar from '../PrincipalAvatar';
import { useLocalize } from '~/hooks';
@ -10,7 +10,7 @@ import { useLocalize } from '~/hooks';
interface SelectedPrincipalsListProps {
principles: TPrincipal[];
onRemoveHandler: (idOnTheSource: string) => void;
onRoleChange?: (idOnTheSource: string, newRoleId: string) => void;
onRoleChange?: (idOnTheSource: string, newRoleId: AccessRoleIds) => void;
availableRoles?: Omit<TAccessRole, 'resourceType'>[];
className?: string;
}
@ -98,8 +98,8 @@ export default function SelectedPrincipalsList({
}
interface RoleSelectorProps {
currentRole: ACCESS_ROLE_IDS;
onRoleChange: (newRole: ACCESS_ROLE_IDS) => void;
currentRole: AccessRoleIds;
onRoleChange: (newRole: AccessRoleIds) => void;
availableRoles: Omit<TAccessRole, 'resourceType'>[];
}
@ -108,7 +108,7 @@ function RoleSelector({ currentRole, onRoleChange, availableRoles }: RoleSelecto
const [isMenuOpen, setIsMenuOpen] = useState(false);
const localize = useLocalize();
const getLocalizedRoleName = (roleId: ACCESS_ROLE_IDS) => {
const getLocalizedRoleName = (roleId: AccessRoleIds) => {
const keys = getRoleLocalizationKeys(roleId);
return localize(keys.name);
};

View file

@ -0,0 +1,3 @@
export { default as PeoplePicker } from './PeoplePicker';
export { default as PeoplePickerSearchItem } from './PeoplePickerSearchItem';
export { default as SelectedPrincipalsList } from './SelectedPrincipalsList';

View file

@ -1,7 +1,9 @@
import React from 'react';
import { Globe, Shield } from 'lucide-react';
import { Switch } from '@librechat/client';
import AccessRolesPicker from '../SidePanel/Agents/Sharing/AccessRolesPicker';
import { Globe, Shield } from 'lucide-react';
import type { AccessRoleIds } from 'librechat-data-provider';
import { ResourceType } from 'librechat-data-provider';
import AccessRolesPicker from './AccessRolesPicker';
import { useLocalize } from '~/hooks';
export default function PublicSharingToggle({
@ -9,13 +11,13 @@ export default function PublicSharingToggle({
publicRole,
onPublicToggle,
onPublicRoleChange,
resourceType = 'agent',
resourceType = ResourceType.AGENT,
}: {
isPublic: boolean;
publicRole: string;
publicRole: AccessRoleIds;
onPublicToggle: (isPublic: boolean) => void;
onPublicRoleChange: (role: string) => void;
resourceType?: string;
onPublicRoleChange: (role: AccessRoleIds) => void;
resourceType?: ResourceType;
}) {
const localize = useLocalize();

View file

@ -1,3 +1,6 @@
export { default as AccessRolesPicker } from './AccessRolesPicker';
export { default as GenericGrantAccessDialog } from './GenericGrantAccessDialog';
export { default as GenericManagePermissionsDialog } from './GenericManagePermissionsDialog';
export { default as ManagePermissionsDialog } from './ManagePermissionsDialog';
export { default as PrincipalAvatar } from './PrincipalAvatar';
export { default as PublicSharingToggle } from './PublicSharingToggle';

View file

@ -1,16 +1,16 @@
import * as Ariakit from '@ariakit/react';
import { useMemo, useEffect, useState } from 'react';
import * as Ariakit from '@ariakit/react';
import { ShieldEllipsis } from 'lucide-react';
import { useForm, Controller } from 'react-hook-form';
import { Permissions, SystemRoles, roleDefaults, PermissionTypes } from 'librechat-data-provider';
import {
Button,
Switch,
OGDialog,
DropdownPopup,
OGDialogTitle,
OGDialogContent,
OGDialogTrigger,
Button,
Switch,
DropdownPopup,
useToastContext,
} from '@librechat/client';
import type { Control, UseFormSetValue, UseFormGetValues } from 'react-hook-form';
@ -64,8 +64,8 @@ const LabelController: React.FC<LabelControllerProps> = ({
const AdminSettings = () => {
const localize = useLocalize();
const { user, roles } = useAuthContext();
const { showToast } = useToastContext();
const { user, roles } = useAuthContext();
const { mutate, isLoading } = useUpdateAgentPermissionsMutation({
onSuccess: () => {
showToast({ status: 'success', message: localize('com_ui_saved') });
@ -79,8 +79,9 @@ const AdminSettings = () => {
const [selectedRole, setSelectedRole] = useState<SystemRoles>(SystemRoles.USER);
const defaultValues = useMemo(() => {
if (roles?.[selectedRole]?.permissions) {
return roles[selectedRole].permissions[PermissionTypes.AGENTS];
const rolePerms = roles?.[selectedRole]?.permissions;
if (rolePerms) {
return rolePerms[PermissionTypes.AGENTS];
}
return roleDefaults[selectedRole].permissions[PermissionTypes.AGENTS];
}, [roles, selectedRole]);
@ -98,8 +99,9 @@ const AdminSettings = () => {
});
useEffect(() => {
if (roles?.[selectedRole]?.permissions?.[PermissionTypes.AGENTS]) {
reset(roles[selectedRole].permissions[PermissionTypes.AGENTS]);
const value = roles?.[selectedRole]?.permissions?.[PermissionTypes.AGENTS];
if (value) {
reset(value);
} else {
reset(roleDefaults[selectedRole].permissions[PermissionTypes.AGENTS]);
}
@ -211,7 +213,8 @@ const AdminSettings = () => {
</div>
<div className="flex justify-end">
<button
type="submit"
type="button"
onClick={handleSubmit(onSubmit)}
disabled={isSubmitting || isLoading}
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
>

View file

@ -3,8 +3,9 @@ import { useWatch, useFormContext } from 'react-hook-form';
import {
SystemRoles,
Permissions,
ResourceType,
PermissionTypes,
PERMISSION_BITS,
PermissionBits,
} from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps } from '~/common';
import { useLocalize, useAuthContext, useHasAccess, useResourcePermissions } from '~/hooks';
@ -46,8 +47,8 @@ export default function AgentFooter({
agent?._id || '',
);
const canShareThisAgent = hasPermission(PERMISSION_BITS.SHARE);
const canDeleteThisAgent = hasPermission(PERMISSION_BITS.DELETE);
const canShareThisAgent = hasPermission(PermissionBits.SHARE);
const canDeleteThisAgent = hasPermission(PermissionBits.DELETE);
const renderSaveButton = () => {
if (createMutation.isLoading || updateMutation.isLoading) {
return <Spinner className="icon-md" aria-hidden="true" />;
@ -84,7 +85,7 @@ export default function AgentFooter({
resourceDbId={agent?._id}
resourceId={agent_id}
resourceName={agent?.name ?? ''}
resourceType="agent"
resourceType={ResourceType.AGENT}
/>
)}
{agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />}

View file

@ -8,7 +8,7 @@ import {
Constants,
SystemRoles,
EModelEndpoint,
PERMISSION_BITS,
PermissionBits,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import type { AgentForm, StringOption } from '~/common';
@ -57,7 +57,7 @@ export default function AgentPanel() {
basicAgentQuery.data?._id || '',
);
const canEdit = hasPermission(PERMISSION_BITS.EDIT);
const canEdit = hasPermission(PermissionBits.EDIT);
const expandedAgentQuery = useGetExpandedAgentByIdQuery(current_agent_id ?? '', {
enabled:

View file

@ -7,7 +7,6 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { TSpecialVarLabel } from 'librechat-data-provider';
import type { AgentForm } from '~/common';
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
// import { ControlCombobox } from '@librechat/client';
import { useLocalize } from '~/hooks';
const inputClass = cn(
@ -49,26 +48,6 @@ export default function Instructions() {
{localize('com_ui_instructions')}
</label>
<div className="ml-auto" title="Add variables to instructions">
{/* ControlCombobox implementation
<ControlCombobox
selectedValue=""
displayValue="Add variables"
items={variableOptions.map((option) => ({
label: option.label,
value: option.value,
}))}
setValue={handleAddVariable}
ariaLabel="Add variable to instructions"
searchPlaceholder="Search variables"
selectPlaceholder="Add"
isCollapsed={false}
SelectIcon={<PlusCircle className="h-3 w-3 text-text-secondary" />}
containerClassName="w-fit"
className="h-7 gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
iconSide="left"
showCarat={false}
/>
*/}
<DropdownPopup
portal={true}
mountByState={true}

View file

@ -1,59 +0,0 @@
import React from 'react';
import { Globe } from 'lucide-react';
import { Switch } from '@librechat/client';
import AccessRolesPicker from './AccessRolesPicker';
import { useLocalize } from '~/hooks';
interface PublicSharingToggleProps {
isPublic: boolean;
publicRole: string;
onPublicToggle: (isPublic: boolean) => void;
onPublicRoleChange: (role: string) => void;
className?: string;
resourceType?: string;
}
export default function PublicSharingToggle({
isPublic,
publicRole,
onPublicToggle,
onPublicRoleChange,
className = '',
resourceType = 'agent',
}: PublicSharingToggleProps) {
const localize = useLocalize();
return (
<div className={`space-y-3 border-t pt-4 ${className}`}>
<div className="flex items-center justify-between">
<div>
<h3 className="flex items-center gap-2 text-sm font-medium">
<Globe className="h-4 w-4" />
{localize('com_ui_share_with_everyone')}
</h3>
<p className="mt-1 text-xs text-muted-foreground">
{localize('com_ui_make_agent_available_all_users')}
</p>
</div>
<Switch
checked={isPublic}
onCheckedChange={onPublicToggle}
aria-label={localize('com_ui_share_with_everyone')}
/>
</div>
{isPublic && (
<div>
<label className="mb-2 block text-sm font-medium">
{localize('com_ui_public_access_level')}
</label>
<AccessRolesPicker
resourceType={resourceType}
selectedRoleId={publicRole}
onRoleChange={onPublicRoleChange}
/>
</div>
)}
</div>
);
}

View file

@ -3,7 +3,7 @@ import { SystemRoles } from 'librechat-data-provider';
import { render, screen } from '@testing-library/react';
import type { UseMutationResult } from '@tanstack/react-query';
import '@testing-library/jest-dom/extend-expect';
import type { Agent, AgentCreateParams, TUser } from 'librechat-data-provider';
import type { Agent, AgentCreateParams, TUser, ResourceType } from 'librechat-data-provider';
import AgentFooter from '../AgentFooter';
import { Panel } from '~/common';
@ -171,7 +171,7 @@ jest.mock('~/components/Sharing', () => ({
resourceDbId: string;
resourceId: string;
resourceName: string;
resourceType: string;
resourceType: ResourceType;
}) => (
<div
data-testid="grant-access-dialog"

View file

@ -1,3 +1,2 @@
export * from './ui';
export * from './Plugins';
export * from './svg';

View file

@ -1,5 +1,5 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { dataService, MutationKeys, PERMISSION_BITS, QueryKeys } from 'librechat-data-provider';
import { dataService, MutationKeys, PermissionBits, QueryKeys } from 'librechat-data-provider';
import type * as t from 'librechat-data-provider';
import type { QueryClient, UseMutationResult } from '@tanstack/react-query';
@ -7,8 +7,8 @@ import type { QueryClient, UseMutationResult } from '@tanstack/react-query';
* AGENTS
*/
export const allAgentViewAndEditQueryKeys: t.AgentListParams[] = [
{ requiredPermission: PERMISSION_BITS.VIEW },
{ requiredPermission: PERMISSION_BITS.EDIT },
{ requiredPermission: PermissionBits.VIEW },
{ requiredPermission: PermissionBits.EDIT },
];
/**
* Create a new agent

View file

@ -3,7 +3,7 @@ import {
dataService,
Permissions,
EModelEndpoint,
PERMISSION_BITS,
PermissionBits,
PermissionTypes,
} from 'librechat-data-provider';
import { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
@ -26,7 +26,7 @@ export const useAgentListingDefaultPermissionLevel = () => {
// When marketplace is active: EDIT permissions (builder mode)
// When marketplace is not active: VIEW permissions (browse mode)
return hasMarketplaceAccess ? PERMISSION_BITS.EDIT : PERMISSION_BITS.VIEW;
return hasMarketplaceAccess ? PermissionBits.EDIT : PermissionBits.VIEW;
};
/**
@ -34,7 +34,7 @@ export const useAgentListingDefaultPermissionLevel = () => {
*/
export const defaultAgentParams: t.AgentListParams = {
limit: 10,
requiredPermission: PERMISSION_BITS.EDIT,
requiredPermission: PermissionBits.EDIT,
};
/**
* Hook for getting all available tools for A

View file

@ -8,7 +8,7 @@ import {
isAgentsEndpoint,
getConfigDefaults,
isAssistantsEndpoint,
PERMISSION_BITS,
PermissionBits,
} from 'librechat-data-provider';
import type { TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
import type { MentionOption } from '~/common';
@ -81,7 +81,7 @@ export default function useMentions({
[startupConfig?.interface],
);
const { data: agentsList = null } = useListAgentsQuery(
{ requiredPermission: PERMISSION_BITS.VIEW },
{ requiredPermission: PermissionBits.VIEW },
{
enabled: hasAgentAccess && interfaceConfig.modelSelect === true,
select: (res) => {

View file

@ -8,7 +8,7 @@ import {
isAgentsEndpoint,
tQueryParamsSchema,
isAssistantsEndpoint,
PERMISSION_BITS,
PermissionBits,
} from 'librechat-data-provider';
import type {
TPreset,
@ -80,7 +80,7 @@ const processValidSettings = (queryParams: Record<string, string>) => {
};
const injectAgentIntoAgentsMap = (queryClient: QueryClient, agent: any) => {
const editCacheKey = [QueryKeys.agents, { requiredPermission: PERMISSION_BITS.EDIT }];
const editCacheKey = [QueryKeys.agents, { requiredPermission: PermissionBits.EDIT }];
const editCache = queryClient.getQueryData<AgentListResponse>(editCacheKey);
if (editCache?.data && !editCache.data.some((cachedAgent) => cachedAgent.id === agent.id)) {

View file

@ -3,18 +3,18 @@ import {
useGetResourcePermissionsQuery,
useUpdateResourcePermissionsMutation,
} from 'librechat-data-provider/react-query';
import type { TPrincipal } from 'librechat-data-provider';
import type { TPrincipal, ResourceType, AccessRoleIds } from 'librechat-data-provider';
import { getResourceConfig } from '~/utils';
/**
* Hook to manage resource permission state including current shares, public access, and mutations
* @param resourceType - Type of resource (e.g., 'agent', 'promptGroup')
* @param resourceType - Type of resource (e.g., ResourceType.AGENT, ResourceType.PROMPTGROUP)
* @param resourceDbId - Database ID of the resource
* @param isModalOpen - Whether the modal is open (for effect dependencies)
* @returns Object with permission state and update mutation
*/
export const useResourcePermissionState = (
resourceType: string,
resourceType: ResourceType,
resourceDbId: string | null | undefined,
isModalOpen: boolean = false,
) => {
@ -52,13 +52,15 @@ export const useResourcePermissionState = (
// State for managing public access
const [isPublic, setIsPublic] = useState(false);
const [publicRole, setPublicRole] = useState<string>(config?.defaultViewerRoleId ?? '');
const [publicRole, setPublicRole] = useState<AccessRoleIds | undefined>(
config?.defaultViewerRoleId,
);
// Sync state with permissions data when modal opens
useEffect(() => {
if (permissionsData && isModalOpen) {
setIsPublic(currentIsPublic ?? false);
setPublicRole(currentPublicRole ?? '');
setPublicRole(currentPublicRole);
}
}, [permissionsData, isModalOpen, currentIsPublic, currentPublicRole]);

View file

@ -13,6 +13,7 @@ export * from './Messages';
export * from './Plugins';
export * from './Prompts';
export * from './Roles';
export * from './Sharing';
export * from './SSE';
export * from './AuthContext';
export * from './ScreenshotContext';

View file

@ -1,16 +1,17 @@
import {
useGetEffectivePermissionsQuery,
hasPermissions,
useGetEffectivePermissionsQuery,
} from 'librechat-data-provider/react-query';
import type { ResourceType } from 'librechat-data-provider';
/**
* fetches resource permissions once and returns a function to check any permission
* More efficient when checking multiple permissions for the same resource
* @param resourceType - Type of resource (e.g., 'agent')
* @param resourceType - Type of resource (e.g., ResourceType.AGENT)
* @param resourceId - ID of the resource
* @returns Object with hasPermission function and loading state
*/
export const useResourcePermissions = (resourceType: string, resourceId: string) => {
export const useResourcePermissions = (resourceType: ResourceType, resourceId: string) => {
const { data, isLoading } = useGetEffectivePermissionsQuery(resourceType, resourceId);
const hasPermission = (requiredPermission: number): boolean => {

View file

@ -8,6 +8,7 @@ import {
TwoFactorScreen,
RequestPasswordReset,
} from '~/components/Auth';
import AgentMarketplace from '~/components/Agents/Marketplace';
import { OAuthSuccess, OAuthError } from '~/components/OAuth';
import { AuthContextProvider } from '~/hooks/AuthContext';
import RouteErrorBoundary from './RouteErrorBoundary';
@ -18,7 +19,6 @@ import ShareRoute from './ShareRoute';
import ChatRoute from './ChatRoute';
import Search from './Search';
import Root from './Root';
import AgentMarketplace from '~/components/SidePanel/Agents/AgentMarketplace';
const AuthLayout = () => (
<AuthContextProvider>

View file

@ -1,10 +1,10 @@
import { ACCESS_ROLE_IDS } from 'librechat-data-provider';
import { AccessRoleIds, ResourceType } from 'librechat-data-provider';
export interface ResourceConfig {
resourceType: string;
defaultViewerRoleId: string;
defaultEditorRoleId: string;
defaultOwnerRoleId: string;
resourceType: ResourceType;
defaultViewerRoleId: AccessRoleIds;
defaultEditorRoleId: AccessRoleIds;
defaultOwnerRoleId: AccessRoleIds;
getResourceUrl?: (resourceId: string) => string;
getResourceName: (resourceName?: string) => string;
getShareMessage: (resourceName?: string) => string;
@ -12,12 +12,12 @@ export interface ResourceConfig {
getCopyUrlMessage: () => string;
}
export const RESOURCE_CONFIGS: Record<string, ResourceConfig> = {
agent: {
resourceType: 'agent',
defaultViewerRoleId: ACCESS_ROLE_IDS.AGENT_VIEWER,
defaultEditorRoleId: ACCESS_ROLE_IDS.AGENT_EDITOR,
defaultOwnerRoleId: ACCESS_ROLE_IDS.AGENT_OWNER,
export const RESOURCE_CONFIGS: Record<ResourceType, ResourceConfig> = {
[ResourceType.AGENT]: {
resourceType: ResourceType.AGENT,
defaultViewerRoleId: AccessRoleIds.AGENT_VIEWER,
defaultEditorRoleId: AccessRoleIds.AGENT_EDITOR,
defaultOwnerRoleId: AccessRoleIds.AGENT_OWNER,
getResourceUrl: (agentId: string) => `${window.location.origin}/c/new?agent_id=${agentId}`,
getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'),
getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'),
@ -25,11 +25,11 @@ export const RESOURCE_CONFIGS: Record<string, ResourceConfig> = {
`Manage permissions for ${name && name !== '' ? `"${name}"` : 'agent'}`,
getCopyUrlMessage: () => 'Agent URL copied',
},
promptGroup: {
resourceType: 'promptGroup',
defaultViewerRoleId: ACCESS_ROLE_IDS.PROMPTGROUP_VIEWER,
defaultEditorRoleId: ACCESS_ROLE_IDS.PROMPTGROUP_EDITOR,
defaultOwnerRoleId: ACCESS_ROLE_IDS.PROMPTGROUP_OWNER,
[ResourceType.PROMPTGROUP]: {
resourceType: ResourceType.PROMPTGROUP,
defaultViewerRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
defaultEditorRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
defaultOwnerRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'),
getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'),
getManageMessage: (name?: string) =>
@ -38,6 +38,6 @@ export const RESOURCE_CONFIGS: Record<string, ResourceConfig> = {
},
};
export const getResourceConfig = (resourceType: string): ResourceConfig | undefined => {
export const getResourceConfig = (resourceType: ResourceType): ResourceConfig | undefined => {
return RESOURCE_CONFIGS[resourceType];
};

View file

@ -1,4 +1,4 @@
import type { ACCESS_ROLE_IDS } from 'librechat-data-provider';
import type { AccessRoleIds } from 'librechat-data-provider';
import type { TranslationKeys } from '~/hooks/useLocalize';
/**
@ -43,7 +43,7 @@ export const ROLE_LOCALIZATIONS = {
* @returns Object with name and description localization keys, or unknown keys if not found
*/
export const getRoleLocalizationKeys = (
roleId: ACCESS_ROLE_IDS,
roleId: AccessRoleIds,
): {
name: TranslationKeys;
description: TranslationKeys;