mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-10 04:28:50 +01:00
🔖 feat: Enhance Bookmarks UX, add RBAC, toggle via librechat.yaml (#3747)
* chore: update package version to 0.7.416 * chore: Update Role.js imports order * refactor: move updateTagsInConvo to tags route, add RBAC for tags * refactor: add updateTagsInConvoOptions * fix: loading state for bookmark form * refactor: update primaryText class in TitleButton component * refactor: remove duplicate bookmarks and theming * refactor: update EditIcon component to use React.forwardRef * refactor: add _id field to tConversationTagSchema * refactor: remove promises * refactor: move mutation logic from BookmarkForm -> BookmarkEditDialog * refactor: update button class in BookmarkForm component * fix: conversation mutations and add better logging to useConversationTagMutation * refactor: update logger message in BookmarkEditDialog component * refactor: improve UI consistency in BookmarkNav and NewChat components * refactor: update logger message in BookmarkEditDialog component * refactor: Add tags prop to BookmarkForm component * refactor: Update BookmarkForm to avoid tag mutation if the tag already exists; also close dialog on submission programmatically * refactor: general role helper function to support updating access permissions for different permission types * refactor: Update getLatestText function to handle undefined values in message.content * refactor: Update useHasAccess hook to handle null role values for authenticated users * feat: toggle bookmarks access * refactor: Update PromptsCommand to handle access permissions for prompts * feat: updateConversationSelector * refactor: rename `vars` to `tagToDelete` for clarity * fix: prevent recreation of deleted tags in BookmarkMenu on Item Click * ci: mock updateBookmarksAccess function * ci: mock updateBookmarksAccess function
This commit is contained in:
parent
366e4c5adb
commit
f86e9dd04c
39 changed files with 530 additions and 298 deletions
|
|
@ -1,14 +1,14 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { getConfigDefaults } from 'librechat-data-provider';
|
||||
import { getConfigDefaults, PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import type { ContextType } from '~/common';
|
||||
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
|
||||
import ExportAndShareMenu from './ExportAndShareMenu';
|
||||
import { useMediaQuery, useHasAccess } from '~/hooks';
|
||||
import HeaderOptions from './Input/HeaderOptions';
|
||||
import BookmarkMenu from './Menus/BookmarkMenu';
|
||||
import AddMultiConvo from './AddMultiConvo';
|
||||
import { useMediaQuery } from '~/hooks';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
|
|
@ -21,6 +21,11 @@ export default function Header() {
|
|||
[startupConfig],
|
||||
);
|
||||
|
||||
const hasAccessToBookmarks = useHasAccess({
|
||||
permissionType: PermissionTypes.BOOKMARKS,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
|
||||
return (
|
||||
|
|
@ -28,11 +33,11 @@ export default function Header() {
|
|||
<div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
|
||||
<div className="flex items-center gap-2">
|
||||
{!navVisible && <HeaderNewChat />}
|
||||
{interfaceConfig.endpointsMenu && <EndpointsMenu />}
|
||||
{interfaceConfig.endpointsMenu === true && <EndpointsMenu />}
|
||||
{modelSpecs.length > 0 && <ModelSpecsMenu modelSpecs={modelSpecs} />}
|
||||
{<HeaderOptions interfaceConfig={interfaceConfig} />}
|
||||
{interfaceConfig.presets && <PresetsMenu />}
|
||||
<BookmarkMenu />
|
||||
{interfaceConfig.presets === true && <PresetsMenu />}
|
||||
{hasAccessToBookmarks === true && <BookmarkMenu />}
|
||||
<AddMultiConvo />
|
||||
{isSmallScreen && (
|
||||
<ExportAndShareMenu
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import type { PromptOption } from '~/common';
|
||||
import { removeCharIfLast, mapPromptGroups, detectVariables } from '~/utils';
|
||||
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
|
||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||
import { useLocalize, useCombobox, useHasAccess } from '~/hooks';
|
||||
import { useGetAllPromptGroups } from '~/data-provider';
|
||||
import { useLocalize, useCombobox } from '~/hooks';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import MentionItem from './MentionItem';
|
||||
import store from '~/store';
|
||||
|
|
@ -51,8 +52,13 @@ function PromptsCommand({
|
|||
submitPrompt: (textPrompt: string) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const hasAccess = useHasAccess({
|
||||
permissionType: PermissionTypes.PROMPTS,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
|
||||
const { data, isLoading } = useGetAllPromptGroups(undefined, {
|
||||
enabled: hasAccess,
|
||||
select: (data) => {
|
||||
const mappedArray = data.map((group) => ({
|
||||
id: group._id,
|
||||
|
|
@ -144,6 +150,10 @@ function PromptsCommand({
|
|||
currentActiveItem?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
||||
}, [activeIndex]);
|
||||
|
||||
if (!hasAccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverContainer
|
||||
index={index}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { useState, type FC, useCallback } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Constants, QueryKeys } from 'librechat-data-provider';
|
||||
import { Menu, MenuButton, MenuItems } from '@headlessui/react';
|
||||
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
|
||||
import type { TConversationTag } from 'librechat-data-provider';
|
||||
import { useConversationTagsQuery, useTagConversationMutation } from '~/data-provider';
|
||||
import { BookmarkMenuItems } from './Bookmarks/BookmarkMenuItems';
|
||||
import { BookmarkContext } from '~/Providers/BookmarkContext';
|
||||
|
|
@ -11,19 +13,32 @@ import { NotificationSeverity } from '~/common';
|
|||
import { useToastContext } from '~/Providers';
|
||||
import { useBookmarkSuccess } from '~/hooks';
|
||||
import { Spinner } from '~/components';
|
||||
import { cn } from '~/utils';
|
||||
import { cn, logger } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const BookmarkMenu: FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
||||
const conversationId = conversation?.conversationId ?? '';
|
||||
const onSuccess = useBookmarkSuccess(conversationId);
|
||||
const [tags, setTags] = useState<string[]>(conversation?.tags || []);
|
||||
const [open, setOpen] = useState(false);
|
||||
const updateConvoTags = useBookmarkSuccess(conversationId);
|
||||
|
||||
const { mutateAsync, isLoading } = useTagConversationMutation(conversationId);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [tags, setTags] = useState<string[]>(conversation?.tags || []);
|
||||
|
||||
const mutation = useTagConversationMutation(conversationId, {
|
||||
onSuccess: (newTags: string[]) => {
|
||||
setTags(newTags);
|
||||
updateConvoTags(newTags);
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: 'Error adding bookmark',
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = useConversationTagsQuery();
|
||||
|
||||
|
|
@ -35,7 +50,7 @@ const BookmarkMenu: FC = () => {
|
|||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (tag?: string): Promise<void> => {
|
||||
(tag?: string) => {
|
||||
if (tag === undefined || tag === '' || !conversationId) {
|
||||
showToast({
|
||||
message: 'Invalid tag or conversationId',
|
||||
|
|
@ -44,34 +59,29 @@ const BookmarkMenu: FC = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const newTags = tags.includes(tag) ? tags.filter((t) => t !== tag) : [...tags, tag];
|
||||
await mutateAsync(
|
||||
{
|
||||
tags: newTags,
|
||||
},
|
||||
{
|
||||
onSuccess: (newTags: string[]) => {
|
||||
setTags(newTags);
|
||||
onSuccess(newTags);
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: 'Error adding bookmark',
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags before setting', tags);
|
||||
const allTags =
|
||||
queryClient.getQueryData<TConversationTag[]>([QueryKeys.conversationTags]) ?? [];
|
||||
const existingTags = allTags.map((t) => t.tag);
|
||||
const filteredTags = tags.filter((t) => existingTags.includes(t));
|
||||
logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags after filtering', filteredTags);
|
||||
const newTags = filteredTags.includes(tag)
|
||||
? filteredTags.filter((t) => t !== tag)
|
||||
: [...filteredTags, tag];
|
||||
logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags after', newTags);
|
||||
mutation.mutate({
|
||||
tags: newTags,
|
||||
});
|
||||
},
|
||||
[tags, conversationId, mutateAsync, setTags, onSuccess, showToast],
|
||||
[tags, conversationId, mutation, queryClient, showToast],
|
||||
);
|
||||
|
||||
if (!isActiveConvo) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderButtonContent = () => {
|
||||
if (isLoading) {
|
||||
if (mutation.isLoading) {
|
||||
return <Spinner aria-label="Spinner" />;
|
||||
}
|
||||
if (tags.length > 0) {
|
||||
|
|
@ -80,10 +90,7 @@ const BookmarkMenu: FC = () => {
|
|||
return <BookmarkIcon className="icon-sm" aria-label="Bookmark" />;
|
||||
};
|
||||
|
||||
const handleToggleOpen = () => {
|
||||
setOpen(!open);
|
||||
return Promise.resolve();
|
||||
};
|
||||
const handleToggleOpen = () => setOpen(!open);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -116,6 +123,7 @@ const BookmarkMenu: FC = () => {
|
|||
)}
|
||||
</Menu>
|
||||
<BookmarkEditDialog
|
||||
context="BookmarkMenu - BookmarkEditDialog"
|
||||
conversation={conversation}
|
||||
tags={tags}
|
||||
setTags={setTags}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { useLocalize } from '~/hooks';
|
|||
|
||||
export const BookmarkMenuItems: FC<{
|
||||
tags: string[];
|
||||
handleToggleOpen?: () => Promise<void>;
|
||||
handleSubmit: (tag?: string) => Promise<void>;
|
||||
handleToggleOpen?: () => void;
|
||||
handleSubmit: (tag?: string) => void;
|
||||
}> = ({
|
||||
tags,
|
||||
handleSubmit,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default function TitleButton({ primaryText = '', secondaryText = '' }) {
|
|||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div>
|
||||
<span className="text-token-text-secondary"> {primaryText} </span>
|
||||
<span className="text-text-primary"> {primaryText} </span>
|
||||
{!!secondaryText && <span className="text-token-text-secondary">{secondaryText}</span>}
|
||||
</div>
|
||||
<ChevronDown className="text-token-text-secondary size-4" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue