🗓️ fix: Hide Unsupported Schedule Variables (#15053)

This commit is contained in:
Danny Avila 2026-08-20 18:33:00 -04:00 committed by GitHub
parent 6baeff801c
commit 276f5f88fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 133 additions and 49 deletions

View file

@ -0,0 +1,33 @@
import { ToastProvider } from '@librechat/client';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { FormProvider, useForm } from 'react-hook-form';
import type { AgentForm } from '~/common';
import Instructions from '../Instructions';
jest.mock('~/hooks', () => ({
useLocalize: () => (key: string) => key,
}));
function InstructionsHarness() {
const methods = useForm<AgentForm>({ defaultValues: { instructions: '' } });
return (
<ToastProvider>
<FormProvider {...methods}>
<Instructions />
</FormProvider>
</ToastProvider>
);
}
describe('Agent Instructions', () => {
it('offers special-variable insertion by default', async () => {
const user = userEvent.setup();
render(<InstructionsHarness />);
await user.click(screen.getByRole('button', { name: 'com_ui_variables' }));
expect(
await screen.findByRole('menuitem', { name: 'com_ui_special_var_current_date' }),
).toBeInTheDocument();
});
});

View file

@ -418,6 +418,7 @@ export default function ScheduleDialog({
required={true}
invalid={errors.prompt != null}
describedBy="schedule-prompt-message"
showVariables={false}
portal={false}
/>
<FieldMessage id="schedule-prompt-message" message={errors.prompt?.message} />

View file

@ -1,7 +1,7 @@
import { createElement } from 'react';
import { ToastProvider } from '@librechat/client';
import userEvent from '@testing-library/user-event';
import { render, screen, within } from '@testing-library/react';
import { render, screen, waitFor, within } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';
import ScheduleDialog from '../ScheduleDialog';
@ -97,13 +97,54 @@ describe('ScheduleDialog', () => {
expect(screen.getByPlaceholderText('com_ui_schedule_prompt_placeholder')).toBeInTheDocument();
});
it('offers the special-variable menu on the prompt field', async () => {
it('does not offer special-variable insertion in either prompt editor', async () => {
const user = userEvent.setup();
renderDialog();
await user.click(screen.getByRole('button', { name: 'com_ui_variables' }));
expect(screen.queryByRole('button', { name: 'com_ui_variables' })).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'com_ui_expand_editor' }));
const expandedDialog = screen.getAllByRole('dialog').at(-1);
expect(expandedDialog).toBeDefined();
expect(
await screen.findByRole('menuitem', { name: 'com_ui_special_var_current_date' }),
).toBeInTheDocument();
within(expandedDialog as HTMLElement).queryByRole('button', { name: 'com_ui_variables' }),
).not.toBeInTheDocument();
});
it('keeps the expanded prompt editor available and synchronized', async () => {
const user = userEvent.setup();
renderDialog();
const prompt = screen.getByPlaceholderText('com_ui_schedule_prompt_placeholder');
await user.type(prompt, 'Initial prompt');
await user.click(screen.getByRole('button', { name: 'com_ui_expand_editor' }));
const expandedDialog = screen.getAllByRole('dialog').at(-1);
expect(expandedDialog).toBeDefined();
const expandedPrompt = within(expandedDialog as HTMLElement).getByRole('textbox', {
name: 'com_ui_prompt',
});
expect(expandedPrompt).toHaveValue('Initial prompt');
await user.type(expandedPrompt, ' with details');
expect(prompt).toHaveValue('Initial prompt with details');
});
it('preserves prompt validation and accessibility relationships', async () => {
const user = userEvent.setup();
renderDialog();
const prompt = screen.getByPlaceholderText('com_ui_schedule_prompt_placeholder');
expect(prompt).toHaveAccessibleName('com_ui_prompt');
expect(prompt).toHaveAttribute('aria-required', 'true');
expect(prompt).toHaveAttribute('aria-describedby', 'schedule-prompt-message');
expect(prompt).toHaveAttribute('aria-invalid', 'false');
await user.click(screen.getByRole('button', { name: 'com_ui_create' }));
await waitFor(() => expect(prompt).toHaveAttribute('aria-invalid', 'true'));
expect(document.getElementById('schedule-prompt-message')).toHaveTextContent(
'com_ui_field_required',
);
});
});

