siyuan/kernel/model/attribute_view_render.go
2025-08-23 22:16:05 +08:00

519 lines
15 KiB
Go

// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package model
import (
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"time"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/siyuan-note/dejavu/entity"
"github.com/siyuan-note/filelock"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/av"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/util"
)
func RenderAttributeView(blockID, avID, viewID, query string, page, pageSize int, groupPaging map[string]interface{}) (viewable av.Viewable, attrView *av.AttributeView, err error) {
waitForSyncingStorages()
if avJSONPath := av.GetAttributeViewDataPath(avID); !filelock.IsExist(avJSONPath) {
attrView = av.NewAttributeView(avID)
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
}
attrView, err = av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
viewable, err = renderAttributeView(attrView, blockID, viewID, query, page, pageSize, groupPaging)
return
}
const (
groupValueDefault = "_@default@_" // 默认分组值(值为空的默认分组)
groupValueNotInRange = "_@notInRange@_" // 不再范围内的分组值(只有数字类型的分组才可能是该值)
groupValueLast30Days, groupValueLast7Days = "_@last30Days@_", "_@last7Days@_"
groupValueYesterday, groupValueToday, groupValueTomorrow = "_@yesterday@_", "_@today@_", "_@tomorrow@_"
groupValueNext7Days, groupValueNext30Days = "_@next7Days@_", "_@next30Days@_"
)
func renderAttributeView(attrView *av.AttributeView, nodeID, viewID, query string, page, pageSize int, groupPaging map[string]interface{}) (viewable av.Viewable, err error) {
// 获取待渲染的视图
view, err := getRenderAttributeViewView(attrView, viewID, nodeID)
if nil != err {
return
}
// 做一些数据兼容和订正处理
checkAttrView(attrView, view)
upgradeAttributeViewSpec(attrView)
// 渲染视图
viewable = sql.RenderView(attrView, view, query)
err = renderViewableInstance(viewable, view, attrView, page, pageSize)
if nil != err {
return
}
// 渲染分组视图
err = renderAttributeViewGroups(viewable, attrView, view, query, page, pageSize, groupPaging)
return
}
func renderAttributeViewGroups(viewable av.Viewable, attrView *av.AttributeView, view *av.View, query string, page, pageSize int, groupPaging map[string]interface{}) (err error) {
// 当前日期可能会变,所以如果是按日期分组则需要重新生成分组
if isGroupByDate(view) {
createdDate := time.UnixMilli(view.GroupCreated).Format("2006-01-02")
if time.Now().Format("2006-01-02") != createdDate {
regenAttrViewGroups(attrView)
av.SaveAttributeView(attrView)
}
}
groupKey := view.GetGroupKey(attrView)
if nil == groupKey {
return
}
// 如果存在分组的话渲染分组视图
fixDev := false
for _, groupView := range view.Groups {
if (nil == groupView.GroupVal || nil == groupView.GroupKey) && !fixDev {
// TODO 分组上线后删除,预计 2025 年 9 月后可以删除
regenAttrViewGroups(attrView)
av.SaveAttributeView(attrView)
fixDev = true
}
switch groupView.GetGroupValue() {
case groupValueDefault:
groupView.Name = fmt.Sprintf(Conf.language(264), groupKey.Name)
case groupValueNotInRange:
groupView.Name = Conf.language(265)
case groupValueLast30Days:
groupView.Name = fmt.Sprintf(Conf.language(259), 30)
case groupValueLast7Days:
groupView.Name = fmt.Sprintf(Conf.language(259), 7)
case groupValueYesterday:
groupView.Name = Conf.language(260)
case groupValueToday:
groupView.Name = Conf.language(261)
case groupValueTomorrow:
groupView.Name = Conf.language(262)
case groupValueNext7Days:
groupView.Name = fmt.Sprintf(Conf.language(263), 7)
case groupValueNext30Days:
groupView.Name = fmt.Sprintf(Conf.language(263), 30)
default:
groupView.Name = groupView.GetGroupValue()
}
}
sortGroupViews(attrView, view)
var groups []av.Viewable
for _, groupView := range view.Groups {
groupViewable := sql.RenderGroupView(attrView, view, groupView, query)
groupPage, groupPageSize := page, pageSize
if nil != groupPaging {
if paging := groupPaging[groupView.ID]; nil != paging {
pagingMap := paging.(map[string]interface{})
if nil != pagingMap["page"] {
groupPage = int(pagingMap["page"].(float64))
}
if nil != pagingMap["pageSize"] {
groupPageSize = int(pagingMap["pageSize"].(float64))
}
}
}
err = renderViewableInstance(groupViewable, view, attrView, groupPage, groupPageSize)
if nil != err {
return
}
hideEmptyGroupViews(view, groupViewable)
groups = append(groups, groupViewable)
// 将分组视图的分组字段清空,减少冗余(字段信息可以在总的视图 view 对象上获取到)
switch groupView.LayoutType {
case av.LayoutTypeTable:
groupView.Table.Columns = nil
case av.LayoutTypeGallery:
groupView.Gallery.CardFields = nil
}
}
viewable.SetGroups(groups)
// 将总的视图上的项目清空,减少冗余
viewable.(av.Collection).SetItems(nil)
return
}
func hideEmptyGroupViews(view *av.View, viewable av.Viewable) {
if nil == view.Group {
return
}
groupHidden := viewable.GetGroupHidden()
if !view.Group.HideEmpty {
if 2 != groupHidden {
viewable.SetGroupHidden(0)
}
return
}
itemCount := viewable.(av.Collection).CountItems()
if 1 == groupHidden && 0 < itemCount {
viewable.SetGroupHidden(0)
}
}
func sortGroupViews(attrView *av.AttributeView, view *av.View) {
if av.GroupOrderMan == view.Group.Order {
sort.Slice(view.Groups, func(i, j int) bool { return view.Groups[i].GroupSort < view.Groups[j].GroupSort })
return
}
if av.GroupMethodDateRelative == view.Group.Method {
var relativeDateGroups []*av.View
var last30Days, last7Days, yesterday, today, tomorrow, next7Days, next30Days, defaultGroup *av.View
for _, groupView := range view.Groups {
_, err := time.Parse("2006-01", groupView.GetGroupValue())
if nil == err { // 如果能解析出来说明是 30 天之前或 30 天之后的分组形式
relativeDateGroups = append(relativeDateGroups, groupView)
} else { // 否则是相对日期分组形式
switch groupView.GetGroupValue() {
case groupValueLast30Days:
last30Days = groupView
case groupValueLast7Days:
last7Days = groupView
case groupValueYesterday:
yesterday = groupView
case groupValueToday:
today = groupView
case groupValueTomorrow:
tomorrow = groupView
case groupValueNext7Days:
next7Days = groupView
case groupValueNext30Days:
next30Days = groupView
case groupValueDefault:
defaultGroup = groupView
}
}
}
sort.SliceStable(relativeDateGroups, func(i, j int) bool {
return relativeDateGroups[i].GetGroupValue() < relativeDateGroups[j].GetGroupValue()
})
var lastNext30Days []*av.View
if nil != next30Days {
lastNext30Days = append(lastNext30Days, next30Days)
}
if nil != next7Days {
lastNext30Days = append(lastNext30Days, next7Days)
}
if nil != tomorrow {
lastNext30Days = append(lastNext30Days, tomorrow)
}
if nil != today {
lastNext30Days = append(lastNext30Days, today)
}
if nil != yesterday {
lastNext30Days = append(lastNext30Days, yesterday)
}
if nil != last7Days {
lastNext30Days = append(lastNext30Days, last7Days)
}
if nil != last30Days {
lastNext30Days = append(lastNext30Days, last30Days)
}
startIdx := -1
todayStart := util.GetTodayStart()
thisMonth := todayStart.Format("2006-01")
for i, monthGroup := range relativeDateGroups {
if monthGroup.GetGroupValue() < thisMonth {
startIdx = i + 1
}
}
if -1 == startIdx {
startIdx = 0
}
for _, g := range lastNext30Days {
relativeDateGroups = util.InsertElem(relativeDateGroups, startIdx, g)
}
if av.GroupOrderDesc == view.Group.Order {
slices.Reverse(relativeDateGroups)
}
if nil != defaultGroup {
relativeDateGroups = append(relativeDateGroups, defaultGroup)
}
view.Groups = relativeDateGroups
return
}
if av.GroupOrderAsc == view.Group.Order || av.GroupOrderDesc == view.Group.Order {
defaultGroup := view.GetGroupByGroupValue(groupValueDefault)
if nil != defaultGroup {
view.RemoveGroupByID(defaultGroup.ID)
}
sort.SliceStable(view.Groups, func(i, j int) bool {
iVal, jVal := view.Groups[i].GetGroupValue(), view.Groups[j].GetGroupValue()
if av.GroupOrderAsc == view.Group.Order {
return util.NaturalCompare(iVal, jVal)
}
return util.NaturalCompare(jVal, iVal)
})
if nil != defaultGroup {
view.Groups = append(view.Groups, defaultGroup)
}
return
}
if av.GroupOrderSelectOption == view.Group.Order {
groupKey := view.GetGroupKey(attrView)
if nil == groupKey {
return
}
if av.KeyTypeSelect != groupKey.Type && av.KeyTypeMSelect != groupKey.Type {
return
}
sortGroupsBySelectOption(view, groupKey)
return
}
}
func sortGroupsBySelectOption(view *av.View, groupKey *av.Key) {
optionSort := map[string]int{}
for i, op := range groupKey.Options {
optionSort[op.Name] = i
}
defaultGroup := view.GetGroupByGroupValue(groupValueDefault)
if nil != defaultGroup {
view.RemoveGroupByID(defaultGroup.ID)
}
sort.Slice(view.Groups, func(i, j int) bool {
vSort := optionSort[view.Groups[i].GetGroupValue()]
oSort := optionSort[view.Groups[j].GetGroupValue()]
return vSort < oSort
})
if nil != defaultGroup {
view.Groups = append(view.Groups, defaultGroup)
}
}
func isGroupByDate(view *av.View) bool {
if nil == view.Group {
return false
}
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
}
func renderViewableInstance(viewable av.Viewable, view *av.View, attrView *av.AttributeView, page, pageSize int) (err error) {
if nil == viewable {
err = av.ErrViewNotFound
logging.LogErrorf("render attribute view [%s] failed", attrView.ID)
return
}
av.Filter(viewable, attrView)
av.Sort(viewable, attrView)
av.Calc(viewable, attrView)
// 分页
switch viewable.GetType() {
case av.LayoutTypeTable:
table := viewable.(*av.Table)
table.RowCount = len(table.Rows)
table.PageSize = view.PageSize
if 1 > pageSize {
pageSize = table.PageSize
}
start := (page - 1) * pageSize
end := start + pageSize
if len(table.Rows) < end {
end = len(table.Rows)
}
table.Rows = table.Rows[start:end]
case av.LayoutTypeGallery:
gallery := viewable.(*av.Gallery)
gallery.CardCount = len(gallery.Cards)
gallery.PageSize = view.PageSize
if 1 > pageSize {
pageSize = gallery.PageSize
}
start := (page - 1) * pageSize
end := start + pageSize
if len(gallery.Cards) < end {
end = len(gallery.Cards)
}
gallery.Cards = gallery.Cards[start:end]
}
return
}
func getRenderAttributeViewView(attrView *av.AttributeView, viewID, nodeID string) (ret *av.View, err error) {
if 1 > len(attrView.Views) {
view, _, _ := av.NewTableViewWithBlockKey(ast.NewNodeID())
attrView.Views = append(attrView.Views, view)
attrView.ViewID = view.ID
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", attrView.ID, err)
return
}
}
if "" == viewID && "" != nodeID {
node, _, _ := getNodeByBlockID(nil, nodeID)
if nil != node {
viewID = node.IALAttr(av.NodeAttrView)
}
}
if "" != viewID {
ret, _ = attrView.GetCurrentView(viewID)
if nil != ret && ret.ID != attrView.ViewID {
attrView.ViewID = ret.ID
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", attrView.ID, err)
return
}
}
} else {
ret = attrView.GetView(attrView.ViewID)
}
if nil == ret {
ret = attrView.Views[0]
}
return
}
func RenderRepoSnapshotAttributeView(indexID, avID string) (viewable av.Viewable, attrView *av.AttributeView, err error) {
repo, err := newRepository()
if err != nil {
return
}
index, err := repo.GetIndex(indexID)
if err != nil {
return
}
files, err := repo.GetFiles(index)
if err != nil {
return
}
var avFile *entity.File
for _, f := range files {
if "/storage/av/"+avID+".json" == f.Path {
avFile = f
break
}
}
if nil == avFile {
attrView = av.NewAttributeView(avID)
} else {
data, readErr := repo.OpenFile(avFile)
if nil != readErr {
logging.LogErrorf("read attribute view [%s] failed: %s", avID, readErr)
return
}
attrView = &av.AttributeView{}
if err = gulu.JSON.UnmarshalJSON(data, attrView); err != nil {
logging.LogErrorf("unmarshal attribute view [%s] failed: %s", avID, err)
return
}
}
viewable, err = renderAttributeView(attrView, "", "", "", 1, -1, nil)
return
}
func RenderHistoryAttributeView(avID, created string) (viewable av.Viewable, attrView *av.AttributeView, err error) {
createdUnix, parseErr := strconv.ParseInt(created, 10, 64)
if nil != parseErr {
logging.LogErrorf("parse created [%s] failed: %s", created, parseErr)
return
}
dirPrefix := time.Unix(createdUnix, 0).Format("2006-01-02-150405")
globPath := filepath.Join(util.HistoryDir, dirPrefix+"*")
matches, err := filepath.Glob(globPath)
if err != nil {
logging.LogErrorf("glob [%s] failed: %s", globPath, err)
return
}
if 1 > len(matches) {
return
}
historyDir := matches[0]
avJSONPath := filepath.Join(historyDir, "storage", "av", avID+".json")
if !gulu.File.IsExist(avJSONPath) {
avJSONPath = filepath.Join(util.DataDir, "storage", "av", avID+".json")
}
if !gulu.File.IsExist(avJSONPath) {
attrView = av.NewAttributeView(avID)
} else {
data, readErr := os.ReadFile(avJSONPath)
if nil != readErr {
logging.LogErrorf("read attribute view [%s] failed: %s", avID, readErr)
return
}
attrView = &av.AttributeView{}
if err = gulu.JSON.UnmarshalJSON(data, attrView); err != nil {
logging.LogErrorf("unmarshal attribute view [%s] failed: %s", avID, err)
return
}
}
viewable, err = renderAttributeView(attrView, "", "", "", 1, -1, nil)
return
}