🔀 fix: Rerender Edge Cases After Migration to Shared Package (#8713)

* fix: render issues in PromptForm by decoupling nested dependencies as a result of @librechat/client components

* fix: MemoryViewer flicker by moving EditMemoryButton and DeleteMemoryButton outside of rendering

* fix: CategorySelector to use DropdownPopup for improved mobile compatibility

* chore: imports
This commit is contained in:
Danny Avila 2025-07-28 15:14:37 -04:00 committed by GitHub
parent 8e6eef04ab
commit a4ca4b7d9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 401 additions and 268 deletions

View file

@ -1,10 +1,13 @@
import React, { useMemo } from 'react';
import { Dropdown } from '@librechat/client';
import React, { useMemo, useState } from 'react';
import * as Ariakit from '@ariakit/react';
import { useTranslation } from 'react-i18next';
import { useFormContext, Controller } from 'react-hook-form';
import { DropdownPopup } from '@librechat/client';
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 { useCategories } from '~/hooks';
import { cn } from '~/utils';
interface CategorySelectorProps {
currentCategory?: string;
@ -20,10 +23,11 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
const { t } = useTranslation();
const formContext = useFormContext();
const { categories, emptyCategory } = useCategories();
const [isOpen, setIsOpen] = useState(false);
const control = formContext.control;
const watch = formContext.watch;
const setValue = formContext.setValue;
const control = formContext?.control;
const watch = formContext?.watch;
const setValue = formContext?.setValue;
const watchedCategory = watch ? watch('category') : currentCategory;
@ -46,53 +50,71 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
return categoryOption;
}, [categoryOption, t]);
const menuItems: MenuItemProps[] = useMemo(() => {
if (!categories) return [];
return categories.map((category) => ({
id: category.value,
label: category.label,
icon: 'icon' in category ? category.icon : undefined,
onClick: () => {
const value = category.value || '';
if (formContext && setValue) {
setValue('category', value, { shouldDirty: false });
}
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
onValueChange?.(value);
setIsOpen(false);
},
}));
}, [categories, formContext, setValue, onValueChange]);
const trigger = (
<Ariakit.MenuButton
className={cn(
'focus:ring-offset-ring-offset relative inline-flex items-center justify-between rounded-xl border border-input bg-background px-3 py-2 text-sm text-text-primary transition-all duration-200 ease-in-out hover:bg-accent hover:text-accent-foreground focus:ring-ring-primary',
'w-fit gap-2',
className,
)}
onClick={() => setIsOpen(!isOpen)}
aria-label="Prompt's category selector"
aria-labelledby="category-selector-label"
>
<div className="flex items-center space-x-2">
{'icon' in displayCategory && displayCategory.icon != null && (
<span>{displayCategory.icon as ReactNode}</span>
)}
<span>{displayCategory.value ? displayCategory.label : t('com_ui_category')}</span>
</div>
<Ariakit.MenuButtonArrow />
</Ariakit.MenuButton>
);
return formContext ? (
<Controller
name="category"
control={control}
render={() => (
<Dropdown
value={displayCategory.value ?? ''}
label={displayCategory.value ? undefined : t('com_ui_category')}
onChange={(value: string) => {
setValue('category', value, { shouldDirty: false });
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
onValueChange?.(value);
}}
aria-labelledby="category-selector-label"
ariaLabel="Prompt's category selector"
className={className}
options={categories || []}
renderValue={() => (
<div className="flex items-center space-x-2">
{'icon' in displayCategory && displayCategory.icon != null && (
<span>{displayCategory.icon as ReactNode}</span>
)}
<span>{displayCategory.label}</span>
</div>
)}
<DropdownPopup
trigger={trigger}
items={menuItems}
isOpen={isOpen}
setIsOpen={setIsOpen}
menuId="category-selector-menu"
className="mt-2"
portal={true}
/>
)}
/>
) : (
<Dropdown
value={currentCategory ?? ''}
onChange={(value: string) => {
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
onValueChange?.(value);
}}
aria-labelledby="category-selector-label"
ariaLabel="Prompt's category selector"
className={className}
options={categories || []}
renderValue={() => (
<div className="flex items-center space-x-2">
{'icon' in displayCategory && displayCategory.icon != null && (
<span>{displayCategory.icon as ReactNode}</span>
)}
<span>{displayCategory.label}</span>
</div>
)}
<DropdownPopup
trigger={trigger}
items={menuItems}
isOpen={isOpen}
setIsOpen={setIsOpen}
menuId="category-selector-menu"
className="mt-2"
portal={true}
/>
);
};