refactor: extract conflict panel dom helper
This commit is contained in:
@@ -87,3 +87,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
|||||||
- 修改:`rust/crates/mnote-web/browser/local-upload-runtime.js`、`rust/crates/mnote-web/src/ssr/pages/layout.rs`。
|
- 修改:`rust/crates/mnote-web/browser/local-upload-runtime.js`、`rust/crates/mnote-web/src/ssr/pages/layout.rs`。
|
||||||
- 已验证:`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 -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder -- --test-threads=1`、`node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。
|
- 已验证:`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 -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder -- --test-threads=1`、`node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。
|
||||||
- 未完成:`uploadFileToMediaAsset` 编排壳、`uploadFilesWithResolvedTarget`、conflict merge / accept / keep 业务逻辑、secondary pane conflict smoke 仍未迁出。
|
- 未完成:`uploadFileToMediaAsset` 编排壳、`uploadFilesWithResolvedTarget`、conflict merge / accept / keep 业务逻辑、secondary pane conflict smoke 仍未迁出。
|
||||||
|
- 2026-05-25:Codex 主控完成 document conflict panel 第二刀:`document-conflict-panel-runtime.js` 新增并导出 `createSessionConflictPanel(session, message, deps)`,只负责冲突面板 DOM 创建和按钮回调 wiring;`web_shell.rs` 仍持有 `acceptDiskVersion`、`keepCurrentEditorVersion`、`openConflictDiffPanel` 等业务逻辑,并保留完整 inline fallback。
|
||||||
|
- 修改:`rust/crates/mnote-web/browser/document-conflict-panel-runtime.js`、`rust/crates/mnote-web/src/routes/web_shell.rs`。
|
||||||
|
- 已验证:`node --check rust/crates/mnote-web/browser/document-conflict-panel-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 document_conflict_panel_runtime_contains_dom_helpers -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`。
|
||||||
|
- 未完成:conflict merge / accept / keep 业务逻辑、document session lifecycle、secondary pane conflict smoke 仍未迁出;本轮未声称真实冲突 UI 全链路完成。
|
||||||
|
|||||||
@@ -23,6 +23,65 @@ function clearSessionConflictSurface(session, deps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSessionConflictPanel(session, message, deps) {
|
||||||
|
deps = deps || {};
|
||||||
|
var externalConflictMessage = String(deps.externalConflictMessage || '').trim() || '文件已在外部修改';
|
||||||
|
var panel = document.createElement('section');
|
||||||
|
panel.className = 'mnote-editor-conflict-panel';
|
||||||
|
panel.setAttribute('data-testid', 'mnote-editor-conflict-panel');
|
||||||
|
panel.setAttribute('role', 'status');
|
||||||
|
panel.setAttribute('aria-live', 'polite');
|
||||||
|
|
||||||
|
var heading = document.createElement('h2');
|
||||||
|
heading.textContent = '文件冲突';
|
||||||
|
var text = document.createElement('p');
|
||||||
|
text.textContent = message || externalConflictMessage;
|
||||||
|
var meta = document.createElement('div');
|
||||||
|
meta.className = 'mnote-conflict-meta';
|
||||||
|
var fileLabel = session && session.sessionKind === 'resource'
|
||||||
|
? String(session.rootUri || '') + '/' + String(session.resourcePath || session.documentId || '')
|
||||||
|
: String(session && (session.rootUri || session.documentId) || '');
|
||||||
|
var sourceLabel = typeof deps.conflictSourceLabel === 'function'
|
||||||
|
? deps.conflictSourceLabel(session)
|
||||||
|
: '';
|
||||||
|
meta.textContent = '文件:' + fileLabel + ' · 来源:' + (sourceLabel || '本地文件变更');
|
||||||
|
|
||||||
|
var actions = document.createElement('div');
|
||||||
|
actions.className = 'mnote-conflict-actions';
|
||||||
|
var acceptDisk = document.createElement('button');
|
||||||
|
acceptDisk.type = 'button';
|
||||||
|
acceptDisk.textContent = '接受磁盘版本';
|
||||||
|
acceptDisk.setAttribute('data-testid', 'mnote-conflict-accept-disk');
|
||||||
|
var keepCurrent = document.createElement('button');
|
||||||
|
keepCurrent.type = 'button';
|
||||||
|
keepCurrent.textContent = '保留当前编辑器版本';
|
||||||
|
keepCurrent.setAttribute('data-testid', 'mnote-conflict-keep-current');
|
||||||
|
var openDiff = document.createElement('button');
|
||||||
|
openDiff.type = 'button';
|
||||||
|
openDiff.textContent = '打开 diff';
|
||||||
|
openDiff.setAttribute('data-testid', 'mnote-conflict-open-diff');
|
||||||
|
actions.append(acceptDisk, keepCurrent, openDiff);
|
||||||
|
|
||||||
|
var diffPanel = document.createElement('div');
|
||||||
|
diffPanel.className = 'mnote-conflict-diff-panel';
|
||||||
|
diffPanel.setAttribute('data-testid', 'mnote-conflict-diff-panel');
|
||||||
|
diffPanel.hidden = true;
|
||||||
|
panel.append(heading, text, meta, actions, diffPanel);
|
||||||
|
|
||||||
|
acceptDisk.addEventListener('click', function() {
|
||||||
|
if (typeof deps.onAcceptDisk === 'function') deps.onAcceptDisk(session, panel);
|
||||||
|
});
|
||||||
|
keepCurrent.addEventListener('click', function() {
|
||||||
|
if (typeof deps.onKeepCurrent === 'function') deps.onKeepCurrent(session, panel);
|
||||||
|
});
|
||||||
|
openDiff.addEventListener('click', function() {
|
||||||
|
if (typeof deps.onOpenDiff === 'function') deps.onOpenDiff(session, panel);
|
||||||
|
});
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
window.__mnoteDocumentConflictPanelRuntime = {
|
window.__mnoteDocumentConflictPanelRuntime = {
|
||||||
clearSessionConflictSurface: clearSessionConflictSurface
|
clearSessionConflictSurface: clearSessionConflictSurface,
|
||||||
|
createSessionConflictPanel: createSessionConflictPanel
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2103,58 +2103,83 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
|||||||
sessionViews(session).forEach((view) => {
|
sessionViews(session).forEach((view) => {
|
||||||
const host = view.runtimeDescriptor.root.closest('.document-pane') || view.runtimeDescriptor.root;
|
const host = view.runtimeDescriptor.root.closest('.document-pane') || view.runtimeDescriptor.root;
|
||||||
if (!(host instanceof HTMLElement)) return;
|
if (!(host instanceof HTMLElement)) return;
|
||||||
const panel = document.createElement('section');
|
const runtime = window.__mnoteDocumentConflictPanelRuntime;
|
||||||
panel.className = 'mnote-editor-conflict-panel';
|
let panel = null;
|
||||||
panel.setAttribute('data-testid', 'mnote-editor-conflict-panel');
|
if (runtime && typeof runtime.createSessionConflictPanel === 'function') {
|
||||||
panel.setAttribute('role', 'status');
|
panel = runtime.createSessionConflictPanel(session, message, {
|
||||||
panel.setAttribute('aria-live', 'polite');
|
externalConflictMessage,
|
||||||
|
conflictSourceLabel,
|
||||||
const heading = document.createElement('h2');
|
onAcceptDisk: () => {
|
||||||
heading.textContent = '文件冲突';
|
acceptDiskVersion(session).catch((error) => {
|
||||||
const text = document.createElement('p');
|
setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error));
|
||||||
text.textContent = message || externalConflictMessage;
|
renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error));
|
||||||
const meta = document.createElement('div');
|
});
|
||||||
meta.className = 'mnote-conflict-meta';
|
},
|
||||||
const fileLabel = session.sessionKind === 'resource'
|
onKeepCurrent: () => {
|
||||||
? `${session.rootUri || ''}/${session.resourcePath || session.documentId}`
|
keepCurrentEditorVersion(session).catch((error) => {
|
||||||
: (session.rootUri || session.documentId);
|
setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error));
|
||||||
meta.textContent = `文件:${fileLabel} · 来源:${conflictSourceLabel(session) || '本地文件变更'}`;
|
renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error));
|
||||||
const actions = document.createElement('div');
|
});
|
||||||
actions.className = 'mnote-conflict-actions';
|
},
|
||||||
const acceptDisk = document.createElement('button');
|
onOpenDiff: (_session, createdPanel) => {
|
||||||
acceptDisk.type = 'button';
|
openConflictDiffPanel(session, createdPanel);
|
||||||
acceptDisk.textContent = '接受磁盘版本';
|
},
|
||||||
acceptDisk.setAttribute('data-testid', 'mnote-conflict-accept-disk');
|
|
||||||
const keepCurrent = document.createElement('button');
|
|
||||||
keepCurrent.type = 'button';
|
|
||||||
keepCurrent.textContent = '保留当前编辑器版本';
|
|
||||||
keepCurrent.setAttribute('data-testid', 'mnote-conflict-keep-current');
|
|
||||||
const openDiff = document.createElement('button');
|
|
||||||
openDiff.type = 'button';
|
|
||||||
openDiff.textContent = '打开 diff';
|
|
||||||
openDiff.setAttribute('data-testid', 'mnote-conflict-open-diff');
|
|
||||||
actions.append(acceptDisk, keepCurrent, openDiff);
|
|
||||||
const diffPanel = document.createElement('div');
|
|
||||||
diffPanel.className = 'mnote-conflict-diff-panel';
|
|
||||||
diffPanel.setAttribute('data-testid', 'mnote-conflict-diff-panel');
|
|
||||||
diffPanel.hidden = true;
|
|
||||||
panel.append(heading, text, meta, actions, diffPanel);
|
|
||||||
|
|
||||||
acceptDisk.addEventListener('click', () => {
|
|
||||||
acceptDiskVersion(session).catch((error) => {
|
|
||||||
setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error));
|
|
||||||
renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error));
|
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
keepCurrent.addEventListener('click', () => {
|
if (!(panel instanceof HTMLElement)) {
|
||||||
keepCurrentEditorVersion(session).catch((error) => {
|
panel = document.createElement('section');
|
||||||
setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error));
|
panel.className = 'mnote-editor-conflict-panel';
|
||||||
renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error));
|
panel.setAttribute('data-testid', 'mnote-editor-conflict-panel');
|
||||||
|
panel.setAttribute('role', 'status');
|
||||||
|
panel.setAttribute('aria-live', 'polite');
|
||||||
|
|
||||||
|
const heading = document.createElement('h2');
|
||||||
|
heading.textContent = '文件冲突';
|
||||||
|
const text = document.createElement('p');
|
||||||
|
text.textContent = message || externalConflictMessage;
|
||||||
|
const meta = document.createElement('div');
|
||||||
|
meta.className = 'mnote-conflict-meta';
|
||||||
|
const fileLabel = session.sessionKind === 'resource'
|
||||||
|
? `${session.rootUri || ''}/${session.resourcePath || session.documentId}`
|
||||||
|
: (session.rootUri || session.documentId);
|
||||||
|
meta.textContent = `文件:${fileLabel} · 来源:${conflictSourceLabel(session) || '本地文件变更'}`;
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'mnote-conflict-actions';
|
||||||
|
const acceptDisk = document.createElement('button');
|
||||||
|
acceptDisk.type = 'button';
|
||||||
|
acceptDisk.textContent = '接受磁盘版本';
|
||||||
|
acceptDisk.setAttribute('data-testid', 'mnote-conflict-accept-disk');
|
||||||
|
const keepCurrent = document.createElement('button');
|
||||||
|
keepCurrent.type = 'button';
|
||||||
|
keepCurrent.textContent = '保留当前编辑器版本';
|
||||||
|
keepCurrent.setAttribute('data-testid', 'mnote-conflict-keep-current');
|
||||||
|
const openDiff = document.createElement('button');
|
||||||
|
openDiff.type = 'button';
|
||||||
|
openDiff.textContent = '打开 diff';
|
||||||
|
openDiff.setAttribute('data-testid', 'mnote-conflict-open-diff');
|
||||||
|
actions.append(acceptDisk, keepCurrent, openDiff);
|
||||||
|
const diffPanel = document.createElement('div');
|
||||||
|
diffPanel.className = 'mnote-conflict-diff-panel';
|
||||||
|
diffPanel.setAttribute('data-testid', 'mnote-conflict-diff-panel');
|
||||||
|
diffPanel.hidden = true;
|
||||||
|
panel.append(heading, text, meta, actions, diffPanel);
|
||||||
|
|
||||||
|
acceptDisk.addEventListener('click', () => {
|
||||||
|
acceptDiskVersion(session).catch((error) => {
|
||||||
|
setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error));
|
||||||
|
renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
keepCurrent.addEventListener('click', () => {
|
||||||
openDiff.addEventListener('click', () => {
|
keepCurrentEditorVersion(session).catch((error) => {
|
||||||
openConflictDiffPanel(session, panel);
|
setSessionStatus(session, 'external-change-conflict', error instanceof Error ? error.message : String(error));
|
||||||
});
|
renderSessionConflictSurface(session, error instanceof Error ? error.message : String(error));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
openDiff.addEventListener('click', () => {
|
||||||
|
openConflictDiffPanel(session, panel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const header = host.querySelector('.document-shell-header');
|
const header = host.querySelector('.document-shell-header');
|
||||||
if (header && header.parentNode) {
|
if (header && header.parentNode) {
|
||||||
@@ -5579,6 +5604,24 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn document_conflict_panel_runtime_contains_dom_helpers() {
|
||||||
|
const DOCUMENT_CONFLICT_PANEL_RUNTIME_JS: &str =
|
||||||
|
include_str!("../../browser/document-conflict-panel-runtime.js");
|
||||||
|
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("function clearSessionConflictSurface"));
|
||||||
|
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("function createSessionConflictPanel"));
|
||||||
|
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
|
||||||
|
.contains("data-testid', 'mnote-editor-conflict-panel"));
|
||||||
|
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS
|
||||||
|
.contains("data-testid', 'mnote-conflict-accept-disk"));
|
||||||
|
assert!(
|
||||||
|
DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("data-testid', 'mnote-conflict-open-diff")
|
||||||
|
);
|
||||||
|
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onAcceptDisk"));
|
||||||
|
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onKeepCurrent"));
|
||||||
|
assert!(DOCUMENT_CONFLICT_PANEL_RUNTIME_JS.contains("onOpenDiff"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn sidebar_and_filetree_do_not_return_dev_fixtures_by_default() {
|
async fn sidebar_and_filetree_do_not_return_dev_fixtures_by_default() {
|
||||||
let config = AppConfig {
|
let config = AppConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user