fix: open markdown attachments in resource tabs
This commit is contained in:
@@ -78,7 +78,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
};
|
};
|
||||||
|
|
||||||
function closestAction(target, selector) {
|
function closestAction(target, selector) {
|
||||||
return target && typeof target.closest === 'function' ? target.closest(selector) : null;
|
var node = target && target.nodeType === Node.TEXT_NODE ? target.parentElement : target;
|
||||||
|
return node && typeof node.closest === 'function' ? node.closest(selector) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleWorkspaceSidebar(trigger) {
|
function toggleWorkspaceSidebar(trigger) {
|
||||||
@@ -7135,6 +7136,21 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function localFileOpenPathFromHref(href) {
|
||||||
|
try {
|
||||||
|
var url = new URL(String(href || ''), window.location.origin);
|
||||||
|
if (url.pathname !== '/api/local-folder/files/open') return '';
|
||||||
|
return String(url.searchParams.get('path') || '').trim();
|
||||||
|
} catch (_) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileNameFromPath(path) {
|
||||||
|
var value = String(path || '').trim();
|
||||||
|
return value.indexOf('/') >= 0 ? value.split('/').pop() : value;
|
||||||
|
}
|
||||||
|
|
||||||
function isOnlyOfficeAttachmentHref(href) {
|
function isOnlyOfficeAttachmentHref(href) {
|
||||||
try {
|
try {
|
||||||
var url = new URL(String(href || ''), window.location.origin);
|
var url = new URL(String(href || ''), window.location.origin);
|
||||||
@@ -7169,9 +7185,10 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
function detailFromEditorAttachmentLink(link) {
|
function detailFromEditorAttachmentLink(link) {
|
||||||
var rawHref = link instanceof HTMLAnchorElement ? link.href : '';
|
var rawHref = link instanceof HTMLAnchorElement ? link.href : '';
|
||||||
var params = attachmentQueryParams(rawHref);
|
var params = attachmentQueryParams(rawHref);
|
||||||
var fileName = params.get('fileName') || (link ? link.textContent : '') || '未命名附件';
|
var localFilePath = localFileOpenPathFromHref(rawHref);
|
||||||
|
var fileName = params.get('fileName') || fileNameFromPath(localFilePath) || (link ? link.textContent : '') || '未命名附件';
|
||||||
var fileType = params.get('fileType') || inferOnlyOfficeFileType(fileName, '');
|
var fileType = params.get('fileType') || inferOnlyOfficeFileType(fileName, '');
|
||||||
var assetId = params.get('assetId') || (link ? link.getAttribute('data-asset-id') : '') || '';
|
var assetId = params.get('assetId') || (link ? link.getAttribute('data-asset-id') : '') || (localFilePath ? 'local-file:' + localFilePath : '');
|
||||||
var fileUrl = params.get('fileUrl') || '';
|
var fileUrl = params.get('fileUrl') || '';
|
||||||
var href = rawHref;
|
var href = rawHref;
|
||||||
if (!isOnlyOfficeAttachmentHref(rawHref) && fileType) {
|
if (!isOnlyOfficeAttachmentHref(rawHref) && fileType) {
|
||||||
@@ -7205,14 +7222,16 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
if (!(link instanceof HTMLAnchorElement)) return;
|
if (!(link instanceof HTMLAnchorElement)) return;
|
||||||
var href = link.getAttribute('href') || '';
|
var href = link.getAttribute('href') || '';
|
||||||
var params = attachmentQueryParams(href);
|
var params = attachmentQueryParams(href);
|
||||||
var fileName = params.get('fileName') || link.textContent || '';
|
var localFilePath = localFileOpenPathFromHref(href);
|
||||||
|
var fileName = params.get('fileName') || fileNameFromPath(localFilePath) || link.textContent || '';
|
||||||
var className = link.getAttribute('class') || '';
|
var className = link.getAttribute('class') || '';
|
||||||
var shouldEnhance = isOnlyOfficeAttachmentHref(href)
|
var shouldEnhance = isOnlyOfficeAttachmentHref(href)
|
||||||
|| className.indexOf('mnote-uploaded-attachment-row') >= 0
|
|| className.indexOf('mnote-uploaded-attachment-row') >= 0
|
||||||
|| isOfficeFileName(fileName);
|
|| isOfficeFileName(fileName)
|
||||||
|
|| Boolean(localFilePath && (isPdfAttachmentFileName(fileName) || isCodeAttachmentFileName(fileName)));
|
||||||
if (!shouldEnhance) return;
|
if (!shouldEnhance) return;
|
||||||
link.setAttribute('data-mnote-attachment-link', 'true');
|
link.setAttribute('data-mnote-attachment-link', 'true');
|
||||||
var assetId = params.get('assetId') || link.getAttribute('data-asset-id') || '';
|
var assetId = params.get('assetId') || link.getAttribute('data-asset-id') || (localFilePath ? 'local-file:' + localFilePath : '');
|
||||||
if (assetId) link.setAttribute('data-asset-id', assetId);
|
if (assetId) link.setAttribute('data-asset-id', assetId);
|
||||||
if (isOnlyOfficeAttachmentHref(href)) {
|
if (isOnlyOfficeAttachmentHref(href)) {
|
||||||
link.setAttribute('href', buildOnlyOfficeOpenPath({
|
link.setAttribute('href', buildOnlyOfficeOpenPath({
|
||||||
@@ -7458,6 +7477,27 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
var attachmentObserver = new MutationObserver(function() { enhanceEditorAttachmentLinks(); });
|
var attachmentObserver = new MutationObserver(function() { enhanceEditorAttachmentLinks(); });
|
||||||
attachmentObserver.observe(document.documentElement, { childList: true, subtree: true });
|
attachmentObserver.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
function interceptEditorAttachmentLink(event) {
|
||||||
|
var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row');
|
||||||
|
if (!(editorAttachmentLink instanceof HTMLAnchorElement)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
|
||||||
|
openEditorAttachmentLink(editorAttachmentLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('mousedown', interceptEditorAttachmentLink, true);
|
||||||
|
window.addEventListener('click', interceptEditorAttachmentLink, true);
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var editorAttachmentLink = closestAction(e.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row');
|
||||||
|
if (!(editorAttachmentLink instanceof HTMLAnchorElement)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
|
||||||
|
openEditorAttachmentLink(editorAttachmentLink);
|
||||||
|
}, true);
|
||||||
|
|
||||||
document.addEventListener('mouseover', function(event) {
|
document.addEventListener('mouseover', function(event) {
|
||||||
var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row');
|
var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row');
|
||||||
if (!(link instanceof HTMLAnchorElement)) return;
|
if (!(link instanceof HTMLAnchorElement)) return;
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const os = require("node:os");
|
||||||
|
const path = require("node:path");
|
||||||
|
const { chromium } = require("playwright");
|
||||||
|
|
||||||
|
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||||
|
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
|
||||||
|
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||||||
|
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium"]
|
||||||
|
.find((candidate) => fs.existsSync(candidate));
|
||||||
|
|
||||||
|
function fileUrl(localPath) {
|
||||||
|
return `file://${localPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function localMdDocumentId(relativePath) {
|
||||||
|
return `local-md:${relativePath.replaceAll("/", "~2F")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function documentUrl(root, relativePath) {
|
||||||
|
const url = new URL(`${BASE_URL}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`);
|
||||||
|
url.searchParams.set("sourceKind", "local_folder");
|
||||||
|
url.searchParams.set("rootUri", fileUrl(root));
|
||||||
|
url.searchParams.set("treeView", "filetree");
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeWorkspaceManifest(root, ownerId) {
|
||||||
|
const metadataDir = path.join(root, ".mnote");
|
||||||
|
fs.mkdirSync(metadataDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(metadataDir, "workspace.json"),
|
||||||
|
`${JSON.stringify({
|
||||||
|
workspaceId: `local-ws:${ownerId}:task459`,
|
||||||
|
ownerId,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
capabilities: ["local_files", "markdown_edit", "asset_upload"],
|
||||||
|
}, null, 2)}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quickLogin(page) {
|
||||||
|
await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||||
|
const quickLoginButton = page.getByRole("button", { name: "测试账号快速登录" });
|
||||||
|
if (await quickLoginButton.count()) {
|
||||||
|
await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
|
||||||
|
await page.waitForURL((url) => url.pathname === "/", { timeout: UI_TIMEOUT_MS }).catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task459-md-attachment-"));
|
||||||
|
const relativePath = "README.md";
|
||||||
|
const documentId = localMdDocumentId(relativePath);
|
||||||
|
writeWorkspaceManifest(root, "user_real");
|
||||||
|
fs.mkdirSync(path.join(root, "README"), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(root, relativePath),
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"title: MD Attachment",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"# MD Attachment",
|
||||||
|
"",
|
||||||
|
"[resource-note.md](README/resource-note.md)",
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(path.join(root, "README", "resource-note.md"), "# Resource Note\n\n资源正文\n", "utf8");
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: process.env.HEADFUL !== "1",
|
||||||
|
executablePath: CHROMIUM_EXECUTABLE_PATH,
|
||||||
|
});
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport: { width: 1360, height: 900 },
|
||||||
|
extraHTTPHeaders: {
|
||||||
|
"x-mnote-actor-id": "user_real",
|
||||||
|
"x-mnote-actor-type": "user",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
const popups = [];
|
||||||
|
page.on("popup", (popup) => popups.push(popup));
|
||||||
|
try {
|
||||||
|
await quickLogin(page);
|
||||||
|
await page.goto(documentUrl(root, relativePath), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||||
|
await page.locator(".document-pane[data-pane-role=\"primary\"] .editor-surface .ProseMirror").first().waitFor({
|
||||||
|
state: "visible",
|
||||||
|
timeout: UI_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
const link = page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"][href*="README%2Fresource-note.md"]').first();
|
||||||
|
await link.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const node = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"][href*="README%2Fresource-note.md"]');
|
||||||
|
return node && node.classList.contains("mnote-uploaded-attachment-row") && node.getAttribute("data-mnote-attachment-link") === "true";
|
||||||
|
}, null, { timeout: UI_TIMEOUT_MS });
|
||||||
|
|
||||||
|
await link.click({ timeout: UI_TIMEOUT_MS });
|
||||||
|
await page.locator('[data-testid="mnote-resource-tab-host"]:not([hidden])').waitFor({
|
||||||
|
state: "visible",
|
||||||
|
timeout: UI_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
await page.locator('.mnote-main-tab.is-active[data-mnote-tab-kind="markdown"]').waitFor({
|
||||||
|
state: "visible",
|
||||||
|
timeout: UI_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
const resourceEditor = page.locator('.mnote-resource-tab-panel:not([hidden]) .editor-surface .ProseMirror[contenteditable="true"]').first();
|
||||||
|
await resourceEditor.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||||
|
const resourceText = await resourceEditor.innerText({ timeout: UI_TIMEOUT_MS });
|
||||||
|
assert(resourceText.includes("资源正文"), `MD 附件应在资源 tab 中用 tiptap 打开:${resourceText}`);
|
||||||
|
assert.equal(popups.length, 0, `点击 MD 附件不应打开浏览器新窗口,实际 popup=${popups.length}`);
|
||||||
|
const pageText = await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first().innerText({
|
||||||
|
timeout: UI_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
assert(pageText.includes("resource-note.md"), "主页面正文应保持原附件链接");
|
||||||
|
console.log(JSON.stringify({ ok: true, root, documentId, popups: popups.length }, null, 2));
|
||||||
|
} finally {
|
||||||
|
await context.close().catch(() => undefined);
|
||||||
|
await browser.close().catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error && error.stack ? error.stack : error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user