0.3.6 修复shitf选择问题
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { OnlyOfficeAiAgentPanel } from "@/components/onlyoffice/OnlyOfficeAiAgentPanel";
|
||||
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
|
||||
@@ -358,6 +358,7 @@ export default function OnlyOfficePage() {
|
||||
const [resolvedMode, setResolvedMode] = useState<EditorMode | null>(mode === "view" ? "view" : null);
|
||||
const [resolvedDisableDownload, setResolvedDisableDownload] = useState(false);
|
||||
const [resolvedDisableCopy, setResolvedDisableCopy] = useState(false);
|
||||
const [permissionResolved, setPermissionResolved] = useState(false);
|
||||
const [assetSignedUrl, setAssetSignedUrl] = useState<string>("");
|
||||
const [assetStorageId, setAssetStorageId] = useState<string>("");
|
||||
const [forceSaveState, setForceSaveState] = useState<{
|
||||
@@ -413,6 +414,7 @@ export default function OnlyOfficePage() {
|
||||
const storageHostOverride = runtimeConfig.onlyofficeStorageHostOverride;
|
||||
const proxyOrigin = runtimeConfig.onlyofficeProxyOrigin;
|
||||
const callbackOrigin = runtimeConfig.onlyofficeCallbackOrigin;
|
||||
const docEditorRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// 说明:为了让 document.key 在同一附件的多次打开之间保持稳定(提升缓存命中与加载速度),
|
||||
@@ -472,12 +474,17 @@ export default function OnlyOfficePage() {
|
||||
setResolvedMode(mode === "view" ? "view" : mode);
|
||||
setResolvedDisableDownload(false);
|
||||
setResolvedDisableCopy(false);
|
||||
setPermissionResolved(true);
|
||||
return;
|
||||
}
|
||||
if (!authedUserId) {
|
||||
setPermissionResolved(false);
|
||||
return;
|
||||
}
|
||||
if (!authedUserId) return;
|
||||
|
||||
let canceled = false;
|
||||
void (async () => {
|
||||
setPermissionResolved(false);
|
||||
try {
|
||||
const perm = await convex.query(api.documents.getPermissionForUser, {
|
||||
userId: authedUserId,
|
||||
@@ -488,6 +495,7 @@ export default function OnlyOfficePage() {
|
||||
const disableCopy = Boolean((perm as any)?.disableCopy);
|
||||
setResolvedDisableDownload(disableDownload);
|
||||
setResolvedDisableCopy(disableCopy);
|
||||
setPermissionResolved(true);
|
||||
|
||||
if (mode === "view") {
|
||||
setResolvedMode("view");
|
||||
@@ -504,6 +512,7 @@ export default function OnlyOfficePage() {
|
||||
setResolvedMode(mode === "view" ? "view" : mode);
|
||||
setResolvedDisableDownload(false);
|
||||
setResolvedDisableCopy(false);
|
||||
setPermissionResolved(true);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -731,7 +740,12 @@ export default function OnlyOfficePage() {
|
||||
fileName,
|
||||
fileType,
|
||||
mode,
|
||||
resolvedMode,
|
||||
resolvedDisableDownload,
|
||||
resolvedDisableCopy,
|
||||
permissionResolved,
|
||||
assetId,
|
||||
documentId,
|
||||
docKey,
|
||||
};
|
||||
} catch {
|
||||
@@ -747,11 +761,34 @@ export default function OnlyOfficePage() {
|
||||
fileName,
|
||||
fileType,
|
||||
resolvedMode,
|
||||
resolvedDisableCopy,
|
||||
resolvedDisableDownload,
|
||||
permissionResolved,
|
||||
documentId,
|
||||
proxyOrigin,
|
||||
resolvedFileUrl,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
const destroyEditor = () => {
|
||||
try {
|
||||
const inst = docEditorRef.current;
|
||||
docEditorRef.current = null;
|
||||
if (inst && typeof inst.destroyEditor === "function") {
|
||||
inst.destroyEditor();
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const el = document.getElementById("onlyoffice-frame");
|
||||
if (el) el.innerHTML = "";
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
// 说明:如果是附件(assetId)编辑模式,则必须拿到真实 userId 才能让
|
||||
// /api/onlyoffice/callback 通过工作空间成员校验并把文件写回存储。
|
||||
// 否则会出现:编辑器里看似“已保存”,但下载/再次打开仍是旧文件。
|
||||
@@ -789,6 +826,7 @@ export default function OnlyOfficePage() {
|
||||
}
|
||||
|
||||
if (!resolvedMode) return;
|
||||
if (documentId && !permissionResolved) return;
|
||||
if (resolvedMode !== "view" && assetId && !authedUserId) return;
|
||||
|
||||
const scriptUrl = `${baseUrl.replace(/\/$/, "")}/web-apps/apps/api/documents/api.js`;
|
||||
@@ -909,6 +947,7 @@ export default function OnlyOfficePage() {
|
||||
documentToken: string | null;
|
||||
editorConfigToken: string | null;
|
||||
};
|
||||
if (canceled) return;
|
||||
|
||||
// 兼容不同 ONLYOFFICE 配置:有些版本/配置会校验 document.token/editorConfig.token。
|
||||
if (token) config.token = token;
|
||||
@@ -922,7 +961,8 @@ export default function OnlyOfficePage() {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
new (window as any).DocsAPI.DocEditor("onlyoffice-frame", config);
|
||||
if (canceled) return;
|
||||
docEditorRef.current = new (window as any).DocsAPI.DocEditor("onlyoffice-frame", config);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
// 说明:如果服务端强制 JWT 且你未配置 ONLYOFFICE_JWT_SECRET,会在这里失败或在编辑器内报错。
|
||||
@@ -938,6 +978,11 @@ export default function OnlyOfficePage() {
|
||||
}
|
||||
setError(err.message);
|
||||
});
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
destroyEditor();
|
||||
};
|
||||
}, [
|
||||
assetId,
|
||||
authedUserId,
|
||||
@@ -952,6 +997,11 @@ export default function OnlyOfficePage() {
|
||||
mode,
|
||||
proxyOrigin,
|
||||
resolvedFileUrl,
|
||||
documentId,
|
||||
permissionResolved,
|
||||
resolvedDisableCopy,
|
||||
resolvedDisableDownload,
|
||||
resolvedMode,
|
||||
targetDocType,
|
||||
runtimeConfig.onlyofficeBaseUrlDesktop,
|
||||
]);
|
||||
|
||||
@@ -46,7 +46,7 @@ import { useCurrentDocumentStore } from "@/store/current-document";
|
||||
import { FileTree } from "@/components/sidebar/file-tree";
|
||||
import { buildVisibleRows } from "@/lib/file-tree/rows";
|
||||
import type { FileTreeSelectionState } from "@/lib/file-tree/selection";
|
||||
import { reduceFileTreeSelection } from "@/lib/file-tree/selection";
|
||||
import { normalizeFileTreeSelectionForVisibleRows, reduceFileTreeSelection } from "@/lib/file-tree/selection";
|
||||
import type { FileTreeRow } from "@/lib/file-tree/types";
|
||||
import { buildParentById, filterTopLevelDocIds, inferDropTargetDocId, isInvalidDocDrop } from "@/lib/file-tree/dnd";
|
||||
import { isRealFileAsset } from "@/lib/file-tree/asset";
|
||||
@@ -260,10 +260,6 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
setTableAssets(sidebarData.tableAssets ?? []);
|
||||
}, [sidebarData.tableAssets]);
|
||||
|
||||
useEffect(() => {
|
||||
setFileTreeSelection({ selectedRowIds: new Set(), anchorRowId: null, focusedRowId: null });
|
||||
}, [mediaAssets, mindmapAssets, tableAssets, sidebarData.documents]);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
}, [activeId, setOpen]);
|
||||
@@ -566,6 +562,10 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
const fileTreeVisibleRowIds = useMemo(() => fileTreeRows.map((row) => row.rowId), [fileTreeRows]);
|
||||
const fileTreeRowById = useMemo(() => new Map(fileTreeRows.map((row) => [row.rowId, row])), [fileTreeRows]);
|
||||
|
||||
useEffect(() => {
|
||||
setFileTreeSelection((prev) => normalizeFileTreeSelectionForVisibleRows(prev, fileTreeVisibleRowIds));
|
||||
}, [fileTreeVisibleRowIds]);
|
||||
|
||||
const docParentById = useMemo(
|
||||
() =>
|
||||
buildParentById(
|
||||
@@ -1703,6 +1703,61 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
[activeId, refreshTree, router],
|
||||
);
|
||||
|
||||
const handleDeleteFromContextMenuNode = useCallback(
|
||||
async (node: DocumentNode) => {
|
||||
if (viewMode === "filesystem") {
|
||||
await handleDeleteFileTreeSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = window.confirm("确认删除该页面到垃圾桶吗?");
|
||||
if (!ok) return;
|
||||
|
||||
try {
|
||||
await handleDelete(node.id);
|
||||
} catch (error) {
|
||||
window.alert(error instanceof Error ? error.message : "删除失败");
|
||||
}
|
||||
},
|
||||
[handleDelete, handleDeleteFileTreeSelection, viewMode],
|
||||
);
|
||||
|
||||
const handleDeleteFromAssetContextMenu = useCallback(
|
||||
async (assetIds: string[], assetHint?: MediaAsset) => {
|
||||
const uniqueAssetIds = Array.from(new Set(assetIds));
|
||||
if (uniqueAssetIds.length === 0) return;
|
||||
|
||||
const assets = uniqueAssetIds
|
||||
.map((id) => mediaAssets.find((item) => item.id === id) ?? mindmapAssets.find((item) => item.id === id) ?? tableAssets.find((item) => item.id === id))
|
||||
.filter(Boolean) as MediaAsset[];
|
||||
if (assetHint && !assets.find((item) => item.id === assetHint.id)) {
|
||||
assets.unshift(assetHint);
|
||||
}
|
||||
|
||||
const mindmapCount = assets.filter((item) => item.asset_type === "mindmap").length;
|
||||
const tableCount = assets.filter((item) => item.asset_type === "luckysheet").length;
|
||||
const fileCount = assets.filter((item) => item.asset_type !== "mindmap" && item.asset_type !== "luckysheet").length;
|
||||
const unknownCount = Math.max(0, uniqueAssetIds.length - mindmapCount - tableCount - fileCount);
|
||||
|
||||
const parts: string[] = [];
|
||||
if (fileCount > 0) parts.push(`${fileCount} 个附件(删除,10 分钟内可撤销)`);
|
||||
if (mindmapCount > 0) parts.push(`${mindmapCount} 个思维导图(删除)`);
|
||||
if (tableCount > 0) parts.push(`${tableCount} 个在线表格(删除)`);
|
||||
if (unknownCount > 0) parts.push(`${unknownCount} 个对象(删除)`);
|
||||
|
||||
const ok = window.confirm(`确认删除选中的 ${parts.join(" + ")} 吗?`);
|
||||
if (!ok) return;
|
||||
|
||||
try {
|
||||
await handleDeleteAssets(uniqueAssetIds, assetHint);
|
||||
setFileTreeSelection((prev) => reduceFileTreeSelection(prev, { type: "clear" }));
|
||||
} catch (error) {
|
||||
window.alert(error instanceof Error ? error.message : "删除失败");
|
||||
}
|
||||
},
|
||||
[handleDeleteAssets, mediaAssets, mindmapAssets, tableAssets],
|
||||
);
|
||||
|
||||
const handleConvertToChild = useCallback(
|
||||
async (documentId: string) => {
|
||||
const flat = flattenedPrivate;
|
||||
@@ -2373,7 +2428,7 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
onRename={() => void handleRename(contextMenu.node.id, contextMenu.node.title)}
|
||||
onCreateChild={() => void handleCreate(contextMenu.node.id)}
|
||||
onConvertChild={() => void handleConvertToChild(contextMenu.node.id)}
|
||||
onDelete={() => void handleDeleteFileTreeSelection()}
|
||||
onDelete={() => void handleDeleteFromContextMenuNode(contextMenu.node)}
|
||||
/>
|
||||
)}
|
||||
{shareTarget && (
|
||||
@@ -2449,7 +2504,7 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
onCopyPath={handleCopyAssetPath}
|
||||
onRename={handleRenameAsset}
|
||||
onMove={handleMoveAsset}
|
||||
onDelete={() => void handleDeleteFileTreeSelection()}
|
||||
onDelete={(assetIds) => void handleDeleteFromAssetContextMenu(assetIds, assetMenu.asset)}
|
||||
onDownload={handleDownloadAsset}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { reduceFileTreeSelection } from "./selection";
|
||||
import { normalizeFileTreeSelectionForVisibleRows, reduceFileTreeSelection } from "./selection";
|
||||
|
||||
describe("reduceFileTreeSelection", () => {
|
||||
const visible = ["a", "b", "c", "d"];
|
||||
@@ -56,5 +56,14 @@ describe("reduceFileTreeSelection", () => {
|
||||
expect(next.anchorRowId).toBeNull();
|
||||
expect(next.focusedRowId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("可见行变化时保留仍可见的选择", () => {
|
||||
const next = normalizeFileTreeSelectionForVisibleRows(
|
||||
{ selectedRowIds: new Set(["b", "x"]), anchorRowId: "b", focusedRowId: "x" },
|
||||
["a", "b", "c"],
|
||||
);
|
||||
expect(Array.from(next.selectedRowIds)).toEqual(["b"]);
|
||||
expect(next.anchorRowId).toBe("b");
|
||||
expect(next.focusedRowId).toBe("b");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,3 +82,51 @@ export function reduceFileTreeSelection(
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeFileTreeSelectionForVisibleRows(
|
||||
prev: FileTreeSelectionState,
|
||||
visibleRowIds: string[],
|
||||
): FileTreeSelectionState {
|
||||
const visibleSet = new Set(visibleRowIds);
|
||||
|
||||
let selectedRowIds = prev.selectedRowIds;
|
||||
let removedAny = false;
|
||||
const kept: string[] = [];
|
||||
prev.selectedRowIds.forEach((id) => {
|
||||
if (visibleSet.has(id)) {
|
||||
kept.push(id);
|
||||
return;
|
||||
}
|
||||
removedAny = true;
|
||||
});
|
||||
if (removedAny) {
|
||||
selectedRowIds = new Set(kept);
|
||||
}
|
||||
|
||||
let anchorRowId = prev.anchorRowId;
|
||||
if (anchorRowId && !visibleSet.has(anchorRowId)) {
|
||||
anchorRowId = null;
|
||||
}
|
||||
|
||||
let focusedRowId = prev.focusedRowId;
|
||||
if (focusedRowId && !visibleSet.has(focusedRowId)) {
|
||||
focusedRowId = null;
|
||||
}
|
||||
|
||||
if (!focusedRowId && selectedRowIds.size > 0) {
|
||||
focusedRowId = visibleRowIds.find((id) => selectedRowIds.has(id)) ?? null;
|
||||
}
|
||||
|
||||
if (!anchorRowId && focusedRowId) {
|
||||
anchorRowId = focusedRowId;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedRowIds === prev.selectedRowIds &&
|
||||
anchorRowId === prev.anchorRowId &&
|
||||
focusedRowId === prev.focusedRowId
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return { selectedRowIds, anchorRowId, focusedRowId };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user