mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
feat: add file renaming functionality in ContextView
- Implemented a rename dialog for files, allowing users to rename selected context files. - Added state management for the rename dialog and file name input. - Enhanced file handling to check for existing names and update file paths accordingly. - Updated UI to include a pencil icon for triggering the rename action on files. - Improved user experience by ensuring the renamed file is selected after the operation.
This commit is contained in:
@@ -328,8 +328,8 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute px-2 rounded-md z-10",
|
"absolute px-2 py-1 text-sm font-bold rounded-md flex items-center justify-center z-10",
|
||||||
"top-2 left-2",
|
"top-2 left-2 min-w-[36px]",
|
||||||
feature.priority === 1 &&
|
feature.priority === 1 &&
|
||||||
"bg-red-500/20 text-red-500 border-2 border-red-500/50",
|
"bg-red-500/20 text-red-500 border-2 border-red-500/50",
|
||||||
feature.priority === 2 &&
|
feature.priority === 2 &&
|
||||||
@@ -337,22 +337,9 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
feature.priority === 3 &&
|
feature.priority === 3 &&
|
||||||
"bg-blue-500/20 text-blue-500 border-2 border-blue-500/50"
|
"bg-blue-500/20 text-blue-500 border-2 border-blue-500/50"
|
||||||
)}
|
)}
|
||||||
style={{ height: "28px" }}
|
|
||||||
data-testid={`priority-badge-${feature.id}`}
|
data-testid={`priority-badge-${feature.id}`}
|
||||||
>
|
>
|
||||||
{Array.from({ length: 4 - feature.priority }).map((_, i) => (
|
P{feature.priority}
|
||||||
<ChevronUp
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
top: `${2 + i * 3}px`,
|
|
||||||
width: "12px",
|
|
||||||
height: "12px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" className="text-xs">
|
<TooltipContent side="right" className="text-xs">
|
||||||
|
|||||||
@@ -788,8 +788,14 @@ export function useBoardActions({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort by priority (lower number = higher priority, priority 1 is highest)
|
||||||
|
// This matches the auto mode service behavior for consistency
|
||||||
|
const sortedBacklog = [...backlogFeatures].sort(
|
||||||
|
(a, b) => (a.priority || 999) - (b.priority || 999)
|
||||||
|
);
|
||||||
|
|
||||||
// Start only one feature per keypress (user must press again for next)
|
// Start only one feature per keypress (user must press again for next)
|
||||||
const featuresToStart = backlogFeatures.slice(0, 1);
|
const featuresToStart = sortedBacklog.slice(0, 1);
|
||||||
|
|
||||||
for (const feature of featuresToStart) {
|
for (const feature of featuresToStart) {
|
||||||
// Only create worktrees if the feature is enabled
|
// Only create worktrees if the feature is enabled
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
BookOpen,
|
BookOpen,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
Eye,
|
Eye,
|
||||||
|
Pencil,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
useKeyboardShortcuts,
|
useKeyboardShortcuts,
|
||||||
@@ -56,6 +57,8 @@ export function ContextView() {
|
|||||||
const [editedContent, setEditedContent] = useState("");
|
const [editedContent, setEditedContent] = useState("");
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
|
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||||
|
const [renameFileName, setRenameFileName] = useState("");
|
||||||
const [newFileName, setNewFileName] = useState("");
|
const [newFileName, setNewFileName] = useState("");
|
||||||
const [newFileType, setNewFileType] = useState<"text" | "image">("text");
|
const [newFileType, setNewFileType] = useState<"text" | "image">("text");
|
||||||
const [uploadedImageData, setUploadedImageData] = useState<string | null>(
|
const [uploadedImageData, setUploadedImageData] = useState<string | null>(
|
||||||
@@ -240,6 +243,60 @@ export function ContextView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Rename selected file
|
||||||
|
const handleRenameFile = async () => {
|
||||||
|
const contextPath = getContextPath();
|
||||||
|
if (!selectedFile || !contextPath || !renameFileName.trim()) return;
|
||||||
|
|
||||||
|
const newName = renameFileName.trim();
|
||||||
|
if (newName === selectedFile.name) {
|
||||||
|
setIsRenameDialogOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
const newPath = `${contextPath}/${newName}`;
|
||||||
|
|
||||||
|
// Check if file with new name already exists
|
||||||
|
const exists = await api.exists(newPath);
|
||||||
|
if (exists) {
|
||||||
|
console.error("A file with this name already exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current file content
|
||||||
|
const result = await api.readFile(selectedFile.path);
|
||||||
|
if (!result.success || result.content === undefined) {
|
||||||
|
console.error("Failed to read file for rename");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to new path
|
||||||
|
await api.writeFile(newPath, result.content);
|
||||||
|
|
||||||
|
// Delete old file
|
||||||
|
await api.deleteFile(selectedFile.path);
|
||||||
|
|
||||||
|
setIsRenameDialogOpen(false);
|
||||||
|
setRenameFileName("");
|
||||||
|
|
||||||
|
// Reload files and select the renamed file
|
||||||
|
await loadContextFiles();
|
||||||
|
|
||||||
|
// Update selected file with new name and path
|
||||||
|
const renamedFile: ContextFile = {
|
||||||
|
name: newName,
|
||||||
|
type: isImageFile(newName) ? "image" : "text",
|
||||||
|
path: newPath,
|
||||||
|
content: result.content,
|
||||||
|
};
|
||||||
|
setSelectedFile(renamedFile);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to rename file:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle image upload
|
// Handle image upload
|
||||||
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
@@ -418,24 +475,40 @@ export function ContextView() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{contextFiles.map((file) => (
|
{contextFiles.map((file) => (
|
||||||
<button
|
<div
|
||||||
key={file.path}
|
key={file.path}
|
||||||
onClick={() => handleSelectFile(file)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left transition-colors",
|
"group w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-colors",
|
||||||
selectedFile?.path === file.path
|
selectedFile?.path === file.path
|
||||||
? "bg-primary/20 text-foreground border border-primary/30"
|
? "bg-primary/20 text-foreground border border-primary/30"
|
||||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
data-testid={`context-file-${file.name}`}
|
|
||||||
>
|
>
|
||||||
{file.type === "image" ? (
|
<button
|
||||||
<ImageIcon className="w-4 h-4 flex-shrink-0" />
|
onClick={() => handleSelectFile(file)}
|
||||||
) : (
|
className="flex-1 flex items-center gap-2 text-left min-w-0"
|
||||||
<FileText className="w-4 h-4 flex-shrink-0" />
|
data-testid={`context-file-${file.name}`}
|
||||||
)}
|
>
|
||||||
<span className="truncate text-sm">{file.name}</span>
|
{file.type === "image" ? (
|
||||||
</button>
|
<ImageIcon className="w-4 h-4 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<FileText className="w-4 h-4 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
<span className="truncate text-sm">{file.name}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setRenameFileName(file.name);
|
||||||
|
setSelectedFile(file);
|
||||||
|
setIsRenameDialogOpen(true);
|
||||||
|
}}
|
||||||
|
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-accent rounded transition-opacity"
|
||||||
|
data-testid={`rename-context-file-${file.name}`}
|
||||||
|
>
|
||||||
|
<Pencil className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -730,6 +803,53 @@ export function ContextView() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Rename Dialog */}
|
||||||
|
<Dialog open={isRenameDialogOpen} onOpenChange={setIsRenameDialogOpen}>
|
||||||
|
<DialogContent data-testid="rename-context-dialog">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Rename Context File</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Enter a new name for "{selectedFile?.name}".
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="rename-filename">File Name</Label>
|
||||||
|
<Input
|
||||||
|
id="rename-filename"
|
||||||
|
value={renameFileName}
|
||||||
|
onChange={(e) => setRenameFileName(e.target.value)}
|
||||||
|
placeholder="Enter new filename"
|
||||||
|
data-testid="rename-file-input"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && renameFileName.trim()) {
|
||||||
|
handleRenameFile();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsRenameDialogOpen(false);
|
||||||
|
setRenameFileName("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleRenameFile}
|
||||||
|
disabled={!renameFileName.trim() || renameFileName === selectedFile?.name}
|
||||||
|
data-testid="confirm-rename-file"
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,8 +139,15 @@ test.describe("Feature Lifecycle Tests", () => {
|
|||||||
// Perform the drag and drop using dnd-kit compatible method
|
// Perform the drag and drop using dnd-kit compatible method
|
||||||
await dragAndDropWithDndKit(page, dragHandle, inProgressColumn);
|
await dragAndDropWithDndKit(page, dragHandle, inProgressColumn);
|
||||||
|
|
||||||
// Wait for the feature to move to in_progress
|
// First verify that the drag succeeded by checking for in_progress status
|
||||||
await page.waitForTimeout(500);
|
// This helps diagnose if the drag-drop is working or not
|
||||||
|
await expect(async () => {
|
||||||
|
const featureData = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(featuresDir, featureId, "feature.json"), "utf-8")
|
||||||
|
);
|
||||||
|
// Feature should be either in_progress (agent running) or waiting_approval (agent done)
|
||||||
|
expect(["in_progress", "waiting_approval"]).toContain(featureData.status);
|
||||||
|
}).toPass({ timeout: 15000 });
|
||||||
|
|
||||||
// The mock agent should complete quickly (about 1.3 seconds based on the sleep times)
|
// The mock agent should complete quickly (about 1.3 seconds based on the sleep times)
|
||||||
// Wait for the feature to move to waiting_approval (manual review)
|
// Wait for the feature to move to waiting_approval (manual review)
|
||||||
@@ -349,15 +356,16 @@ test.describe("Feature Lifecycle Tests", () => {
|
|||||||
|
|
||||||
await dragAndDropWithDndKit(page, dragHandle, inProgressColumn);
|
await dragAndDropWithDndKit(page, dragHandle, inProgressColumn);
|
||||||
|
|
||||||
// Wait for the feature to be in in_progress
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Verify feature file still exists and is readable
|
// Verify feature file still exists and is readable
|
||||||
const featureFilePath = path.join(featuresDir, testFeatureId, "feature.json");
|
const featureFilePath = path.join(featuresDir, testFeatureId, "feature.json");
|
||||||
expect(fs.existsSync(featureFilePath)).toBe(true);
|
expect(fs.existsSync(featureFilePath)).toBe(true);
|
||||||
|
|
||||||
// Wait a bit for the agent to start
|
// First verify that the drag succeeded by checking for in_progress status
|
||||||
await page.waitForTimeout(1000);
|
await expect(async () => {
|
||||||
|
const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||||
|
// Feature should be either in_progress (agent running) or waiting_approval (agent done)
|
||||||
|
expect(["in_progress", "waiting_approval"]).toContain(featureData.status);
|
||||||
|
}).toPass({ timeout: 15000 });
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Step 3: Wait for the mock agent to complete (it's fast in mock mode)
|
// Step 3: Wait for the mock agent to complete (it's fast in mock mode)
|
||||||
@@ -421,8 +429,15 @@ test.describe("Feature Lifecycle Tests", () => {
|
|||||||
// Drag to in_progress to restart
|
// Drag to in_progress to restart
|
||||||
await dragAndDropWithDndKit(page, restartDragHandle, inProgressColumnRestart);
|
await dragAndDropWithDndKit(page, restartDragHandle, inProgressColumnRestart);
|
||||||
|
|
||||||
// Wait for the feature to be processed
|
// Verify the feature file still exists
|
||||||
await page.waitForTimeout(2000);
|
expect(fs.existsSync(featureFilePath)).toBe(true);
|
||||||
|
|
||||||
|
// First verify that the restart drag succeeded by checking for in_progress status
|
||||||
|
await expect(async () => {
|
||||||
|
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||||
|
// Feature should be either in_progress (agent running) or waiting_approval (agent done)
|
||||||
|
expect(["in_progress", "waiting_approval"]).toContain(data.status);
|
||||||
|
}).toPass({ timeout: 15000 });
|
||||||
|
|
||||||
// Verify no "Feature not found" errors in console
|
// Verify no "Feature not found" errors in console
|
||||||
const featureNotFoundErrors = consoleErrors.filter(
|
const featureNotFoundErrors = consoleErrors.filter(
|
||||||
@@ -430,9 +445,6 @@ test.describe("Feature Lifecycle Tests", () => {
|
|||||||
);
|
);
|
||||||
expect(featureNotFoundErrors).toEqual([]);
|
expect(featureNotFoundErrors).toEqual([]);
|
||||||
|
|
||||||
// Verify the feature file still exists
|
|
||||||
expect(fs.existsSync(featureFilePath)).toBe(true);
|
|
||||||
|
|
||||||
// Wait for the mock agent to complete and move to waiting_approval
|
// Wait for the mock agent to complete and move to waiting_approval
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||||
|
|||||||
@@ -3,12 +3,22 @@ import { Page, Locator } from "@playwright/test";
|
|||||||
/**
|
/**
|
||||||
* Perform a drag and drop operation that works with @dnd-kit
|
* Perform a drag and drop operation that works with @dnd-kit
|
||||||
* This uses explicit mouse movements with pointer events
|
* This uses explicit mouse movements with pointer events
|
||||||
|
*
|
||||||
|
* NOTE: dnd-kit requires careful timing for drag activation. In CI environments,
|
||||||
|
* we need longer delays and more movement steps for reliable detection.
|
||||||
*/
|
*/
|
||||||
export async function dragAndDropWithDndKit(
|
export async function dragAndDropWithDndKit(
|
||||||
page: Page,
|
page: Page,
|
||||||
sourceLocator: Locator,
|
sourceLocator: Locator,
|
||||||
targetLocator: Locator
|
targetLocator: Locator
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// Ensure elements are visible and stable before getting bounding boxes
|
||||||
|
await sourceLocator.waitFor({ state: "visible", timeout: 5000 });
|
||||||
|
await targetLocator.waitFor({ state: "visible", timeout: 5000 });
|
||||||
|
|
||||||
|
// Small delay to ensure layout is stable
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
|
||||||
const sourceBox = await sourceLocator.boundingBox();
|
const sourceBox = await sourceLocator.boundingBox();
|
||||||
const targetBox = await targetLocator.boundingBox();
|
const targetBox = await targetLocator.boundingBox();
|
||||||
|
|
||||||
@@ -24,11 +34,29 @@ export async function dragAndDropWithDndKit(
|
|||||||
const endX = targetBox.x + targetBox.width / 2;
|
const endX = targetBox.x + targetBox.width / 2;
|
||||||
const endY = targetBox.y + targetBox.height / 2;
|
const endY = targetBox.y + targetBox.height / 2;
|
||||||
|
|
||||||
// Perform the drag and drop with pointer events
|
// Move to source element first
|
||||||
await page.mouse.move(startX, startY);
|
await page.mouse.move(startX, startY);
|
||||||
|
await page.waitForTimeout(50);
|
||||||
|
|
||||||
|
// Press and hold - dnd-kit needs time to activate the drag sensor
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.waitForTimeout(150); // Give dnd-kit time to recognize the drag
|
await page.waitForTimeout(300); // Longer delay for CI - dnd-kit activation threshold
|
||||||
await page.mouse.move(endX, endY, { steps: 15 });
|
|
||||||
await page.waitForTimeout(100); // Allow time for drop detection
|
// Move slightly first to trigger drag detection (dnd-kit has a distance threshold)
|
||||||
|
const smallMoveX = startX + 10;
|
||||||
|
const smallMoveY = startY + 10;
|
||||||
|
await page.mouse.move(smallMoveX, smallMoveY, { steps: 3 });
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
|
||||||
|
// Now move to target with slower, more deliberate movement
|
||||||
|
await page.mouse.move(endX, endY, { steps: 25 });
|
||||||
|
|
||||||
|
// Pause over target for drop detection
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
|
||||||
|
// Release
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
|
|
||||||
|
// Allow time for the drop handler to process
|
||||||
|
await page.waitForTimeout(100);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user