0.3.6 修复shitf选择问题
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { OnlyOfficeAiAgentPanel } from "@/components/onlyoffice/OnlyOfficeAiAgentPanel";
|
import { OnlyOfficeAiAgentPanel } from "@/components/onlyoffice/OnlyOfficeAiAgentPanel";
|
||||||
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
|
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 [resolvedMode, setResolvedMode] = useState<EditorMode | null>(mode === "view" ? "view" : null);
|
||||||
const [resolvedDisableDownload, setResolvedDisableDownload] = useState(false);
|
const [resolvedDisableDownload, setResolvedDisableDownload] = useState(false);
|
||||||
const [resolvedDisableCopy, setResolvedDisableCopy] = useState(false);
|
const [resolvedDisableCopy, setResolvedDisableCopy] = useState(false);
|
||||||
|
const [permissionResolved, setPermissionResolved] = useState(false);
|
||||||
const [assetSignedUrl, setAssetSignedUrl] = useState<string>("");
|
const [assetSignedUrl, setAssetSignedUrl] = useState<string>("");
|
||||||
const [assetStorageId, setAssetStorageId] = useState<string>("");
|
const [assetStorageId, setAssetStorageId] = useState<string>("");
|
||||||
const [forceSaveState, setForceSaveState] = useState<{
|
const [forceSaveState, setForceSaveState] = useState<{
|
||||||
@@ -413,6 +414,7 @@ export default function OnlyOfficePage() {
|
|||||||
const storageHostOverride = runtimeConfig.onlyofficeStorageHostOverride;
|
const storageHostOverride = runtimeConfig.onlyofficeStorageHostOverride;
|
||||||
const proxyOrigin = runtimeConfig.onlyofficeProxyOrigin;
|
const proxyOrigin = runtimeConfig.onlyofficeProxyOrigin;
|
||||||
const callbackOrigin = runtimeConfig.onlyofficeCallbackOrigin;
|
const callbackOrigin = runtimeConfig.onlyofficeCallbackOrigin;
|
||||||
|
const docEditorRef = useRef<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 说明:为了让 document.key 在同一附件的多次打开之间保持稳定(提升缓存命中与加载速度),
|
// 说明:为了让 document.key 在同一附件的多次打开之间保持稳定(提升缓存命中与加载速度),
|
||||||
@@ -472,12 +474,17 @@ export default function OnlyOfficePage() {
|
|||||||
setResolvedMode(mode === "view" ? "view" : mode);
|
setResolvedMode(mode === "view" ? "view" : mode);
|
||||||
setResolvedDisableDownload(false);
|
setResolvedDisableDownload(false);
|
||||||
setResolvedDisableCopy(false);
|
setResolvedDisableCopy(false);
|
||||||
|
setPermissionResolved(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!authedUserId) {
|
||||||
|
setPermissionResolved(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!authedUserId) return;
|
|
||||||
|
|
||||||
let canceled = false;
|
let canceled = false;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
|
setPermissionResolved(false);
|
||||||
try {
|
try {
|
||||||
const perm = await convex.query(api.documents.getPermissionForUser, {
|
const perm = await convex.query(api.documents.getPermissionForUser, {
|
||||||
userId: authedUserId,
|
userId: authedUserId,
|
||||||
@@ -488,6 +495,7 @@ export default function OnlyOfficePage() {
|
|||||||
const disableCopy = Boolean((perm as any)?.disableCopy);
|
const disableCopy = Boolean((perm as any)?.disableCopy);
|
||||||
setResolvedDisableDownload(disableDownload);
|
setResolvedDisableDownload(disableDownload);
|
||||||
setResolvedDisableCopy(disableCopy);
|
setResolvedDisableCopy(disableCopy);
|
||||||
|
setPermissionResolved(true);
|
||||||
|
|
||||||
if (mode === "view") {
|
if (mode === "view") {
|
||||||
setResolvedMode("view");
|
setResolvedMode("view");
|
||||||
@@ -504,6 +512,7 @@ export default function OnlyOfficePage() {
|
|||||||
setResolvedMode(mode === "view" ? "view" : mode);
|
setResolvedMode(mode === "view" ? "view" : mode);
|
||||||
setResolvedDisableDownload(false);
|
setResolvedDisableDownload(false);
|
||||||
setResolvedDisableCopy(false);
|
setResolvedDisableCopy(false);
|
||||||
|
setPermissionResolved(true);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -731,7 +740,12 @@ export default function OnlyOfficePage() {
|
|||||||
fileName,
|
fileName,
|
||||||
fileType,
|
fileType,
|
||||||
mode,
|
mode,
|
||||||
|
resolvedMode,
|
||||||
|
resolvedDisableDownload,
|
||||||
|
resolvedDisableCopy,
|
||||||
|
permissionResolved,
|
||||||
assetId,
|
assetId,
|
||||||
|
documentId,
|
||||||
docKey,
|
docKey,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
@@ -747,11 +761,34 @@ export default function OnlyOfficePage() {
|
|||||||
fileName,
|
fileName,
|
||||||
fileType,
|
fileType,
|
||||||
resolvedMode,
|
resolvedMode,
|
||||||
|
resolvedDisableCopy,
|
||||||
|
resolvedDisableDownload,
|
||||||
|
permissionResolved,
|
||||||
|
documentId,
|
||||||
proxyOrigin,
|
proxyOrigin,
|
||||||
resolvedFileUrl,
|
resolvedFileUrl,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
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 才能让
|
// 说明:如果是附件(assetId)编辑模式,则必须拿到真实 userId 才能让
|
||||||
// /api/onlyoffice/callback 通过工作空间成员校验并把文件写回存储。
|
// /api/onlyoffice/callback 通过工作空间成员校验并把文件写回存储。
|
||||||
// 否则会出现:编辑器里看似“已保存”,但下载/再次打开仍是旧文件。
|
// 否则会出现:编辑器里看似“已保存”,但下载/再次打开仍是旧文件。
|
||||||
@@ -789,6 +826,7 @@ export default function OnlyOfficePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!resolvedMode) return;
|
if (!resolvedMode) return;
|
||||||
|
if (documentId && !permissionResolved) return;
|
||||||
if (resolvedMode !== "view" && assetId && !authedUserId) return;
|
if (resolvedMode !== "view" && assetId && !authedUserId) return;
|
||||||
|
|
||||||
const scriptUrl = `${baseUrl.replace(/\/$/, "")}/web-apps/apps/api/documents/api.js`;
|
const scriptUrl = `${baseUrl.replace(/\/$/, "")}/web-apps/apps/api/documents/api.js`;
|
||||||
@@ -909,6 +947,7 @@ export default function OnlyOfficePage() {
|
|||||||
documentToken: string | null;
|
documentToken: string | null;
|
||||||
editorConfigToken: string | null;
|
editorConfigToken: string | null;
|
||||||
};
|
};
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
// 兼容不同 ONLYOFFICE 配置:有些版本/配置会校验 document.token/editorConfig.token。
|
// 兼容不同 ONLYOFFICE 配置:有些版本/配置会校验 document.token/editorConfig.token。
|
||||||
if (token) config.token = token;
|
if (token) config.token = token;
|
||||||
@@ -922,7 +961,8 @@ export default function OnlyOfficePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 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) => {
|
.catch((err: Error) => {
|
||||||
// 说明:如果服务端强制 JWT 且你未配置 ONLYOFFICE_JWT_SECRET,会在这里失败或在编辑器内报错。
|
// 说明:如果服务端强制 JWT 且你未配置 ONLYOFFICE_JWT_SECRET,会在这里失败或在编辑器内报错。
|
||||||
@@ -938,6 +978,11 @@ export default function OnlyOfficePage() {
|
|||||||
}
|
}
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
canceled = true;
|
||||||
|
destroyEditor();
|
||||||
|
};
|
||||||
}, [
|
}, [
|
||||||
assetId,
|
assetId,
|
||||||
authedUserId,
|
authedUserId,
|
||||||
@@ -952,6 +997,11 @@ export default function OnlyOfficePage() {
|
|||||||
mode,
|
mode,
|
||||||
proxyOrigin,
|
proxyOrigin,
|
||||||
resolvedFileUrl,
|
resolvedFileUrl,
|
||||||
|
documentId,
|
||||||
|
permissionResolved,
|
||||||
|
resolvedDisableCopy,
|
||||||
|
resolvedDisableDownload,
|
||||||
|
resolvedMode,
|
||||||
targetDocType,
|
targetDocType,
|
||||||
runtimeConfig.onlyofficeBaseUrlDesktop,
|
runtimeConfig.onlyofficeBaseUrlDesktop,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import { useCurrentDocumentStore } from "@/store/current-document";
|
|||||||
import { FileTree } from "@/components/sidebar/file-tree";
|
import { FileTree } from "@/components/sidebar/file-tree";
|
||||||
import { buildVisibleRows } from "@/lib/file-tree/rows";
|
import { buildVisibleRows } from "@/lib/file-tree/rows";
|
||||||
import type { FileTreeSelectionState } from "@/lib/file-tree/selection";
|
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 type { FileTreeRow } from "@/lib/file-tree/types";
|
||||||
import { buildParentById, filterTopLevelDocIds, inferDropTargetDocId, isInvalidDocDrop } from "@/lib/file-tree/dnd";
|
import { buildParentById, filterTopLevelDocIds, inferDropTargetDocId, isInvalidDocDrop } from "@/lib/file-tree/dnd";
|
||||||
import { isRealFileAsset } from "@/lib/file-tree/asset";
|
import { isRealFileAsset } from "@/lib/file-tree/asset";
|
||||||
@@ -260,10 +260,6 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
|||||||
setTableAssets(sidebarData.tableAssets ?? []);
|
setTableAssets(sidebarData.tableAssets ?? []);
|
||||||
}, [sidebarData.tableAssets]);
|
}, [sidebarData.tableAssets]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFileTreeSelection({ selectedRowIds: new Set(), anchorRowId: null, focusedRowId: null });
|
|
||||||
}, [mediaAssets, mindmapAssets, tableAssets, sidebarData.documents]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}, [activeId, setOpen]);
|
}, [activeId, setOpen]);
|
||||||
@@ -566,6 +562,10 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
|||||||
const fileTreeVisibleRowIds = useMemo(() => fileTreeRows.map((row) => row.rowId), [fileTreeRows]);
|
const fileTreeVisibleRowIds = useMemo(() => fileTreeRows.map((row) => row.rowId), [fileTreeRows]);
|
||||||
const fileTreeRowById = useMemo(() => new Map(fileTreeRows.map((row) => [row.rowId, row])), [fileTreeRows]);
|
const fileTreeRowById = useMemo(() => new Map(fileTreeRows.map((row) => [row.rowId, row])), [fileTreeRows]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFileTreeSelection((prev) => normalizeFileTreeSelectionForVisibleRows(prev, fileTreeVisibleRowIds));
|
||||||
|
}, [fileTreeVisibleRowIds]);
|
||||||
|
|
||||||
const docParentById = useMemo(
|
const docParentById = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildParentById(
|
buildParentById(
|
||||||
@@ -1703,6 +1703,61 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
|||||||
[activeId, refreshTree, router],
|
[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(
|
const handleConvertToChild = useCallback(
|
||||||
async (documentId: string) => {
|
async (documentId: string) => {
|
||||||
const flat = flattenedPrivate;
|
const flat = flattenedPrivate;
|
||||||
@@ -2373,7 +2428,7 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
|||||||
onRename={() => void handleRename(contextMenu.node.id, contextMenu.node.title)}
|
onRename={() => void handleRename(contextMenu.node.id, contextMenu.node.title)}
|
||||||
onCreateChild={() => void handleCreate(contextMenu.node.id)}
|
onCreateChild={() => void handleCreate(contextMenu.node.id)}
|
||||||
onConvertChild={() => void handleConvertToChild(contextMenu.node.id)}
|
onConvertChild={() => void handleConvertToChild(contextMenu.node.id)}
|
||||||
onDelete={() => void handleDeleteFileTreeSelection()}
|
onDelete={() => void handleDeleteFromContextMenuNode(contextMenu.node)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{shareTarget && (
|
{shareTarget && (
|
||||||
@@ -2449,7 +2504,7 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
|||||||
onCopyPath={handleCopyAssetPath}
|
onCopyPath={handleCopyAssetPath}
|
||||||
onRename={handleRenameAsset}
|
onRename={handleRenameAsset}
|
||||||
onMove={handleMoveAsset}
|
onMove={handleMoveAsset}
|
||||||
onDelete={() => void handleDeleteFileTreeSelection()}
|
onDelete={(assetIds) => void handleDeleteFromAssetContextMenu(assetIds, assetMenu.asset)}
|
||||||
onDownload={handleDownloadAsset}
|
onDownload={handleDownloadAsset}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { reduceFileTreeSelection } from "./selection";
|
import { normalizeFileTreeSelectionForVisibleRows, reduceFileTreeSelection } from "./selection";
|
||||||
|
|
||||||
describe("reduceFileTreeSelection", () => {
|
describe("reduceFileTreeSelection", () => {
|
||||||
const visible = ["a", "b", "c", "d"];
|
const visible = ["a", "b", "c", "d"];
|
||||||
@@ -56,5 +56,14 @@ describe("reduceFileTreeSelection", () => {
|
|||||||
expect(next.anchorRowId).toBeNull();
|
expect(next.anchorRowId).toBeNull();
|
||||||
expect(next.focusedRowId).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