Skip to content

Commit a4ca4b7

Browse files
authored
🔀 fix: Rerender Edge Cases After Migration to Shared Package (danny-avila#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
1 parent 8e6eef0 commit a4ca4b7

4 files changed

Lines changed: 398 additions & 265 deletions

File tree

client/src/components/Prompts/DeleteVersion.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import React, { useCallback } from 'react';
12
import { Trash2 } from 'lucide-react';
3+
import { useDeletePrompt } from '~/data-provider';
24
import { Button, OGDialog, OGDialogTrigger, Label, OGDialogTemplate } from '@librechat/client';
35
import { useLocalize } from '~/hooks';
46

5-
const DeleteVersion = ({
7+
const DeleteConfirmDialog = ({
68
name,
79
disabled,
810
selectHandler,
@@ -58,4 +60,42 @@ const DeleteVersion = ({
5860
);
5961
};
6062

61-
export default DeleteVersion;
63+
interface DeletePromptProps {
64+
promptId?: string;
65+
groupId: string;
66+
promptName: string;
67+
disabled: boolean;
68+
}
69+
70+
const DeletePrompt = React.memo(
71+
({ promptId, groupId, promptName, disabled }: DeletePromptProps) => {
72+
const deletePromptMutation = useDeletePrompt();
73+
74+
const handleDelete = useCallback(() => {
75+
if (!promptId) {
76+
console.warn('No prompt ID provided for deletion');
77+
return;
78+
}
79+
deletePromptMutation.mutate({
80+
_id: promptId,
81+
groupId,
82+
});
83+
}, [promptId, groupId, deletePromptMutation]);
84+
85+
if (!promptId) {
86+
return null;
87+
}
88+
89+
return (
90+
<DeleteConfirmDialog
91+
name={promptName}
92+
disabled={disabled || !promptId}
93+
selectHandler={handleDelete}
94+
/>
95+
);
96+
},
97+
);
98+
99+
DeletePrompt.displayName = 'DeletePrompt';
100+
101+
export default DeletePrompt;

client/src/components/Prompts/Groups/CategorySelector.tsx

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import React, { useMemo } from 'react';
2-
import { Dropdown } from '@librechat/client';
1+
import React, { useMemo, useState } from 'react';
2+
import * as Ariakit from '@ariakit/react';
33
import { useTranslation } from 'react-i18next';
4-
import { useFormContext, Controller } from 'react-hook-form';
4+
import { DropdownPopup } from '@librechat/client';
55
import { LocalStorageKeys } from 'librechat-data-provider';
6+
import { useFormContext, Controller } from 'react-hook-form';
7+
import type { MenuItemProps } from '@librechat/client';
68
import type { ReactNode } from 'react';
79
import { useCategories } from '~/hooks';
10+
import { cn } from '~/utils';
811

912
interface CategorySelectorProps {
1013
currentCategory?: string;
@@ -20,10 +23,11 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
2023
const { t } = useTranslation();
2124
const formContext = useFormContext();
2225
const { categories, emptyCategory } = useCategories();
26+
const [isOpen, setIsOpen] = useState(false);
2327

24-
const control = formContext.control;
25-
const watch = formContext.watch;
26-
const setValue = formContext.setValue;
28+
const control = formContext?.control;
29+
const watch = formContext?.watch;
30+
const setValue = formContext?.setValue;
2731

2832
const watchedCategory = watch ? watch('category') : currentCategory;
2933

@@ -46,53 +50,71 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
4650
return categoryOption;
4751
}, [categoryOption, t]);
4852

53+
const menuItems: MenuItemProps[] = useMemo(() => {
54+
if (!categories) return [];
55+
56+
return categories.map((category) => ({
57+
id: category.value,
58+
label: category.label,
59+
icon: 'icon' in category ? category.icon : undefined,
60+
onClick: () => {
61+
const value = category.value || '';
62+
if (formContext && setValue) {
63+
setValue('category', value, { shouldDirty: false });
64+
}
65+
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
66+
onValueChange?.(value);
67+
setIsOpen(false);
68+
},
69+
}));
70+
}, [categories, formContext, setValue, onValueChange]);
71+
72+
const trigger = (
73+
<Ariakit.MenuButton
74+
className={cn(
75+
'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',
76+
'w-fit gap-2',
77+
className,
78+
)}
79+
onClick={() => setIsOpen(!isOpen)}
80+
aria-label="Prompt's category selector"
81+
aria-labelledby="category-selector-label"
82+
>
83+
<div className="flex items-center space-x-2">
84+
{'icon' in displayCategory && displayCategory.icon != null && (
85+
<span>{displayCategory.icon as ReactNode}</span>
86+
)}
87+
<span>{displayCategory.value ? displayCategory.label : t('com_ui_category')}</span>
88+
</div>
89+
<Ariakit.MenuButtonArrow />
90+
</Ariakit.MenuButton>
91+
);
92+
4993
return formContext ? (
5094
<Controller
5195
name="category"
5296
control={control}
5397
render={() => (
54-
<Dropdown
55-
value={displayCategory.value ?? ''}
56-
label={displayCategory.value ? undefined : t('com_ui_category')}
57-
onChange={(value: string) => {
58-
setValue('category', value, { shouldDirty: false });
59-
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
60-
onValueChange?.(value);
61-
}}
62-
aria-labelledby="category-selector-label"
63-
ariaLabel="Prompt's category selector"
64-
className={className}
65-
options={categories || []}
66-
renderValue={() => (
67-
<div className="flex items-center space-x-2">
68-
{'icon' in displayCategory && displayCategory.icon != null && (
69-
<span>{displayCategory.icon as ReactNode}</span>
70-
)}
71-
<span>{displayCategory.label}</span>
72-
</div>
73-
)}
98+
<DropdownPopup
99+
trigger={trigger}
100+
items={menuItems}
101+
isOpen={isOpen}
102+
setIsOpen={setIsOpen}
103+
menuId="category-selector-menu"
104+
className="mt-2"
105+
portal={true}
74106
/>
75107
)}
76108
/>
77109
) : (
78-
<Dropdown
79-
value={currentCategory ?? ''}
80-
onChange={(value: string) => {
81-
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
82-
onValueChange?.(value);
83-
}}
84-
aria-labelledby="category-selector-label"
85-
ariaLabel="Prompt's category selector"
86-
className={className}
87-
options={categories || []}
88-
renderValue={() => (
89-
<div className="flex items-center space-x-2">
90-
{'icon' in displayCategory && displayCategory.icon != null && (
91-
<span>{displayCategory.icon as ReactNode}</span>
92-
)}
93-
<span>{displayCategory.label}</span>
94-
</div>
95-
)}
110+
<DropdownPopup
111+
trigger={trigger}
112+
items={menuItems}
113+
isOpen={isOpen}
114+
setIsOpen={setIsOpen}
115+
menuId="category-selector-menu"
116+
className="mt-2"
117+
portal={true}
96118
/>
97119
);
98120
};

0 commit comments

Comments
 (0)