Skip to content

Commit 4c1242d

Browse files
committed
Feat(Viewer): Allow to rename/move runs
1 parent 2f1381a commit 4c1242d

5 files changed

Lines changed: 288 additions & 3 deletions

File tree

.cursor/rules/viewer.mdc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ The application has two main pages:
268268
- Click elsewhere on game row: Navigate to viewer (supports middle-click)
269269
- "Open" button: Navigate to viewer (supports middle-click)
270270
- "↗" button: Always open viewer in new tab
271+
- "📁" button: Open move/rename dialog for individual folders
271272
* **Batch Operations**:
272273
- "Select All" checkbox to toggle all games
273274
- Styled action dropdown with custom arrow and hover effects for batch operations on selected games
@@ -279,6 +280,7 @@ The application has two main pages:
279280
### Layout
280281

281282
* Full-width table structure with six columns: Select, Game Session, Note, Rounds, Models, Action
283+
* **Action column includes**: Open, Open in new tab (↗), and Move/rename (📁) buttons for each game folder
282284
* Responsive design that adapts to mobile screens (hides some columns on mobile)
283285
* Hover effects and visual feedback
284286
* Base directory path display at the top
@@ -379,6 +381,33 @@ The game picker includes four batch operations for selected game folders, access
379381
- Validates subfolder name (no path separators)
380382
- Shows success message and refreshes page after operation
381383

384+
## Individual Move/Rename Functionality
385+
386+
Each game row in the picker includes a move/rename button (📁) that provides individual folder operations:
387+
388+
### Move/Rename Dialog
389+
- **Trigger**: Click the 📁 button next to any game folder
390+
- **Interface**: Minimal unstyled dialog with text input field
391+
- **Behavior**:
392+
- Pre-fills input with current full path
393+
- User can edit path to rename or move folder
394+
- Supports moving to new directory structures
395+
- **Keyboard shortcuts**:
396+
- `Escape`: Close dialog
397+
- `Enter`: Confirm move operation
398+
399+
### Move/Rename Operations
400+
- **Server endpoint**: `POST /move-folder`
401+
- **Path validation**: Ensures both source and target are within logs directory
402+
- **Intermediate folders**: Creates intermediate directories if necessary
403+
- **Conflict handling**: Prevents overwriting existing folders
404+
- **Error handling**: Provides clear error messages for invalid operations
405+
- **Success feedback**: Shows confirmation message and refreshes page
406+
407+
### Use Cases
408+
- **Simple rename**: Change folder name (e.g., `experiment1` → `experiment1_final`)
409+
- **Move to subdirectory**: Move folder to organized structure (e.g., `experiment1` → `completed/experiment1`)
410+
- **Reorganize structure**: Move folders between different directory hierarchies
382411

383412
### Important
384413

codeclash/viewer/app.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,4 +787,56 @@ def move_to_subfolder():
787787
return jsonify({"success": False, "error": str(e)})
788788

789789

790+
@app.route("/move-folder", methods=["POST"])
791+
def move_folder():
792+
"""Move/rename a single folder to a new path"""
793+
try:
794+
data = request.get_json()
795+
old_path = data.get("old_path", "")
796+
new_path = data.get("new_path", "")
797+
798+
if not old_path:
799+
return jsonify({"success": False, "error": "No old path provided"})
800+
801+
if not new_path:
802+
return jsonify({"success": False, "error": "No new path provided"})
803+
804+
# Convert to Path objects relative to LOG_BASE_DIR
805+
old_full_path = LOG_BASE_DIR / old_path
806+
new_full_path = LOG_BASE_DIR / new_path
807+
808+
# Validate old path exists
809+
if not old_full_path.exists():
810+
return jsonify({"success": False, "error": "Source folder does not exist"})
811+
812+
# Validate new path doesn't already exist
813+
if new_full_path.exists():
814+
return jsonify({"success": False, "error": "Target path already exists"})
815+
816+
# Security check: ensure both paths are within our expected logs directory
817+
try:
818+
old_full_path.relative_to(LOG_BASE_DIR)
819+
new_full_path.relative_to(LOG_BASE_DIR)
820+
except ValueError:
821+
return jsonify({"success": False, "error": "Invalid path - must be within logs directory"})
822+
823+
# Create intermediate directories if necessary
824+
new_full_path.parent.mkdir(parents=True, exist_ok=True)
825+
826+
# Perform the move
827+
old_full_path.rename(new_full_path)
828+
829+
return jsonify(
830+
{
831+
"success": True,
832+
"message": f"Successfully moved folder from '{old_path}' to '{new_path}'",
833+
"old_path": old_path,
834+
"new_path": new_path,
835+
}
836+
)
837+
838+
except Exception as e:
839+
return jsonify({"success": False, "error": str(e)})
840+
841+
790842
# Use run_viewer.py to launch the application

codeclash/viewer/static/css/picker.css

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,15 @@
111111
padding: 1rem;
112112
border-bottom: 1px solid var(--border-color);
113113
display: grid;
114-
grid-template-columns: 60px 2.5fr 3fr 100px 3fr 140px;
114+
grid-template-columns: 60px 2.5fr 3fr 100px 3fr 180px;
115115
gap: 1rem;
116116
font-weight: 600;
117117
color: var(--text-primary);
118118
}
119119

120120
.game-row {
121121
display: grid;
122-
grid-template-columns: 60px 2.5fr 3fr 100px 3fr 140px;
122+
grid-template-columns: 60px 2.5fr 3fr 100px 3fr 180px;
123123
gap: 1rem;
124124
padding: 0.75rem 1rem;
125125
border-bottom: 1px solid var(--border-color);
@@ -310,7 +310,7 @@
310310
display: flex;
311311
justify-content: center;
312312
gap: 0.5rem;
313-
min-width: 140px;
313+
min-width: 180px;
314314
}
315315

316316
.open-button {
@@ -347,6 +347,22 @@
347347
background-color: var(--accent-hover);
348348
}
349349

350+
.move-button {
351+
background-color: var(--warning-color);
352+
color: white;
353+
border: none;
354+
padding: 0.4rem 0.6rem;
355+
border-radius: 0.25rem;
356+
font-size: 0.8rem;
357+
font-weight: 500;
358+
cursor: pointer;
359+
transition: all 0.2s ease;
360+
}
361+
362+
.move-button:hover {
363+
filter: brightness(1.1);
364+
}
365+
350366
.no-games-message {
351367
text-align: center;
352368
padding: 3rem;
@@ -368,6 +384,91 @@
368384
z-index: 100;
369385
}
370386

387+
/* Move Dialog Styles */
388+
.move-dialog {
389+
position: fixed;
390+
top: 0;
391+
left: 0;
392+
width: 100%;
393+
height: 100%;
394+
background-color: rgba(0, 0, 0, 0.5);
395+
z-index: 1000;
396+
display: flex;
397+
align-items: center;
398+
justify-content: center;
399+
}
400+
401+
.move-dialog-content {
402+
background-color: var(--bg-primary);
403+
border: 1px solid var(--border-color);
404+
border-radius: 0.5rem;
405+
padding: 1.5rem;
406+
max-width: 500px;
407+
width: 90%;
408+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
409+
}
410+
411+
.move-dialog-content h3 {
412+
margin: 0 0 1rem 0;
413+
color: var(--text-primary);
414+
}
415+
416+
.move-dialog-content p {
417+
margin: 0 0 1rem 0;
418+
color: var(--text-secondary);
419+
}
420+
421+
.move-path-input {
422+
width: 100%;
423+
padding: 0.75rem;
424+
border: 2px solid var(--border-color);
425+
border-radius: 0.25rem;
426+
background-color: var(--bg-secondary);
427+
color: var(--text-primary);
428+
font-family: monospace;
429+
font-size: 0.9rem;
430+
margin-bottom: 1rem;
431+
}
432+
433+
.move-path-input:focus {
434+
outline: none;
435+
border-color: var(--accent-color);
436+
}
437+
438+
.move-dialog-buttons {
439+
display: flex;
440+
gap: 0.5rem;
441+
justify-content: flex-end;
442+
}
443+
444+
.move-dialog-buttons button {
445+
padding: 0.5rem 1rem;
446+
border: none;
447+
border-radius: 0.25rem;
448+
font-weight: 500;
449+
cursor: pointer;
450+
transition: all 0.2s ease;
451+
}
452+
453+
.move-dialog-buttons button:first-child {
454+
background-color: var(--success-color);
455+
color: white;
456+
}
457+
458+
.move-dialog-buttons button:first-child:hover {
459+
filter: brightness(1.1);
460+
}
461+
462+
.move-dialog-buttons button:last-child {
463+
background-color: var(--bg-secondary);
464+
color: var(--text-primary);
465+
border: 1px solid var(--border-color);
466+
}
467+
468+
.move-dialog-buttons button:last-child:hover {
469+
background-color: var(--bg-tertiary);
470+
}
471+
371472
/* Mobile responsiveness */
372473
@media (max-width: 768px) {
373474
.picker-container {

codeclash/viewer/static/js/picker.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,92 @@ function moveToSubfolder() {
412412
});
413413
}
414414

415+
// Move/Rename Dialog Variables
416+
let currentMovePath = "";
417+
418+
function showMoveDialog(gamePath) {
419+
currentMovePath = gamePath;
420+
const dialog = document.getElementById("move-dialog");
421+
const input = document.getElementById("move-path-input");
422+
423+
input.value = gamePath;
424+
dialog.style.display = "flex";
425+
426+
// Focus and select the input text
427+
setTimeout(() => {
428+
input.focus();
429+
input.select();
430+
}, 100);
431+
}
432+
433+
function cancelMove() {
434+
const dialog = document.getElementById("move-dialog");
435+
dialog.style.display = "none";
436+
currentMovePath = "";
437+
}
438+
439+
function confirmMove() {
440+
const input = document.getElementById("move-path-input");
441+
const newPath = input.value.trim();
442+
443+
if (!newPath) {
444+
alert("Please enter a valid path");
445+
return;
446+
}
447+
448+
if (newPath === currentMovePath) {
449+
cancelMove();
450+
return;
451+
}
452+
453+
// Send move request to server
454+
fetch("/move-folder", {
455+
method: "POST",
456+
headers: {
457+
"Content-Type": "application/json",
458+
},
459+
body: JSON.stringify({
460+
old_path: currentMovePath,
461+
new_path: newPath,
462+
}),
463+
})
464+
.then((response) => response.json())
465+
.then((data) => {
466+
if (data.success) {
467+
showCopyMessage(`Moved folder to: ${newPath}`);
468+
// Refresh the page to show updated folder structure
469+
setTimeout(() => window.location.reload(), 1500);
470+
} else {
471+
alert("Failed to move folder: " + data.error);
472+
}
473+
})
474+
.catch((err) => {
475+
console.error("Failed to move folder: ", err);
476+
alert("Failed to move folder. Please try again.");
477+
});
478+
479+
cancelMove();
480+
}
481+
482+
// Close dialog on Escape key
483+
document.addEventListener("keydown", function (event) {
484+
if (event.key === "Escape") {
485+
const dialog = document.getElementById("move-dialog");
486+
if (dialog.style.display === "flex") {
487+
cancelMove();
488+
}
489+
}
490+
});
491+
492+
// Close dialog when clicking outside the dialog content
493+
document
494+
.getElementById("move-dialog")
495+
.addEventListener("click", function (event) {
496+
if (event.target === this) {
497+
cancelMove();
498+
}
499+
});
500+
415501
// Initialize theme and other functionality on page load
416502
document.addEventListener("DOMContentLoaded", function () {
417503
initializeTheme();
@@ -420,4 +506,5 @@ document.addEventListener("DOMContentLoaded", function () {
420506
console.log("Available keyboard shortcuts:");
421507
console.log(" Ctrl/Cmd + D: Toggle dark mode");
422508
console.log(" Shift + Click: Range select checkboxes");
509+
console.log(" Escape: Close move dialog");
423510
});

codeclash/viewer/templates/picker.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ <h3>📁 Base Directory:</h3>
123123
<button class="open-new-tab-button" onclick="event.stopPropagation(); openGameInNewTab('{{ game.name }}')" title="Open in new tab">
124124
125125
</button>
126+
<button class="move-button" onclick="event.stopPropagation(); showMoveDialog('{{ game.name }}')" title="Move/rename">
127+
📁
128+
</button>
126129
{% else %}
127130
<span class="folder-indicator">Folder</span>
128131
{% endif %}
@@ -139,6 +142,19 @@ <h3>No Game Sessions Found</h3>
139142
{% endif %}
140143
</div>
141144

145+
<!-- Move/Rename Dialog -->
146+
<div id="move-dialog" class="move-dialog" style="display: none;">
147+
<div class="move-dialog-content">
148+
<h3>Move/Rename Folder</h3>
149+
<p>Edit the path to move or rename the folder:</p>
150+
<input type="text" id="move-path-input" class="move-path-input" />
151+
<div class="move-dialog-buttons">
152+
<button onclick="confirmMove()">Move</button>
153+
<button onclick="cancelMove()">Cancel</button>
154+
</div>
155+
</div>
156+
</div>
157+
142158
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
143159
<script src="{{ url_for('static', filename='js/picker.js') }}"></script>
144160
</body>

0 commit comments

Comments
 (0)