refactor: externalize editor upload context
This commit is contained in:
@@ -466,6 +466,36 @@ node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
|
||||
- `local-upload-runtime.js` route 返回 `200`,`content-type=application/javascript; charset=utf-8`。
|
||||
- `task479` 最终 `ok=true`,覆盖主编辑区上传目录、filetree folder drop、broken link 保留、无 active document 附件 tab、文件树滚动、标题来源、单附件删除保留相邻附件。
|
||||
|
||||
### 9.5 Batch 4 执行记录(2026-05-24)
|
||||
|
||||
Reasonix read-only audit:
|
||||
|
||||
- run id: `reasonix-2026-05-24T15-27-18-395Z-12afda31`
|
||||
- worker worktree: `/mnt/Data1T/mnote-wt-3-19-local-upload-context-b4`
|
||||
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T15-27-18-395Z-12afda31/process-handoff.md`
|
||||
- 类型:只读审计,主控已读取 Hindsight recall、`process-handoff.md`、`process-handoff.json`。
|
||||
|
||||
采纳内容:
|
||||
|
||||
- `local-upload-runtime.js` 新增 `resolveEditorUploadContext`、`editorRootFromUploadOptions`、`openEditorUploadFilePicker`。
|
||||
- `SIDEBAR_TREE_JS` 中保留同名 wrapper 和 inline fallback;优先委托 `window.__mnoteLocalUploadRuntime`,模块加载失败时保留旧行为。
|
||||
- 本批不迁 `uploadFileToMediaAsset`、`uploadFilesWithResolvedTarget`、`insertUploadedAssetIntoEditor`。这些函数仍混合 API POST、file tree refresh、Tiptap chain 插入和 OnlyOffice/resource tab 逻辑,后续需要继续拆边界。
|
||||
- `scripts/task472-side-target-secondary-pane-smoke.js` 补齐 `/api/local-folder/assets/upload` 当前必需的 `uploadIntent=editor.markdown.attach`,让 secondary pane smoke 恢复可运行。
|
||||
|
||||
已通过验证:
|
||||
|
||||
- `node --check rust/crates/mnote-web/browser/local-upload-runtime.js`
|
||||
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
|
||||
- `cargo check --manifest-path rust/Cargo.toml -p mnote-web`
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_upload_runtime_contains_editor_upload_context_helpers`
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder`
|
||||
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
|
||||
- 结果:`ok=true`
|
||||
- 证据:命令 stdout JSON,覆盖 `editor-upload-to-page-resource-dir`、`filetree-folder-drop-to-target`、`broken-link-after-real-file-delete`、`direct-attach-open-without-active-md`、`editor-delete-one-attachment-preserves-adjacent` 等检查。
|
||||
- `node scripts/task472-side-target-secondary-pane-smoke.js`
|
||||
- 结果:`ok=true`
|
||||
- 证据:命令 stdout JSON,`assetId=local:asset:README/side-target-resource.md`、`officeAssetId=local:asset:README/side-target-office.docx`。
|
||||
|
||||
## 10. 分阶段执行清单
|
||||
|
||||
### P0:定位辅助与基线保护
|
||||
@@ -498,7 +528,7 @@ node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
|
||||
- office -> OnlyOffice path builder
|
||||
- [ ] 从 `SIDEBAR_TREE_JS` 中迁出本地文件夹上传目标计算和 adapter 调用;资源归属语义仍由 Rust API / kernel 合同决定。
|
||||
- 2026-05-24 / `reasonix-2026-05-24T15-17-16-430Z-18054487` 只读审计:`leptos-tiptap` 只应发出上传 intent;本地 Markdown 路径、`rootUri`、`/api/local-folder/files/open` URL、`markdownRelativePath` 与真实上传落盘都归本设计的 shell host runtime。
|
||||
- 下一刀低风险候选:`resolveEditorUploadContext`、`editorRootFromUploadOptions`、`openEditorUploadFilePicker`,优先保持调用 `uploadFilesWithResolvedTarget`,不同时迁上传主流程。
|
||||
- 2026-05-24 / Batch 4 已迁:`resolveEditorUploadContext`、`editorRootFromUploadOptions`、`openEditorUploadFilePicker`,并保持调用 `uploadFilesWithResolvedTarget`,未同时迁上传主流程。
|
||||
- 暂不迁:`uploadFileToMediaAsset`、`uploadFilesWithResolvedTarget`、`insertUploadedAssetIntoEditor`。这些函数同时涉及 API POST、file tree refresh、Tiptap chain 插入、OnlyOffice/resource tab,需在后续批次继续拆边界。
|
||||
- [ ] 保持现有 smoke:
|
||||
- `node scripts/task472-side-target-secondary-pane-smoke.js`
|
||||
|
||||
@@ -20,6 +20,110 @@ function currentRootUri() {
|
||||
: '';
|
||||
}
|
||||
|
||||
function editorUploadRootFromElementWithDeps(target, deps) {
|
||||
if (deps && typeof deps.editorUploadRootFromElement === 'function') {
|
||||
return deps.editorUploadRootFromElement(target);
|
||||
}
|
||||
if (!(target instanceof Element)) return null;
|
||||
var selector = String(deps && deps.rootSelector || '[data-editor-host-kind="leptos_tiptap_island"], [data-editor-host-kind="leptos_tiptap_resource"]');
|
||||
var root = target.closest(selector);
|
||||
return root instanceof HTMLElement ? root : null;
|
||||
}
|
||||
|
||||
function resolveEditorUploadContext(detail, deps) {
|
||||
var root = null;
|
||||
var selector = detail && detail.editorRootSelector ? String(detail.editorRootSelector) : '';
|
||||
if (selector) {
|
||||
try {
|
||||
var selected = document.querySelector(selector);
|
||||
if (selected instanceof HTMLElement) root = selected;
|
||||
} catch (_) {}
|
||||
}
|
||||
if (!root && window.__mnoteIntendedSlashRoot instanceof HTMLElement && window.__mnoteIntendedSlashRoot.isConnected) {
|
||||
root = window.__mnoteIntendedSlashRoot;
|
||||
}
|
||||
if (!root && window.__mnoteLastEditorUploadRoot instanceof HTMLElement && window.__mnoteLastEditorUploadRoot.isConnected) {
|
||||
root = window.__mnoteLastEditorUploadRoot;
|
||||
}
|
||||
if (!root && document.activeElement instanceof Element) {
|
||||
root = editorUploadRootFromElementWithDeps(document.activeElement, deps);
|
||||
}
|
||||
if (!root) {
|
||||
var rootSelector = String(deps && deps.rootSelector || '[data-editor-host-kind="leptos_tiptap_island"], [data-editor-host-kind="leptos_tiptap_resource"]');
|
||||
var focused = document.querySelector(rootSelector + ' .ProseMirror:focus-within');
|
||||
root = editorUploadRootFromElementWithDeps(focused, deps);
|
||||
}
|
||||
if (!root) {
|
||||
root = document.querySelector('[data-editor-host-kind="leptos_tiptap_island"][data-pane-role="primary"]');
|
||||
}
|
||||
var pane = root instanceof Element ? root.closest('.document-pane[data-pane-role]') : null;
|
||||
var shell = root instanceof Element ? root.closest('.document-shell[data-document-id]') : null;
|
||||
var currentDocumentId = deps && typeof deps.currentDocumentId === 'function' ? deps.currentDocumentId : function() { return ''; };
|
||||
var resolveWorkspaceId = deps && typeof deps.resolveWorkspaceId === 'function' ? deps.resolveWorkspaceId : function() { return ''; };
|
||||
return {
|
||||
root: root instanceof HTMLElement ? root : null,
|
||||
documentId: String(
|
||||
detail && detail.documentId
|
||||
|| (root instanceof HTMLElement && root.getAttribute('data-document-id'))
|
||||
|| (pane instanceof HTMLElement && pane.getAttribute('data-pane-document-id'))
|
||||
|| (shell instanceof HTMLElement && shell.getAttribute('data-document-id'))
|
||||
|| currentDocumentId()
|
||||
|| ''
|
||||
).trim(),
|
||||
workspaceId: String(
|
||||
detail && detail.workspaceId
|
||||
|| (root instanceof HTMLElement && root.getAttribute('data-workspace-id'))
|
||||
|| (pane instanceof HTMLElement && pane.getAttribute('data-pane-workspace-id'))
|
||||
|| (shell instanceof HTMLElement && shell.getAttribute('data-workspace-id'))
|
||||
|| resolveWorkspaceId(document.body)
|
||||
|| ''
|
||||
).trim()
|
||||
};
|
||||
}
|
||||
|
||||
function editorRootFromUploadOptions(options, deps) {
|
||||
if (options && options.editorRoot instanceof HTMLElement) return options.editorRoot;
|
||||
if (window.__mnoteIntendedSlashRoot instanceof HTMLElement && window.__mnoteIntendedSlashRoot.isConnected) {
|
||||
return window.__mnoteIntendedSlashRoot;
|
||||
}
|
||||
if (window.__mnoteLastEditorUploadRoot instanceof HTMLElement && window.__mnoteLastEditorUploadRoot.isConnected) {
|
||||
return window.__mnoteLastEditorUploadRoot;
|
||||
}
|
||||
var rootSelector = String(deps && deps.rootSelector || '[data-editor-host-kind="leptos_tiptap_island"], [data-editor-host-kind="leptos_tiptap_resource"]');
|
||||
var focused = document.querySelector(rootSelector + ' .ProseMirror:focus-within');
|
||||
return editorUploadRootFromElementWithDeps(focused, deps);
|
||||
}
|
||||
|
||||
function openEditorUploadFilePicker(detail, deps) {
|
||||
var uploadContext = resolveEditorUploadContext(detail || {}, deps || {});
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = detail && detail.multiple !== false;
|
||||
if (detail && detail.accept) input.accept = String(detail.accept);
|
||||
input.style.position = 'fixed';
|
||||
input.style.left = '-9999px';
|
||||
input.style.top = '-9999px';
|
||||
document.body.appendChild(input);
|
||||
input.addEventListener('change', function() {
|
||||
var files = Array.from(input.files || []);
|
||||
input.remove();
|
||||
var resolveWorkspaceId = deps && typeof deps.resolveWorkspaceId === 'function' ? deps.resolveWorkspaceId : function() { return ''; };
|
||||
var currentDocumentId = deps && typeof deps.currentDocumentId === 'function' ? deps.currentDocumentId : function() { return ''; };
|
||||
var uploadFilesWithResolvedTarget = deps && typeof deps.uploadFilesWithResolvedTarget === 'function' ? deps.uploadFilesWithResolvedTarget : null;
|
||||
if (!uploadFilesWithResolvedTarget) return;
|
||||
void uploadFilesWithResolvedTarget(files, {
|
||||
workspaceId: uploadContext.workspaceId || resolveWorkspaceId(document.body),
|
||||
documentId: uploadContext.documentId || currentDocumentId(),
|
||||
targetRowId: null,
|
||||
uploadIntent: 'editor.markdown.attach'
|
||||
}, {
|
||||
insertIntoEditor: detail && detail.insertIntoEditor !== false,
|
||||
editorRoot: uploadContext.root
|
||||
});
|
||||
}, { once: true });
|
||||
input.click();
|
||||
}
|
||||
|
||||
function localAssetOpenUrl(asset, download, context) {
|
||||
if (!isLocalUploadedAsset(asset)) return '';
|
||||
var rootUri = String(context && context.rootUri || asset && (asset.rootUri || asset.root_uri) || '').trim() || currentRootUri();
|
||||
@@ -145,6 +249,9 @@ function uploadedFileSize(asset) {
|
||||
window.__mnoteLocalUploadRuntime = {
|
||||
uploadedAssetTitle: uploadedAssetTitle,
|
||||
uploadedAssetUrl: uploadedAssetUrl,
|
||||
resolveEditorUploadContext: resolveEditorUploadContext,
|
||||
editorRootFromUploadOptions: editorRootFromUploadOptions,
|
||||
openEditorUploadFilePicker: openEditorUploadFilePicker,
|
||||
localAssetOpenUrl: localAssetOpenUrl,
|
||||
uploadedAssetType: uploadedAssetType,
|
||||
fileTreeIconKindForFileName: fileTreeIconKindForFileName,
|
||||
|
||||
@@ -3775,6 +3775,15 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function resolveEditorUploadContext(detail) {
|
||||
var runtimeFn = localUploadRuntimeFunction('resolveEditorUploadContext');
|
||||
if (runtimeFn) {
|
||||
return runtimeFn(detail || {}, {
|
||||
rootSelector: EDITOR_UPLOAD_ROOT_SELECTOR,
|
||||
editorUploadRootFromElement: editorUploadRootFromElement,
|
||||
currentDocumentId: currentDocumentId,
|
||||
resolveWorkspaceId: resolveWorkspaceId
|
||||
});
|
||||
}
|
||||
var root = null;
|
||||
var selector = detail && detail.editorRootSelector ? String(detail.editorRootSelector) : '';
|
||||
if (selector) {
|
||||
@@ -3823,6 +3832,13 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function editorRootFromUploadOptions(options) {
|
||||
var runtimeFn = localUploadRuntimeFunction('editorRootFromUploadOptions');
|
||||
if (runtimeFn) {
|
||||
return runtimeFn(options || {}, {
|
||||
rootSelector: EDITOR_UPLOAD_ROOT_SELECTOR,
|
||||
editorUploadRootFromElement: editorUploadRootFromElement
|
||||
});
|
||||
}
|
||||
if (options && options.editorRoot instanceof HTMLElement) return options.editorRoot;
|
||||
if (window.__mnoteIntendedSlashRoot instanceof HTMLElement && window.__mnoteIntendedSlashRoot.isConnected) {
|
||||
return window.__mnoteIntendedSlashRoot;
|
||||
@@ -4029,6 +4045,16 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function openEditorUploadFilePicker(detail) {
|
||||
var runtimeFn = localUploadRuntimeFunction('openEditorUploadFilePicker');
|
||||
if (runtimeFn) {
|
||||
return runtimeFn(detail || {}, {
|
||||
rootSelector: EDITOR_UPLOAD_ROOT_SELECTOR,
|
||||
editorUploadRootFromElement: editorUploadRootFromElement,
|
||||
currentDocumentId: currentDocumentId,
|
||||
resolveWorkspaceId: resolveWorkspaceId,
|
||||
uploadFilesWithResolvedTarget: uploadFilesWithResolvedTarget
|
||||
});
|
||||
}
|
||||
var uploadContext = resolveEditorUploadContext(detail || {});
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
@@ -10795,12 +10821,29 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_JS.contains("refreshLocalFolderSidebarSnapshot"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("wolai:local-assets-changed"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("function resolveEditorUploadContext"));
|
||||
assert!(
|
||||
SIDEBAR_TREE_JS.contains("localUploadRuntimeFunction('resolveEditorUploadContext')")
|
||||
);
|
||||
assert!(
|
||||
SIDEBAR_TREE_JS.contains("localUploadRuntimeFunction('openEditorUploadFilePicker')")
|
||||
);
|
||||
assert!(SIDEBAR_TREE_JS.contains("__mnoteLastEditorUploadRoot"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-pane-role=\"primary\""));
|
||||
assert!(SIDEBAR_TREE_JS.contains("insertUploadedAssetIntoEditor(localPayload.asset, editorRootFromUploadOptions(options))"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("editorRoot: uploadContext.root"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_upload_runtime_contains_editor_upload_context_helpers() {
|
||||
const LOCAL_UPLOAD_RUNTIME_JS: &str =
|
||||
include_str!("../../../browser/local-upload-runtime.js");
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function resolveEditorUploadContext"));
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function editorRootFromUploadOptions"));
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function openEditorUploadFilePicker"));
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("uploadFilesWithResolvedTarget"));
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_tree_runtime_focuses_restored_local_folder_row() {
|
||||
assert!(SIDEBAR_TREE_JS.contains("applyPendingLocalFolderRestoreFocusOnce"));
|
||||
|
||||
@@ -64,6 +64,7 @@ async function uploadLocalAsset(page, root, documentId, fileName, mimeType, byte
|
||||
const form = new FormData();
|
||||
form.append("rootUri", rootUri);
|
||||
form.append("documentId", documentId);
|
||||
form.append("uploadIntent", "editor.markdown.attach");
|
||||
form.append("kind", kind);
|
||||
form.append("file", new File([new Uint8Array(bytes)], fileName, { type: mimeType }));
|
||||
const response = await fetch("/api/local-folder/assets/upload", { method: "POST", body: form });
|
||||
|
||||
Reference in New Issue
Block a user