View file

@ -44,6 +44,8 @@ interface VariableEditorProps {
className?: string;
labelClassName?: string;
containerClassName?: string;
/** Whether to expose special-variable insertion controls. Defaults to true. */
showVariables?: boolean;
/**
* Ariakit popovers portal to the body by default, which puts them outside a Radix
* dialog's focus trap. Pass `false` from inside a dialog, and give that dialog
@ -73,6 +75,7 @@ export default function VariableEditor({
className,
labelClassName,
containerClassName,
showVariables = true,
portal = true,
}: VariableEditorProps) {
const menuId = useId();
@ -102,27 +105,29 @@ export default function VariableEditor({
{label}
</Label>
<div className="flex items-center gap-0.5">
<DropdownPopup
portal={portal}
mountByState={true}
unmountOnHide={true}
preserveTabOrder={true}
isOpen={isMenuOpen}
setIsOpen={setIsMenuOpen}
trigger={
<Menu.MenuButton
id={`${id}-variables-menu-button`}
aria-label={localize('com_ui_variables')}
title={localize('com_ui_variables')}
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-text-secondary transition-colors hover:bg-surface-secondary hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
>
<PlusCircle className="h-4 w-4" strokeWidth={1.75} aria-hidden={true} />
</Menu.MenuButton>
}
items={variableItems}
menuId={menuId}
className="pointer-events-auto z-30"
/>
{showVariables && (
<DropdownPopup
portal={portal}
mountByState={true}
unmountOnHide={true}
preserveTabOrder={true}
isOpen={isMenuOpen}
setIsOpen={setIsMenuOpen}
trigger={
<Menu.MenuButton
id={`${id}-variables-menu-button`}
aria-label={localize('com_ui_variables')}
title={localize('com_ui_variables')}
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-text-secondary transition-colors hover:bg-surface-secondary hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
>
<PlusCircle className="h-4 w-4" strokeWidth={1.75} aria-hidden={true} />
</Menu.MenuButton>
}
items={variableItems}
menuId={menuId}
className="pointer-events-auto z-30"
/>
)}
<TooltipAnchor
description={localize('com_ui_expand_editor')}
render={
@ -169,29 +174,33 @@ export default function VariableEditor({
placeholder={placeholder}
aria-label={label}
/>
<div className="flex items-center justify-between">
<DropdownPopup
portal={portal}
mountByState={true}
unmountOnHide={true}
preserveTabOrder={true}
isOpen={isDialogMenuOpen}
setIsOpen={setIsDialogMenuOpen}
trigger={
<Menu.MenuButton
id={`${id}-variables-menu-button-dialog`}
render={
<Button type="button" variant="outline" className="gap-1.5">
<PlusCircle className="h-4 w-4" strokeWidth={1.75} aria-hidden={true} />
{localize('com_ui_variables')}
</Button>
}
/>
}
items={variableItems}
menuId={dialogMenuId}
className="pointer-events-auto z-[200]"
/>
<div
className={cn('flex items-center', showVariables ? 'justify-between' : 'justify-end')}
>
{showVariables && (
<DropdownPopup
portal={portal}
mountByState={true}
unmountOnHide={true}
preserveTabOrder={true}
isOpen={isDialogMenuOpen}
setIsOpen={setIsDialogMenuOpen}
trigger={
<Menu.MenuButton
id={`${id}-variables-menu-button-dialog`}
render={
<Button type="button" variant="outline" className="gap-1.5">
<PlusCircle className="h-4 w-4" strokeWidth={1.75} aria-hidden={true} />
{localize('com_ui_variables')}
</Button>
}
/>
}
items={variableItems}
menuId={dialogMenuId}
className="pointer-events-auto z-[200]"
/>
)}
<OGDialogClose asChild>
<Button type="button">{localize('com_ui_done')}</Button>
</OGDialogClose>