refactor: split tree shell filetree helpers
This commit is contained in:
+1
-1
@@ -206,7 +206,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib ssr::pages::layout
|
||||
|
||||
- [x] D1. `tree-shell-state-runtime.js`:state hydration、serialization、patch dispatch。
|
||||
- [x] D2. `tree-shell-page-runtime.js`:page tree keyboard、expand、focus、drag/drop intent。
|
||||
- [ ] D3. `tree-shell-filetree-runtime.js`:filetree row normalization、resource meta、open target。
|
||||
- [x] D3. `tree-shell-filetree-runtime.js`:filetree row normalization、resource meta、open target。
|
||||
- [ ] D4. `tree-shell-picker-runtime.js`:picker search/focus/pick root。
|
||||
- [ ] D5. `tree-shell-dom-runtime.js`:DOM patch/render helpers。
|
||||
- [x] D6. `tree-shell-icons-runtime.js`:icon templates 和 resource kind badge。
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// MNote debug /tree shell filetree row normalization helpers.
|
||||
|
||||
import {
|
||||
normalizeTreeShellNumber,
|
||||
normalizeTreeShellText,
|
||||
} from "./tree-shell-state-runtime.js";
|
||||
|
||||
export function buildFileTreeRuntimeEnvironment(runtimeContext) {
|
||||
const fileTreeRowById = runtimeContext.fileTreeRowById || new Map();
|
||||
return {
|
||||
visibleRowIds: Array.isArray(runtimeContext.visibleFileTreeRowIds)
|
||||
? runtimeContext.visibleFileTreeRowIds
|
||||
: [],
|
||||
rows: Array.from(fileTreeRowById.values()).map((entry) => ({
|
||||
rowId: entry.rowId,
|
||||
rowKind: entry.rowKind,
|
||||
documentId: entry.documentId || null,
|
||||
assetId: entry.assetId || null,
|
||||
})),
|
||||
rootUri: runtimeContext.sourceKind === "local_folder" ? runtimeContext.rootUri || null : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function compareTreeShellItems(left, right) {
|
||||
const byPosition = left.position - right.position;
|
||||
if (byPosition !== 0) return byPosition;
|
||||
return left.title.localeCompare(right.title, "zh-CN");
|
||||
}
|
||||
|
||||
export function normalizeTreeShellResourceMeta(value) {
|
||||
if (!value || typeof value !== "object") {
|
||||
return {
|
||||
resourceKind: "",
|
||||
documentId: "",
|
||||
assetId: "",
|
||||
assetKind: "",
|
||||
objectIdentity: null,
|
||||
blockAssetRelation: null,
|
||||
};
|
||||
}
|
||||
const objectIdentity =
|
||||
value?.objectIdentity && typeof value.objectIdentity === "object"
|
||||
? value.objectIdentity
|
||||
: null;
|
||||
const blockAssetRelation =
|
||||
value?.blockAssetRelation && typeof value.blockAssetRelation === "object"
|
||||
? value.blockAssetRelation
|
||||
: null;
|
||||
return {
|
||||
resourceKind: normalizeTreeShellText(value?.resourceKind),
|
||||
documentId: normalizeTreeShellText(value?.documentId),
|
||||
assetId: normalizeTreeShellText(value?.assetId),
|
||||
assetKind: normalizeTreeShellText(value?.assetKind),
|
||||
objectIdentity,
|
||||
blockAssetRelation,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeTreeShellRowKind(value) {
|
||||
const normalized = normalizeTreeShellText(value).toLowerCase();
|
||||
if (normalized === "index") return "index";
|
||||
if (normalized === "asset") return "asset";
|
||||
if (normalized === "folder") return "folder";
|
||||
if (normalized === "markdown") return "markdown";
|
||||
if (normalized === "asset_folder") return "asset_folder";
|
||||
return "document";
|
||||
}
|
||||
|
||||
export function normalizeTreeShellTreeItems(items, options = {}) {
|
||||
const excludedIds = options.excludedIds instanceof Set ? options.excludedIds : new Set();
|
||||
return Array.isArray(items)
|
||||
? items
|
||||
.map((item) => {
|
||||
const nodeId = normalizeTreeShellText(item?.nodeId);
|
||||
const resourceMeta = normalizeTreeShellResourceMeta(item?.resourceMeta);
|
||||
const rowKind = normalizeTreeShellRowKind(item?.rowKind);
|
||||
const fallbackRowId =
|
||||
rowKind === "index"
|
||||
? `index:${resourceMeta.documentId || nodeId.replace(/^index:/, "")}`
|
||||
: rowKind === "asset"
|
||||
? `asset:${resourceMeta.assetId || nodeId.replace(/^asset:/, "")}`
|
||||
: rowKind === "asset_folder"
|
||||
? `asset-folder:${resourceMeta.assetId || nodeId.replace(/^asset-folder:/, "")}`
|
||||
: `doc:${resourceMeta.documentId || nodeId}`;
|
||||
return {
|
||||
rowId: normalizeTreeShellText(item?.rowId, fallbackRowId),
|
||||
rowKind,
|
||||
nodeId,
|
||||
parentNodeId: normalizeTreeShellText(item?.parentNodeId) || null,
|
||||
title: normalizeTreeShellText(item?.title, rowKind === "index" ? "index.md" : "无标题"),
|
||||
depth: normalizeTreeShellNumber(item?.depth, 0),
|
||||
childCount: normalizeTreeShellNumber(item?.childCount, 0),
|
||||
position: normalizeTreeShellNumber(item?.position),
|
||||
expandedByDefault: item?.expandedByDefault !== false,
|
||||
iconHint: normalizeTreeShellText(item?.iconHint),
|
||||
capabilities: Array.isArray(item?.capabilities)
|
||||
? item.capabilities
|
||||
.map((capability) => normalizeTreeShellText(capability))
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
resourceMeta,
|
||||
};
|
||||
})
|
||||
.filter((item) => item.nodeId && !excludedIds.has(item.nodeId))
|
||||
: [];
|
||||
}
|
||||
|
||||
export function getFileTreeRowDocumentId(item) {
|
||||
if (!item) return "";
|
||||
if (item.rowKind === "document" || item.rowKind === "markdown") return item.nodeId;
|
||||
if (item.rowKind === "index") return item.nodeId.replace(/^index:/, "");
|
||||
return "";
|
||||
}
|
||||
|
||||
export function getFileTreeRowOwnerDocumentId(item) {
|
||||
if (!item) return "";
|
||||
if (item.resourceMeta?.documentId) return item.resourceMeta.documentId;
|
||||
return getFileTreeRowDocumentId(item);
|
||||
}
|
||||
|
||||
export function getFileTreeRowAssetId(item) {
|
||||
if (!item) return "";
|
||||
if (item.resourceMeta?.assetId) return item.resourceMeta.assetId;
|
||||
if (item.rowKind === "asset") return item.nodeId.replace(/^asset:/, "");
|
||||
if (item.rowKind === "asset_folder") return item.nodeId.replace(/^asset-folder:/, "");
|
||||
return "";
|
||||
}
|
||||
|
||||
export function getFileTreeRowIconKind(item) {
|
||||
if (!item) return "file";
|
||||
const iconHint = normalizeTreeShellText(item.iconHint).toLowerCase();
|
||||
if (iconHint === "mindmap") return "mindmap";
|
||||
if (iconHint === "table") return "table";
|
||||
if (iconHint === "index") return "index";
|
||||
if (iconHint === "page") return "page";
|
||||
if (item.rowKind === "document") return "page";
|
||||
if (item.rowKind === "folder") return "folder";
|
||||
if (item.rowKind === "markdown") return "file";
|
||||
if (item.rowKind === "index") return "index";
|
||||
if (item.rowKind === "asset_folder") return "mindmap";
|
||||
if (item.resourceMeta?.resourceKind === "table") return "table";
|
||||
if (item.resourceMeta?.resourceKind === "mindmap") return "mindmap";
|
||||
return "file";
|
||||
}
|
||||
|
||||
export function getFileTreeRowMetaLabel(item) {
|
||||
if (!item) return "";
|
||||
if (item.rowKind === "document") {
|
||||
return `${getFileTreeRowDocumentId(item)} · 页面`;
|
||||
}
|
||||
if (item.rowKind === "folder") {
|
||||
return "本地文件夹";
|
||||
}
|
||||
if (item.rowKind === "markdown") {
|
||||
return `${getFileTreeRowDocumentId(item)} · Markdown`;
|
||||
}
|
||||
if (item.rowKind === "index") {
|
||||
return "页面正文";
|
||||
}
|
||||
if (item.rowKind === "asset_folder") {
|
||||
return `${item.resourceMeta?.resourceKind || "mindmap"} · 资源目录`;
|
||||
}
|
||||
return `${item.resourceMeta?.resourceKind || item.resourceMeta?.assetKind || "asset"} · ${getFileTreeRowAssetId(item)}`;
|
||||
}
|
||||
@@ -10,6 +10,16 @@ import {
|
||||
createTreeShellActionButton,
|
||||
createTreeShellKindBadge as createKindBadge,
|
||||
} from "./tree-shell-icons-runtime.js";
|
||||
import {
|
||||
buildFileTreeRuntimeEnvironment,
|
||||
compareTreeShellItems,
|
||||
getFileTreeRowAssetId,
|
||||
getFileTreeRowDocumentId,
|
||||
getFileTreeRowIconKind,
|
||||
getFileTreeRowMetaLabel,
|
||||
getFileTreeRowOwnerDocumentId,
|
||||
normalizeTreeShellTreeItems,
|
||||
} from "./tree-shell-filetree-runtime.js";
|
||||
import {
|
||||
normalizeTreeShellNumber as normalizeNumber,
|
||||
normalizeTreeShellStringArray as normalizeStringArray,
|
||||
@@ -20,22 +30,6 @@ import {
|
||||
resolveTreeShellTargetOrigin,
|
||||
} from "./tree-shell-state-runtime.js";
|
||||
|
||||
function buildFileTreeRuntimeEnvironment(runtimeContext) {
|
||||
const fileTreeRowById = runtimeContext.fileTreeRowById || new Map();
|
||||
return {
|
||||
visibleRowIds: Array.isArray(runtimeContext.visibleFileTreeRowIds)
|
||||
? runtimeContext.visibleFileTreeRowIds
|
||||
: [],
|
||||
rows: Array.from(fileTreeRowById.values()).map((entry) => ({
|
||||
rowId: entry.rowId,
|
||||
rowKind: entry.rowKind,
|
||||
documentId: entry.documentId || null,
|
||||
assetId: entry.assetId || null,
|
||||
})),
|
||||
rootUri: runtimeContext.sourceKind === "local_folder" ? runtimeContext.rootUri || null : null,
|
||||
};
|
||||
}
|
||||
|
||||
function startTreeShellRuntime() {
|
||||
const stateElement = document.getElementById("tree-shell-state");
|
||||
const appElement = document.getElementById("tree-shell-app");
|
||||
@@ -173,98 +167,8 @@ function startTreeShellRuntime() {
|
||||
"/api/tree/runtime/reduce",
|
||||
);
|
||||
|
||||
const normalizeParent = (value) => {
|
||||
const normalized = normalizeText(value);
|
||||
return normalized || null;
|
||||
};
|
||||
|
||||
const normalizeRowKind = (value) => {
|
||||
const normalized = normalizeText(value).toLowerCase();
|
||||
if (normalized === "index") return "index";
|
||||
if (normalized === "asset") return "asset";
|
||||
if (normalized === "folder") return "folder";
|
||||
if (normalized === "markdown") return "markdown";
|
||||
if (normalized === "asset_folder") return "asset_folder";
|
||||
return "document";
|
||||
};
|
||||
|
||||
const normalizeCapabilities = (value) =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
.map((item) => normalizeText(item))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
const normalizeResourceMeta = (value) => {
|
||||
if (!value || typeof value !== "object") {
|
||||
return {
|
||||
resourceKind: "",
|
||||
documentId: "",
|
||||
assetId: "",
|
||||
assetKind: "",
|
||||
objectIdentity: null,
|
||||
blockAssetRelation: null,
|
||||
};
|
||||
}
|
||||
const objectIdentity =
|
||||
value?.objectIdentity && typeof value.objectIdentity === "object"
|
||||
? value.objectIdentity
|
||||
: null;
|
||||
const blockAssetRelation =
|
||||
value?.blockAssetRelation && typeof value.blockAssetRelation === "object"
|
||||
? value.blockAssetRelation
|
||||
: null;
|
||||
return {
|
||||
resourceKind: normalizeText(value?.resourceKind),
|
||||
documentId: normalizeText(value?.documentId),
|
||||
assetId: normalizeText(value?.assetId),
|
||||
assetKind: normalizeText(value?.assetKind),
|
||||
objectIdentity,
|
||||
blockAssetRelation,
|
||||
};
|
||||
};
|
||||
|
||||
const compareItems = (left, right) => {
|
||||
const byPosition = left.position - right.position;
|
||||
if (byPosition !== 0) return byPosition;
|
||||
return left.title.localeCompare(right.title, "zh-CN");
|
||||
};
|
||||
|
||||
const normalizeTreeItems = (items) =>
|
||||
Array.isArray(items)
|
||||
? items
|
||||
.map((item) => {
|
||||
const nodeId = normalizeText(item?.nodeId);
|
||||
const resourceMeta = normalizeResourceMeta(item?.resourceMeta);
|
||||
const rowKind = normalizeRowKind(item?.rowKind);
|
||||
const fallbackRowId =
|
||||
rowKind === "index"
|
||||
? `index:${resourceMeta.documentId || nodeId.replace(/^index:/, "")}`
|
||||
: rowKind === "asset"
|
||||
? `asset:${resourceMeta.assetId || nodeId.replace(/^asset:/, "")}`
|
||||
: rowKind === "asset_folder"
|
||||
? `asset-folder:${resourceMeta.assetId || nodeId.replace(/^asset-folder:/, "")}`
|
||||
: `doc:${resourceMeta.documentId || nodeId}`;
|
||||
return {
|
||||
rowId: normalizeText(item?.rowId, fallbackRowId),
|
||||
rowKind,
|
||||
nodeId,
|
||||
parentNodeId: normalizeParent(item?.parentNodeId),
|
||||
title: normalizeText(item?.title, rowKind === "index" ? "index.md" : "无标题"),
|
||||
depth: normalizeNumber(item?.depth, 0),
|
||||
childCount: normalizeNumber(item?.childCount, 0),
|
||||
position: normalizeNumber(item?.position),
|
||||
expandedByDefault: item?.expandedByDefault !== false,
|
||||
iconHint: normalizeText(item?.iconHint),
|
||||
capabilities: normalizeCapabilities(item?.capabilities),
|
||||
resourceMeta,
|
||||
};
|
||||
})
|
||||
.filter((item) => item.nodeId && !excludedIds.has(item.nodeId))
|
||||
: [];
|
||||
|
||||
let rawItems = Array.isArray(hostOverride.items) ? hostOverride.items : state.items;
|
||||
let normalizedItems = normalizeTreeItems(rawItems);
|
||||
let normalizedItems = normalizeTreeShellTreeItems(rawItems, { excludedIds });
|
||||
let itemById = new Map();
|
||||
let fileTreeRowById = new Map();
|
||||
let childrenByParentId = new Map();
|
||||
@@ -301,8 +205,8 @@ function startTreeShellRuntime() {
|
||||
bucket.push(item);
|
||||
childrenByParentId.set(parentId, bucket);
|
||||
});
|
||||
roots.sort(compareItems);
|
||||
childrenByParentId.forEach((bucket) => bucket.sort(compareItems));
|
||||
roots.sort(compareTreeShellItems);
|
||||
childrenByParentId.forEach((bucket) => bucket.sort(compareTreeShellItems));
|
||||
};
|
||||
rebuildTreeIndexes();
|
||||
|
||||
@@ -448,64 +352,6 @@ function startTreeShellRuntime() {
|
||||
|
||||
const FILETREE_DRAG_MIME = "application/x-mnote-filetree-row-ids";
|
||||
|
||||
const getFileTreeRowDocumentId = (item) => {
|
||||
if (!item) return "";
|
||||
if (item.rowKind === "document" || item.rowKind === "markdown") return item.nodeId;
|
||||
if (item.rowKind === "index") return item.nodeId.replace(/^index:/, "");
|
||||
return "";
|
||||
};
|
||||
|
||||
const getFileTreeRowOwnerDocumentId = (item) => {
|
||||
if (!item) return "";
|
||||
if (item.resourceMeta?.documentId) return item.resourceMeta.documentId;
|
||||
return getFileTreeRowDocumentId(item);
|
||||
};
|
||||
|
||||
const getFileTreeRowAssetId = (item) => {
|
||||
if (!item) return "";
|
||||
if (item.resourceMeta?.assetId) return item.resourceMeta.assetId;
|
||||
if (item.rowKind === "asset") return item.nodeId.replace(/^asset:/, "");
|
||||
if (item.rowKind === "asset_folder") return item.nodeId.replace(/^asset-folder:/, "");
|
||||
return "";
|
||||
};
|
||||
|
||||
const getFileTreeRowIconKind = (item) => {
|
||||
if (!item) return "file";
|
||||
const iconHint = normalizeText(item.iconHint).toLowerCase();
|
||||
if (iconHint === "mindmap") return "mindmap";
|
||||
if (iconHint === "table") return "table";
|
||||
if (iconHint === "index") return "index";
|
||||
if (iconHint === "page") return "page";
|
||||
if (item.rowKind === "document") return "page";
|
||||
if (item.rowKind === "folder") return "folder";
|
||||
if (item.rowKind === "markdown") return "file";
|
||||
if (item.rowKind === "index") return "index";
|
||||
if (item.rowKind === "asset_folder") return "mindmap";
|
||||
if (item.resourceMeta?.resourceKind === "table") return "table";
|
||||
if (item.resourceMeta?.resourceKind === "mindmap") return "mindmap";
|
||||
return "file";
|
||||
};
|
||||
|
||||
const getFileTreeRowMetaLabel = (item) => {
|
||||
if (!item) return "";
|
||||
if (item.rowKind === "document") {
|
||||
return `${getFileTreeRowDocumentId(item)} · 页面`;
|
||||
}
|
||||
if (item.rowKind === "folder") {
|
||||
return "本地文件夹";
|
||||
}
|
||||
if (item.rowKind === "markdown") {
|
||||
return `${getFileTreeRowDocumentId(item)} · Markdown`;
|
||||
}
|
||||
if (item.rowKind === "index") {
|
||||
return "页面正文";
|
||||
}
|
||||
if (item.rowKind === "asset_folder") {
|
||||
return `${item.resourceMeta?.resourceKind || "mindmap"} · 资源目录`;
|
||||
}
|
||||
return `${item.resourceMeta?.resourceKind || item.resourceMeta?.assetKind || "asset"} · ${getFileTreeRowAssetId(item)}`;
|
||||
};
|
||||
|
||||
const canExpandFileTreeRow = (item) => {
|
||||
if (!item) return false;
|
||||
return item.childCount > 0 || item.capabilities.includes("expand");
|
||||
@@ -1522,7 +1368,7 @@ function startTreeShellRuntime() {
|
||||
mindmapAssets = Array.isArray(nextState.mindmapAssets) ? nextState.mindmapAssets : [];
|
||||
tableAssets = Array.isArray(nextState.tableAssets) ? nextState.tableAssets : [];
|
||||
rawItems = Array.isArray(nextState.items) ? nextState.items : [];
|
||||
normalizedItems = normalizeTreeItems(rawItems);
|
||||
normalizedItems = normalizeTreeShellTreeItems(rawItems, { excludedIds });
|
||||
rebuildTreeIndexes();
|
||||
if (currentActiveDocumentId && !itemById.has(currentActiveDocumentId)) {
|
||||
currentActiveDocumentId = roots[0]?.nodeId || "";
|
||||
@@ -1573,14 +1419,14 @@ function startTreeShellRuntime() {
|
||||
if (parentId) {
|
||||
const bucket = childrenByParentId.get(parentId) || [];
|
||||
bucket.push(item);
|
||||
bucket.sort(compareItems);
|
||||
bucket.sort(compareTreeShellItems);
|
||||
childrenByParentId.set(parentId, bucket);
|
||||
const parentItem = itemById.get(parentId);
|
||||
if (parentItem) parentItem.childCount = Math.max(parentItem.childCount || 0, bucket.length);
|
||||
expanded.add(parentId);
|
||||
} else {
|
||||
roots.push(item);
|
||||
roots.sort(compareItems);
|
||||
roots.sort(compareTreeShellItems);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -178,6 +178,10 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/mnote-browser-runtime/tree-shell-icons-runtime.js",
|
||||
get(web_shell::tree_shell_icons_runtime_asset),
|
||||
)
|
||||
.route(
|
||||
"/api/mnote-browser-runtime/tree-shell-filetree-runtime.js",
|
||||
get(web_shell::tree_shell_filetree_runtime_asset),
|
||||
)
|
||||
.route(
|
||||
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
|
||||
get(web_shell::document_conflict_panel_runtime_asset),
|
||||
@@ -632,6 +636,7 @@ mod tests {
|
||||
"/api/mnote-browser-runtime/tree-shell-page-runtime.js",
|
||||
"/api/mnote-browser-runtime/tree-shell-state-runtime.js",
|
||||
"/api/mnote-browser-runtime/tree-shell-icons-runtime.js",
|
||||
"/api/mnote-browser-runtime/tree-shell-filetree-runtime.js",
|
||||
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
|
||||
"/api/mnote-browser-runtime/document-pane-runtime.js",
|
||||
"/api/mnote-browser-runtime/document-mindmap-host-runtime.js",
|
||||
|
||||
@@ -2393,6 +2393,8 @@ pub async fn reduce_tree_shell_runtime(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
const TREE_SHELL_RUNTIME_JS: &str = include_str!("../../browser/tree-shell-runtime.js");
|
||||
const TREE_SHELL_FILETREE_RUNTIME_JS: &str =
|
||||
include_str!("../../browser/tree-shell-filetree-runtime.js");
|
||||
|
||||
use super::{
|
||||
collect_filetree_render_rows, create_command_wire, TreeCommandEnvelopeContext,
|
||||
@@ -2673,7 +2675,10 @@ mod tests {
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("tree.filetree.external-drop"));
|
||||
assert!(html.contains("\"rowKind\":\"asset_folder\""));
|
||||
assert!(html.contains("\"resourceMeta\""));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("const getFileTreeRowOwnerDocumentId = (item) => {"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("tree-shell-filetree-runtime.js"));
|
||||
assert!(
|
||||
TREE_SHELL_FILETREE_RUNTIME_JS.contains("function getFileTreeRowOwnerDocumentId(item)")
|
||||
);
|
||||
assert!(TREE_SHELL_RUNTIME_JS
|
||||
.contains("row.dataset.ownerDocumentId = ownerDocumentId || \"\";"));
|
||||
assert!(
|
||||
|
||||
@@ -980,6 +980,20 @@ pub async fn tree_shell_icons_runtime_asset() -> Response {
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
}
|
||||
|
||||
pub async fn tree_shell_filetree_runtime_asset() -> Response {
|
||||
const JS: &str = include_str!("../../browser/tree-shell-filetree-runtime.js");
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
}
|
||||
|
||||
pub async fn document_conflict_panel_runtime_asset() -> Response {
|
||||
// include_str! 路径相对于当前源文件 (src/routes/web_shell.rs -> ../../browser/)
|
||||
const JS: &str = include_str!("../../browser/document-conflict-panel-runtime.js");
|
||||
|
||||
Reference in New Issue
Block a user