Compare commits
2
Commits
main
..
dev/v0.8.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d2a3d7707 | ||
|
|
7baf2653e0 |
No files matched your search
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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/>";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user