Skip to content

Commit 8d3e228

Browse files
committed
♻️ Refactor recent documents handling
1 parent 84f884d commit 8d3e228

File tree

8 files changed

+347
-133
lines changed

8 files changed

+347
-133
lines changed

app/src/business/openRecentDocs.ts

Lines changed: 40 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -10,44 +10,39 @@ import {focusByRange} from "../protyle/util/selection";
1010
import {hasClosestByClassName} from "../protyle/util/hasClosest";
1111
import {hideElements} from "../protyle/ui/hideElements";
1212

13-
const getHTML = async (data: {
13+
const renderRecentDocsContent = async (data: {
1414
rootID: string,
1515
icon: string,
1616
title: string,
1717
viewedAt?: number,
1818
closedAt?: number,
1919
openAt?: number,
20-
updated?: number
21-
}[], element: Element, key?: string, sortBy: TRecentDocsSort = "viewedAt") => {
20+
}[], element: Element, key?: string) => {
2221
let tabHtml = "";
2322
let index = 0;
2423

25-
// 根据排序字段对数据进行排序
26-
const sortedData = [...data].sort((a, b) => {
27-
const aValue = a[sortBy] || 0;
28-
const bValue = b[sortBy] || 0;
29-
return bValue - aValue; // 降序排序
30-
});
31-
32-
sortedData.forEach((item) => {
33-
if (!key || item.title.toLowerCase().includes(key.toLowerCase())) {
34-
tabHtml += `<li data-index="${index}" data-node-id="${item.rootID}" class="b3-list-item${index === 0 ? " b3-list-item--focus" : ""}">
35-
${unicode2Emoji(item.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file, "b3-list-item__graphic", true)}
36-
<span class="b3-list-item__text">${escapeHtml(item.title)}</span>
24+
if (key) {
25+
data = data.filter((item) => {
26+
return item.title.toLowerCase().includes(key.toLowerCase());
27+
});
28+
}
29+
data.forEach((item) => {
30+
tabHtml += `<li data-index="${index}" data-node-id="${item.rootID}" class="b3-list-item${index === 0 ? " b3-list-item--focus" : ""}">
31+
${unicode2Emoji(item.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file, "b3-list-item__graphic", true)}
32+
<span class="b3-list-item__text">${escapeHtml(item.title)}</span>
3733
</li>`;
38-
index++;
39-
}
34+
index++;
4035
});
4136
let switchPath = "";
4237
if (tabHtml) {
4338
const pathResponse = await fetchSyncPost("/api/filetree/getFullHPathByID", {
44-
id: data[0].rootID
39+
id: data[0].rootID // 过滤后的第一个文档 ID
4540
});
4641
switchPath = escapeHtml(pathResponse.data);
4742
}
4843
let dockHtml = "";
4944
if (!isWindow()) {
50-
dockHtml = '<ul class="b3-list b3-list--background" style="overflow: auto;width: 200px;">';
45+
let docIndex = 0;
5146
if (!key || window.siyuan.languages.riffCard.toLowerCase().includes(key.toLowerCase())) {
5247
dockHtml += `<li data-type="riffCard" data-index="0" class="b3-list-item${!switchPath ? " b3-list-item--focus" : ""}">
5348
<svg class="b3-list-item__graphic"><use xlink:href="#iconRiffCard"></use></svg>
@@ -57,30 +52,30 @@ ${unicode2Emoji(item.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file,
5752
if (!switchPath) {
5853
switchPath = window.siyuan.languages.riffCard;
5954
}
55+
docIndex++;
6056
}
61-
let docIndex = 1;
6257
getAllDocks().forEach((item) => {
6358
if (!key || item.title.toLowerCase().includes(key.toLowerCase())) {
6459
dockHtml += `<li data-type="${item.type}" data-index="${docIndex}" class="b3-list-item${!switchPath ? " b3-list-item--focus" : ""}">
6560
<svg class="b3-list-item__graphic"><use xlink:href="#${item.icon}"></use></svg>
6661
<span class="b3-list-item__text">${item.title}</span>
67-
<span class="b3-list-item__meta">${updateHotkeyTip(item.hotkey || "")}</span>
62+
<span class="b3-list-item__meta">${updateHotkeyTip(item.hotkey)}</span>
6863
</li>`;
69-
docIndex++;
7064
if (!switchPath) {
71-
switchPath = window.siyuan.languages.riffCard;
65+
switchPath = item.title;
7266
}
67+
docIndex++;
7368
}
7469
});
75-
dockHtml = dockHtml + "</ul>";
70+
dockHtml = '<ul class="b3-list b3-list--background" style="overflow: auto;width: 200px;">' + dockHtml + "</ul>";
7671
}
7772

7873
const pathElement = element.querySelector(".switch-doc__path");
7974
pathElement.innerHTML = switchPath;
80-
pathElement.previousElementSibling.innerHTML = `<div class="fn__flex fn__flex-1" style="overflow:auto;">
81-
${dockHtml}
82-
<ul style="${isWindow() ? "border-left:0;" : ""}min-width:360px;" class="b3-list b3-list--background fn__flex-1">${tabHtml}</ul>
83-
</div>`;
75+
pathElement.previousElementSibling.innerHTML = `<div class="fn__flex fn__flex-1" style="overflow: auto;">
76+
${dockHtml}
77+
<ul style="${isWindow() ? "border-left: 0;" : ""}min-width: 360px;" class="b3-list b3-list--background fn__flex-1">${tabHtml}</ul>
78+
</div>`;
8479
};
8580

8681
export const openRecentDocs = () => {
@@ -93,7 +88,8 @@ export const openRecentDocs = () => {
9388
hideElements(["dialog"]);
9489
return;
9590
}
96-
fetchPost("/api/storage/getRecentDocs", {sortBy: "viewedAt"}, (response) => {
91+
const sortBy = window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type;
92+
fetchPost("/api/storage/getRecentDocs", {sortBy}, (response) => {
9793
let range: Range;
9894
if (getSelection().rangeCount > 0) {
9995
range = getSelection().getRangeAt(0);
@@ -110,15 +106,15 @@ export const openRecentDocs = () => {
110106
<span class="fn__space"></span>
111107
<div class="fn__flex-center">
112108
<select class="b3-select" id="recentDocsSort">
113-
<option value="viewedAt"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "viewedAt" ? " selected" : ""}>${window.siyuan.languages.recentViewed}</option>
114-
<option value="updated"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "updated" ? " selected" : ""}>${window.siyuan.languages.recentModified}</option>
115-
<option value="openAt"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "openAt" ? " selected" : ""}>${window.siyuan.languages.recentOpened}</option>
116-
<option value="closedAt"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "closedAt" ? " selected" : ""}>${window.siyuan.languages.recentClosed}</option>
109+
<option value="viewedAt">${window.siyuan.languages.recentViewed}</option>
110+
<option value="updated">${window.siyuan.languages.recentModified}</option>
111+
<option value="openAt">${window.siyuan.languages.recentOpened}</option>
112+
<option value="closedAt">${window.siyuan.languages.recentClosed}</option>
117113
</select>
118114
</div>
119115
</div>`,
120116
content: `<div class="fn__flex-column switch-doc">
121-
<div class="fn__flex fn__flex-1" style="overflow:auto;"></div>
117+
<div class="fn__flex fn__flex-1" style="overflow: auto;"></div>
122118
<div class="switch-doc__path"></div>
123119
</div>`,
124120
height: "80vh",
@@ -128,16 +124,18 @@ export const openRecentDocs = () => {
128124
}
129125
}
130126
});
127+
const sortSelect = dialog.element.querySelector("#recentDocsSort") as HTMLSelectElement;
128+
sortSelect.value = sortBy;
131129
const searchElement = dialog.element.querySelector("input");
132130
searchElement.focus();
133131
searchElement.addEventListener("compositionend", () => {
134-
getHTML(response.data, dialog.element, searchElement.value, sortSelect.value as TRecentDocsSort);
132+
renderRecentDocsContent(response.data, dialog.element, searchElement.value);
135133
});
136134
searchElement.addEventListener("input", (event: InputEvent) => {
137135
if (event.isComposing) {
138136
return;
139137
}
140-
getHTML(response.data, dialog.element, searchElement.value, sortSelect.value as TRecentDocsSort);
138+
renderRecentDocsContent(response.data, dialog.element, searchElement.value);
141139
});
142140
dialog.element.setAttribute("data-key", Constants.DIALOG_RECENTDOCS);
143141
dialog.element.addEventListener("click", (event) => {
@@ -152,45 +150,16 @@ export const openRecentDocs = () => {
152150
});
153151

154152
// 添加排序下拉框事件监听
155-
const sortSelect = dialog.element.querySelector("#recentDocsSort") as HTMLSelectElement;
156153
sortSelect.addEventListener("change", () => {
157-
// 重新调用API获取排序后的数据
158-
if (sortSelect.value === "updated") {
159-
// 使用SQL查询获取最近修改的文档
160-
const data = {
161-
stmt: "SELECT * FROM blocks WHERE type = 'd' ORDER BY updated DESC LIMIT 33"
162-
};
163-
fetchSyncPost("/api/query/sql", data).then((sqlResponse) => {
164-
if (sqlResponse.data && sqlResponse.data.length > 0) {
165-
// 转换SQL查询结果格式
166-
const recentModifiedDocs = sqlResponse.data.map((block: any) => {
167-
// 从ial中解析icon
168-
let icon = "";
169-
if (block.ial) {
170-
const iconMatch = block.ial.match(/icon="([^"]*)"/);
171-
if (iconMatch) {
172-
icon = iconMatch[1];
173-
}
174-
}
175-
return {
176-
rootID: block.id,
177-
icon,
178-
title: block.content,
179-
updated: block.updated
180-
};
181-
});
182-
getHTML(recentModifiedDocs, dialog.element, searchElement.value, "updated");
183-
}
184-
});
185-
} else {
186-
fetchPost("/api/storage/getRecentDocs", {sortBy: sortSelect.value}, (newResponse) => {
187-
getHTML(newResponse.data, dialog.element, searchElement.value, sortSelect.value as TRecentDocsSort);
188-
});
189-
}
154+
// 重新调用 API 获取排序后的数据
155+
fetchPost("/api/storage/getRecentDocs", {sortBy: sortSelect.value}, (newResponse) => {
156+
response = newResponse;
157+
renderRecentDocsContent(newResponse.data, dialog.element, searchElement.value);
158+
});
190159
window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type = sortSelect.value;
191160
setStorageVal(Constants.LOCAL_RECENT_DOCS, window.siyuan.storage[Constants.LOCAL_RECENT_DOCS]);
192161
});
193162

194-
getHTML(response.data, dialog.element);
163+
renderRecentDocsContent(response.data, dialog.element);
195164
});
196165
};

app/src/layout/Wnd.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ export class Wnd {
778778
model.send("closews", {});
779779
}
780780

781-
private removeTabAction = (id: string, closeAll = false, animate = true, isSaveLayout = true) => {
781+
private removeTabAction = (id: string, isBatchClose = false, animate = true, isSaveLayout = true) => {
782782
clearCounter();
783783
this.children.find((item, index) => {
784784
if (item.id === id) {
@@ -794,8 +794,10 @@ export class Wnd {
794794
}
795795
if (item.model instanceof Editor) {
796796
saveScroll(item.model.editor.protyle);
797-
// 更新文档关闭时间
798-
fetchPost("/api/storage/updateRecentDocCloseTime", {rootID: item.model.editor.protyle.block.rootID});
797+
// 更新文档关闭时间(批量关闭页签时由 closeTabByType 批量处理,这里不单独调用)
798+
if (!isBatchClose) {
799+
fetchPost("/api/storage/updateRecentDocCloseTime", {rootID: item.model.editor.protyle.block.rootID});
800+
}
799801
}
800802
if (this.children.length === 1) {
801803
this.destroyModel(this.children[0].model);
@@ -841,7 +843,7 @@ export class Wnd {
841843
}
842844
}
843845
});
844-
if (latestHeadElement && !closeAll) {
846+
if (latestHeadElement && !isBatchClose) {
845847
this.switchTab(latestHeadElement, true, true, false, false);
846848
this.showHeading();
847849
}
@@ -889,7 +891,7 @@ export class Wnd {
889891
/// #endif
890892
};
891893

892-
public removeTab(id: string, closeAll = false, animate = true, isSaveLayout = true) {
894+
public removeTab(id: string, isBatchClose = false, animate = true, isSaveLayout = true) {
893895
for (let index = 0; index < this.children.length; index++) {
894896
const item = this.children[index];
895897
if (item.id === id) {
@@ -898,9 +900,9 @@ export class Wnd {
898900
showMessage(window.siyuan.languages.uploading);
899901
return;
900902
}
901-
this.removeTabAction(id, closeAll, animate, isSaveLayout);
903+
this.removeTabAction(id, isBatchClose, animate, isSaveLayout);
902904
} else {
903-
this.removeTabAction(id, closeAll, animate, isSaveLayout);
905+
this.removeTabAction(id, isBatchClose, animate, isSaveLayout);
904906
}
905907
return;
906908
}

app/src/layout/tabUtil.ts

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {openHistory} from "../history/history";
2323
import {newFile} from "../util/newFile";
2424
import {mountHelp, newNotebook} from "../util/mount";
2525
import {Constants} from "../constants";
26+
import {fetchPost} from "../util/fetch";
2627

2728
export const getActiveTab = (wndActive = true) => {
2829
const activeTabElement = document.querySelector(".layout__wnd--active .item--focus");
@@ -360,28 +361,58 @@ export const copyTab = (app: App, tab: Tab) => {
360361
};
361362

362363
export const closeTabByType = async (tab: Tab, type: "closeOthers" | "closeAll" | "other", tabs?: Tab[]) => {
364+
const tabsToClose: Tab[] = [];
363365
if (type === "closeOthers") {
364-
for (let index = 0; index < tab.parent.children.length; index++) {
365-
if (tab.parent.children[index].id !== tab.id && !tab.parent.children[index].headElement.classList.contains("item--pin")) {
366-
await tab.parent.children[index].parent.removeTab(tab.parent.children[index].id, true, false);
367-
index--;
366+
for (const item of tab.parent.children) {
367+
if (item.id !== tab.id && !item.headElement.classList.contains("item--pin")) {
368+
tabsToClose.push(item);
368369
}
369370
}
370371
} else if (type === "closeAll") {
371-
for (let index = 0; index < tab.parent.children.length; index++) {
372-
if (!tab.parent.children[index].headElement.classList.contains("item--pin")) {
373-
await tab.parent.children[index].parent.removeTab(tab.parent.children[index].id, true);
374-
index--;
372+
for (const item of tab.parent.children) {
373+
if (!item.headElement.classList.contains("item--pin")) {
374+
tabsToClose.push(item);
375375
}
376376
}
377-
} else if (tabs.length > 0) {
378-
for (let index = 0; index < tabs.length; index++) {
379-
if (!tabs[index].headElement.classList.contains("item--pin")) {
380-
await tabs[index].parent.removeTab(tabs[index].id);
377+
} else if (tabs && tabs.length > 0) {
378+
for (const item of tabs) {
379+
if (!item.headElement.classList.contains("item--pin")) {
380+
tabsToClose.push(item);
381381
}
382382
}
383383
}
384384

385+
// 收集所有需要关闭的文档 rootID 并批量关闭页签
386+
const rootIDs: string[] = [];
387+
for (const item of tabsToClose) {
388+
let rootID;
389+
if (item.model instanceof Editor) {
390+
rootID = item.model.editor.protyle.block.rootID;
391+
} else if (!item.model) {
392+
const initTab = item.headElement.getAttribute("data-initdata");
393+
if (initTab) {
394+
const initTabData = JSON.parse(initTab);
395+
if (initTabData && initTabData.instance === "Editor" && initTabData.rootId) {
396+
rootID = initTabData.rootId;
397+
}
398+
}
399+
}
400+
if (rootID) {
401+
rootIDs.push(rootID);
402+
}
403+
404+
if (type === "closeOthers") {
405+
item.parent.removeTab(item.id, true, false);
406+
} else {
407+
item.parent.removeTab(item.id, true);
408+
}
409+
}
410+
411+
// 批量更新文档关闭时间
412+
if (rootIDs.length > 0) {
413+
fetchPost("/api/storage/batchUpdateRecentDocCloseTime", {rootIDs});
414+
}
415+
385416
if (tab.headElement.parentElement && !tab.headElement.parentElement.querySelector(".item--focus")) {
386417
tab.parent.switchTab(tab.headElement, true);
387418
} else if (tab.parent.children.length > 0) {

kernel/api/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func ServeAPI(ginServer *gin.Engine) {
8080
ginServer.Handle("POST", "/api/storage/getRecentDocs", model.CheckAuth, getRecentDocs)
8181
ginServer.Handle("POST", "/api/storage/updateRecentDocViewTime", model.CheckAuth, updateRecentDocViewTime)
8282
ginServer.Handle("POST", "/api/storage/updateRecentDocCloseTime", model.CheckAuth, updateRecentDocCloseTime)
83+
ginServer.Handle("POST", "/api/storage/batchUpdateRecentDocCloseTime", model.CheckAuth, batchUpdateRecentDocCloseTime)
8384
ginServer.Handle("POST", "/api/storage/updateRecentDocOpenTime", model.CheckAuth, updateRecentDocOpenTime)
8485

8586
ginServer.Handle("POST", "/api/storage/getOutlineStorage", model.CheckAuth, getOutlineStorage)

kernel/api/storage.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,26 @@ func updateRecentDocCloseTime(c *gin.Context) {
301301
return
302302
}
303303
}
304+
305+
func batchUpdateRecentDocCloseTime(c *gin.Context) {
306+
ret := gulu.Ret.NewResult()
307+
defer c.JSON(http.StatusOK, ret)
308+
309+
arg, ok := util.JsonArg(c, ret)
310+
if !ok {
311+
return
312+
}
313+
314+
rootIDsArg := arg["rootIDs"].([]interface{})
315+
var rootIDs []string
316+
for _, id := range rootIDsArg {
317+
rootIDs = append(rootIDs, id.(string))
318+
}
319+
320+
err := model.BatchUpdateRecentDocCloseTime(rootIDs)
321+
if err != nil {
322+
ret.Code = -1
323+
ret.Msg = err.Error()
324+
return
325+
}
326+
}

kernel/model/blockial.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,15 @@ func setNodeAttrs(node *ast.Node, tree *parse.Tree, nameValues map[string]string
182182

183183
pushBroadcastAttrTransactions(oldAttrs, node)
184184

185+
// 更新 recent-doc.json
186+
if node.Type == ast.NodeDocument {
187+
_, hasTitle := nameValues["title"]
188+
_, hasIcon := nameValues["icon"]
189+
if hasTitle || hasIcon {
190+
go UpdateRecentDocTitleAndIcon(node.ID, node.IALAttr("title"), node.IALAttr("icon"))
191+
}
192+
}
193+
185194
go func() {
186195
sql.FlushQueue()
187196
refreshDynamicRefText(node, tree)

kernel/model/file.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1678,6 +1678,8 @@ func RenameDoc(boxID, p, title string) (err error) {
16781678

16791679
box.renameSubTrees(tree)
16801680
updateRefTextRenameDoc(tree)
1681+
// 更新 recent-doc.json
1682+
go UpdateRecentDocTitleAndIcon(tree.Root.ID, title, tree.Root.IALAttr("icon"))
16811683
IncSync()
16821684
return
16831685
}

0 commit comments

Comments
 (0)