fix(frontend): save button always disabled on System Prompts page
Root cause: PromptEditor useEffect synced localPrompts back to match prompts after every keystroke, making isDirty() always false.
- Delegate disabled control to parent via hasChanges prop (no local sync)
- Derive currentPrompts synchronously to avoid empty-textarea flash
- Add key={selectedProfile} for clean remount on profile switch
- Update PromptEditor tests for new hasChanges prop
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
9f41a328e3
commit
0d3e8ce0ce
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { RotateCcw, AlertTriangle } from 'lucide-react'
|
import { RotateCcw, AlertTriangle } from 'lucide-react'
|
||||||
|
|
||||||
export interface PromptEditorProps {
|
export interface PromptEditorProps {
|
||||||
profileName: string
|
profileName: string
|
||||||
prompts: Record<string, string>
|
prompts: Record<string, string>
|
||||||
|
hasChanges: boolean
|
||||||
isSaving: boolean
|
isSaving: boolean
|
||||||
onUpdate: (step: string, template: string) => void
|
onUpdate: (step: string, template: string) => void
|
||||||
onSave: () => void
|
onSave: () => void
|
||||||
|
|
@ -33,6 +34,7 @@ const findUnknownPlaceholders = (template: string, stepKey: string): string[] =>
|
||||||
export const PromptEditor: React.FC<PromptEditorProps> = ({
|
export const PromptEditor: React.FC<PromptEditorProps> = ({
|
||||||
profileName,
|
profileName,
|
||||||
prompts,
|
prompts,
|
||||||
|
hasChanges,
|
||||||
isSaving,
|
isSaving,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onSave,
|
onSave,
|
||||||
|
|
@ -40,9 +42,9 @@ export const PromptEditor: React.FC<PromptEditorProps> = ({
|
||||||
onResetAll,
|
onResetAll,
|
||||||
onCancel,
|
onCancel,
|
||||||
}) => {
|
}) => {
|
||||||
const [localPrompts, setLocalPrompts] = useState<Record<string, string>>({})
|
const [localPrompts, setLocalPrompts] = useState<Record<string, string>>({ ...prompts })
|
||||||
|
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
setLocalPrompts({ ...prompts })
|
setLocalPrompts({ ...prompts })
|
||||||
}, [prompts])
|
}, [prompts])
|
||||||
|
|
||||||
|
|
@ -63,10 +65,6 @@ export const PromptEditor: React.FC<PromptEditorProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDirty = useCallback(() => {
|
|
||||||
return STEPS.some((step) => localPrompts[step.key] !== prompts[step.key])
|
|
||||||
}, [localPrompts, prompts])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm p-6 space-y-6">
|
<div className="bg-white rounded-lg border border-gray-200 shadow-sm p-6 space-y-6">
|
||||||
<div className="flex items-center gap-2 border-b border-gray-200 pb-3">
|
<div className="flex items-center gap-2 border-b border-gray-200 pb-3">
|
||||||
|
|
@ -136,7 +134,7 @@ export const PromptEditor: React.FC<PromptEditorProps> = ({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
disabled={isSaving || !isDirty()}
|
disabled={isSaving || !hasChanges}
|
||||||
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 transition-all duration-200"
|
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 transition-all duration-200"
|
||||||
>
|
>
|
||||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,9 @@ export const SystemPromptsPage: React.FC = () => {
|
||||||
setEditPrompts(profileData.prompts)
|
setEditPrompts(profileData.prompts)
|
||||||
setHasChanges(false)
|
setHasChanges(false)
|
||||||
}
|
}
|
||||||
}, [profileData])
|
}, [profileData]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const currentPrompts = hasChanges ? editPrompts : (profileData?.prompts ?? {})
|
||||||
|
|
||||||
const handleSelectProfile = (name: string) => {
|
const handleSelectProfile = (name: string) => {
|
||||||
if (selectedProfile === name) {
|
if (selectedProfile === name) {
|
||||||
|
|
@ -214,8 +216,10 @@ export const SystemPromptsPage: React.FC = () => {
|
||||||
<>
|
<>
|
||||||
<PlaceholderDocs />
|
<PlaceholderDocs />
|
||||||
<PromptEditor
|
<PromptEditor
|
||||||
|
key={selectedProfile}
|
||||||
profileName={selectedProfile}
|
profileName={selectedProfile}
|
||||||
prompts={editPrompts}
|
prompts={currentPrompts}
|
||||||
|
hasChanges={hasChanges}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const mockPrompts: Record<string, string> = {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
profileName: 'A',
|
profileName: 'A',
|
||||||
prompts: mockPrompts,
|
prompts: mockPrompts,
|
||||||
|
hasChanges: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
onUpdate: vi.fn(),
|
onUpdate: vi.fn(),
|
||||||
onSave: vi.fn(),
|
onSave: vi.fn(),
|
||||||
|
|
@ -85,11 +86,7 @@ describe('PromptEditor', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls onSave when "Save Changes" button is clicked', () => {
|
it('calls onSave when "Save Changes" button is clicked', () => {
|
||||||
render(<PromptEditor {...defaultProps} />)
|
render(<PromptEditor {...defaultProps} hasChanges={true} />)
|
||||||
|
|
||||||
// The Save button is disabled when not dirty, so we need to change something first
|
|
||||||
const textareas = screen.getAllByRole('textbox')
|
|
||||||
fireEvent.change(textareas[0], { target: { value: 'Modified decompose template' } })
|
|
||||||
|
|
||||||
const saveButton = screen.getByRole('button', { name: /save changes/i })
|
const saveButton = screen.getByRole('button', { name: /save changes/i })
|
||||||
expect(saveButton).not.toBeDisabled()
|
expect(saveButton).not.toBeDisabled()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue