fix(mnote-web): 即时显示页面内新建思维导图
This commit is contained in:
+12
-4
@@ -145,6 +145,10 @@
|
||||
- `create` 成功后立即插入 Page Tree 行,以及 File Tree 的 `doc:<id>` / `index:<id>` 行。
|
||||
- `purge/delete/archive` 成功后立即从 Page Tree 与 File Tree 移除目标页面行。
|
||||
- 命令成功后清理按钮 pending 状态,避免无整页刷新时按钮残留禁用。
|
||||
6. `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- 追加修复 Rust 3000 页面内 mindmap 创建路径:主文档壳现在同时监听 `wolai:assets-changed`,并观察同页 `/api/mindmap/<doc>/<mindmap>` POST 成功请求。
|
||||
- mindmap 创建成功后立即把 `mindmap-<id>.json` 作为 File Tree asset row 插入当前页面下,避免等刷新后 projection 才出现。
|
||||
- 这覆盖 Leptos/Rust island 未触发 React `emitAssetsChanged()` 的路径。
|
||||
|
||||
2026-05-14 回归复现与补修:
|
||||
|
||||
@@ -152,6 +156,8 @@
|
||||
- 复现证据:旧 3000 进程为 `/mnt/Data1T/mnote/rust/target/debug/mnote-web (deleted)`;点击“新建页面”后接口返回成功并跳转到 `/documents/tree_1778714360642_10?...`,但 5 秒后 `#sidebar-tree-root` 仍不包含该 document id。
|
||||
- 当前源码 3002 在补修前也复现同类问题:新建后跳转到 `/documents/tree_1778714400752_12?...`,但 Page Tree DOM 不包含当前 document id。
|
||||
- 补修后已重启 3000,并在 3000 主入口完成新建/删除无刷新 smoke。
|
||||
- 用户继续复测确认页面新建/删除已恢复,但页面内新建思维导图仍需刷新。真实 smoke 复核后确认:3000 页面内导图创建会走 `/api/mindmap/<doc>/<mindmap>` 写入链路,主文档壳原先没有把该成功写入同步 materialize 到 File Tree asset row。
|
||||
- 补修后已在 3000 主入口通过 slash 菜单真实新建思维导图,并确认 File Tree 无刷新出现 `asset:<mindmapId>` 行。
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
@@ -159,7 +165,7 @@
|
||||
|
||||
1. [x] 在 `http://127.0.0.1:3000/documents/<id>` 主页面壳中新建页面后,不刷新浏览器即可在 Page Tree 与 File Tree 中看到新页面。代码证据:React Sidebar 路径由 `handleCreate()` 本地 upsert documents;Rust 3000 主文档壳由 `tree:local-command` 本地插入 Page Tree 行与 File Tree `doc:<id>` / `index:<id>` 行;单测覆盖:`sidebar-local-projection.test.ts`,Rust runtime 覆盖:`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture`。
|
||||
2. [x] 在主页面壳中删除页面后,不刷新浏览器即可在 Page Tree 与 File Tree 中看到目标页面消失。代码证据:React Sidebar 路径由 `handleDelete()` / `handleDeleteResourceSelection()` 同步 `removeSidebarDocumentRecords()`;Rust 3000 主文档壳由 `tree:local-command` 在 `purge/delete/archive` 成功后调用 remove delta 本地移除;单测覆盖:`sidebar-local-projection.test.ts` 与 `sidebar-delete-preflight-source.test.ts`,Rust runtime 覆盖:`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture`。
|
||||
3. [x] 在主页面壳中新建 mindmap 后,不刷新浏览器即可在 File Tree 中看到对应 `mindmap-<id>.json` 行。代码证据:`emitAssetsChanged(asset)` 已进入本地 `mindmapAssets`,`localFileTreeProjection` 用本地 mindmap assets 重建 asset row;单测覆盖:`sidebar-local-projection.test.ts`。
|
||||
3. [x] 在主页面壳中新建 mindmap 后,不刷新浏览器即可在 File Tree 中看到对应 `mindmap-<id>.json` 行。代码证据:React Sidebar 路径由 `emitAssetsChanged(asset)` 进入本地 `mindmapAssets` 并重建 asset row;Rust 3000 主文档壳由 `wolai:assets-changed` listener 和 `/api/mindmap/<doc>/<mindmap>` POST observer 本地插入 File Tree asset row;单测覆盖:`sidebar-local-projection.test.ts`,Rust runtime 覆盖:`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture`。
|
||||
4. [x] 上述三条路径均不得依赖 `window.location.reload()` 或顶层 navigation。代码证据:本轮没有新增 reload;create/delete 以本地 state/projection apply 为主,显式 refetch 仅作 snapshot 校准。
|
||||
5. [x] smoke 需要记录事件或本地 apply 标记、最终 DOM 状态与相关网络请求。React Sidebar 路径保留 `tmp/task426-sidebar-main-no-reload-smoke/result.json` 作为 Next/3001 与 mindmap asset 证据;Rust 3000 主文档壳新增 `tmp/task426-mnote-web-main-no-reload-smoke/result.json`,覆盖真实 3000 页面新建/删除无刷新 DOM 结果。代码级测试同时覆盖:`sidebar-delete-preflight-source.test.ts` 锁定 create/delete 事件与本地 documents apply;`use-sidebar-data.test.tsx` 锁定 Convex live 显式 refetch 会真实拉 `/api/sidebar`;`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture` 锁定 Rust 主文档壳 runtime 基础行为。
|
||||
6. [x] 修复后补测试,防止 `sidebarQuery.refetch()` 在 Convex live 模式下继续作为空操作吞掉显式刷新请求。测试覆盖:`use-sidebar-data.test.tsx`。
|
||||
@@ -195,10 +201,12 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task426-mnote-web-main-no-r
|
||||
|
||||
- `ok=true`
|
||||
- `baseUrl=http://127.0.0.1:3000`
|
||||
- `createdDocumentId=tree_1778715292685_1`
|
||||
- 新建后:`localApplied=create`、`pageRows=1`、`fileRows=["doc:tree_1778715292685_1","index:tree_1778715292685_1"]`
|
||||
- `createdDocumentId=tree_1778720530823_2`
|
||||
- `mindmapId=mindmap_1778720531308`
|
||||
- 新建页面后:`localApplied=create`、`pageRows=1`、`fileRows=["doc:tree_1778720530823_2","index:tree_1778720530823_2"]`
|
||||
- 新建思维导图后:`localApplied=true`、`rows=[{"rowId":"asset:mindmap_1778720531308","docId":"tree_1778720530823_2","title":"mindmap-mindmap_1778720531308.json…","kind":"mindmap"}]`
|
||||
- 删除后:`localApplied=remove`、`pageRows=0`、`fileRows=[]`
|
||||
- `requests` 仅包含两次 `POST /api/tree/commands`,无浏览器刷新依赖
|
||||
- `requests` 包含两次 `POST /api/tree/commands`、一次 mindmap projection GET、两次 `/api/mindmap/<doc>/<mindmap>` POST;无浏览器刷新依赖
|
||||
- 结果文件:`tmp/task426-mnote-web-main-no-reload-smoke/result.json`
|
||||
|
||||
补充对比:
|
||||
|
||||
@@ -1997,6 +1997,120 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
document.documentElement.setAttribute('data-mnote-last-upload-asset-id', assetId);
|
||||
}
|
||||
|
||||
function removeFileTreeAssetRow(assetId) {
|
||||
var normalized = String(assetId || '').trim();
|
||||
if (!normalized) return false;
|
||||
var row = document.querySelector('.tree-row[data-shell-mode="filetree"][data-asset-id="' + cssEscape(normalized) + '"]');
|
||||
var node = row ? row.closest('.tree-node') : null;
|
||||
if (!node || !node.parentElement) return false;
|
||||
node.parentElement.removeChild(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyAssetsChangedToFileTree(detail) {
|
||||
detail = detail || {};
|
||||
var docId = String(detail.docId || detail.documentId || currentDocumentId() || '').trim();
|
||||
var changed = false;
|
||||
var explicitAsset = detail.asset && typeof detail.asset === 'object' ? detail.asset : null;
|
||||
if (explicitAsset) {
|
||||
appendUploadedAssetRow(explicitAsset, docId);
|
||||
changed = true;
|
||||
}
|
||||
var mindmapAssetIds = Array.isArray(detail.mindmapAssetIds) ? detail.mindmapAssetIds : [];
|
||||
if (detail.mindmapDeleted === true) {
|
||||
mindmapAssetIds.forEach(function(assetId) {
|
||||
if (removeFileTreeAssetRow(assetId)) changed = true;
|
||||
});
|
||||
}
|
||||
if (changed) {
|
||||
document.documentElement.setAttribute('data-mnote-assets-local-applied', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
function mindmapAssetFromTarget(documentId, mindmapId) {
|
||||
var docId = String(documentId || currentDocumentId() || '').trim();
|
||||
var assetId = String(mindmapId || '').trim();
|
||||
if (!docId || !assetId) return null;
|
||||
var fileName = 'mindmap-' + assetId + '.json';
|
||||
return {
|
||||
id: assetId,
|
||||
document_id: docId,
|
||||
asset_type: 'mindmap',
|
||||
file_name: fileName,
|
||||
file_url: '/documents/' + encodeURIComponent(docId) + '/' + encodeURIComponent(fileName)
|
||||
};
|
||||
}
|
||||
|
||||
function parseMindmapApiTarget(input) {
|
||||
try {
|
||||
var rawUrl = typeof input === 'string'
|
||||
? input
|
||||
: input && typeof input.url === 'string'
|
||||
? input.url
|
||||
: '';
|
||||
if (!rawUrl) return null;
|
||||
var url = new URL(rawUrl, window.location.origin);
|
||||
var match = /^\/api\/mindmap\/([^/]+)\/([^/]+)$/.exec(url.pathname);
|
||||
if (!match) return null;
|
||||
return {
|
||||
documentId: decodeURIComponent(match[1]),
|
||||
mindmapId: decodeURIComponent(match[2])
|
||||
};
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function requestMethod(input, init) {
|
||||
return String(
|
||||
init && init.method
|
||||
? init.method
|
||||
: input && typeof input.method === 'string'
|
||||
? input.method
|
||||
: 'GET'
|
||||
).toUpperCase();
|
||||
}
|
||||
|
||||
function requestBodyHasMindmapCreateOnly(init) {
|
||||
var body = init && typeof init.body === 'string' ? init.body : '';
|
||||
if (!body) return false;
|
||||
try {
|
||||
var payload = JSON.parse(body);
|
||||
return payload && payload.createOnly === true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyMindmapApiMutationToFileTree(target) {
|
||||
if (!target || !target.documentId || !target.mindmapId) return;
|
||||
var asset = mindmapAssetFromTarget(target.documentId, target.mindmapId);
|
||||
if (!asset) return;
|
||||
appendUploadedAssetRow(asset, target.documentId);
|
||||
document.documentElement.setAttribute('data-mnote-assets-local-applied', 'true');
|
||||
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', target.mindmapId);
|
||||
}
|
||||
|
||||
function installMindmapAssetFetchObserver() {
|
||||
if (window.__mnoteMindmapAssetFetchObserverInstalled === true) return;
|
||||
if (typeof window.fetch !== 'function') return;
|
||||
window.__mnoteMindmapAssetFetchObserverInstalled = true;
|
||||
var originalFetch = window.fetch.bind(window);
|
||||
window.fetch = function(input, init) {
|
||||
var target = parseMindmapApiTarget(input);
|
||||
var method = requestMethod(input, init);
|
||||
var createOnly = requestBodyHasMindmapCreateOnly(init || {});
|
||||
return originalFetch(input, init).then(function(response) {
|
||||
if (target && method === 'POST' && response && response.ok) {
|
||||
if (createOnly || target.documentId === currentDocumentId()) {
|
||||
applyMindmapApiMutationToFileTree(target);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async function insertUploadedAssetIntoEditor(asset) {
|
||||
var editorRoot = document.querySelector('.editor-surface .ProseMirror');
|
||||
var editor = editorRoot && editorRoot.editor;
|
||||
@@ -2090,6 +2204,11 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return payload.asset;
|
||||
}
|
||||
|
||||
window.addEventListener('wolai:assets-changed', function(event) {
|
||||
applyAssetsChangedToFileTree(event.detail || {});
|
||||
});
|
||||
installMindmapAssetFetchObserver();
|
||||
|
||||
async function uploadFilesWithResolvedTarget(files, detail, options) {
|
||||
var list = Array.from(files || []).filter(Boolean);
|
||||
if (!list.length) return [];
|
||||
@@ -4614,6 +4733,11 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_JS.contains("applyCreatedDocumentLocally"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-mnote-tree-local-command-applied', 'create"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-mnote-tree-local-command-applied', 'remove"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("wolai:assets-changed"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("applyAssetsChangedToFileTree"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("installMindmapAssetFetchObserver"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("parseMindmapApiTarget"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-mnote-assets-local-applied"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("application/x-mnote-page-tree-node"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("dragstart"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("drop"));
|
||||
|
||||
@@ -41,6 +41,44 @@ async function readCreatedState(page, documentId) {
|
||||
}, documentId);
|
||||
}
|
||||
|
||||
async function insertMindmapThroughSlash(page) {
|
||||
const editor = page
|
||||
.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]')
|
||||
.first();
|
||||
await editor.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await editor.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.keyboard.type("/");
|
||||
const item = page.getByTestId("slash-item-mindmap").first();
|
||||
await item.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await item.click({ timeout: UI_TIMEOUT_MS });
|
||||
const root = page.getByTestId("mnote-mindmap-editor-root").first();
|
||||
await root.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const mindmapId = await root.evaluate((node) => node instanceof HTMLElement ? node.dataset.mnoteMindmapId || "" : "");
|
||||
assert(mindmapId, "新建思维导图后缺少 mindmapId");
|
||||
return mindmapId;
|
||||
}
|
||||
|
||||
async function readMindmapAssetState(page, documentId, mindmapId) {
|
||||
return page.evaluate(
|
||||
({ documentId: docId, mindmapId: assetId }) => {
|
||||
const rows = Array.from(document.querySelectorAll("#sidebar-file-tree-root .tree-row[data-shell-mode=\"filetree\"]"))
|
||||
.filter((row) => row.getAttribute("data-asset-id") === assetId)
|
||||
.map((row) => ({
|
||||
rowId: row.getAttribute("data-row-id"),
|
||||
docId: row.getAttribute("data-doc-id") || row.getAttribute("data-document-id"),
|
||||
title: row.textContent?.trim() || "",
|
||||
kind: row.querySelector(".tree-kind-badge")?.getAttribute("data-kind") || "",
|
||||
}));
|
||||
return {
|
||||
rows,
|
||||
localApplied: document.documentElement.getAttribute("data-mnote-assets-local-applied") || "",
|
||||
expectedDocId: docId,
|
||||
};
|
||||
},
|
||||
{ documentId, mindmapId },
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await fs.mkdir(OUT_DIR, { recursive: true });
|
||||
const browser = await chromium.launch({ headless: process.env.HEADFUL !== "1" });
|
||||
@@ -49,7 +87,7 @@ async function main() {
|
||||
const requests = [];
|
||||
page.on("response", (response) => {
|
||||
const url = response.url();
|
||||
if (url.includes("/api/tree/commands") || url.includes("/api/sidebar") || url.includes("/api/tree/events")) {
|
||||
if (url.includes("/api/tree/commands") || url.includes("/api/sidebar") || url.includes("/api/tree/events") || url.includes("/api/mindmap/")) {
|
||||
requests.push({ url, status: response.status() });
|
||||
}
|
||||
});
|
||||
@@ -78,6 +116,20 @@ async function main() {
|
||||
assert.equal(afterCreate.pageRows, 1, "Page Tree 应立即出现新页面");
|
||||
assert.deepEqual(afterCreate.fileRows.sort(), [`doc:${createdDocumentId}`, `index:${createdDocumentId}`].sort(), "File Tree 应立即出现新页面 doc/index 行");
|
||||
|
||||
const mindmapId = await insertMindmapThroughSlash(page);
|
||||
await page.waitForFunction(
|
||||
({ assetId }) => {
|
||||
return Boolean(document.querySelector(`#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-asset-id="${CSS.escape(assetId)}"]`));
|
||||
},
|
||||
{ assetId: mindmapId },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const afterMindmapCreate = await readMindmapAssetState(page, createdDocumentId, mindmapId);
|
||||
assert.equal(afterMindmapCreate.localApplied, "true", "新建思维导图应由 assets-changed 本地 apply 到 File Tree");
|
||||
assert.equal(afterMindmapCreate.rows.length, 1, "File Tree 应立即出现新建思维导图 asset 行");
|
||||
assert.equal(afterMindmapCreate.rows[0].rowId, `asset:${mindmapId}`, "File Tree mindmap rowId 应匹配 asset id");
|
||||
assert.equal(afterMindmapCreate.rows[0].docId, createdDocumentId, "File Tree mindmap asset 应挂在当前页面下");
|
||||
|
||||
const rowSelector = `#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="${cssString(createdDocumentId)}"]`;
|
||||
await page.locator(rowSelector).scrollIntoViewIfNeeded({ timeout: UI_TIMEOUT_MS });
|
||||
page.once("dialog", async (dialog) => {
|
||||
@@ -105,7 +157,9 @@ async function main() {
|
||||
baseUrl: BASE_URL,
|
||||
beforeCount,
|
||||
createdDocumentId,
|
||||
mindmapId,
|
||||
afterCreate,
|
||||
afterMindmapCreate,
|
||||
afterDelete,
|
||||
requests,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user