mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-11 00:02:38 +02:00
Update debounce logic for assistant renaming
This commit is contained in:
parent
0a6794a467
commit
b4b862c8e0
2 changed files with 64 additions and 37 deletions
|
|
@ -8,6 +8,7 @@ interface TopBarProps {
|
||||||
localProjectName: string;
|
localProjectName: string;
|
||||||
projectNameError: string | null;
|
projectNameError: string | null;
|
||||||
onProjectNameChange: (value: string) => void;
|
onProjectNameChange: (value: string) => void;
|
||||||
|
onProjectNameCommit: (value: string) => void;
|
||||||
publishing: boolean;
|
publishing: boolean;
|
||||||
isLive: boolean;
|
isLive: boolean;
|
||||||
showCopySuccess: boolean;
|
showCopySuccess: boolean;
|
||||||
|
|
@ -29,6 +30,7 @@ export function TopBar({
|
||||||
localProjectName,
|
localProjectName,
|
||||||
projectNameError,
|
projectNameError,
|
||||||
onProjectNameChange,
|
onProjectNameChange,
|
||||||
|
onProjectNameCommit,
|
||||||
publishing,
|
publishing,
|
||||||
isLive,
|
isLive,
|
||||||
showCopySuccess,
|
showCopySuccess,
|
||||||
|
|
@ -58,6 +60,12 @@ export function TopBar({
|
||||||
type="text"
|
type="text"
|
||||||
value={localProjectName}
|
value={localProjectName}
|
||||||
onChange={(e) => onProjectNameChange(e.target.value)}
|
onChange={(e) => onProjectNameChange(e.target.value)}
|
||||||
|
onBlur={() => onProjectNameCommit(localProjectName)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
isInvalid={!!projectNameError}
|
isInvalid={!!projectNameError}
|
||||||
errorMessage={projectNameError}
|
errorMessage={projectNameError}
|
||||||
placeholder="Project name..."
|
placeholder="Project name..."
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { Copilot } from "../copilot/app";
|
||||||
import { publishWorkflow } from "@/app/actions/project.actions";
|
import { publishWorkflow } from "@/app/actions/project.actions";
|
||||||
import { saveWorkflow } from "@/app/actions/project.actions";
|
import { saveWorkflow } from "@/app/actions/project.actions";
|
||||||
import { updateProjectName } from "@/app/actions/project.actions";
|
import { updateProjectName } from "@/app/actions/project.actions";
|
||||||
|
import { listProjects } from "@/app/actions/project.actions";
|
||||||
import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons";
|
import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons";
|
||||||
import { CopyIcon, ImportIcon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, XIcon, SettingsIcon, ChevronDownIcon, PhoneIcon, MessageCircleIcon, ZapIcon } from "lucide-react";
|
import { CopyIcon, ImportIcon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, XIcon, SettingsIcon, ChevronDownIcon, PhoneIcon, MessageCircleIcon, ZapIcon } from "lucide-react";
|
||||||
import { EntityList } from "./entity_list";
|
import { EntityList } from "./entity_list";
|
||||||
|
|
@ -887,8 +888,8 @@ export function WorkflowEditor({
|
||||||
// Project name state
|
// Project name state
|
||||||
const [localProjectName, setLocalProjectName] = useState<string>(projectConfig.name || '');
|
const [localProjectName, setLocalProjectName] = useState<string>(projectConfig.name || '');
|
||||||
const [projectNameError, setProjectNameError] = useState<string | null>(null);
|
const [projectNameError, setProjectNameError] = useState<string | null>(null);
|
||||||
const editingNameRef = useRef<string | null>(null);
|
const [isEditingProjectName, setIsEditingProjectName] = useState<boolean>(false);
|
||||||
const projectNameDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const [pendingProjectName, setPendingProjectName] = useState<string | null>(null);
|
||||||
|
|
||||||
// Load agent order from localStorage on mount
|
// Load agent order from localStorage on mount
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
|
|
@ -1182,12 +1183,23 @@ export function WorkflowEditor({
|
||||||
}
|
}
|
||||||
}, [state.present.workflow, state.present.pendingChanges, processQueue, state]);
|
}, [state.present.workflow, state.present.pendingChanges, processQueue, state]);
|
||||||
|
|
||||||
// Sync project name when projectConfig changes, but not while actively editing
|
// Sync project name from server when not editing and no pending commit in-flight
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingNameRef.current === null) {
|
if (!isEditingProjectName && pendingProjectName === null) {
|
||||||
setLocalProjectName(projectConfig.name || '');
|
setLocalProjectName(projectConfig.name || '');
|
||||||
}
|
}
|
||||||
}, [projectConfig.name]);
|
}, [projectConfig.name, isEditingProjectName, pendingProjectName]);
|
||||||
|
|
||||||
|
// When a commit is pending, wait until server reflects it to clear the lock
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
pendingProjectName &&
|
||||||
|
(projectConfig.name || '').trim().toLowerCase() === pendingProjectName.trim().toLowerCase()
|
||||||
|
) {
|
||||||
|
setPendingProjectName(null);
|
||||||
|
setLocalProjectName(projectConfig.name || '');
|
||||||
|
}
|
||||||
|
}, [projectConfig.name, pendingProjectName]);
|
||||||
|
|
||||||
function handlePlaygroundClick() {
|
function handlePlaygroundClick() {
|
||||||
setIsInitialState(false);
|
setIsInitialState(false);
|
||||||
|
|
@ -1204,41 +1216,47 @@ export function WorkflowEditor({
|
||||||
|
|
||||||
const handleProjectNameChange = (value: string) => {
|
const handleProjectNameChange = (value: string) => {
|
||||||
setLocalProjectName(value);
|
setLocalProjectName(value);
|
||||||
editingNameRef.current = value;
|
setIsEditingProjectName(true);
|
||||||
|
// Do not validate or save on every keystroke
|
||||||
if (projectNameDebounceRef.current) {
|
|
||||||
clearTimeout(projectNameDebounceRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
projectNameDebounceRef.current = setTimeout(async () => {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!validateProjectName(trimmed)) {
|
|
||||||
editingNameRef.current = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (trimmed !== (projectConfig.name || '')) {
|
|
||||||
await updateProjectName(projectId, trimmed);
|
|
||||||
onProjectConfigUpdated?.();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setProjectNameError("Failed to update project name");
|
|
||||||
console.error('Failed to update project name:', error);
|
|
||||||
} finally {
|
|
||||||
editingNameRef.current = null;
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear any pending debounce on unmount
|
const handleProjectNameCommit = async (value: string) => {
|
||||||
useEffect(() => {
|
const trimmed = value.trim();
|
||||||
return () => {
|
// If unchanged, just clear editing state
|
||||||
if (projectNameDebounceRef.current) {
|
if (trimmed === (projectConfig.name || '')) {
|
||||||
clearTimeout(projectNameDebounceRef.current);
|
setProjectNameError(null);
|
||||||
|
setIsEditingProjectName(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateProjectName(trimmed)) {
|
||||||
|
setIsEditingProjectName(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate uniqueness against other projects (case-insensitive)
|
||||||
|
const projects = await listProjects();
|
||||||
|
const isDuplicate = projects.some(p => ((p as any).id ?? (p as any)._id) !== projectId && (p.name || '').trim().toLowerCase() === trimmed.toLowerCase());
|
||||||
|
if (isDuplicate) {
|
||||||
|
setProjectNameError("This name is already taken by another project");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
// Lock local sync until server reflects the change
|
||||||
}, []);
|
setPendingProjectName(trimmed);
|
||||||
|
await updateProjectName(projectId, trimmed);
|
||||||
|
onProjectConfigUpdated?.();
|
||||||
|
setProjectNameError(null);
|
||||||
|
} catch (error) {
|
||||||
|
setProjectNameError("Failed to update project name");
|
||||||
|
console.error('Failed to update project name:', error);
|
||||||
|
// Clear pending state so we resync from server
|
||||||
|
setPendingProjectName(null);
|
||||||
|
setLocalProjectName(projectConfig.name || '');
|
||||||
|
} finally {
|
||||||
|
setIsEditingProjectName(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntitySelectionContext.Provider value={{
|
<EntitySelectionContext.Provider value={{
|
||||||
|
|
@ -1252,6 +1270,7 @@ export function WorkflowEditor({
|
||||||
localProjectName={localProjectName}
|
localProjectName={localProjectName}
|
||||||
projectNameError={projectNameError}
|
projectNameError={projectNameError}
|
||||||
onProjectNameChange={handleProjectNameChange}
|
onProjectNameChange={handleProjectNameChange}
|
||||||
|
onProjectNameCommit={handleProjectNameCommit}
|
||||||
publishing={state.present.publishing}
|
publishing={state.present.publishing}
|
||||||
isLive={isLive}
|
isLive={isLive}
|
||||||
showCopySuccess={showCopySuccess}
|
showCopySuccess={showCopySuccess}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue