feat: file edit added
This commit is contained in:
@@ -23,6 +23,11 @@ let state = {
|
||||
};
|
||||
const ROW_JUMP_STEP = 10;
|
||||
let wildcardDialogMode = "select";
|
||||
let editorState = {
|
||||
path: null,
|
||||
originalContent: "",
|
||||
modified: null,
|
||||
};
|
||||
|
||||
function paneState(pane) {
|
||||
return state.panes[pane];
|
||||
@@ -70,6 +75,20 @@ function viewerElements() {
|
||||
};
|
||||
}
|
||||
|
||||
function editorElements() {
|
||||
return {
|
||||
overlay: document.getElementById("editor-modal"),
|
||||
title: document.getElementById("editor-title"),
|
||||
fileName: document.getElementById("editor-file-name"),
|
||||
filePath: document.getElementById("editor-file-path"),
|
||||
error: document.getElementById("editor-error"),
|
||||
content: document.getElementById("editor-content"),
|
||||
closeButton: document.getElementById("editor-close-btn"),
|
||||
saveButton: document.getElementById("editor-save-btn"),
|
||||
cancelButton: document.getElementById("editor-cancel-btn"),
|
||||
};
|
||||
}
|
||||
|
||||
async function apiRequest(method, url, body) {
|
||||
const options = { method, headers: {} };
|
||||
if (body !== undefined) {
|
||||
@@ -158,12 +177,25 @@ function updateActionButtons() {
|
||||
const exactlyOne = count === 1;
|
||||
const allFiles = hasSelection && selectedItems.every((item) => item.kind === "file");
|
||||
document.getElementById("view-btn").disabled = !exactlyOne || !allFiles;
|
||||
document.getElementById("edit-btn").disabled = !exactlyOne || !allFiles || !isEditableSelection(selectedItems[0] || null);
|
||||
document.getElementById("rename-btn").disabled = !exactlyOne;
|
||||
document.getElementById("delete-btn").disabled = !hasSelection;
|
||||
document.getElementById("copy-btn").disabled = !allFiles;
|
||||
document.getElementById("move-btn").disabled = !allFiles;
|
||||
}
|
||||
|
||||
function isEditableSelection(item) {
|
||||
if (!item || item.kind !== "file") {
|
||||
return false;
|
||||
}
|
||||
const name = item.name || "";
|
||||
const lower = name.toLowerCase();
|
||||
if (lower === "dockerfile" || lower === "containerfile") {
|
||||
return true;
|
||||
}
|
||||
return [".txt", ".log", ".md", ".yml", ".yaml", ".json", ".js", ".css", ".html"].some((suffix) => lower.endsWith(suffix));
|
||||
}
|
||||
|
||||
function currentParentPath(path) {
|
||||
if (!path.includes("/")) {
|
||||
return null;
|
||||
@@ -650,6 +682,10 @@ function isViewerOpen() {
|
||||
return !viewerElements().overlay.classList.contains("hidden");
|
||||
}
|
||||
|
||||
function isEditorOpen() {
|
||||
return !editorElements().overlay.classList.contains("hidden");
|
||||
}
|
||||
|
||||
function escapeRegExp(text) {
|
||||
return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
||||
}
|
||||
@@ -739,6 +775,33 @@ function closeViewer() {
|
||||
viewer.content.textContent = "";
|
||||
}
|
||||
|
||||
function editorIsDirty() {
|
||||
return editorElements().content.value !== editorState.originalContent;
|
||||
}
|
||||
|
||||
function resetEditorState() {
|
||||
editorState = {
|
||||
path: null,
|
||||
originalContent: "",
|
||||
modified: null,
|
||||
};
|
||||
}
|
||||
|
||||
function attemptCloseEditor() {
|
||||
if (editorIsDirty() && !window.confirm("Discard unsaved changes?")) {
|
||||
return;
|
||||
}
|
||||
closeEditor();
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
const editor = editorElements();
|
||||
editor.overlay.classList.add("hidden");
|
||||
editor.error.textContent = "";
|
||||
editor.content.value = "";
|
||||
resetEditorState();
|
||||
}
|
||||
|
||||
async function openViewer() {
|
||||
const selectedItems = activePaneState().selectedItems;
|
||||
if (selectedItems.length !== 1 || selectedItems[0].kind !== "file") {
|
||||
@@ -766,6 +829,59 @@ async function openViewer() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openEditor() {
|
||||
const selectedItems = activePaneState().selectedItems;
|
||||
if (selectedItems.length !== 1 || !isEditableSelection(selectedItems[0])) {
|
||||
return;
|
||||
}
|
||||
const selected = selectedItems[0];
|
||||
const editor = editorElements();
|
||||
editor.overlay.classList.remove("hidden");
|
||||
editor.title.textContent = "Edit";
|
||||
editor.fileName.textContent = selected.name;
|
||||
editor.filePath.textContent = selected.path;
|
||||
editor.error.textContent = "";
|
||||
editor.content.value = "";
|
||||
editor.content.disabled = true;
|
||||
editor.saveButton.disabled = true;
|
||||
try {
|
||||
const data = await apiRequest("GET", `/api/files/view?${new URLSearchParams({ path: selected.path, for_edit: "true" }).toString()}`);
|
||||
editor.fileName.textContent = data.name;
|
||||
editor.filePath.textContent = data.path;
|
||||
editor.content.value = data.content;
|
||||
editor.content.disabled = false;
|
||||
editor.saveButton.disabled = false;
|
||||
editorState.path = data.path;
|
||||
editorState.originalContent = data.content;
|
||||
editorState.modified = data.modified;
|
||||
editor.content.focus();
|
||||
} catch (err) {
|
||||
editor.error.textContent = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveEditor() {
|
||||
if (!editorState.path) {
|
||||
return;
|
||||
}
|
||||
const editor = editorElements();
|
||||
editor.error.textContent = "";
|
||||
try {
|
||||
const response = await apiRequest("POST", "/api/files/save", {
|
||||
path: editorState.path,
|
||||
content: editor.content.value,
|
||||
expected_modified: editorState.modified,
|
||||
});
|
||||
editorState.originalContent = editor.content.value;
|
||||
editorState.modified = response.modified;
|
||||
setStatus(`Saved ${response.path}`);
|
||||
closeEditor();
|
||||
await loadBrowsePane(state.activePane);
|
||||
} catch (err) {
|
||||
editor.error.textContent = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
function moveCurrentRow(delta) {
|
||||
const pane = state.activePane;
|
||||
const model = paneState(pane);
|
||||
@@ -821,6 +937,13 @@ function clearSelectionForActivePane() {
|
||||
}
|
||||
|
||||
function handleKeyboardShortcuts(event) {
|
||||
if (isEditorOpen()) {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
attemptCloseEditor();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isViewerOpen()) {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
@@ -910,6 +1033,7 @@ function setupEvents() {
|
||||
setupPaneEvents("right");
|
||||
document.addEventListener("keydown", handleKeyboardShortcuts);
|
||||
document.getElementById("view-btn").onclick = openViewer;
|
||||
document.getElementById("edit-btn").onclick = openEditor;
|
||||
document.getElementById("rename-btn").onclick = renameSelected;
|
||||
document.getElementById("delete-btn").onclick = deleteSelected;
|
||||
document.getElementById("copy-btn").onclick = startCopySelected;
|
||||
@@ -943,6 +1067,16 @@ function setupEvents() {
|
||||
closeViewer();
|
||||
}
|
||||
};
|
||||
|
||||
const editor = editorElements();
|
||||
editor.closeButton.onclick = attemptCloseEditor;
|
||||
editor.cancelButton.onclick = attemptCloseEditor;
|
||||
editor.saveButton.onclick = saveEditor;
|
||||
editor.overlay.onclick = (event) => {
|
||||
if (event.target === editor.overlay) {
|
||||
attemptCloseEditor();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function init() {
|
||||
|
||||
@@ -99,6 +99,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editor-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="editor-title">
|
||||
<div class="popup-card viewer-card">
|
||||
<button id="editor-close-btn" class="viewer-close" type="button" aria-label="Close editor">X</button>
|
||||
<h3 id="editor-title">Edit</h3>
|
||||
<div id="editor-file-name" class="popup-meta"></div>
|
||||
<div id="editor-file-path" class="popup-meta"></div>
|
||||
<div id="editor-error" class="error"></div>
|
||||
<textarea id="editor-content" class="editor-content" spellcheck="false"></textarea>
|
||||
<div class="popup-actions">
|
||||
<button id="editor-save-btn" type="button">Save</button>
|
||||
<button id="editor-cancel-btn" type="button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/ui/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -397,6 +397,21 @@ button:disabled {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
margin: 6px 0 8px 0;
|
||||
padding: 10px;
|
||||
min-height: 280px;
|
||||
max-height: calc(100vh - 220px);
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--border);
|
||||
background: #f8fafc;
|
||||
color: var(--text);
|
||||
font: 12px/1.45 "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.workspace {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
Reference in New Issue
Block a user