Compare commits

..
2 Commits
12 changed files with 172 additions and 60 deletions

No files matched your search

+11
View File
@@ -2,6 +2,17 @@
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.8.4-beta
**New Features**:
- Remember folder view type (and sorting) for each folder #966
**Notes**:
-
**BugFixes**:
-
## v0.8.4-beta
**New Features**:
+1 -1
View File
@@ -332,7 +332,7 @@ func createConfig(configpath string) {
}
// cleanup database if it exists
if _, err := os.Stat(config.Server.Database); err == nil {
response := askYesNoQuestion(reader, "Database specified already exists. Move database file to backup to start fresh?", "no")
response := askYesNoQuestion(reader, "Database specified already exists. Move databse file to backup to start fresh?", "no")
if !response {
return
}
+38 -35
View File
@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"
@@ -49,7 +50,21 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required")
}
themeMode := utils.Ternary(d.user.DarkMode, "dark", "light")
var sourceInfo settings.Source
var ok bool
if d.fileInfo.Hash != "" {
sourceInfo, ok = settings.Config.Server.SourceMap[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
} else {
sourceInfo, ok = settings.Config.Server.NameToSource[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
}
if d.fileInfo.Hash == "" {
// Build file info based on whether this is a share or regular request
// Regular user request - need to resolve scope
@@ -58,27 +73,20 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
logger.Errorf("OnlyOffice: source %s not available for user %s: %v", source, d.user.Username, scopeErr)
return http.StatusForbidden, fmt.Errorf("source %s is not available", source)
}
resolvedPath := utils.JoinPathAsUnix(userScope, path)
logger.Debugf("OnlyOffice user request: resolved path=%s", resolvedPath)
indexPath := utils.JoinPathAsUnix(userScope, path)
logger.Debugf("OnlyOffice user request: resolved path=%s", indexPath)
fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{
Path: resolvedPath,
Path: indexPath,
Modify: d.user.Permissions.Modify,
Source: source,
Expand: false,
})
if err != nil {
logger.Errorf("OnlyOffice: failed to get file info for source=%s, path=%s: %v", source, resolvedPath, err)
logger.Errorf("OnlyOffice: failed to get file info for source=%s, path=%s: %v", source, indexPath, err)
return errToStatus(err), err
}
d.fileInfo = *fileInfo
} else {
// is a share, use the file info from the share middleware
sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source]
if !ok {
logger.Error("OnlyOffice: source from share not found")
return http.StatusInternalServerError, fmt.Errorf("source not found for share")
}
source = sourceInfo.Name
// path is index path, so we build from share path
path = utils.JoinPathAsUnix(d.share.Path, path)
@@ -92,7 +100,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
}
// Determine file type and editing permissions
fileType := getFileExtension(d.fileInfo.Name)
fileType := strings.TrimPrefix(filepath.Ext(d.fileInfo.Name), ".")
canEdit := iteminfo.CanEditOnlyOffice(d.user.Permissions.Modify, fileType)
canEditMode := utils.Ternary(canEdit, "edit", "view")
if d.fileInfo.Hash != "" {
@@ -111,7 +119,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
}
// Generate document ID for OnlyOffice
documentId, err := getOnlyOfficeId(source, path)
documentId, err := getOnlyOfficeId(d.fileInfo.RealPath)
if err != nil {
logger.Errorf("OnlyOffice: failed to generate document ID for source=%s, path=%s: %v", source, path, err)
return http.StatusNotFound, fmt.Errorf("failed to generate document ID: %v", err)
@@ -166,15 +174,6 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
return renderJSON(w, r, clientConfig)
}
// getFileExtension extracts file extension from filename
func getFileExtension(filename string) string {
parts := strings.Split(filename, ".")
if len(parts) < 2 {
return ""
}
return parts[len(parts)-1]
}
// buildOnlyOfficeDownloadURL constructs the download URL that OnlyOffice server will use to fetch the file
func buildOnlyOfficeDownloadURL(source, path, hash, token string) string {
// Determine base URL (internal URL takes priority for OnlyOffice server communication)
@@ -254,6 +253,21 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path)
return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required")
}
var sourceInfo settings.Source
var ok bool
if d.fileInfo.Hash != "" {
sourceInfo, ok = settings.Config.Server.SourceMap[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
} else {
sourceInfo, ok = settings.Config.Server.NameToSource[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
}
if d.fileInfo.Hash == "" {
// Regular user request - need to resolve scope
@@ -264,12 +278,6 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
}
path = utils.JoinPathAsUnix(userScope, path)
} else {
// is a share, use the file info from the share middleware
sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source]
if !ok {
logger.Error("OnlyOffice: source from share not found")
return http.StatusInternalServerError, fmt.Errorf("source not found for share")
}
source = sourceInfo.Name
// path is index path, so we build from share path
path = utils.JoinPathAsUnix(d.share.Path, path)
@@ -339,12 +347,7 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
return renderJSON(w, r, resp)
}
func getOnlyOfficeId(source, path string) (string, error) {
idx := indexing.GetIndex(source)
if idx == nil {
return "", fmt.Errorf("source not found")
}
realpath, _, _ := idx.GetRealPath(path)
func getOnlyOfficeId(realpath string) (string, error) {
// error is intentionally ignored in order treat errors
// the same as a cache-miss
cachedDocumentKey, ok := utils.OnlyOfficeCache.Get(realpath).(string)
+3 -3
View File
@@ -31,7 +31,7 @@ import {
baseURL,
} from "@/utils/constants";
import { getTypeInfo } from "@/utils/mimetype";
import { mutations, state } from "@/store";
import { mutations, state, getters } from "@/store";
import { shareInfo } from "@/utils/constants";
// NEW: Define placeholder and error image URLs for easy configuration
@@ -126,10 +126,10 @@ export default {
return this.imageTargetSrc;
},
showLargeIcon() {
return state.user.viewMode === "gallery" && state.user.preview.highQuality;
return getters.viewMode() === "gallery" && state.user.preview.highQuality;
},
showLarger() {
return state.user.viewMode === "gallery" || state.user.viewMode === "normal";
return getters.viewMode() === "gallery" || getters.viewMode() === "normal";
},
hasMotion() {
return (
@@ -109,7 +109,7 @@ export default {
],
computed: {
galleryView() {
return state.user.viewMode === "gallery";
return getters.viewMode() === "gallery";
},
quickDownloadEnabled() {
// @ts-ignore
+18 -1
View File
@@ -32,9 +32,26 @@ export const getters = {
}
return true
},
displayPreference: () => {
let source = state.sources.current;
if (getters.isShare()) {
source = getters.currentHash();
}
const path = state.route.path;
if (state.displayPreferences?.[source]?.[path]) {
return state.displayPreferences[source][path];
}
return null;
},
viewMode: () => {
return getters.displayPreference()?.viewMode || state.user.viewMode;
},
sorting: () => {
return getters.displayPreference()?.sorting || state.user.sorting;
},
previewType: () => getTypeInfo(state.req.type).simpleType,
isCardView: () =>
(state.user.viewMode == 'gallery' || state.user.viewMode == 'normal') &&
(getters.viewMode() == 'gallery' || getters.viewMode() == 'normal') &&
getters.currentView() == 'listingView',
currentHash: () => shareInfo.hash,
isMobile: () => state.isMobile,
+44 -4
View File
@@ -1,5 +1,6 @@
import * as i18n from "@/i18n";
import { state } from "./state.js";
import { getters } from "./getters.js";
import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
import { usersApi } from "@/api";
import { notify } from "@/notify";
@@ -251,6 +252,7 @@ export const mutations = {
i18n.setLocale(value.locale);
}
state.user = value;
state.user.sorting = {};
state.user.sorting.by = "name";
state.user.sorting.asc = true;
} catch (error) {
@@ -377,8 +379,9 @@ export const mutations = {
}
let sortby = "name"
let asc = true
sortby = state.user.sorting.by;
asc = state.user.sorting.asc;
const sorting = getters.sorting();
sortby = sorting.by;
asc = sorting.asc;
// Separate directories and files
const dirs = value.items.filter((item) => item.type === 'directory');
const files = value.items.filter((item) => item.type !== 'directory');
@@ -404,8 +407,7 @@ export const mutations = {
if (!state.user.sorting) {
state.user.sorting = {};
}
state.user.sorting.by = field;
state.user.sorting.asc = asc;
mutations.updateDisplayPreferences({ sorting: { by: field, asc: asc } });
emitStateChanged();
},
updateListingItems: () => {
@@ -461,4 +463,42 @@ export const mutations = {
state.user.fileLoading.maxConcurrentUpload = value;
emitStateChanged();
},
updateViewModeHistory: ({ source, path, viewMode }) => {
if (!source || !path) return;
if (!state.viewModeHistory) {
state.viewModeHistory = {};
}
if (!state.viewModeHistory[source]) {
state.viewModeHistory[source] = {};
}
state.viewModeHistory[source][path] = viewMode;
localStorage.setItem("viewModeHistory", JSON.stringify(state.viewModeHistory));
emitStateChanged();
},
updateDisplayPreferences: (payload) => {
let source = state.sources.current;
if (getters.isShare()) {
source = getters.currentHash();
}
const path = state.route.path;
if (!source || !path) return;
if (!state.displayPreferences) {
state.displayPreferences = {};
}
if (!state.displayPreferences[source]) {
state.displayPreferences[source] = {};
}
if (!state.displayPreferences[source][path]) {
state.displayPreferences[source][path] = {};
}
state.displayPreferences[source][path] = {
...state.displayPreferences[source][path],
...payload,
};
localStorage.setItem("displayPreferences", JSON.stringify(state.displayPreferences));
emitStateChanged();
},
};
+6
View File
@@ -1,6 +1,11 @@
import { reactive } from 'vue';
import { detectLocale } from "@/i18n";
function loadDisplayPreferences() {
const history = localStorage.getItem("displayPreferences");
return history ? JSON.parse(history) : {};
}
export const state = reactive({
tooltip: {
show: false,
@@ -24,6 +29,7 @@ export const state = reactive({
isMobile: window.innerWidth <= 800,
isSearchActive: false,
showSidebar: false,
displayPreferences: loadDisplayPreferences(),
usages: {},
editor: null,
serverHasMultipleSources: false,
+5 -3
View File
@@ -92,7 +92,7 @@ export default {
normal: "view_module",
gallery: "grid_view",
};
return icons[state.user.viewMode] || "grid_view";
return icons[getters.viewMode()] || "grid_view";
},
isShare() {
return getters.isShare();
@@ -157,9 +157,11 @@ export default {
},
switchView() {
mutations.closeHovers();
const index = this.viewModes.indexOf(state.user.viewMode);
const index = this.viewModes.indexOf(getters.viewMode());
const next = (index + 1) % this.viewModes.length;
mutations.updateCurrentUser({ viewMode: this.viewModes[next] });
const newViewMode = this.viewModes[next];
mutations.updateDisplayPreferences({ viewMode: newViewMode });
mutations.updateCurrentUser({ viewMode: newViewMode });
},
multiAction() {
const listingView = getters.currentView();
+7 -7
View File
@@ -281,16 +281,16 @@ export default {
return state.multiple;
},
nameSorted() {
return state.user.sorting.by === "name";
return getters.sorting().by === "name";
},
sizeSorted() {
return state.user.sorting.by === "size";
return getters.sorting().by === "size";
},
modifiedSorted() {
return state.user.sorting.by === "modified";
return getters.sorting().by === "modified";
},
ascOrdered() {
return state.user.sorting.asc;
return getters.sorting().asc;
},
items() {
return getters.reqItems();
@@ -335,11 +335,11 @@ export default {
normal: "grid_view",
gallery: "view_list",
};
return icons[state.user.viewMode];
return icons[getters.viewMode()];
},
listingViewMode() {
this.colunmsResize();
return state.user.viewMode;
return getters.viewMode();
},
selectedCount() {
return state.selected.length;
@@ -843,7 +843,7 @@ export default {
`calc(${100 / this.numColumns}% - 1em)`
);
if (state.user.viewMode == "gallery") {
if (getters.viewMode() == "gallery") {
document.documentElement.style.setProperty(
"--item-height",
`calc(${this.columnWidth / 20}em)`
+37 -4
View File
@@ -104,6 +104,36 @@ export default {
}
},
methods: {
getInternalUrlInfo() {
if (this.clientConfig && this.clientConfig.document && this.clientConfig.document.url) {
try {
const docUrlOrigin = new URL(this.clientConfig.document.url).origin;
const windowOrigin = window.location.origin;
if (docUrlOrigin !== windowOrigin) {
return {
isSet: true,
message: `${docUrlOrigin}`
};
} else {
return {
isSet: false,
message: "Not set, using window.location"
};
}
} catch (e) {
return {
isSet: false,
message: "⚠️ Error parsing document URL"
};
}
}
return {
isSet: false,
message: "Analyzing..."
};
},
close() {
const current = window.location.pathname;
const newpath = removeLastDir(current);
@@ -137,7 +167,7 @@ export default {
<div style="margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
<strong>Configuration:</strong><br/>
OnlyOffice URL: ${this.onlyOfficeUrl}<br/>
Internal URL: Check server logs<br/>
Internal URL: ${this.getInternalUrlInfo().message}<br/>
Base URL: ${baseURL}<br/>
Source: ${this.source}<br/>
Path: ${this.path}<br/>
@@ -175,6 +205,7 @@ export default {
const filename = this.path.split('/').pop() || "";
const isShare = getters.isShare();
const internalUrlInfo = this.getInternalUrlInfo();
// Build config details if available
let configDetailsHtml = "";
@@ -196,9 +227,8 @@ export default {
Callback URL: ${editor && editor.callbackUrl ? editor.callbackUrl.substring(0, 80) + '...' : 'N/A'}<br/>
<br/>
<strong>Network Flow:</strong><br/>
Browser ↔ OnlyOffice: ${this.onlyOfficeUrl}<br/>
Browser (${window.location.origin}) ↔ OnlyOffice: ${this.onlyOfficeUrl}<br/>
OnlyOffice → FileBrowser: ${downloadDomain}<br/>
OnlyOffice → FileBrowser: ${callbackDomain}<br/>
</div>
`;
}
@@ -234,7 +264,7 @@ export default {
<div style="margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
<strong>Basic Configuration:</strong><br/>
OnlyOffice URL: ${this.onlyOfficeUrl}<br/>
Internal URL: Check server logs<br/>
Internal URL: ${internalUrlInfo.message}<br/>
Base URL: ${baseURL}<br/>
Source: ${this.source}<br/>
Path: ${this.path}<br/>
@@ -372,6 +402,9 @@ export default {
errorMsg += "<strong>Error Code -4:</strong> Document download failed<br/>";
errorMsg += "• Authentication may have expired<br/>";
errorMsg += "• Check download URL accessibility<br/>";
if (!this.getInternalUrlInfo().isSet) {
errorMsg += "• <strong>Suggestion:</strong> consider adding `server.internalUrl` if your OnlyOffice service is on the same network as FileBrowser<br/>";
}
} else {
errorMsg += `<strong>Error Code:</strong> ${errorInfo || 'Unknown error'}<br/>`;
errorMsg += "• Check browser console for detailed error messages<br/>";
+1 -1
View File
@@ -19,7 +19,7 @@ async function globalSetup() {
await page.locator('button[aria-label="File-Actions"]').click();
await page.locator('button[aria-label="Share"]').click();
await page.locator('button[aria-label="Share-Confirm"]').click();
await page.waitForResponse(response => response.url().includes('/api/share') && response.status() === 200);
await page.waitForResponse(response => response.status() === 200);
await expect(page.locator("div[aria-label='share-prompt'] .card-content table tbody tr:not(:has(th))")).toHaveCount(1);
const shareHash = await page.locator("div[aria-label='share-prompt'] .card-content table tbody tr:not(:has(th)) td").first().textContent();
if (!shareHash) {