mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-21 21:31:12 +02:00
Add page-level save for scenarios
This commit is contained in:
parent
68cf6d6fd1
commit
00bd476b76
2 changed files with 87 additions and 52 deletions
|
|
@ -196,14 +196,9 @@ export default function SimulationApp() {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Refresh scenarios list and update only the modified scenario
|
// Just refresh the scenarios list without setting selected scenario
|
||||||
const updatedScenarios = await getScenarios(projectId as string);
|
const updatedScenarios = await getScenarios(projectId as string);
|
||||||
setScenarios(updatedScenarios);
|
setScenarios(updatedScenarios);
|
||||||
|
|
||||||
const refreshedScenario = updatedScenarios.find(s => s._id === updatedScenario._id);
|
|
||||||
if (refreshedScenario) {
|
|
||||||
setSelectedScenario(refreshedScenario);
|
|
||||||
}
|
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback, ChangeEvent } from 'react';
|
||||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
import { WithStringId } from '../../../../lib/types/types';
|
import { WithStringId } from '../../../../lib/types/types';
|
||||||
import { Scenario } from "../../../../lib/types/testing_types";
|
import { Scenario } from "../../../../lib/types/testing_types";
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { EditableField } from '../../../../lib/components/editable-field';
|
|
||||||
|
|
||||||
type ScenarioType = WithStringId<z.infer<typeof Scenario>>;
|
type ScenarioType = WithStringId<z.infer<typeof Scenario>>;
|
||||||
|
|
||||||
|
|
@ -17,73 +16,114 @@ interface ScenarioViewerProps {
|
||||||
|
|
||||||
export function ScenarioViewer({ scenario, onSave, onClose }: ScenarioViewerProps) {
|
export function ScenarioViewer({ scenario, onSave, onClose }: ScenarioViewerProps) {
|
||||||
const [editedScenario, setEditedScenario] = useState<ScenarioType>(scenario);
|
const [editedScenario, setEditedScenario] = useState<ScenarioType>(scenario);
|
||||||
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
|
|
||||||
// Reset state when scenario changes
|
// Reset state when scenario changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditedScenario(scenario);
|
setEditedScenario(scenario);
|
||||||
|
setIsDirty(false);
|
||||||
}, [scenario]);
|
}, [scenario]);
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof ScenarioType) => (value: string) => {
|
const handleChange = useCallback((field: keyof ScenarioType, event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
const updatedScenario = {
|
event.preventDefault();
|
||||||
...editedScenario,
|
const value = event.target.value;
|
||||||
|
|
||||||
|
setEditedScenario(prev => ({
|
||||||
|
...prev,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
};
|
}));
|
||||||
setEditedScenario(updatedScenario);
|
setIsDirty(true);
|
||||||
onSave(updatedScenario);
|
}, []);
|
||||||
};
|
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
onSave(editedScenario);
|
||||||
|
onClose();
|
||||||
|
}, [editedScenario, onSave, onClose]);
|
||||||
|
|
||||||
|
const adjustTextareaHeight = useCallback((element: HTMLTextAreaElement) => {
|
||||||
|
element.style.height = 'auto';
|
||||||
|
element.style.height = `${element.scrollHeight}px`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h1 className="text-2xl font-bold">Scenario Details</h1>
|
<h1 className="text-2xl font-bold">Scenario Details</h1>
|
||||||
<button
|
<div className="flex gap-2">
|
||||||
onClick={onClose}
|
{isDirty && (
|
||||||
className="p-2 rounded-full hover:bg-gray-100"
|
<button
|
||||||
title="Close"
|
onClick={handleSave}
|
||||||
>
|
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||||
<XMarkIcon className="h-5 w-5 text-gray-600" />
|
>
|
||||||
</button>
|
Save
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 rounded-full hover:bg-gray-100"
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-5 w-5 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<EditableField
|
<div className="flex flex-col">
|
||||||
label="NAME"
|
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">NAME</div>
|
||||||
value={editedScenario.name}
|
<input
|
||||||
onChange={handleFieldChange('name')}
|
type="text"
|
||||||
placeholder="Enter scenario name..."
|
value={editedScenario.name}
|
||||||
/>
|
onChange={(e) => handleChange('name', e)}
|
||||||
|
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-gray-200 my-4"></div>
|
<div className="border-t border-gray-200 my-4"></div>
|
||||||
|
|
||||||
<EditableField
|
<div className="flex flex-col">
|
||||||
label="DESCRIPTION"
|
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">DESCRIPTION</div>
|
||||||
value={editedScenario.description}
|
<textarea
|
||||||
onChange={handleFieldChange('description')}
|
value={editedScenario.description}
|
||||||
placeholder="Enter scenario description..."
|
onChange={(e) => handleChange('description', e)}
|
||||||
multiline
|
onInput={(e) => adjustTextareaHeight(e.target as HTMLTextAreaElement)}
|
||||||
markdown
|
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 min-h-[24px] resize-none"
|
||||||
/>
|
style={{ height: 'auto', minHeight: '24px' }}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-gray-200 my-4"></div>
|
<div className="border-t border-gray-200 my-4"></div>
|
||||||
|
|
||||||
<EditableField
|
<div className="flex flex-col">
|
||||||
label="CRITERIA"
|
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">CRITERIA</div>
|
||||||
value={editedScenario.criteria}
|
<textarea
|
||||||
onChange={handleFieldChange('criteria')}
|
value={editedScenario.criteria}
|
||||||
placeholder="Enter success criteria..."
|
onChange={(e) => handleChange('criteria', e)}
|
||||||
multiline
|
onInput={(e) => adjustTextareaHeight(e.target as HTMLTextAreaElement)}
|
||||||
markdown
|
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 min-h-[24px] resize-none"
|
||||||
/>
|
style={{ height: 'auto', minHeight: '24px' }}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-gray-200 my-4"></div>
|
<div className="border-t border-gray-200 my-4"></div>
|
||||||
|
|
||||||
<EditableField
|
<div className="flex flex-col">
|
||||||
label="CONTEXT"
|
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">CONTEXT</div>
|
||||||
value={editedScenario.context}
|
<textarea
|
||||||
onChange={handleFieldChange('context')}
|
value={editedScenario.context}
|
||||||
placeholder="Enter scenario context..."
|
onChange={(e) => handleChange('context', e)}
|
||||||
multiline
|
onInput={(e) => adjustTextareaHeight(e.target as HTMLTextAreaElement)}
|
||||||
markdown
|
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 min-h-[24px] resize-none"
|
||||||
/>
|
style={{ height: 'auto', minHeight: '24px' }}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue