diff --git a/client/src/components/Plugins/Store/PluginAuthForm.tsx b/client/src/components/Plugins/Store/PluginAuthForm.tsx index 91f46aaf39..3bf7b20a1e 100644 --- a/client/src/components/Plugins/Store/PluginAuthForm.tsx +++ b/client/src/components/Plugins/Store/PluginAuthForm.tsx @@ -1,5 +1,13 @@ import { useForm, useWatch } from 'react-hook-form'; -import { HoverCard, HoverCardTrigger, SecretInput, Input, Label, Button } from '@librechat/client'; +import { + Input, + Label, + Button, + Spinner, + HoverCard, + SecretInput, + HoverCardTrigger, +} from '@librechat/client'; import type { TPlugin, TPluginAuthConfig, TPluginAction } from 'librechat-data-provider'; import type { RegisterOptions } from 'react-hook-form'; import PluginTooltip from './PluginTooltip'; @@ -70,9 +78,19 @@ type TPluginAuthFormProps = { plugin: TPlugin | undefined; onSubmit: (installActionData: TPluginAction) => void; isEntityTool?: boolean; + /** External in-flight state (e.g. a mutation) so the button reflects the real save. */ + isSaving?: boolean; + /** When provided, renders a Cancel button (used when re-editing saved credentials). */ + onCancel?: () => void; }; -function PluginAuthForm({ plugin, onSubmit, isEntityTool }: TPluginAuthFormProps) { +function PluginAuthForm({ + plugin, + onSubmit, + isEntityTool, + isSaving, + onCancel, +}: TPluginAuthFormProps) { const { register, control, @@ -84,6 +102,7 @@ function PluginAuthForm({ plugin, onSubmit, isEntityTool }: TPluginAuthFormProps const watchedValues = useWatch({ control }); const authConfig = plugin?.authConfig ?? []; const allFieldsOptional = authConfig.length > 0 && authConfig.every((c) => c.optional === true); + const saving = isSubmitting || isSaving === true; const submit = handleSubmit((auth) => onSubmit({ @@ -178,14 +197,20 @@ function PluginAuthForm({ plugin, onSubmit, isEntityTool }: TPluginAuthFormProps ); })} -
+
+ {onCancel && ( + + )}
diff --git a/client/src/components/Plugins/Store/__tests__/PluginAuthForm.spec.tsx b/client/src/components/Plugins/Store/__tests__/PluginAuthForm.spec.tsx index 99f65f7c4e..0b3c7402cc 100644 --- a/client/src/components/Plugins/Store/__tests__/PluginAuthForm.spec.tsx +++ b/client/src/components/Plugins/Store/__tests__/PluginAuthForm.spec.tsx @@ -1,5 +1,5 @@ -import { render, screen } from 'test/layout-test-utils'; import userEvent from '@testing-library/user-event'; +import { render, screen } from 'test/layout-test-utils'; import PluginAuthForm from '../PluginAuthForm'; describe('PluginAuthForm', () => { @@ -76,4 +76,29 @@ describe('PluginAuthForm', () => { }, }); }); + + it('reflects an external saving state as a disabled, in-progress submit button', () => { + //@ts-ignore - dont need all props of plugin + render(); + + const button = screen.getByRole('button', { name: 'Saving...' }); + expect(button).toBeDisabled(); + expect(screen.queryByRole('button', { name: 'Save' })).not.toBeInTheDocument(); + }); + + it('renders a Cancel button when onCancel is provided and invokes it on click', async () => { + const onCancel = jest.fn(); + //@ts-ignore - dont need all props of plugin + render(); + + await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onCancel).toHaveBeenCalledTimes(1); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('does not render a Cancel button by default', () => { + //@ts-ignore - dont need all props of plugin + render(); + expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument(); + }); }); diff --git a/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/ToolSection.tsx b/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/ToolSection.tsx index 9a5745de71..c4e974ffa7 100644 --- a/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/ToolSection.tsx +++ b/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/ToolSection.tsx @@ -83,7 +83,15 @@ export default function ToolSection({ item }: Props) {
)} - {showForm && } + {showForm && ( + setEditing(false) : undefined} + onSubmit={handleSubmit} + /> + )} ); }