From 208be7c06c21ab1fcab9390095269f1ec0b96e4b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 18 Sep 2025 10:00:33 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=A8=EF=B8=8F=20refactor:=20Only=20Allo?= =?UTF-8?q?w=20Prompt=20Queries=20with=20Access=20(#9688)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/Providers/PromptGroupsContext.tsx | 19 ++++++--- .../components/Chat/Input/PromptsCommand.tsx | 10 +---- .../Prompts/Groups/CategorySelector.tsx | 4 +- .../Prompts/Groups/CreatePromptForm.tsx | 5 ++- .../Prompts/Groups/FilterPrompts.tsx | 4 +- client/src/components/Prompts/PromptForm.tsx | 7 +++- client/src/hooks/Prompts/useCategories.tsx | 12 +++++- .../src/hooks/Prompts/usePromptGroupsNav.ts | 41 +++++++++++-------- 8 files changed, 64 insertions(+), 38 deletions(-) diff --git a/client/src/Providers/PromptGroupsContext.tsx b/client/src/Providers/PromptGroupsContext.tsx index 1dd158d7d..7c9dbe825 100644 --- a/client/src/Providers/PromptGroupsContext.tsx +++ b/client/src/Providers/PromptGroupsContext.tsx @@ -1,9 +1,10 @@ import React, { createContext, useContext, ReactNode, useMemo } from 'react'; +import { PermissionTypes, Permissions } from 'librechat-data-provider'; import type { TPromptGroup } from 'librechat-data-provider'; import type { PromptOption } from '~/common'; import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; +import { usePromptGroupsNav, useHasAccess } from '~/hooks'; import { useGetAllPromptGroups } from '~/data-provider'; -import { usePromptGroupsNav } from '~/hooks'; import { mapPromptGroups } from '~/utils'; type AllPromptGroupsData = @@ -19,14 +20,21 @@ type PromptGroupsContextType = data: AllPromptGroupsData; isLoading: boolean; }; + hasAccess: boolean; }) | null; const PromptGroupsContext = createContext(null); export const PromptGroupsProvider = ({ children }: { children: ReactNode }) => { - const promptGroupsNav = usePromptGroupsNav(); + const hasAccess = useHasAccess({ + permissionType: PermissionTypes.PROMPTS, + permission: Permissions.USE, + }); + + const promptGroupsNav = usePromptGroupsNav(hasAccess); const { data: allGroupsData, isLoading: isLoadingAll } = useGetAllPromptGroups(undefined, { + enabled: hasAccess, select: (data) => { const mappedArray: PromptOption[] = data.map((group) => ({ id: group._id ?? '', @@ -55,11 +63,12 @@ export const PromptGroupsProvider = ({ children }: { children: ReactNode }) => { () => ({ ...promptGroupsNav, allPromptGroups: { - data: allGroupsData, - isLoading: isLoadingAll, + data: hasAccess ? allGroupsData : undefined, + isLoading: hasAccess ? isLoadingAll : false, }, + hasAccess, }), - [promptGroupsNav, allGroupsData, isLoadingAll], + [promptGroupsNav, allGroupsData, isLoadingAll, hasAccess], ); return ( diff --git a/client/src/components/Chat/Input/PromptsCommand.tsx b/client/src/components/Chat/Input/PromptsCommand.tsx index 5f384e631..c2dd30b57 100644 --- a/client/src/components/Chat/Input/PromptsCommand.tsx +++ b/client/src/components/Chat/Input/PromptsCommand.tsx @@ -2,14 +2,13 @@ import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react'; import { AutoSizer, List } from 'react-virtualized'; import { Spinner, useCombobox } from '@librechat/client'; 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, detectVariables } from '~/utils'; import VariableDialog from '~/components/Prompts/Groups/VariableDialog'; import { usePromptGroupsContext } from '~/Providers'; -import { useLocalize, useHasAccess } from '~/hooks'; import MentionItem from './MentionItem'; +import { useLocalize } from '~/hooks'; import store from '~/store'; const commandChar = '/'; @@ -54,12 +53,7 @@ function PromptsCommand({ submitPrompt: (textPrompt: string) => void; }) { const localize = useLocalize(); - const hasAccess = useHasAccess({ - permissionType: PermissionTypes.PROMPTS, - permission: Permissions.USE, - }); - - const { allPromptGroups } = usePromptGroupsContext(); + const { allPromptGroups, hasAccess } = usePromptGroupsContext(); const { data, isLoading } = allPromptGroups; const [activeIndex, setActiveIndex] = useState(0); diff --git a/client/src/components/Prompts/Groups/CategorySelector.tsx b/client/src/components/Prompts/Groups/CategorySelector.tsx index e8661b8b6..6d45db42e 100644 --- a/client/src/components/Prompts/Groups/CategorySelector.tsx +++ b/client/src/components/Prompts/Groups/CategorySelector.tsx @@ -6,6 +6,7 @@ import { LocalStorageKeys } from 'librechat-data-provider'; import { useFormContext, Controller } from 'react-hook-form'; import type { MenuItemProps } from '@librechat/client'; import type { ReactNode } from 'react'; +import { usePromptGroupsContext } from '~/Providers'; import { useCategories } from '~/hooks'; import { cn } from '~/utils'; @@ -22,8 +23,9 @@ const CategorySelector: React.FC = ({ }) => { const { t } = useTranslation(); const formContext = useFormContext(); - const { categories, emptyCategory } = useCategories(); const [isOpen, setIsOpen] = useState(false); + const { hasAccess } = usePromptGroupsContext(); + const { categories, emptyCategory } = useCategories({ hasAccess }); const control = formContext?.control; const watch = formContext?.watch; diff --git a/client/src/components/Prompts/Groups/CreatePromptForm.tsx b/client/src/components/Prompts/Groups/CreatePromptForm.tsx index 0cb04c377..c7bbbd726 100644 --- a/client/src/components/Prompts/Groups/CreatePromptForm.tsx +++ b/client/src/components/Prompts/Groups/CreatePromptForm.tsx @@ -7,6 +7,7 @@ import CategorySelector from '~/components/Prompts/Groups/CategorySelector'; import VariablesDropdown from '~/components/Prompts/VariablesDropdown'; import PromptVariables from '~/components/Prompts/PromptVariables'; import Description from '~/components/Prompts/Description'; +import { usePromptGroupsContext } from '~/Providers'; import { useLocalize, useHasAccess } from '~/hooks'; import Command from '~/components/Prompts/Command'; import { useCreatePrompt } from '~/data-provider'; @@ -37,10 +38,12 @@ const CreatePromptForm = ({ }) => { const localize = useLocalize(); const navigate = useNavigate(); - const hasAccess = useHasAccess({ + const { hasAccess: hasUseAccess } = usePromptGroupsContext(); + const hasCreateAccess = useHasAccess({ permissionType: PermissionTypes.PROMPTS, permission: Permissions.CREATE, }); + const hasAccess = hasUseAccess && hasCreateAccess; useEffect(() => { let timeoutId: ReturnType; diff --git a/client/src/components/Prompts/Groups/FilterPrompts.tsx b/client/src/components/Prompts/Groups/FilterPrompts.tsx index 5ed3d51fa..7f2b4d301 100644 --- a/client/src/components/Prompts/Groups/FilterPrompts.tsx +++ b/client/src/components/Prompts/Groups/FilterPrompts.tsx @@ -11,8 +11,8 @@ import store from '~/store'; export default function FilterPrompts({ className = '' }: { className?: string }) { const localize = useLocalize(); - const { name, setName } = usePromptGroupsContext(); - const { categories } = useCategories('h-4 w-4'); + const { name, setName, hasAccess } = usePromptGroupsContext(); + const { categories } = useCategories({ className: 'h-4 w-4', hasAccess }); const [displayName, setDisplayName] = useState(name || ''); const [isSearching, setIsSearching] = useState(false); const [categoryFilter, setCategory] = useRecoilState(store.promptsCategory); diff --git a/client/src/components/Prompts/PromptForm.tsx b/client/src/components/Prompts/PromptForm.tsx index b98e46579..e1ba411c0 100644 --- a/client/src/components/Prompts/PromptForm.tsx +++ b/client/src/components/Prompts/PromptForm.tsx @@ -167,6 +167,7 @@ const PromptForm = () => { const params = useParams(); const localize = useLocalize(); const { showToast } = useToastContext(); + const { hasAccess } = usePromptGroupsContext(); const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd); const promptId = params.promptId || ''; @@ -179,10 +180,12 @@ const PromptForm = () => { const [showSidePanel, setShowSidePanel] = useState(false); const sidePanelWidth = '320px'; - const { data: group, isLoading: isLoadingGroup } = useGetPromptGroup(promptId); + const { data: group, isLoading: isLoadingGroup } = useGetPromptGroup(promptId, { + enabled: hasAccess && !!promptId, + }); const { data: prompts = [], isLoading: isLoadingPrompts } = useGetPrompts( { groupId: promptId }, - { enabled: !!promptId }, + { enabled: hasAccess && !!promptId }, ); const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions( diff --git a/client/src/hooks/Prompts/useCategories.tsx b/client/src/hooks/Prompts/useCategories.tsx index b4611e8dc..b9e116880 100644 --- a/client/src/hooks/Prompts/useCategories.tsx +++ b/client/src/hooks/Prompts/useCategories.tsx @@ -1,6 +1,6 @@ -import { useGetCategories } from '~/data-provider'; import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; import { useLocalize, TranslationKeys } from '~/hooks'; +import { useGetCategories } from '~/data-provider'; const loadingCategories: { label: TranslationKeys; value: string }[] = [ { @@ -14,9 +14,17 @@ const emptyCategory: { label: TranslationKeys; value: string } = { value: '', }; -const useCategories = (className = '') => { +const useCategories = ({ + className = '', + hasAccess = true, +}: { + className?: string; + hasAccess?: boolean; +}) => { const localize = useLocalize(); + const { data: categories = loadingCategories } = useGetCategories({ + enabled: hasAccess, select: (data) => data.map((category) => ({ label: localize(category.label as TranslationKeys), diff --git a/client/src/hooks/Prompts/usePromptGroupsNav.ts b/client/src/hooks/Prompts/usePromptGroupsNav.ts index a0d51fdb4..76a092955 100644 --- a/client/src/hooks/Prompts/usePromptGroupsNav.ts +++ b/client/src/hooks/Prompts/usePromptGroupsNav.ts @@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil'; import { usePromptGroupsInfiniteQuery } from '~/data-provider'; import store from '~/store'; -export default function usePromptGroupsNav() { +export default function usePromptGroupsNav(hasAccess = true) { const [pageSize] = useRecoilState(store.promptsPageSize); const [category] = useRecoilState(store.promptsCategory); const [name, setName] = useRecoilState(store.promptsName); @@ -14,21 +14,26 @@ export default function usePromptGroupsNav() { const prevFiltersRef = useRef({ name, category }); - const groupsQuery = usePromptGroupsInfiniteQuery({ - name, - pageSize, - category, - }); + const groupsQuery = usePromptGroupsInfiniteQuery( + { + name, + pageSize, + category, + }, + { + enabled: hasAccess, + }, + ); // Get the current page data const currentPageData = useMemo(() => { - if (!groupsQuery.data?.pages || groupsQuery.data.pages.length === 0) { + if (!hasAccess || !groupsQuery.data?.pages || groupsQuery.data.pages.length === 0) { return null; } // Ensure we don't go out of bounds const pageIndex = Math.min(currentPageIndex, groupsQuery.data.pages.length - 1); return groupsQuery.data.pages[pageIndex]; - }, [groupsQuery.data?.pages, currentPageIndex]); + }, [hasAccess, groupsQuery.data?.pages, currentPageIndex]); // Get prompt groups for current page const promptGroups = useMemo(() => { @@ -54,7 +59,7 @@ export default function usePromptGroupsNav() { // Navigate to next page const nextPage = useCallback(async () => { - if (!hasNextPage) return; + if (!hasAccess || !hasNextPage) return; const nextPageIndex = currentPageIndex + 1; @@ -72,16 +77,18 @@ export default function usePromptGroupsNav() { } setCurrentPageIndex(nextPageIndex); - }, [currentPageIndex, hasNextPage, groupsQuery]); + }, [hasAccess, currentPageIndex, hasNextPage, groupsQuery]); // Navigate to previous page const prevPage = useCallback(() => { - if (!hasPreviousPage) return; + if (!hasAccess || !hasPreviousPage) return; setCurrentPageIndex(currentPageIndex - 1); - }, [currentPageIndex, hasPreviousPage]); + }, [hasAccess, currentPageIndex, hasPreviousPage]); // Reset when filters change useEffect(() => { + if (!hasAccess) return; + const filtersChanged = prevFiltersRef.current.name !== name || prevFiltersRef.current.category !== category; @@ -90,18 +97,18 @@ export default function usePromptGroupsNav() { cursorHistoryRef.current = [null]; prevFiltersRef.current = { name, category }; } - }, [name, category]); + }, [hasAccess, name, category]); return { - promptGroups, + promptGroups: hasAccess ? promptGroups : [], groupsQuery, currentPage, totalPages, - hasNextPage, - hasPreviousPage, + hasNextPage: hasAccess && hasNextPage, + hasPreviousPage: hasAccess && hasPreviousPage, nextPage, prevPage, - isFetching: groupsQuery.isFetching, + isFetching: hasAccess ? groupsQuery.isFetching : false, name, setName, };