openclaw/apps/web/app/components/workspace/inline-rename.tsx
kumarabhirup 6d8623b00f
Dench workspace: file manager, relation resolution, and chat refactor
File Manager & Filesystem Operations:
- Add FileManagerTree component with drag-and-drop (dnd-kit), inline
  rename, right-click context menu, and compact sidebar mode
- Add context-menu component (open, new file/folder, rename, duplicate,
  copy, paste, move, delete) rendered via portal
- Add InlineRename component with validation and shake-on-error animation
- Add useWorkspaceWatcher hook with SSE live-reload and polling fallback
- Add API routes: mkdir, rename, copy, move, watch (SSE file-change
  events), and DELETE on /api/workspace/file with system-file protection
- Add safeResolveNewPath and isSystemFile helpers to workspace lib
- Replace inline WorkspaceTreeNode in sidebar with shared FileManagerTree
  (compact mode), add workspace refresh callback

Object Relation Resolution:
- Resolve relation fields to human-readable display labels server-side
  (resolveRelationLabels, resolveDisplayField helpers)
- Add reverse relation discovery (findReverseRelations) — surfaces
  incoming links from other objects
- Add display_field column migration (idempotent ALTER TABLE) and
  PATCH /api/workspace/objects/[name]/display-field endpoint
- Enrich object API response with relationLabels, reverseRelations,
  effectiveDisplayField, and related_object_name per field
- Add RelationCell, RelationChip, ReverseRelationCell, LinkIcon
  components to object-table with clickable cross-object navigation
- Add relation label rendering to kanban cards
- Extract ObjectView component in workspace page with display-field
  selector dropdown and relation/reverse-relation badge counts

Chat Panel Extraction:
- Extract chat logic from page.tsx into standalone ChatPanel component
  with forwardRef/useImperativeHandle for session control
- ChatPanel supports file-scoped sessions (filePath param) and
  context-aware file chat sidebar
- Simplify page.tsx to thin orchestrator delegating to ChatPanel
- Add filePath filter to GET /api/web-sessions for scoped session lists

Dependencies:
- Add @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities
- Add duckdbExec and parseRelationValue to workspace lib

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 19:22:53 -08:00

104 lines
2.7 KiB
TypeScript

"use client";
import { useState, useRef, useEffect, useCallback } from "react";
type InlineRenameProps = {
currentName: string;
onCommit: (newName: string) => void;
onCancel: () => void;
};
/**
* Inline text input that replaces a tree node label for renaming.
* Commits on Enter or blur, cancels on Escape.
* Shows a shake animation on validation error.
*/
export function InlineRename({ currentName, onCommit, onCancel }: InlineRenameProps) {
const [value, setValue] = useState(currentName);
const [error, setError] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
// Auto-focus and select the name (without extension)
useEffect(() => {
const input = inputRef.current;
if (!input) {return;}
input.focus();
const dotIndex = currentName.lastIndexOf(".");
if (dotIndex > 0) {
input.setSelectionRange(0, dotIndex);
} else {
input.select();
}
}, [currentName]);
const validate = useCallback(
(name: string): boolean => {
const trimmed = name.trim();
if (!trimmed) {return false;}
if (trimmed.includes("/") || trimmed.includes("\\")) {return false;}
return true;
},
[],
);
const handleCommit = useCallback(() => {
const trimmed = value.trim();
if (!validate(trimmed)) {
setError(true);
setTimeout(() => setError(false), 500);
return;
}
if (trimmed === currentName) {
onCancel();
return;
}
onCommit(trimmed);
}, [value, currentName, validate, onCommit, onCancel]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
e.stopPropagation();
if (e.key === "Enter") {
e.preventDefault();
handleCommit();
} else if (e.key === "Escape") {
e.preventDefault();
onCancel();
}
},
[handleCommit, onCancel],
);
return (
<input
ref={inputRef}
type="text"
value={value}
onChange={(e) => {
setValue(e.target.value);
setError(false);
}}
onBlur={handleCommit}
onKeyDown={handleKeyDown}
className="flex-1 text-sm rounded px-1 py-0 outline-none min-w-0"
style={{
background: "var(--color-bg)",
color: "var(--color-text)",
border: error ? "1px solid #ef4444" : "1px solid var(--color-accent)",
animation: error ? "renameShake 300ms ease" : undefined,
}}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
/>
);
}
/** Shake animation style (injected once globally via the FileManagerTree) */
export const RENAME_SHAKE_STYLE = `
@keyframes renameShake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-3px); }
40%, 80% { transform: translateX(3px); }
}
`;