mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-22 19:30:15 +01:00
feat: Enhance PeoplePicker component with role-specific UI and localization updates
This commit is contained in:
parent
b680ba2c75
commit
65fdafc0e5
5 changed files with 118 additions and 53 deletions
|
|
@ -2,10 +2,10 @@ import React, { useState, useMemo } from 'react';
|
||||||
import { PrincipalType } from 'librechat-data-provider';
|
import { PrincipalType } from 'librechat-data-provider';
|
||||||
import type { TPrincipal, PrincipalSearchParams } from 'librechat-data-provider';
|
import type { TPrincipal, PrincipalSearchParams } from 'librechat-data-provider';
|
||||||
import { useSearchPrincipalsQuery } from 'librechat-data-provider/react-query';
|
import { useSearchPrincipalsQuery } from 'librechat-data-provider/react-query';
|
||||||
|
import { useLocalize, usePeoplePickerPermissions } from '~/hooks';
|
||||||
import PeoplePickerSearchItem from './PeoplePickerSearchItem';
|
import PeoplePickerSearchItem from './PeoplePickerSearchItem';
|
||||||
import SelectedPrincipalsList from './SelectedPrincipalsList';
|
import SelectedPrincipalsList from './SelectedPrincipalsList';
|
||||||
import { SearchPicker } from './SearchPicker';
|
import { SearchPicker } from './SearchPicker';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
|
|
||||||
interface PeoplePickerProps {
|
interface PeoplePickerProps {
|
||||||
onSelectionChange: (principals: TPrincipal[]) => void;
|
onSelectionChange: (principals: TPrincipal[]) => void;
|
||||||
|
|
@ -21,6 +21,7 @@ export default function PeoplePicker({
|
||||||
typeFilter = null,
|
typeFilter = null,
|
||||||
}: PeoplePickerProps) {
|
}: PeoplePickerProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const { canViewUsers, canViewGroups, canViewRoles } = usePeoplePickerPermissions();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedShares, setSelectedShares] = useState<TPrincipal[]>([]);
|
const [selectedShares, setSelectedShares] = useState<TPrincipal[]>([]);
|
||||||
|
|
||||||
|
|
@ -55,6 +56,28 @@ export default function PeoplePicker({
|
||||||
console.error('Principal search error:', error);
|
console.error('Principal search error:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get appropriate label based on permissions */
|
||||||
|
const getSearchLabel = () => {
|
||||||
|
const permissions = [canViewUsers, canViewGroups, canViewRoles];
|
||||||
|
const permissionCount = permissions.filter(Boolean).length;
|
||||||
|
|
||||||
|
if (permissionCount === 3) {
|
||||||
|
return localize('com_ui_search_users_groups_roles');
|
||||||
|
} else if (permissionCount === 2) {
|
||||||
|
if (canViewUsers && canViewGroups) {
|
||||||
|
return localize('com_ui_search_users_groups');
|
||||||
|
}
|
||||||
|
} else if (canViewUsers) {
|
||||||
|
return localize('com_ui_search_users');
|
||||||
|
} else if (canViewGroups) {
|
||||||
|
return localize('com_ui_search_groups');
|
||||||
|
} else if (canViewRoles) {
|
||||||
|
return localize('com_ui_search_roles');
|
||||||
|
}
|
||||||
|
|
||||||
|
return localize('com_ui_search_users_groups');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-3 ${className}`}>
|
<div className={`space-y-3 ${className}`}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -84,7 +107,7 @@ export default function PeoplePicker({
|
||||||
});
|
});
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
}}
|
}}
|
||||||
label={localize('com_ui_search_users_groups')}
|
label={getSearchLabel()}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
|
import { PrincipalType } from 'librechat-data-provider';
|
||||||
import type { TPrincipal } from 'librechat-data-provider';
|
import type { TPrincipal } from 'librechat-data-provider';
|
||||||
import { cn } from '~/utils';
|
import PrincipalAvatar from '~/components/Sharing/PrincipalAvatar';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import PrincipalAvatar from '../PrincipalAvatar';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
interface PeoplePickerSearchItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface PeoplePickerSearchItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
principal: TPrincipal;
|
principal: TPrincipal;
|
||||||
|
|
@ -16,10 +17,37 @@ const PeoplePickerSearchItem = forwardRef<HTMLDivElement, PeoplePickerSearchItem
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { name, email, type } = principal;
|
const { name, email, type } = principal;
|
||||||
|
|
||||||
// Display name with fallback
|
|
||||||
const displayName = name || localize('com_ui_unknown');
|
const displayName = name || localize('com_ui_unknown');
|
||||||
const subtitle = email || `${type} (${principal.source || 'local'})`;
|
const subtitle = email || `${type} (${principal.source || 'local'})`;
|
||||||
|
|
||||||
|
/** Get badge styling based on type */
|
||||||
|
const getBadgeConfig = () => {
|
||||||
|
switch (type) {
|
||||||
|
case PrincipalType.USER:
|
||||||
|
return {
|
||||||
|
className: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
||||||
|
label: localize('com_ui_user'),
|
||||||
|
};
|
||||||
|
case PrincipalType.GROUP:
|
||||||
|
return {
|
||||||
|
className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||||
|
label: localize('com_ui_group'),
|
||||||
|
};
|
||||||
|
case PrincipalType.ROLE:
|
||||||
|
return {
|
||||||
|
className: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300',
|
||||||
|
label: localize('com_ui_role'),
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
className: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300',
|
||||||
|
label: type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const badgeConfig = getBadgeConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -41,12 +69,10 @@ const PeoplePickerSearchItem = forwardRef<HTMLDivElement, PeoplePickerSearchItem
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium',
|
'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium',
|
||||||
type === 'user'
|
badgeConfig.className,
|
||||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'
|
|
||||||
: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{type === 'user' ? localize('com_ui_user') : localize('com_ui_group')}
|
{badgeConfig.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import * as Menu from '@ariakit/react/menu';
|
||||||
import { Button, DropdownPopup } from '@librechat/client';
|
import { Button, DropdownPopup } from '@librechat/client';
|
||||||
import { Users, X, ExternalLink, ChevronDown } from 'lucide-react';
|
import { Users, X, ExternalLink, ChevronDown } from 'lucide-react';
|
||||||
import type { TPrincipal, TAccessRole, AccessRoleIds } from 'librechat-data-provider';
|
import type { TPrincipal, TAccessRole, AccessRoleIds } from 'librechat-data-provider';
|
||||||
|
import PrincipalAvatar from '~/components/Sharing/PrincipalAvatar';
|
||||||
import { getRoleLocalizationKeys } from '~/utils';
|
import { getRoleLocalizationKeys } from '~/utils';
|
||||||
import PrincipalAvatar from '../PrincipalAvatar';
|
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
interface SelectedPrincipalsListProps {
|
interface SelectedPrincipalsListProps {
|
||||||
|
|
@ -36,7 +36,7 @@ export default function SelectedPrincipalsList({
|
||||||
<div className={`space-y-3 ${className}`}>
|
<div className={`space-y-3 ${className}`}>
|
||||||
<div className="rounded-lg border border-dashed border-border py-8 text-center text-muted-foreground">
|
<div className="rounded-lg border border-dashed border-border py-8 text-center text-muted-foreground">
|
||||||
<Users className="mx-auto mb-2 h-8 w-8 opacity-50" />
|
<Users className="mx-auto mb-2 h-8 w-8 opacity-50" />
|
||||||
<p className="mt-1 text-xs">{localize('com_ui_search_above_to_add')}</p>
|
<p className="mt-1 text-xs">{localize('com_ui_search_above_to_add_all')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Users, User } from 'lucide-react';
|
import { Users, User, Shield } from 'lucide-react';
|
||||||
|
import { PrincipalType } from 'librechat-data-provider';
|
||||||
import type { TPrincipal } from 'librechat-data-provider';
|
import type { TPrincipal } from 'librechat-data-provider';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
|
@ -17,7 +18,6 @@ export default function PrincipalAvatar({
|
||||||
const { avatar, type, name } = principal;
|
const { avatar, type, name } = principal;
|
||||||
const displayName = name || 'Unknown';
|
const displayName = name || 'Unknown';
|
||||||
|
|
||||||
// Size variants
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: 'h-6 w-6',
|
sm: 'h-6 w-6',
|
||||||
md: 'h-8 w-8',
|
md: 'h-8 w-8',
|
||||||
|
|
@ -33,7 +33,38 @@ export default function PrincipalAvatar({
|
||||||
const avatarSizeClass = sizeClasses[size];
|
const avatarSizeClass = sizeClasses[size];
|
||||||
const iconSizeClass = iconSizeClasses[size];
|
const iconSizeClass = iconSizeClasses[size];
|
||||||
|
|
||||||
// Avatar or icon logic
|
/** Get icon component and styling based on type */
|
||||||
|
const getIconConfig = () => {
|
||||||
|
switch (type) {
|
||||||
|
case PrincipalType.USER:
|
||||||
|
return {
|
||||||
|
Icon: User,
|
||||||
|
containerClass: 'bg-blue-100 dark:bg-blue-900',
|
||||||
|
iconClass: 'text-blue-600 dark:text-blue-400',
|
||||||
|
};
|
||||||
|
case PrincipalType.GROUP:
|
||||||
|
return {
|
||||||
|
Icon: Users,
|
||||||
|
containerClass: 'bg-green-100 dark:bg-green-900',
|
||||||
|
iconClass: 'text-green-600 dark:text-green-400',
|
||||||
|
};
|
||||||
|
case PrincipalType.ROLE:
|
||||||
|
return {
|
||||||
|
Icon: Shield,
|
||||||
|
containerClass: 'bg-purple-100 dark:bg-purple-900',
|
||||||
|
iconClass: 'text-purple-600 dark:text-purple-400',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
Icon: User,
|
||||||
|
containerClass: 'bg-gray-100 dark:bg-gray-900',
|
||||||
|
iconClass: 'text-gray-600 dark:text-gray-400',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { Icon, containerClass, iconClass } = getIconConfig();
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex-shrink-0', className)}>
|
<div className={cn('flex-shrink-0', className)}>
|
||||||
|
|
@ -50,52 +81,31 @@ export default function PrincipalAvatar({
|
||||||
/>
|
/>
|
||||||
{/* Hidden fallback icon that shows if image fails */}
|
{/* Hidden fallback icon that shows if image fails */}
|
||||||
<div className={cn('hidden', avatarSizeClass)}>
|
<div className={cn('hidden', avatarSizeClass)}>
|
||||||
{type === 'user' ? (
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
avatarSizeClass,
|
||||||
avatarSizeClass,
|
'flex items-center justify-center rounded-full',
|
||||||
'flex items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900',
|
containerClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<User className={cn(iconSizeClass, 'text-blue-600 dark:text-blue-400')} />
|
<Icon className={cn(iconSizeClass, iconClass)} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
avatarSizeClass,
|
|
||||||
'flex items-center justify-center rounded-full bg-green-100 dark:bg-green-900',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Users className={cn(iconSizeClass, 'text-green-600 dark:text-green-400')} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback icon based on type
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex-shrink-0', className)}>
|
<div className={cn('flex-shrink-0', className)}>
|
||||||
{type === 'user' ? (
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
avatarSizeClass,
|
||||||
avatarSizeClass,
|
'flex items-center justify-center rounded-full',
|
||||||
'flex items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900',
|
containerClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<User className={cn(iconSizeClass, 'text-blue-600 dark:text-blue-400')} />
|
<Icon className={cn(iconSizeClass, iconClass)} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
avatarSizeClass,
|
|
||||||
'flex items-center justify-center rounded-full bg-green-100 dark:bg-green-900',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Users className={cn(iconSizeClass, 'text-green-600 dark:text-green-400')} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -693,10 +693,16 @@
|
||||||
"com_ui_grant_access": "Grant Access",
|
"com_ui_grant_access": "Grant Access",
|
||||||
"com_ui_granting": "Granting...",
|
"com_ui_granting": "Granting...",
|
||||||
"com_ui_search_users_groups": "Search Users and Groups",
|
"com_ui_search_users_groups": "Search Users and Groups",
|
||||||
|
"com_ui_search_users_groups_roles": "Search Users, Groups, and Roles",
|
||||||
|
"com_ui_search_users": "Search Users",
|
||||||
|
"com_ui_search_groups": "Search Groups",
|
||||||
|
"com_ui_search_roles": "Search Roles",
|
||||||
"com_ui_search_default_placeholder": "Search by name or email (min 2 chars)",
|
"com_ui_search_default_placeholder": "Search by name or email (min 2 chars)",
|
||||||
"com_ui_user": "User",
|
"com_ui_user": "User",
|
||||||
"com_ui_group": "Group",
|
"com_ui_group": "Group",
|
||||||
|
"com_ui_role": "Role",
|
||||||
"com_ui_search_above_to_add": "Search above to add users or groups",
|
"com_ui_search_above_to_add": "Search above to add users or groups",
|
||||||
|
"com_ui_search_above_to_add_all": "Search above to add users, groups, or roles",
|
||||||
"com_ui_azure_ad": "Entra ID",
|
"com_ui_azure_ad": "Entra ID",
|
||||||
"com_ui_remove_user": "Remove {{0}}",
|
"com_ui_remove_user": "Remove {{0}}",
|
||||||
"com_ui_create": "Create",
|
"com_ui_create": "Create",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue