Skip to content

Fix edit bug #740

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).

## v0.7.8-beta


**New Features**:
-

**Notes**:
-

**BugFixes**:
- fix save editor info sometimes saves wrong file. https://github.com/gtsteffaniak/filebrowser/issues/701
- make ctrl select work on mac or windows. https://github.com/gtsteffaniak/filebrowser/issues/739

## v0.7.7-beta

This release cleans up some of the native preview (image preview) feature logic. And adds simple docx and epub viewers as well. Going through all of this, I think I know how I can add full-fledge google doc and microsoft office viewer support (no edit). But, for now "onlyOffice" remains the most comprehensive solution with most compatibility and ability to fully edit. One day, I think I will be able to integrate a minimal license-free server into the docker image. But that's something for another time.
Expand Down
2 changes: 1 addition & 1 deletion frontend/public/config.generated.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
server:
numImageProcessors: 8 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
numImageProcessors: 14 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
socket: "" # socket to listen on
tlsKey: "" # path to TLS key
tlsCert: "" # path to TLS cert
Expand Down
101 changes: 62 additions & 39 deletions frontend/src/views/files/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

<script>
import { eventBus } from "@/store/eventBus";
import { state, getters, mutations } from "@/store";

Check warning on line 9 in frontend/src/views/files/Editor.vue

View workflow job for this annotation

GitHub Actions / lint-frontend

'mutations' is defined but never used
import { filesApi } from "@/api";
import { notify } from "@/notify";
import ace, { version as ace_version } from "ace-builds";
import modelist from "ace-builds/src-noconflict/ext-modelist";
import "ace-builds/src-min-noconflict/theme-chrome";
Expand All @@ -18,27 +19,32 @@
data: function () {
return {
editor: null, // The editor instance
filename: "",
};
},
computed: {
isDarkMode() {
return getters.isDarkMode();
},
},
watch: {
$route() {
if (this.editor) {
this.editor.destroy();
this.editor = null;
}
// Wait for the DOM to update after the route change
this.$nextTick(() => {
this.setupEditor();
});
},
beforeRouteUpdate(to, from, next) {
// Destroy the old editor instance to ensure a clean state
if (this.editor) {
this.editor.destroy();
this.editor = null;
}

// Call setupEditor on the next DOM update cycle
this.$nextTick(() => {
this.setupEditor();
});

// Continue with the navigation
next();
},
created() {
window.addEventListener("keydown", this.keyEvent);
eventBus.on("handleEditorValueRequest", this.handleEditorValueRequest);
},
beforeUnmount() {
window.removeEventListener("keydown", this.keyEvent);
Expand All @@ -47,62 +53,79 @@
}
},
mounted: function () {
mutations.resetSelected();
mutations.addSelected({
name: state.req.name,
path: state.req.path,
size: state.req.size,
type: state.req.type,
source: state.req.source,
url: state.req.url,
});
// Wait for the initial DOM render to complete
this.$nextTick(() => {
this.setupEditor();
});
eventBus.on("handleEditorValueRequest", this.handleEditorValueRequest);
this.setupEditor();
},
methods: {
setupEditor() {
setupEditor(attempt = 1) {
try {
this.filename = decodeURIComponent(this.$route.path.split("/").pop());
// Safety Check 1: Use the component's 'filename' data property for comparison
if (state.req.name !== this.filename) {
if (attempt < 5) {
console.warn(
`[Attempt ${attempt}/5] State filename ("${state.req.name}") does not match route filename ("${this.filename}"). Retrying in 500ms...`
);
setTimeout(() => this.setupEditor(attempt + 1), 500);
} else {
const errorMsg = `[FATAL] Failed to sync state with the route for "${this.filename}" after 5 attempts. Aborting editor setup to prevent data corruption.`;
console.error(errorMsg);
notify.showError(errorMsg); // Using the custom notifier
}
return;
}

console.log(
"State and route are in sync. Proceeding with editor setup for",
this.filename
);
const editorEl = document.getElementById("editor");
if (!editorEl) {
console.warn(
"Editor component mounted, but #editor div was not found in the DOM. Aborting setup."
);
return;
}

ace.config.set(
"basePath",
`https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`
);

const fileContent =
state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || "";

this.editor = ace.edit(editorEl, {
mode: modelist.getModeForPath(state.req.name).mode,
value: fileContent,
showPrintMargin: false,
theme: "ace/theme/chrome",
theme: this.isDarkMode ? "ace/theme/twilight" : "ace/theme/chrome",
readOnly: state.req.type === "textImmutable",
wrap: false,
});

if (this.isDarkMode) {
this.editor.setTheme("ace/theme/twilight");
this.filename = decodeURIComponent(this.$route.path.split("/").pop());
} catch (error) {
notify.showError("Failed to initialize the editor. Please reload.");
}
},
handleEditorValueRequest() {
if (this.editor) {
filesApi.put(state.req.path, state.req.source, this.editor.getValue());
// Safety Check 2: Final verification before saving
if (state.req.name !== this.filename) {
// Corrected the error message to be more accurate
const errorMsg = `Save operation aborted. The application's active file ("${state.req.name}") does not match the file you are trying to save ("${this.filename}").`;
notify.showError(errorMsg);
return;
}
try {
if (this.editor) {
filesApi.put(state.req.path, state.req.source, this.editor.getValue());
} else {
notify.showError("Editor instance is not initialized.");
return;
}
notify.showSuccess(`${this.filename} saved successfully.`);
} catch (error) {
notify.showError("Failed to save file. Please try again.");
}
},
keyEvent(event) {
const { key, ctrlKey, metaKey } = event;
if (getters.currentPromptName() != null) return;
if (!ctrlKey && !metaKey) return;
if (key.toLowerCase() === "s") {
if ((ctrlKey || metaKey) && key.toLowerCase() === "s") {
event.preventDefault();
this.handleEditorValueRequest();
}
Expand Down
9 changes: 4 additions & 5 deletions frontend/src/views/files/ListingView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -557,8 +557,9 @@ export default {
}
},
clearCtrKey(event) {
const { ctrlKey } = event;
if (!ctrlKey) {
const { ctrlKey, metaKey } = event;
const modifierKeys = ctrlKey || metaKey;
if (!modifierKeys) {
this.ctrKeyPressed = false;
}
},
Expand Down Expand Up @@ -594,9 +595,7 @@ export default {
let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));

if (modifierKeys) {
if (ctrlKey) {
this.ctrKeyPressed = true;
}
this.ctrKeyPressed = true;
return;
}

Expand Down
3 changes: 2 additions & 1 deletion frontend/tests-playwright/noauth-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ async function globalSetup() {
const context = await browser.newContext();
const page: Page = await context.newPage();

await page.goto("http://127.0.0.1/files", { timeout: 500 });
await page.goto("http://127.0.0.1/files");
await page.waitForURL("**/files/", { timeout: 1000 });

// Create a share of folder
await page.locator('a[aria-label="myfolder"]').waitFor({ state: 'visible' });
Expand Down
Loading