A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1// SiYuan - Refactor your thinking
2// Copyright (c) 2020-present, b3log.org
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17package model
18
19import (
20 "fmt"
21 "os"
22 "path/filepath"
23 "slices"
24 "sort"
25 "strconv"
26 "time"
27
28 "github.com/88250/gulu"
29 "github.com/88250/lute/ast"
30 "github.com/siyuan-note/dejavu/entity"
31 "github.com/siyuan-note/filelock"
32 "github.com/siyuan-note/logging"
33 "github.com/siyuan-note/siyuan/kernel/av"
34 "github.com/siyuan-note/siyuan/kernel/sql"
35 "github.com/siyuan-note/siyuan/kernel/util"
36)
37
38func RenderAttributeView(blockID, avID, viewID, query string, page, pageSize int, groupPaging map[string]interface{}) (viewable av.Viewable, attrView *av.AttributeView, err error) {
39 waitForSyncingStorages()
40
41 if avJSONPath := av.GetAttributeViewDataPath(avID); !filelock.IsExist(avJSONPath) {
42 attrView = av.NewAttributeView(avID)
43 if err = av.SaveAttributeView(attrView); err != nil {
44 logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
45 return
46 }
47 }
48
49 attrView, err = av.ParseAttributeView(avID)
50 if err != nil {
51 logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
52 return
53 }
54
55 viewable, err = renderAttributeView(attrView, blockID, viewID, query, page, pageSize, groupPaging)
56 return
57}
58
59const (
60 groupValueDefault = "_@default@_" // 默认分组值(值为空的默认分组)
61 groupValueNotInRange = "_@notInRange@_" // 不再范围内的分组值(只有数字类型的分组才可能是该值)
62 groupValueLast30Days, groupValueLast7Days = "_@last30Days@_", "_@last7Days@_"
63 groupValueYesterday, groupValueToday, groupValueTomorrow = "_@yesterday@_", "_@today@_", "_@tomorrow@_"
64 groupValueNext7Days, groupValueNext30Days = "_@next7Days@_", "_@next30Days@_"
65)
66
67func renderAttributeView(attrView *av.AttributeView, nodeID, viewID, query string, page, pageSize int, groupPaging map[string]interface{}) (viewable av.Viewable, err error) {
68 // 获取待渲染的视图
69 view, err := getRenderAttributeViewView(attrView, viewID, nodeID)
70 if nil != err {
71 return
72 }
73
74 // 做一些数据兼容和订正处理
75 checkAttrView(attrView, view)
76 upgradeAttributeViewSpec(attrView)
77
78 // 渲染视图
79 viewable = sql.RenderView(attrView, view, query)
80 err = renderViewableInstance(viewable, view, attrView, page, pageSize)
81 if nil != err {
82 return
83 }
84
85 // 渲染分组视图
86 err = renderAttributeViewGroups(viewable, attrView, view, query, page, pageSize, groupPaging)
87 return
88}
89
90func renderAttributeViewGroups(viewable av.Viewable, attrView *av.AttributeView, view *av.View, query string, page, pageSize int, groupPaging map[string]interface{}) (err error) {
91 groupKey := view.GetGroupKey(attrView)
92 if nil == groupKey {
93 return
94 }
95
96 // 当前日期可能会变,所以如果是按日期分组则需要重新生成分组
97 if isGroupByDate(view) {
98 createdDate := time.UnixMilli(view.GroupCreated).Format("2006-01-02")
99 if time.Now().Format("2006-01-02") != createdDate {
100 genAttrViewGroups(view, attrView) // 仅重新生成一个视图的分组以提升性能
101 av.SaveAttributeView(attrView)
102 }
103 }
104
105 // 如果是按模板分组则需要重新生成分组
106 if isGroupByTemplate(attrView, view) {
107 genAttrViewGroups(view, attrView) // 仅重新生成一个视图的分组以提升性能
108 av.SaveAttributeView(attrView)
109 }
110
111 // 如果存在分组的话渲染分组视图
112
113 for _, groupView := range view.Groups {
114 groupView.Name = groupView.GetGroupValue()
115 switch groupView.Name {
116 case groupValueDefault:
117 groupView.Name = fmt.Sprintf(Conf.language(264), groupKey.Name)
118 case groupValueNotInRange:
119 groupView.Name = Conf.language(265)
120 case groupValueLast30Days:
121 groupView.Name = fmt.Sprintf(Conf.language(259), 30)
122 case groupValueLast7Days:
123 groupView.Name = fmt.Sprintf(Conf.language(259), 7)
124 case groupValueYesterday:
125 groupView.Name = Conf.language(260)
126 case groupValueToday:
127 groupView.Name = Conf.language(261)
128 case groupValueTomorrow:
129 groupView.Name = Conf.language(262)
130 case groupValueNext7Days:
131 groupView.Name = fmt.Sprintf(Conf.language(263), 7)
132 case groupValueNext30Days:
133 groupView.Name = fmt.Sprintf(Conf.language(263), 30)
134 }
135 }
136
137 sortGroupViews(attrView, view)
138
139 var groups []av.Viewable
140 for _, groupView := range view.Groups {
141 groupViewable := sql.RenderGroupView(attrView, view, groupView, query)
142
143 groupPage, groupPageSize := page, pageSize
144 if nil != groupPaging {
145 if paging := groupPaging[groupView.ID]; nil != paging {
146 pagingMap := paging.(map[string]interface{})
147 if nil != pagingMap["page"] {
148 groupPage = int(pagingMap["page"].(float64))
149 }
150 if nil != pagingMap["pageSize"] {
151 groupPageSize = int(pagingMap["pageSize"].(float64))
152 }
153 }
154 }
155
156 err = renderViewableInstance(groupViewable, view, attrView, groupPage, groupPageSize)
157 if nil != err {
158 return
159 }
160
161 hideEmptyGroupViews(view, groupViewable)
162 groups = append(groups, groupViewable)
163
164 // 将分组视图的分组字段清空,减少冗余(字段信息可以在总的视图 view 对象上获取到)
165 switch groupView.LayoutType {
166 case av.LayoutTypeTable:
167 groupView.Table.Columns = nil
168 case av.LayoutTypeGallery:
169 groupView.Gallery.CardFields = nil
170 case av.LayoutTypeKanban:
171 groupView.Kanban.Fields = nil
172 }
173 }
174 viewable.SetGroups(groups)
175
176 // 将总的视图上的项目清空,减少冗余
177 viewable.(av.Collection).SetItems(nil)
178 return
179}
180
181func hideEmptyGroupViews(view *av.View, viewable av.Viewable) {
182 if !view.IsGroupView() {
183 return
184 }
185
186 groupHidden := viewable.GetGroupHidden()
187 if !view.Group.HideEmpty {
188 if 2 != groupHidden {
189 viewable.SetGroupHidden(0)
190 }
191 return
192 }
193
194 itemCount := viewable.(av.Collection).CountItems()
195 if 1 == groupHidden && 0 < itemCount {
196 viewable.SetGroupHidden(0)
197 }
198}
199
200func sortGroupViews(attrView *av.AttributeView, view *av.View) {
201 if av.GroupOrderMan == view.Group.Order {
202 sort.Slice(view.Groups, func(i, j int) bool { return view.Groups[i].GroupSort < view.Groups[j].GroupSort })
203 return
204 }
205
206 if av.GroupMethodDateRelative == view.Group.Method {
207 var relativeDateGroups []*av.View
208 var last30Days, last7Days, yesterday, today, tomorrow, next7Days, next30Days, defaultGroup *av.View
209 for _, groupView := range view.Groups {
210 _, err := time.Parse("2006-01", groupView.GetGroupValue())
211 if nil == err { // 如果能解析出来说明是 30 天之前或 30 天之后的分组形式
212 relativeDateGroups = append(relativeDateGroups, groupView)
213 } else { // 否则是相对日期分组形式
214 switch groupView.GetGroupValue() {
215 case groupValueLast30Days:
216 last30Days = groupView
217 case groupValueLast7Days:
218 last7Days = groupView
219 case groupValueYesterday:
220 yesterday = groupView
221 case groupValueToday:
222 today = groupView
223 case groupValueTomorrow:
224 tomorrow = groupView
225 case groupValueNext7Days:
226 next7Days = groupView
227 case groupValueNext30Days:
228 next30Days = groupView
229 case groupValueDefault:
230 defaultGroup = groupView
231 }
232 }
233 }
234
235 sort.SliceStable(relativeDateGroups, func(i, j int) bool {
236 return relativeDateGroups[i].GetGroupValue() < relativeDateGroups[j].GetGroupValue()
237 })
238
239 var lastNext30Days []*av.View
240 if nil != next30Days {
241 lastNext30Days = append(lastNext30Days, next30Days)
242 }
243 if nil != next7Days {
244 lastNext30Days = append(lastNext30Days, next7Days)
245 }
246 if nil != tomorrow {
247 lastNext30Days = append(lastNext30Days, tomorrow)
248 }
249 if nil != today {
250 lastNext30Days = append(lastNext30Days, today)
251 }
252 if nil != yesterday {
253 lastNext30Days = append(lastNext30Days, yesterday)
254 }
255
256 if nil != last7Days {
257 lastNext30Days = append(lastNext30Days, last7Days)
258 }
259 if nil != last30Days {
260 lastNext30Days = append(lastNext30Days, last30Days)
261 }
262
263 startIdx := -1
264 todayStart := util.GetTodayStart()
265 thisMonth := todayStart.Format("2006-01")
266 for i, monthGroup := range relativeDateGroups {
267 if monthGroup.GetGroupValue() < thisMonth {
268 startIdx = i + 1
269 }
270 }
271 if -1 == startIdx {
272 startIdx = 0
273 }
274 for _, g := range lastNext30Days {
275 relativeDateGroups = util.InsertElem(relativeDateGroups, startIdx, g)
276 }
277
278 if av.GroupOrderDesc == view.Group.Order {
279 slices.Reverse(relativeDateGroups)
280 }
281
282 if nil != defaultGroup {
283 relativeDateGroups = append(relativeDateGroups, defaultGroup)
284 }
285
286 view.Groups = relativeDateGroups
287 return
288 }
289
290 if av.GroupOrderAsc == view.Group.Order || av.GroupOrderDesc == view.Group.Order {
291 defaultGroup := view.GetGroupByGroupValue(groupValueDefault)
292 if nil != defaultGroup {
293 view.RemoveGroupByID(defaultGroup.ID)
294 }
295
296 sort.SliceStable(view.Groups, func(i, j int) bool {
297 iVal, jVal := view.Groups[i].GetGroupValue(), view.Groups[j].GetGroupValue()
298 if av.GroupOrderAsc == view.Group.Order {
299 return util.NaturalCompare(iVal, jVal)
300 }
301 return util.NaturalCompare(jVal, iVal)
302 })
303
304 if nil != defaultGroup {
305 view.Groups = append(view.Groups, defaultGroup)
306 }
307 return
308 }
309
310 if av.GroupOrderSelectOption == view.Group.Order {
311 groupKey := view.GetGroupKey(attrView)
312 if nil == groupKey {
313 return
314 }
315
316 if av.KeyTypeSelect != groupKey.Type && av.KeyTypeMSelect != groupKey.Type {
317 return
318 }
319
320 sortGroupsBySelectOption(view, groupKey)
321 return
322 }
323}
324
325func sortGroupsBySelectOption(view *av.View, groupKey *av.Key) {
326 optionSort := map[string]int{}
327 for i, op := range groupKey.Options {
328 optionSort[op.Name] = i
329 }
330
331 defaultGroup := view.GetGroupByGroupValue(groupValueDefault)
332 if nil != defaultGroup {
333 view.RemoveGroupByID(defaultGroup.ID)
334 }
335
336 sort.Slice(view.Groups, func(i, j int) bool {
337 vSort := optionSort[view.Groups[i].GetGroupValue()]
338 oSort := optionSort[view.Groups[j].GetGroupValue()]
339 return vSort < oSort
340 })
341
342 if nil != defaultGroup {
343 view.Groups = append(view.Groups, defaultGroup)
344 }
345}
346
347func isGroupByDate(view *av.View) bool {
348 if !view.IsGroupView() {
349 return false
350 }
351 return av.GroupMethodDateDay == view.Group.Method || av.GroupMethodDateWeek == view.Group.Method || av.GroupMethodDateMonth == view.Group.Method || av.GroupMethodDateYear == view.Group.Method || av.GroupMethodDateRelative == view.Group.Method
352}
353
354func isGroupByTemplate(attrView *av.AttributeView, view *av.View) bool {
355 if !view.IsGroupView() {
356 return false
357 }
358
359 groupKey := view.GetGroupKey(attrView)
360 if nil == groupKey {
361 return false
362 }
363 return av.KeyTypeTemplate == groupKey.Type
364}
365
366func renderViewableInstance(viewable av.Viewable, view *av.View, attrView *av.AttributeView, page, pageSize int) (err error) {
367 if nil == viewable {
368 err = av.ErrViewNotFound
369 logging.LogErrorf("render attribute view [%s] failed", attrView.ID)
370 return
371 }
372
373 cachedAttrViews := map[string]*av.AttributeView{}
374 rollupFurtherCollections := sql.GetFurtherCollections(attrView, cachedAttrViews)
375 av.Filter(viewable, attrView, rollupFurtherCollections, cachedAttrViews)
376 av.Sort(viewable, attrView)
377 av.Calc(viewable, attrView)
378
379 // 分页
380 switch viewable.GetType() {
381 case av.LayoutTypeTable:
382 table := viewable.(*av.Table)
383 table.RowCount = len(table.Rows)
384 table.PageSize = view.PageSize
385 if 1 > pageSize {
386 pageSize = table.PageSize
387 }
388 start := (page - 1) * pageSize
389 end := start + pageSize
390 if len(table.Rows) < end {
391 end = len(table.Rows)
392 }
393 table.Rows = table.Rows[start:end]
394 case av.LayoutTypeGallery:
395 gallery := viewable.(*av.Gallery)
396 gallery.CardCount = len(gallery.Cards)
397 gallery.PageSize = view.PageSize
398 if 1 > pageSize {
399 pageSize = gallery.PageSize
400 }
401 start := (page - 1) * pageSize
402 end := start + pageSize
403 if len(gallery.Cards) < end {
404 end = len(gallery.Cards)
405 }
406 gallery.Cards = gallery.Cards[start:end]
407 case av.LayoutTypeKanban:
408 kanban := viewable.(*av.Kanban)
409 kanban.CardCount = 0
410 kanban.PageSize = view.PageSize
411 if 1 > pageSize {
412 pageSize = kanban.PageSize
413 }
414 start := (page - 1) * pageSize
415 end := start + pageSize
416 if len(kanban.Cards) < end {
417 end = len(kanban.Cards)
418 }
419 kanban.Cards = kanban.Cards[start:end]
420 }
421 return
422}
423
424func getRenderAttributeViewView(attrView *av.AttributeView, viewID, nodeID string) (ret *av.View, err error) {
425 if 1 > len(attrView.Views) {
426 view, _, _ := av.NewTableViewWithBlockKey(ast.NewNodeID())
427 attrView.Views = append(attrView.Views, view)
428 attrView.ViewID = view.ID
429 if err = av.SaveAttributeView(attrView); err != nil {
430 logging.LogErrorf("save attribute view [%s] failed: %s", attrView.ID, err)
431 return
432 }
433 }
434
435 if "" == viewID && "" != nodeID {
436 node, _, _ := getNodeByBlockID(nil, nodeID)
437 if nil != node {
438 viewID = node.IALAttr(av.NodeAttrView)
439 }
440 }
441
442 if "" != viewID {
443 ret, _ = attrView.GetCurrentView(viewID)
444 if nil != ret && ret.ID != attrView.ViewID {
445 attrView.ViewID = ret.ID
446 if err = av.SaveAttributeView(attrView); err != nil {
447 logging.LogErrorf("save attribute view [%s] failed: %s", attrView.ID, err)
448 return
449 }
450 }
451 } else {
452 ret = attrView.GetView(attrView.ViewID)
453 }
454
455 if nil == ret {
456 ret = attrView.Views[0]
457 }
458 return
459}
460
461func RenderRepoSnapshotAttributeView(indexID, avID string) (viewable av.Viewable, attrView *av.AttributeView, err error) {
462 repo, err := newRepository()
463 if err != nil {
464 return
465 }
466
467 index, err := repo.GetIndex(indexID)
468 if err != nil {
469 return
470 }
471
472 files, err := repo.GetFiles(index)
473 if err != nil {
474 return
475 }
476 var avFile *entity.File
477 for _, f := range files {
478 if "/storage/av/"+avID+".json" == f.Path {
479 avFile = f
480 break
481 }
482 }
483
484 if nil == avFile {
485 attrView = av.NewAttributeView(avID)
486 } else {
487 data, readErr := repo.OpenFile(avFile)
488 if nil != readErr {
489 logging.LogErrorf("read attribute view [%s] failed: %s", avID, readErr)
490 return
491 }
492
493 attrView = &av.AttributeView{RenderedViewables: map[string]av.Viewable{}}
494 if err = gulu.JSON.UnmarshalJSON(data, attrView); err != nil {
495 logging.LogErrorf("unmarshal attribute view [%s] failed: %s", avID, err)
496 return
497 }
498 }
499
500 viewable, err = renderAttributeView(attrView, "", "", "", 1, -1, nil)
501 return
502}
503
504func RenderHistoryAttributeView(blockID, avID, viewID, query string, page, pageSize int, groupPaging map[string]interface{}, created string) (viewable av.Viewable, attrView *av.AttributeView, err error) {
505 createdUnix, parseErr := strconv.ParseInt(created, 10, 64)
506 if nil != parseErr {
507 logging.LogErrorf("parse created [%s] failed: %s", created, parseErr)
508 return
509 }
510
511 dirPrefix := time.Unix(createdUnix, 0).Format("2006-01-02-150405")
512 globPath := filepath.Join(util.HistoryDir, dirPrefix+"*")
513 matches, err := filepath.Glob(globPath)
514 if err != nil {
515 logging.LogErrorf("glob [%s] failed: %s", globPath, err)
516 return
517 }
518 if 1 > len(matches) {
519 return
520 }
521
522 historyDir := matches[0]
523 avJSONPath := filepath.Join(historyDir, "storage", "av", avID+".json")
524 if !gulu.File.IsExist(avJSONPath) {
525 logging.LogWarnf("attribute view [%s] not found in history data [%s], use current data instead", avID, historyDir)
526 avJSONPath = filepath.Join(util.DataDir, "storage", "av", avID+".json")
527 }
528 if !gulu.File.IsExist(avJSONPath) {
529 logging.LogWarnf("attribute view [%s] not found in current data", avID)
530 attrView = av.NewAttributeView(avID)
531 } else {
532 data, readErr := os.ReadFile(avJSONPath)
533 if nil != readErr {
534 logging.LogErrorf("read attribute view [%s] failed: %s", avID, readErr)
535 return
536 }
537
538 attrView = &av.AttributeView{RenderedViewables: map[string]av.Viewable{}}
539 if err = gulu.JSON.UnmarshalJSON(data, attrView); err != nil {
540 logging.LogErrorf("unmarshal attribute view [%s] failed: %s", avID, err)
541 return
542 }
543 }
544
545 viewable, err = renderAttributeView(attrView, blockID, viewID, query, page, pageSize, groupPaging)
546 return
547}