// SiYuan - From thought to insight, with agents
// 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 .
package model
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"math"
"math/rand"
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/88250/go-humanize"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/siyuan-note/filelock"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/av"
"github.com/siyuan-note/siyuan/kernel/cache"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/search"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
"github.com/xrash/smetrics"
)
func RemoveUnusedAttributeView(id string) {
// 防御性校验:ID 必须是合法的节点 ID 格式,防止通过路径穿越读取或删除任意文件
if !ast.IsNodeIDPattern(id) {
return
}
base := filepath.Join(util.DataDir, "storage", "av")
absPath := filepath.Join(base, id+".json")
if !filelock.IsExist(absPath) {
return
}
historyDir, err := getHistoryDir(HistoryOpClean)
if err != nil {
logging.LogErrorf("get history dir failed: %s", err)
return
}
newP := strings.TrimPrefix(absPath, util.DataDir)
historyPath := filepath.Join(historyDir, newP)
if filelock.IsExist(absPath) {
if err = filelock.Copy(absPath, historyPath); err != nil {
return
}
}
if err = filelock.RemoveWithoutFatal(absPath); err != nil {
logging.LogErrorf("remove unused asset [%s] failed: %s", absPath, err)
util.PushErrMsg(fmt.Sprintf("%s", err), 7000)
return
}
IncSync()
indexHistoryDir(filepath.Base(historyDir), util.NewLute())
return
}
func RemoveUnusedAttributeViews() (ret []string) {
ret = []string{}
var size int64
msgId := util.PushMsg(Conf.Language(100), 30*1000)
defer func() {
msg := fmt.Sprintf(Conf.Language(280), len(ret), humanize.BytesCustomCeil(uint64(size), 2))
util.PushUpdateMsg(msgId, msg, 7000)
}()
unusedAttributeViews := UnusedAttributeViews(false)
historyDir, err := getHistoryDir(HistoryOpClean)
if err != nil {
logging.LogErrorf("get history dir failed: %s", err)
return
}
for _, unusedAv := range unusedAttributeViews {
id := unusedAv.Item
srcPath := filepath.Join(util.DataDir, "storage", "av", id+".json")
if filelock.IsExist(srcPath) {
historyPath := filepath.Join(historyDir, "storage", "av", id+".json")
if err = filelock.Copy(srcPath, historyPath); err != nil {
return
}
}
}
for _, unusedAv := range unusedAttributeViews {
id := unusedAv.Item
absPath := filepath.Join(util.DataDir, "storage", "av", id+".json")
if filelock.IsExist(absPath) {
info, statErr := os.Stat(absPath)
if statErr == nil {
size += info.Size()
}
if removeErr := filelock.RemoveWithoutFatal(absPath); removeErr != nil {
logging.LogErrorf("remove unused av [%s] failed: %s", absPath, removeErr)
util.PushErrMsg(fmt.Sprintf("%s", removeErr), 7000)
return
}
}
ret = append(ret, absPath)
}
if 0 < len(ret) {
IncSync()
}
indexHistoryDir(filepath.Base(historyDir), util.NewLute())
return
}
func UnusedAttributeViews(sorted bool) (ret []*UnusedItem) {
defer logging.Recover()
ret = []*UnusedItem{}
allAvIDs, err := getAllAvIDs()
if err != nil {
return
}
docReferencedAvIDs := map[string]bool{}
luteEngine := util.NewLute()
boxes := Conf.GetBoxes()
for _, box := range boxes {
pages := pagedPaths(filepath.Join(util.DataDir, box.ID), 32)
for _, paths := range pages {
var trees []*parse.Tree
for _, localPath := range paths {
tree, loadTreeErr := loadTree(localPath, luteEngine)
if nil != loadTreeErr {
continue
}
trees = append(trees, tree)
}
for _, tree := range trees {
for _, id := range getAvIDs(tree, allAvIDs) {
docReferencedAvIDs[id] = true
}
}
}
}
templateAvIDs := search.FindAllMatchedTargets(filepath.Join(util.DataDir, "templates"), allAvIDs)
for _, id := range templateAvIDs {
docReferencedAvIDs[id] = true
}
checkedAvIDs := map[string]bool{}
for _, id := range allAvIDs {
if !docReferencedAvIDs[id] && !isRelatedSrcAvDocReferenced(id, docReferencedAvIDs, checkedAvIDs) {
name, _ := av.GetAttributeViewName(id)
var modTime time.Time
if sorted {
p := filepath.Join(util.DataDir, "storage", "av", id+".json")
if info, statErr := os.Stat(p); nil == statErr {
modTime = info.ModTime()
}
}
ret = append(ret, &UnusedItem{Item: id, Name: name, ModTime: modTime})
}
}
if sorted {
sort.Slice(ret, func(i, j int) bool {
if !ret[i].ModTime.Equal(ret[j].ModTime) {
return ret[i].ModTime.After(ret[j].ModTime)
}
return ret[i].Item > ret[j].Item
})
}
return
}
func isRelatedSrcAvDocReferenced(destAvID string, docReferencedAvIDs, checkedAvIDs map[string]bool) bool {
if checkedAvIDs[destAvID] {
if docReferencedAvIDs[destAvID] {
return true
}
return false
}
checkedAvIDs[destAvID] = true
srcAvIDs := av.GetSrcAvIDs(destAvID)
srcAvIDs = gulu.Str.RemoveElem(srcAvIDs, destAvID) // 忽略自身关联
if 1 > len(srcAvIDs) {
return false
}
for _, srcAvID := range srcAvIDs {
if docReferencedAvIDs[srcAvID] {
return true
}
}
// 递归检查间接关联的 av
for _, srcAvID := range srcAvIDs {
if isRelatedSrcAvDocReferenced(srcAvID, docReferencedAvIDs, checkedAvIDs) {
return true
}
}
return false
}
func getAvIDs(tree *parse.Tree, allAvIDs []string) (ret []string) {
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeAttributeView == n.Type {
ret = append(ret, n.AttributeViewID)
}
for _, kv := range n.KramdownIAL {
ids := util.GetContainsSubStrs(kv[1], allAvIDs)
if 0 < len(ids) {
ret = append(ret, ids...)
}
}
return ast.WalkContinue
})
ret = gulu.Str.RemoveDuplicatedElem(ret)
return
}
func getAllAvIDs() (ret []string, err error) {
ret = []string{}
// 只扫全局 AV 目录。加密笔记本的 AV 存在笔记本级目录(密文),不参与全局枚举——
// 未引用清理功能在加密笔记本锁定时无法确认引用关系(loadTree 失败),枚举加密 AV 有误删风险
entries, err := os.ReadDir(filepath.Join(util.DataDir, "storage", "av"))
if nil != err {
return
}
for _, entry := range entries {
name := entry.Name()
if !strings.HasSuffix(name, ".json") {
continue
}
id := strings.TrimSuffix(name, ".json")
if !ast.IsNodeIDPattern(id) {
continue
}
ret = append(ret, id)
}
ret = gulu.Str.RemoveDuplicatedElem(ret)
return
}
func GetAttributeViewItemIDs(avID string, blockIDs []string) (ret map[string]string) {
ret = map[string]string{}
for _, blockID := range blockIDs {
ret[blockID] = ""
}
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
blockKv := attrView.GetBlockKeyValues()
for _, b := range blockKv.Values {
if _, ok := ret[b.Block.ID]; ok {
ret[b.Block.ID] = b.BlockID
}
}
return
}
func GetAttributeViewBoundBlockIDs(avID string, itemIDs []string) (ret map[string]string) {
ret = map[string]string{}
for _, itemID := range itemIDs {
ret[itemID] = ""
}
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
blockKv := attrView.GetBlockKeyValues()
for _, b := range blockKv.Values {
if _, ok := ret[b.BlockID]; ok {
ret[b.BlockID] = b.Block.ID
}
}
return
}
func GetAttrViewAddingBlockDefaultValues(avID, blockID, viewID, groupID, previousBlockID, addingBlockID string) (ret map[string]*av.Value, err error) {
ret = map[string]*av.Value{}
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
view, err := resolveAttributeViewView(attrView, viewID, "", blockID)
if nil == view {
logging.LogErrorf("view [%s] not found in attribute view [%s]", viewID, avID)
return
}
useGroupDefault := "" != groupID
if 1 > len(view.Filters) && !useGroupDefault {
// 没有过滤条件也没有分组条件时忽略
return
}
groupView := view
if useGroupDefault {
groupView = view.GetGroupByID(groupID)
}
if nil == groupView {
logging.LogErrorf("group [%s] not found in view [%s] of attribute view [%s]", groupID, viewID, avID)
return
}
ret = getAttrViewAddingBlockDefaultValues(attrView, view, groupView, previousBlockID, addingBlockID, true, useGroupDefault)
for _, value := range ret {
// 主键都不返回内容,避免闪烁 https://github.com/siyuan-note/siyuan/issues/15561#issuecomment-3184746195
if av.KeyTypeBlock == value.Type {
value.Block.Content = ""
}
}
return
}
func getAttrViewAddingBlockDefaultValues(attrView *av.AttributeView, view, groupView *av.View, previousItemID, addingItemID string, isCreate, useGroupDefault bool) (ret map[string]*av.Value) {
ret = map[string]*av.Value{}
defer func() {
for keyID, value := range ret {
key, _ := attrView.GetKey(keyID)
normalizeAttrViewAddingDefaultValue(key, value)
}
}()
if 1 > len(view.Filters) && !useGroupDefault {
// 没有过滤条件也没有分组条件时忽略
return
}
nearItem := getNearItem(attrView, view, groupView, previousItemID)
// 使用模板或汇总进行过滤或分组时,需要解析涉及到的其他字段
templateRelevantKeys, rollupRelevantKeys := map[string][]*av.Key{}, map[string]*av.Key{}
for _, keyValues := range attrView.KeyValues {
if av.KeyTypeTemplate == keyValues.Key.Type {
if tplRelevantKeys := sql.GetTemplateKeyRelevantKeys(attrView, keyValues.Key); 0 < len(tplRelevantKeys) {
for _, k := range tplRelevantKeys {
templateRelevantKeys[keyValues.Key.ID] = append(templateRelevantKeys[keyValues.Key.ID], k)
}
}
} else if av.KeyTypeRollup == keyValues.Key.Type {
if nil != keyValues.Key.Rollup {
relKey, _ := attrView.GetKey(keyValues.Key.Rollup.RelationKeyID)
if nil != relKey && nil != relKey.Relation {
if attrView.ID == relKey.Relation.AvID {
if k, _ := attrView.GetKey(keyValues.Key.Rollup.KeyID); nil != k {
rollupRelevantKeys[k.ID] = k
}
}
}
}
}
}
filterKeyIDs := map[string]bool{}
if applyFilterDefaultValues(view.Filters, attrView, addingItemID, nearItem, templateRelevantKeys, rollupRelevantKeys, ret, filterKeyIDs) {
// 遇到 mAsset 过滤即结束全部默认值计算(保留原外层 return 语义)
return
}
if !useGroupDefault {
return
}
groupKey := view.GetGroupKey(attrView)
if nil == groupKey {
return
}
keyValues, _ := attrView.GetKeyValues(groupKey.ID)
if nil == keyValues {
return
}
newValue := getNewValueByNearItem(nearItem, groupKey, addingItemID)
if av.KeyTypeSelect == groupKey.Type || av.KeyTypeMSelect == groupKey.Type {
// 因为单选或多选只能按选项分组,并且可能存在空白分组(找不到临近项),所以单选或多选类型的分组字段使用分组值内容对应的选项
if opt := groupKey.GetOption(groupView.GetGroupValue()); nil != opt && groupValueDefault != groupView.GetGroupValue() {
if nil == newValue {
newValue = ret[groupKey.ID] // 如果没有临近项,则尝试从过滤结果中获取
}
if nil == newValue {
newValue = keyValues.GetValue(addingItemID) // 尝试从已有值中获取
}
if nil != newValue {
if !av.MSelectExistOption(newValue.MSelect, groupView.GetGroupValue()) {
if 1 > len(newValue.MSelect) || av.KeyTypeMSelect == groupKey.Type {
newValue.MSelect = append(newValue.MSelect, &av.ValueSelect{Content: opt.Name, Color: opt.Color})
} else {
newValue.MSelect = []*av.ValueSelect{{Content: opt.Name, Color: opt.Color}}
}
} else {
var vals []*av.ValueSelect
if isCreate {
vals = append(vals, &av.ValueSelect{Content: opt.Name, Color: opt.Color})
} else {
existingVal := keyValues.GetValue(addingItemID)
if nil != existingVal {
if !av.MSelectExistOption(existingVal.MSelect, opt.Name) {
existingVal.MSelect = append(existingVal.MSelect, &av.ValueSelect{Content: opt.Name, Color: opt.Color})
}
vals = existingVal.MSelect
} else {
vals = append(vals, &av.ValueSelect{Content: opt.Name, Color: opt.Color})
}
}
// 添加过滤结果选项的值
if nil != ret[groupKey.ID] {
for _, v := range ret[groupKey.ID].MSelect {
if !av.MSelectExistOption(vals, v.Content) {
vals = append(vals, v)
}
}
}
newValue.MSelect = vals
}
} else {
newValue = av.GetAttributeViewDefaultValue(ast.NewNodeID(), groupKey.ID, addingItemID, groupKey.Type, false)
newValue.MSelect = append(newValue.MSelect, &av.ValueSelect{Content: opt.Name, Color: opt.Color})
}
}
if nil != newValue {
ret[groupKey.ID] = newValue
}
return
}
if av.KeyTypeTemplate == keyValues.Key.Type && nil != nearItem {
if keys := templateRelevantKeys[keyValues.Key.ID]; 0 < len(keys) {
for _, k := range keys {
if nil == ret[k.ID] {
ret[k.ID] = getNewValueByNearItem(nearItem, k, addingItemID)
}
}
}
return
}
if av.KeyTypeRollup == keyValues.Key.Type && nil != nearItem {
if relKey, ok := rollupRelevantKeys[keyValues.Key.ID]; ok {
if nil == ret[relKey.ID] {
ret[relKey.ID] = getNewValueByNearItem(nearItem, relKey, addingItemID)
}
}
return
}
if nil != nearItem && filterKeyIDs[groupKey.ID] {
// 临近项不为空并且分组字段和过滤字段相同时,优先使用临近项 https://github.com/siyuan-note/siyuan/issues/15591
newValue = getNewValueByNearItem(nearItem, groupKey, addingItemID)
ret[groupKey.ID] = newValue
return
}
if nil == nearItem && !filterKeyIDs[groupKey.ID] {
// 没有临近项并且分组字段和过滤字段不同时,使用分组值
newValue = av.GetAttributeViewDefaultValue(ast.NewNodeID(), groupKey.ID, addingItemID, groupKey.Type, false)
if av.KeyTypeText == groupView.GroupVal.Type {
content := groupView.GroupVal.Text.Content
if groupValueDefault == content {
content = ""
}
switch newValue.Type {
case av.KeyTypeBlock:
newValue.Block.Content = content
case av.KeyTypeText:
newValue.Text.Content = content
case av.KeyTypeNumber:
num, _ := strconv.ParseFloat(strings.Split(content, " - ")[0], 64)
newValue.Number.Content = num
newValue.Number.IsNotEmpty = true
case av.KeyTypeURL:
newValue.URL.Content = content
case av.KeyTypeEmail:
newValue.Email.Content = content
case av.KeyTypePhone:
newValue.Phone.Content = content
}
} else if av.KeyTypeCheckbox == groupView.GroupVal.Type {
newValue.Checkbox.Checked = groupView.GroupVal.Checkbox.Checked
} else if av.KeyTypeRelation == groupKey.Type && av.KeyTypeRelation == groupView.GroupVal.Type &&
nil != groupView.GroupVal.Relation && 0 < len(groupView.GroupVal.Relation.BlockIDs) {
newValue.Relation.BlockIDs = []string{groupView.GroupVal.Relation.BlockIDs[0]}
}
ret[groupKey.ID] = newValue
return
}
if nil != newValue && !filterKeyIDs[groupKey.ID] {
ret[groupKey.ID] = newValue
}
return
}
func normalizeAttrViewAddingDefaultValue(key *av.Key, value *av.Value) {
if nil == key || nil == value {
return
}
if av.KeyTypeDate == value.Type && nil != value.Date {
value.Date.IsNotTime = true
if nil != key.Date {
value.Date.IsNotTime = !key.Date.FillSpecificTime
if key.Date.AutoFillNow {
value.Date.Content = util.CurrentTimeMillis()
value.Date.IsNotEmpty = true
}
}
}
if av.KeyTypeRelation == value.Type && nil != value.Relation {
value.Relation.Contents = nil
}
}
// applyFilterDefaultValues 递归遍历过滤节点树,对叶子节点计算新增行的默认值。
// AND 分组合并后验证所有子节点,OR 分组采用首个可满足的分支;无法满足时不生成默认值。
// 返回 true 表示遇到 mAsset 过滤,调用方应立即结束全部默认值计算(保留原外层 return 语义)。
func applyFilterDefaultValues(filters []*av.ViewFilter, attrView *av.AttributeView, addingItemID string, nearItem av.Item,
templateRelevantKeys map[string][]*av.Key, rollupRelevantKeys map[string]*av.Key,
ret map[string]*av.Value, filterKeyIDs map[string]bool) (stop bool) {
originalRet := cloneDefaultValues(ret)
originalFilterKeyIDs := cloneBoolMap(filterKeyIDs)
if applyFilterDefaultValues0(filters, attrView, addingItemID, nearItem, templateRelevantKeys, rollupRelevantKeys, ret, filterKeyIDs) {
return true
}
if !filterBranchesMatchDefaultValues(filters, attrView, addingItemID, ret) {
replaceDefaultValues(ret, originalRet)
replaceBoolMap(filterKeyIDs, originalFilterKeyIDs)
}
return false
}
func applyFilterDefaultValues0(filters []*av.ViewFilter, attrView *av.AttributeView, addingItemID string, nearItem av.Item,
templateRelevantKeys map[string][]*av.Key, rollupRelevantKeys map[string]*av.Key,
ret map[string]*av.Value, filterKeyIDs map[string]bool) (stop bool) {
for _, filter := range filters {
if nil == filter {
continue
}
if filter.IsGroup() {
if av.FilterCombinationOr == filter.Combination && 0 < len(filter.Filters) {
for _, child := range filter.Filters {
candidateRet := cloneDefaultValues(ret)
candidateFilterKeyIDs := cloneBoolMap(filterKeyIDs)
childStop := applyFilterDefaultValues([]*av.ViewFilter{child}, attrView, addingItemID, nearItem,
templateRelevantKeys, rollupRelevantKeys, candidateRet, candidateFilterKeyIDs)
if childStop || filterBranchMatchesDefaultValues(child, attrView, addingItemID, candidateRet) {
replaceDefaultValues(ret, candidateRet)
replaceBoolMap(filterKeyIDs, candidateFilterKeyIDs)
if childStop {
return true
}
break
}
}
continue
}
if applyFilterDefaultValues(filter.Filters, attrView, addingItemID, nearItem, templateRelevantKeys, rollupRelevantKeys, ret, filterKeyIDs) {
return true
}
continue
}
keyValues, _ := attrView.GetKeyValues(filter.Column)
if nil == keyValues {
continue
}
if !filter.IsValid() {
continue
}
filterKeyIDs[filter.Column] = true
if av.KeyTypeTemplate == keyValues.Key.Type && nil != nearItem {
if keys := templateRelevantKeys[keyValues.Key.ID]; 0 < len(keys) {
for _, k := range keys {
if nil == ret[k.ID] {
ret[k.ID] = getNewValueByNearItem(nearItem, k, addingItemID)
}
}
}
continue
}
if av.KeyTypeRollup == keyValues.Key.Type && nil != nearItem {
if relKey, ok := rollupRelevantKeys[keyValues.Key.ID]; ok {
if nil == ret[relKey.ID] {
ret[relKey.ID] = getNewValueByNearItem(nearItem, relKey, addingItemID)
}
}
continue
}
if av.KeyTypeMAsset == keyValues.Key.Type {
if nil != nearItem {
if _, ok := ret[keyValues.Key.ID]; !ok {
ret[keyValues.Key.ID] = getNewValueByNearItem(nearItem, keyValues.Key, addingItemID)
}
}
return true // 保留原语义:遇到 mAsset 过滤即结束默认值计算
}
newValue, allowNearItem := filter.GetAffectValue(keyValues.Key, addingItemID)
if nil == newValue && allowNearItem {
newValue = getNewValueByNearItem(nearItem, keyValues.Key, addingItemID)
}
if nil != newValue {
ret[keyValues.Key.ID] = newValue
}
}
return false
}
func cloneDefaultValues(values map[string]*av.Value) (ret map[string]*av.Value) {
ret = make(map[string]*av.Value, len(values))
for key, value := range values {
ret[key] = value
}
return
}
func cloneBoolMap(values map[string]bool) (ret map[string]bool) {
ret = make(map[string]bool, len(values))
for key, value := range values {
ret[key] = value
}
return
}
func filterBranchesMatchDefaultValues(filters []*av.ViewFilter, attrView *av.AttributeView, addingItemID string,
values map[string]*av.Value) bool {
for _, filter := range filters {
if !filterBranchMatchesDefaultValues(filter, attrView, addingItemID, values) {
return false
}
}
return true
}
func filterBranchMatchesDefaultValues(filter *av.ViewFilter, attrView *av.AttributeView, addingItemID string,
values map[string]*av.Value) bool {
if nil == filter {
return false
}
if filter.IsGroup() {
if 1 > len(filter.Filters) {
return av.FilterCombinationOr != filter.Combination
}
if av.FilterCombinationOr == filter.Combination {
for _, child := range filter.Filters {
if filterBranchMatchesDefaultValues(child, attrView, addingItemID, values) {
return true
}
}
return false
}
return filterBranchesMatchDefaultValues(filter.Filters, attrView, addingItemID, values)
}
keyValues, _ := attrView.GetKeyValues(filter.Column)
if nil == keyValues {
return false
}
if !filter.IsValid() {
return true
}
switch keyValues.Key.Type {
case av.KeyTypeTemplate, av.KeyTypeRollup, av.KeyTypeMAsset, av.KeyTypeCreated, av.KeyTypeUpdated:
// 这些字段的最终值需要在渲染阶段计算,此处无法根据存储值准确判断。
return true
}
value := values[filter.Column]
if nil == value {
value = av.GetAttributeViewDefaultValue(ast.NewNodeID(), filter.Column, addingItemID, keyValues.Key.Type, false)
}
return value.Filter(filter, attrView, addingItemID, nil, nil)
}
func replaceDefaultValues(target, source map[string]*av.Value) {
clear(target)
for key, value := range source {
target[key] = value
}
}
func replaceBoolMap(target, source map[string]bool) {
clear(target)
for key, value := range source {
target[key] = value
}
}
func (tx *Transaction) doSortAttrViewGroup(operation *Operation) (ret *TxErr) {
if err := sortAttributeViewGroup(operation.AvID, operation.BlockID, operation.PreviousID, operation.ID); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func sortAttributeViewGroup(avID, blockID, previousGroupID, groupID string) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return err
}
sortGroupViews(attrView, view)
var groupView *av.View
var index, previousIndex int
for i, g := range view.Groups {
if g.ID == groupID {
groupView = g
index = i
break
}
}
if nil == groupView {
return
}
view.Group.Order = av.GroupOrderMan
view.Groups = append(view.Groups[:index], view.Groups[index+1:]...)
for i, g := range view.Groups {
if g.ID == previousGroupID {
previousIndex = i + 1
break
}
}
view.Groups = util.InsertElem(view.Groups, previousIndex, groupView)
for i, g := range view.Groups {
g.GroupSort = i
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doRemoveAttrViewGroup(operation *Operation) (ret *TxErr) {
if err := removeAttributeViewGroup(operation.AvID, operation.BlockID); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func removeAttributeViewGroup(avID, blockID string) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return err
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return err
}
removeAttributeViewGroup0(view)
err = av.SaveAttributeView(attrView)
if err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return err
}
return nil
}
func removeAttributeViewGroup0(view *av.View) {
view.Group, view.Groups, view.GroupCreated = nil, nil, 0
}
func (tx *Transaction) doSyncAttrViewTableColWidth(operation *Operation) (ret *TxErr) {
err := syncAttrViewTableColWidth(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func syncAttrViewTableColWidth(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view := attrView.GetView(operation.ID)
if nil == view {
err = av.ErrViewNotFound
logging.LogErrorf("view [%s] not found in attribute view [%s]", operation.ID, operation.AvID)
return
}
var width string
switch view.LayoutType {
case av.LayoutTypeTable:
for _, column := range view.Table.Columns {
if column.ID == operation.KeyID {
width = column.Width
break
}
}
case av.LayoutTypeGallery, av.LayoutTypeKanban:
return
}
for _, v := range attrView.Views {
if av.LayoutTypeTable == v.LayoutType {
for _, column := range v.Table.Columns {
if column.ID == operation.KeyID {
column.Width = width
break
}
}
}
}
err = av.SaveAttributeView(attrView)
ReloadAttrView(attrView.ID)
return
}
func (tx *Transaction) doHideAttrViewGroup(operation *Operation) (ret *TxErr) {
if err := hideAttributeViewGroup(operation.AvID, operation.BlockID, operation.ID, int(operation.Data.(float64))); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func hideAttributeViewGroup(avID, blockID, groupID string, hidden int) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return
}
for _, group := range view.Groups {
if group.ID == groupID {
group.GroupHidden = hidden
break
}
}
err = av.SaveAttributeView(attrView)
if err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
return
}
func (tx *Transaction) doHideAttrViewAllGroups(operation *Operation) (ret *TxErr) {
if err := hideAttributeViewAllGroups(operation.AvID, operation.BlockID, operation.Data.(bool)); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func hideAttributeViewAllGroups(avID, blockID string, hidden bool) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return
}
for _, group := range view.Groups {
if hidden {
group.GroupHidden = 2
} else {
group.GroupHidden = 0
}
}
err = av.SaveAttributeView(attrView)
if err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
return
}
func (tx *Transaction) doFoldAttrViewGroup(operation *Operation) (ret *TxErr) {
if err := foldAttrViewGroup(operation.AvID, operation.BlockID, operation.ID, operation.Data.(bool)); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func foldAttrViewGroup(avID, blockID, groupID string, folded bool) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return err
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return err
}
if !view.IsGroupView() {
return
}
for _, group := range view.Groups {
if group.ID == groupID {
group.GroupFolded = folded
break
}
}
err = av.SaveAttributeView(attrView)
if err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return err
}
return nil
}
func (tx *Transaction) doFoldAttrViewGroups(operation *Operation) (ret *TxErr) {
folded := map[string]bool{}
data, err := gulu.JSON.MarshalJSON(operation.Data)
if nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
if err = gulu.JSON.UnmarshalJSON(data, &folded); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
if err = foldAttrViewGroups(operation.AvID, operation.BlockID, operation.ViewID, folded); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func foldAttrViewGroups(avID, blockID, viewID string, folded map[string]bool) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return err
}
var view *av.View
if "" != viewID {
view = attrView.GetView(viewID)
if nil == view {
return av.ErrViewNotFound
}
} else {
view, err = getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return err
}
}
if !view.IsGroupView() {
return
}
for _, group := range view.Groups {
if groupFolded, ok := folded[group.ID]; ok {
group.GroupFolded = groupFolded
}
}
err = av.SaveAttributeView(attrView)
if err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return err
}
return nil
}
func (tx *Transaction) doSetAttrViewGroup(operation *Operation) (ret *TxErr) {
data, err := gulu.JSON.MarshalJSON(operation.Data)
if nil != err {
logging.LogErrorf("marshal operation data failed: %s", err)
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
group := &av.ViewGroup{}
if err = gulu.JSON.UnmarshalJSON(data, &group); nil != err {
logging.LogErrorf("unmarshal operation data failed: %s", err)
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
if err = SetAttributeViewGroup(operation.AvID, operation.BlockID, group); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func SetAttributeViewGroup(avID, blockID string, group *av.ViewGroup) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return err
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return err
}
setAttributeViewGroup(attrView, view, group)
err = av.SaveAttributeView(attrView)
ReloadAttrView(avID)
return
}
func setAttributeViewGroup(attrView *av.AttributeView, view *av.View, group *av.ViewGroup) {
var oldHideEmpty, firstInit, changeGroupField bool
if nil != view.Group {
oldHideEmpty = view.Group.HideEmpty
changeGroupField = group.Field != view.Group.Field
} else {
firstInit = true
}
groupStates := getAttrViewGroupStates(view)
view.Group = group
regenAttrViewGroups(attrView)
setAttrViewGroupStates(view, groupStates)
if view.Group.HideEmpty != oldHideEmpty {
if !oldHideEmpty && view.Group.HideEmpty { // 启用隐藏空分组
for _, g := range view.Groups {
groupViewable := sql.RenderGroupView(attrView, view, g, "")
// 必须经过渲染才能得到最终的条目数
renderViewableInstance(groupViewable, view, attrView, 1, -1, false, "")
if g.GroupHidden == 0 && 1 > groupViewable.(av.Collection).CountItems() {
g.GroupHidden = 1
}
}
}
if oldHideEmpty && !view.Group.HideEmpty { // 禁用隐藏空分组
for _, g := range view.Groups {
groupViewable := sql.RenderGroupView(attrView, view, g, "")
renderViewableInstance(groupViewable, view, attrView, 1, -1, false, "")
if g.GroupHidden == 1 && 1 > groupViewable.(av.Collection).CountItems() {
g.GroupHidden = 0
}
}
}
}
if firstInit || changeGroupField { // 首次设置分组时
if groupKey := view.GetGroupKey(attrView); nil != groupKey {
if av.KeyTypeSelect == groupKey.Type || av.KeyTypeMSelect == groupKey.Type {
// 如果分组字段是单选或多选,则将分组排序方式改为按选项排序 https://github.com/siyuan-note/siyuan/issues/15534
view.Group.Order = av.GroupOrderSelectOption
sortGroupsBySelectOption(view, groupKey)
} else if av.KeyTypeCheckbox == groupKey.Type {
// 如果分组字段是复选框,则将分组排序改为手动排序,并且已勾选在前面
view.Group.Order = av.GroupOrderMan
checked := view.GetGroupByGroupValue(av.CheckboxCheckedStr)
unchecked := view.GetGroupByGroupValue("")
view.Groups = nil
view.Groups = append(view.Groups, checked, unchecked)
}
}
for i, g := range view.Groups {
g.GroupSort = i
}
}
}
func (tx *Transaction) doSetAttrViewCardAspectRatio(operation *Operation) (ret *TxErr) {
err := setAttrViewCardAspectRatio(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewCardAspectRatio(operation *Operation) (err error) {
value, err := getAttrViewOperationNumber(operation)
if nil != err {
return
}
ratio := av.CardAspectRatio(value)
if value != math.Trunc(value) || ratio < av.CardAspectRatio16_9 || av.CardAspectRatio1_1 < ratio {
return fmt.Errorf("invalid card aspect ratio preset [%v]", value)
}
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
return
case av.LayoutTypeGallery:
view.Gallery.CardAspectRatio = ratio
view.Gallery.CardAspectRatioValue = av.CardAspectRatioValueByPreset(ratio)
case av.LayoutTypeKanban:
view.Kanban.CardAspectRatio = ratio
view.Kanban.CardAspectRatioValue = av.CardAspectRatioValueByPreset(ratio)
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewCardAspectRatioValue(operation *Operation) (ret *TxErr) {
if err := setAttrViewCardAspectRatioValue(operation); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewCardAspectRatioValue(operation *Operation) (err error) {
ratio, err := getAttrViewOperationNumber(operation)
if nil != err {
return
}
if math.IsNaN(ratio) || math.IsInf(ratio, 0) ||
ratio < av.CardAspectRatioValueMin || av.CardAspectRatioValueMax < ratio {
return fmt.Errorf("invalid card aspect ratio [%v]", ratio)
}
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
return
case av.LayoutTypeGallery:
view.Gallery.CardAspectRatioValue = ratio
case av.LayoutTypeKanban:
view.Kanban.CardAspectRatioValue = ratio
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewBlockView(operation *Operation) (ret *TxErr) {
err := SetDatabaseBlockView(operation.BlockID, operation.AvID, operation.ID)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func (tx *Transaction) doSetAttrViewBlockVisibleViews(operation *Operation) (ret *TxErr) {
err := SetDatabaseBlockVisibleViews(operation.BlockID, operation.AvID, operation.ViewIDs)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func SetDatabaseBlockVisibleViews(blockID, avID string, viewIDs []string) (err error) {
if 1 > len(viewIDs) {
return errors.New("at least one visible view is required")
}
attrView, err := av.ParseAttributeView(avID)
if nil != err {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
visible := map[string]bool{}
for _, viewID := range viewIDs {
if nil == attrView.GetView(viewID) {
return fmt.Errorf("view [%s] not found in attribute view [%s]", viewID, avID)
}
visible[viewID] = true
}
var normalized []string
for _, view := range attrView.Views {
if visible[view.ID] {
normalized = append(normalized, view.ID)
}
}
if 1 > len(normalized) {
return errors.New("at least one visible view is required")
}
node, tree, err := getNodeByBlockID(nil, blockID)
if nil != err {
return
}
if ast.NodeAttributeView != node.Type || node.AttributeViewID != avID {
return fmt.Errorf("block [%s] is not an instance of attribute view [%s]", blockID, avID)
}
err = setNodeAttrs(node, tree, map[string]string{
av.NodeAttrVisibleViewIDs: strings.Join(normalized, ","),
})
return
}
func (tx *Transaction) doChangeAttrViewLayout(operation *Operation) (ret *TxErr) {
err := ChangeAttrViewLayout(operation.BlockID, operation.AvID, operation.Layout)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func ChangeAttrViewLayout(blockID, avID string, newLayout av.LayoutType) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return
}
if newLayout == view.LayoutType {
return
}
if err = changeAttrViewLayout(attrView, view, newLayout); err != nil {
return
}
blockIDs := treenode.GetMirrorAttrViewBlockIDs(avID)
for _, bID := range blockIDs {
node, tree, _ := getNodeByBlockID(nil, bID)
if nil == node || nil == tree {
logging.LogErrorf("get node by block ID [%s] failed", bID)
continue
}
changed := false
attrs := parse.IAL2Map(node.KramdownIAL)
if blockID == bID { // 当前操作的镜像库
attrs[av.NodeAttrView] = view.ID
node.AttributeViewType = string(view.LayoutType)
changed = true
} else {
if view.ID == attrs[av.NodeAttrView] {
// 仅更新和当前操作的镜像库指定的视图相同的镜像库
node.AttributeViewType = string(view.LayoutType)
changed = true
}
}
if changed {
err = setNodeAttrs(node, tree, attrs)
if err != nil {
logging.LogWarnf("set node [%s] attrs failed: %s", bID, err)
return
}
}
}
regenAttrViewGroups(attrView)
if err = av.SaveAttributeView(attrView); nil != err {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
ReloadAttrView(avID)
return
}
func changeAttrViewLayout(attrView *av.AttributeView, view *av.View, newLayout av.LayoutType) (err error) {
if newLayout == view.LayoutType {
return
}
switch newLayout {
case av.LayoutTypeTable, av.LayoutTypeGallery, av.LayoutTypeKanban:
default:
return av.ErrWrongLayoutType
}
oldLayout := view.LayoutType
view.LayoutType = newLayout
switch newLayout {
case av.LayoutTypeTable:
if view.Name == av.GetAttributeViewI18n("gallery") || view.Name == av.GetAttributeViewI18n("kanban") {
view.Name = av.GetAttributeViewI18n("table")
}
if nil != view.Table {
break
}
view.Table = av.NewLayoutTable()
switch oldLayout {
case av.LayoutTypeGallery:
for _, field := range view.Gallery.CardFields {
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: &av.BaseField{ID: field.ID}})
}
case av.LayoutTypeKanban:
for _, field := range view.Kanban.Fields {
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: &av.BaseField{ID: field.ID}})
}
}
case av.LayoutTypeGallery:
if view.Name == av.GetAttributeViewI18n("table") || view.Name == av.GetAttributeViewI18n("kanban") {
view.Name = av.GetAttributeViewI18n("gallery")
}
if nil != view.Gallery {
break
}
view.Gallery = av.NewLayoutGallery()
switch oldLayout {
case av.LayoutTypeTable:
for _, col := range view.Table.Columns {
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: &av.BaseField{ID: col.ID}})
}
case av.LayoutTypeKanban:
for _, field := range view.Kanban.Fields {
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: &av.BaseField{ID: field.ID}})
}
}
case av.LayoutTypeKanban:
if view.Name == av.GetAttributeViewI18n("table") || view.Name == av.GetAttributeViewI18n("gallery") {
view.Name = av.GetAttributeViewI18n("kanban")
}
if nil != view.Kanban {
break
}
view.Kanban = av.NewLayoutKanban()
switch oldLayout {
case av.LayoutTypeTable:
for _, col := range view.Table.Columns {
view.Kanban.Fields = append(view.Kanban.Fields, &av.ViewKanbanField{BaseField: &av.BaseField{ID: col.ID}})
}
case av.LayoutTypeGallery:
for _, field := range view.Gallery.CardFields {
view.Kanban.Fields = append(view.Kanban.Fields, &av.ViewKanbanField{BaseField: &av.BaseField{ID: field.ID}})
}
}
if !view.IsGroupView() {
preferredGroupKey := getKanbanPreferredGroupKey(attrView)
group := &av.ViewGroup{Field: preferredGroupKey.ID}
setAttributeViewGroup(attrView, view, group)
}
}
return
}
func (tx *Transaction) doSetAttrViewWrapField(operation *Operation) (ret *TxErr) {
err := setAttrViewWrapField(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewWrapField(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
allFieldWrap := operation.Data.(bool)
switch view.LayoutType {
case av.LayoutTypeTable:
view.Table.WrapField = allFieldWrap
for _, col := range view.Table.Columns {
col.Wrap = allFieldWrap
}
case av.LayoutTypeGallery:
view.Gallery.WrapField = allFieldWrap
for _, field := range view.Gallery.CardFields {
field.Wrap = allFieldWrap
}
case av.LayoutTypeKanban:
view.Kanban.WrapField = allFieldWrap
for _, field := range view.Kanban.Fields {
field.Wrap = allFieldWrap
}
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewShowIcon(operation *Operation) (ret *TxErr) {
err := setAttrViewShowIcon(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewShowIcon(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
view.Table.ShowIcon = operation.Data.(bool)
case av.LayoutTypeGallery:
view.Gallery.ShowIcon = operation.Data.(bool)
case av.LayoutTypeKanban:
view.Kanban.ShowIcon = operation.Data.(bool)
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewFitImage(operation *Operation) (ret *TxErr) {
err := setAttrViewFitImage(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewFitImage(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
return
case av.LayoutTypeGallery:
view.Gallery.FitImage = operation.Data.(bool)
case av.LayoutTypeKanban:
view.Kanban.FitImage = operation.Data.(bool)
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewDisplayFieldName(operation *Operation) (ret *TxErr) {
err := setAttrViewDisplayFieldName(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func (tx *Transaction) doSetAttrViewDisplayEmptyFields(operation *Operation) (ret *TxErr) {
err := setAttrViewDisplayEmptyFields(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func (tx *Transaction) doSetAttrViewFillColBackgroundColor(operation *Operation) (ret *TxErr) {
err := setAttrViewFillColBackgroundColor(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewDisplayFieldName(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewOperationView(attrView, operation)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
return
case av.LayoutTypeGallery:
view.Gallery.DisplayFieldName = operation.Data.(bool)
case av.LayoutTypeKanban:
view.Kanban.DisplayFieldName = operation.Data.(bool)
}
err = av.SaveAttributeView(attrView)
return
}
func setAttrViewDisplayEmptyFields(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
return
case av.LayoutTypeGallery:
view.Gallery.DisplayEmptyFields = operation.Data.(bool)
case av.LayoutTypeKanban:
view.Kanban.DisplayEmptyFields = operation.Data.(bool)
}
err = av.SaveAttributeView(attrView)
return
}
func setAttrViewFillColBackgroundColor(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
return
case av.LayoutTypeGallery:
return
case av.LayoutTypeKanban:
view.Kanban.FillColBackgroundColor = operation.Data.(bool)
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewCardSize(operation *Operation) (ret *TxErr) {
err := setAttrViewCardSize(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewCardSize(operation *Operation) (err error) {
value, err := getAttrViewOperationNumber(operation)
if nil != err {
return
}
size := av.CardSize(value)
if value != math.Trunc(value) || size < av.CardSizeSmall || av.CardSizeLarge < size {
return fmt.Errorf("invalid card size preset [%v]", value)
}
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
return
case av.LayoutTypeGallery:
view.Gallery.CardSize = size
view.Gallery.CardWidth = av.CardWidthBySize(size)
case av.LayoutTypeKanban:
view.Kanban.CardSize = size
view.Kanban.CardWidth = av.CardWidthBySize(size)
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewCardWidth(operation *Operation) (ret *TxErr) {
if err := setAttrViewCardWidth(operation); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewCardWidth(operation *Operation) (err error) {
value, err := getAttrViewOperationNumber(operation)
if nil != err {
return
}
if math.IsNaN(value) || math.IsInf(value, 0) || value != math.Trunc(value) ||
value < av.CardWidthMin || av.CardWidthMax < value {
return fmt.Errorf("invalid card width [%v]", value)
}
width := int(value)
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
return
case av.LayoutTypeGallery:
view.Gallery.CardWidth = width
case av.LayoutTypeKanban:
view.Kanban.CardWidth = width
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewCardLayout(operation *Operation) (ret *TxErr) {
if err := setAttrViewCardLayout(operation); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewCardLayout(operation *Operation) (err error) {
value, err := getAttrViewOperationNumber(operation)
if nil != err {
return
}
layout := av.CardLayout(value)
if value != math.Trunc(value) || !layout.IsValid() {
return fmt.Errorf("invalid card layout [%v]", value)
}
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewOperationView(attrView, operation)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeGallery:
view.Gallery.CardLayout = layout
case av.LayoutTypeKanban:
view.Kanban.CardLayout = layout
default:
return av.ErrWrongLayoutType
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewColFullRow(operation *Operation) (ret *TxErr) {
if err := setAttrViewColFullRow(operation); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewColFullRow(operation *Operation) (err error) {
fullRow, ok := operation.Data.(bool)
if !ok {
return fmt.Errorf("invalid card field full row value [%v]", operation.Data)
}
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewOperationView(attrView, operation)
if err != nil {
return
}
found := false
switch view.LayoutType {
case av.LayoutTypeGallery:
for _, field := range view.Gallery.CardFields {
if field.ID == operation.ID {
field.FullRow = fullRow
found = true
break
}
}
case av.LayoutTypeKanban:
for _, field := range view.Kanban.Fields {
if field.ID == operation.ID {
field.FullRow = fullRow
found = true
break
}
}
default:
return av.ErrWrongLayoutType
}
if !found {
return fmt.Errorf("field [%s] not found in view [%s]", operation.ID, view.ID)
}
err = av.SaveAttributeView(attrView)
return
}
func getAttrViewOperationView(attrView *av.AttributeView, operation *Operation) (ret *av.View, err error) {
if "" != operation.ViewID {
ret = attrView.GetView(operation.ViewID)
if nil == ret {
err = av.ErrViewNotFound
}
return
}
return getAttrViewViewByBlockID(attrView, operation.BlockID)
}
func getAttrViewOperationNumber(operation *Operation) (ret float64, err error) {
var ok bool
if ret, ok = operation.Data.(float64); !ok {
err = fmt.Errorf("invalid card configuration value [%v]", operation.Data)
}
return
}
func (tx *Transaction) doSetAttrViewCoverFromAssetKeyID(operation *Operation) (ret *TxErr) {
err := setAttrViewCoverFromAssetKeyID(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewCoverFromAssetKeyID(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
return
case av.LayoutTypeGallery:
view.Gallery.CoverFromAssetKeyID = operation.KeyID
case av.LayoutTypeKanban:
view.Kanban.CoverFromAssetKeyID = operation.KeyID
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewCoverFrom(operation *Operation) (ret *TxErr) {
err := setAttrViewCoverFrom(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewCoverFrom(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
return
case av.LayoutTypeGallery:
view.Gallery.CoverFrom = av.CoverFrom(operation.Data.(float64))
case av.LayoutTypeKanban:
view.Kanban.CoverFrom = av.CoverFrom(operation.Data.(float64))
}
err = av.SaveAttributeView(attrView)
return
}
type setAttrViewCardCoverPositionData struct {
Source string `json:"source"`
Position *av.CardCoverPosition `json:"position"`
}
func (tx *Transaction) doSetAttrViewCardCoverPosition(operation *Operation) (ret *TxErr) {
if err := setAttrViewCardCoverPosition(operation); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewCardCoverPosition(operation *Operation) (err error) {
dataJSON, err := json.Marshal(operation.Data)
if nil != err {
return
}
var data setAttrViewCardCoverPositionData
if err = json.Unmarshal(dataJSON, &data); nil != err {
return
}
if !av.IsValidCardCoverSource(data.Source) {
return fmt.Errorf("invalid card cover source [%s]", data.Source)
}
if nil != data.Position {
if "" == data.Position.Image || 32*1024 < len(data.Position.Image) {
return errors.New("invalid card cover image")
}
if math.IsNaN(data.Position.X) || math.IsInf(data.Position.X, 0) ||
math.IsNaN(data.Position.Y) || math.IsInf(data.Position.Y, 0) ||
data.Position.X < 0 || 100 < data.Position.X || data.Position.Y < 0 || 100 < data.Position.Y {
return fmt.Errorf("invalid card cover position [%v, %v]", data.Position.X, data.Position.Y)
}
}
attrView, err := av.ParseAttributeView(operation.AvID)
if nil != err {
return
}
if nil == attrView.GetBlockValue(operation.RowID) {
return fmt.Errorf("attribute view item [%s] not found", operation.RowID)
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if nil != err {
return
}
if av.LayoutTypeGallery != view.LayoutType && av.LayoutTypeKanban != view.LayoutType {
return av.ErrWrongLayoutType
}
var source string
if av.LayoutTypeGallery == view.LayoutType {
source = av.CardCoverSource(view.Gallery.CoverFrom, view.Gallery.CoverFromAssetKeyID)
} else {
source = av.CardCoverSource(view.Kanban.CoverFrom, view.Kanban.CoverFromAssetKeyID)
}
if data.Source != source {
return fmt.Errorf("card cover source [%s] does not match view [%s]", data.Source, view.ID)
}
if keyID := av.CardCoverSourceAssetKeyID(data.Source); "" != keyID {
key, getErr := attrView.GetKey(keyID)
if nil != getErr || nil == key || av.KeyTypeMAsset != key.Type {
return fmt.Errorf("card cover asset field [%s] not found", keyID)
}
}
attrView.SetCardCoverPosition(operation.RowID, data.Source, data.Position)
err = av.SaveAttributeView(attrView)
return
}
func AppendAttributeViewDetachedBlocksWithValues(avID string, blocksValues [][]*av.Value) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
now := util.CurrentTimeMillis()
var blockIDs []string
for _, blockValues := range blocksValues {
blockID := ast.NewNodeID()
if v := blockValues[0]; "" != v.BlockID {
blockID = v.BlockID
}
blockIDs = append(blockIDs, blockID)
for _, v := range blockValues {
keyValues, _ := attrView.GetKeyValues(v.KeyID)
if nil == keyValues {
err = fmt.Errorf("key [%s] not found", v.KeyID)
return
}
v.ID = ast.NewNodeID()
v.BlockID = blockID
v.Type = keyValues.Key.Type
if av.KeyTypeBlock == v.Type {
v.Block.Created = now
v.Block.Updated = now
v.Block.ID = ""
}
v.IsDetached = true
v.CreatedAt = now
v.UpdatedAt = now
v.IsRenderAutoFill = false
keyValues.Values = append(keyValues.Values, v)
if av.KeyTypeSelect == v.Type || av.KeyTypeMSelect == v.Type {
// 保存选项 https://github.com/siyuan-note/siyuan/issues/12475
key, _ := attrView.GetKey(v.KeyID)
if nil != key && 0 < len(v.MSelect) {
for _, valOpt := range v.MSelect {
if opt := key.GetOption(valOpt.Content); nil == opt {
// 不存在的选项新建保存
opt = &av.SelectOption{Name: valOpt.Content, Color: av.FilterColorValue(valOpt.Color)}
key.Options = append(key.Options, opt)
} else {
// 已经存在的选项颜色需要保持不变
valOpt.Color = opt.Color
}
}
}
}
}
}
for _, v := range attrView.Views {
for _, addingBlockID := range blockIDs {
v.ItemIDs = append(v.ItemIDs, addingBlockID)
}
}
regenAttrViewGroups(attrView)
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
ReloadAttrView(avID)
return
}
// DuplicateAttributeViewRow 创建源行的纯文本副本,复制除主键、Rollup、Created、Updated 外的所有字段值,
// 双向关联同步更新目标属性视图的反向关联列。新行 ID 由调用方(前端)生成并通过 newRowID 传入,
// 副本插入到 previousItemID(通常为最后选中的条目)之后。
func DuplicateAttributeViewRow(tx *Transaction, avID, previousItemID, srcRowID, newRowID string) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
blockKeyValues := attrView.GetBlockKeyValues()
if nil == blockKeyValues {
err = fmt.Errorf("attribute view [%s] has no block key", avID)
return
}
srcBlockVal := blockKeyValues.GetValue(srcRowID)
if nil == srcBlockVal {
err = fmt.Errorf("source row [%s] not found in attribute view [%s]", srcRowID, avID)
return
}
if "" == newRowID {
newRowID = ast.NewNodeID()
}
now := util.CurrentTimeMillis()
// 创建副本的主键值,强制为纯文本(非绑定块)
blockContent := ""
if nil != srcBlockVal.Block {
blockContent = srcBlockVal.Block.Content
}
newBlockVal := &av.Value{
ID: ast.NewNodeID(),
KeyID: blockKeyValues.Key.ID,
BlockID: newRowID,
Type: av.KeyTypeBlock,
IsDetached: true,
CreatedAt: now,
UpdatedAt: now,
Block: &av.ValueBlock{Content: blockContent, Created: now, Updated: now},
}
blockKeyValues.Values = append(blockKeyValues.Values, newBlockVal)
// 收集双向关联字段,循环结束后统一处理,避免跨属性视图重复 parse/save
type pendingTwoWay struct {
key *av.Key
val *av.Value
}
var pendingTwoWays []*pendingTwoWay
for _, keyValues := range attrView.KeyValues {
if av.KeyTypeBlock == keyValues.Key.Type {
continue // 主键已处理
}
if av.KeyTypeRollup == keyValues.Key.Type || av.KeyTypeCreated == keyValues.Key.Type || av.KeyTypeUpdated == keyValues.Key.Type {
continue // 汇总/创建时间/更新时间字段在渲染或自动生成时处理,不复制
}
srcVal := keyValues.GetValue(srcRowID)
if nil == srcVal {
continue // 源行该字段无值,跳过
}
newVal := srcVal.Clone()
if nil == newVal {
continue
}
newVal.ID = ast.NewNodeID()
newVal.BlockID = newRowID
newVal.IsDetached = false
newVal.CreatedAt = now
newVal.UpdatedAt = now
newVal.IsRenderAutoFill = false
if nil != newVal.Relation {
newVal.Relation.Contents = nil // 清除渲染期数据
}
// 单选/多选选项同步,保证目标属性视图存在对应选项 https://github.com/siyuan-note/siyuan/issues/12475
if av.KeyTypeSelect == newVal.Type || av.KeyTypeMSelect == newVal.Type {
if 0 < len(newVal.MSelect) {
for _, valOpt := range newVal.MSelect {
if opt := keyValues.Key.GetOption(valOpt.Content); nil == opt {
opt = &av.SelectOption{Name: valOpt.Content, Color: av.FilterColorValue(valOpt.Color)}
keyValues.Key.Options = append(keyValues.Key.Options, opt)
} else {
valOpt.Color = opt.Color
}
}
}
}
keyValues.Values = append(keyValues.Values, newVal)
// 双向关联值收集,稍后统一更新目标属性视图的反向列
if av.KeyTypeRelation == newVal.Type && nil != keyValues.Key.Relation && keyValues.Key.Relation.IsTwoWay && 0 < len(newVal.Relation.BlockIDs) {
pendingTwoWays = append(pendingTwoWays, &pendingTwoWay{key: keyValues.Key, val: newVal})
}
}
// 在所有视图上添加新行,插入位置为 previousItemID 之后
for _, v := range attrView.Views {
addRowToViewItems(v, newRowID, previousItemID)
}
attrView.CopyCardCoverPositions(srcRowID, newRowID)
// 统一处理双向关联:按目标属性视图聚合,每个目标只 parse/save 一次
twoWayByDestAv := map[string][]*pendingTwoWay{}
for _, p := range pendingTwoWays {
destAvID := p.key.Relation.AvID
twoWayByDestAv[destAvID] = append(twoWayByDestAv[destAvID], p)
}
for destAvID, items := range twoWayByDestAv {
if destAvID == attrView.ID {
// 自关联,直接在内存对象上更新,随主属性视图一起保存
for _, p := range items {
updateTwoWayRelationDestAttrView(attrView, p.key, p.val, 1, nil)
}
continue
}
destAv, parseErr := av.ParseAttributeView(destAvID)
if nil != parseErr || nil == destAv {
logging.LogWarnf("parse dest attribute view [%s] failed: %s", destAvID, parseErr)
continue
}
for _, p := range items {
// 复用反向更新逻辑,传入 mode=1(增加)、oldBlockIDs 为空
updateTwoWayRelationDestAttrView(destAv, p.key, p.val, 1, nil)
}
regenAttrViewGroups(destAv)
if saveErr := av.SaveAttributeView(destAv); nil != saveErr {
logging.LogErrorf("save dest attribute view [%s] failed: %s", destAvID, saveErr)
}
if nil != tx {
tx.relatedAvIDs = append(tx.relatedAvIDs, destAvID)
}
ReloadAttrView(destAvID)
}
regenAttrViewGroups(attrView)
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
return
}
// addRowToViewItems 将 newRowID 插入到视图及其所有分组的项目列表中,位置在 previousItemID 之后;
// 若 previousItemID 为空或未找到,则追加到末尾。
func addRowToViewItems(view *av.View, newRowID, previousItemID string) {
view.ItemIDs = insertItemAfter(view.ItemIDs, newRowID, previousItemID)
for _, g := range view.Groups {
g.GroupItemIDs = insertItemAfter(g.GroupItemIDs, newRowID, previousItemID)
}
}
// insertItemAfter 将 item 插入到 items 中 previousItemID 之后;若 previousItemID 为空或未找到,则追加到末尾。
func insertItemAfter(items []string, item, previousItemID string) []string {
if "" != previousItemID {
for i, id := range items {
if id == previousItemID {
items = append(items[:i+1], append([]string{item}, items[i+1:]...)...)
return items
}
}
}
return append(items, item)
}
func DuplicateDatabaseBlock(avID string) (newAvID, newBlockID string, err error) {
// 加密笔记本的 AV 定义在笔记本级目录,通过 fallback 查找实际路径
oldAvPath, avBoxID := av.FindAttributeViewPath(avID)
if oldAvPath == "" {
oldAvPath = av.GetAttributeViewDataPath(avID)
}
newAvID, newBlockID = ast.NewNodeID(), ast.NewNodeID()
oldAv, err := av.ParseAttributeView(avID)
if err != nil {
return
}
data, err := filelock.ReadFile(oldAvPath)
if err != nil {
logging.LogErrorf("read attribute view [%s] failed: %s", avID, err)
return
}
// 加密笔记本的 AV 是密文,需先解密再处理(av.DecryptAVData 内部按 box 加密/已解锁路由)
if avBoxID != "" && IsEncryptedBox(avBoxID) {
var decErr error
data, decErr = av.DecryptAVData(avBoxID, avID, data)
if decErr != nil {
logging.LogErrorf("decrypt attribute view [%s] failed: %s", avID, decErr)
err = decErr
return
}
}
data = bytes.ReplaceAll(data, []byte(avID), []byte(newAvID))
av.UpsertBlockRel(newAvID, newBlockID)
newAv := &av.AttributeView{}
if err = gulu.JSON.UnmarshalJSON(data, newAv); err != nil {
logging.LogErrorf("unmarshal attribute view [%s] failed: %s", newAvID, err)
return
}
if "" != newAv.Name {
newAv.Name = oldAv.Name + " (Duplicated " + time.Now().Format("2006-01-02 15:04:05") + ")"
}
for _, keyValues := range newAv.KeyValues {
if nil != keyValues.Key.Relation && keyValues.Key.Relation.IsTwoWay {
// 断开双向关联
keyValues.Key.Relation.IsTwoWay = false
keyValues.Key.Relation.BackKeyID = ""
}
}
data, err = gulu.JSON.MarshalJSON(newAv)
if err != nil {
logging.LogErrorf("marshal attribute view [%s] failed: %s", newAvID, err)
return
}
// 加密笔记本的新 AV 定义也存笔记本级目录,且需 avKey 加密
newAvPath := filepath.Join(util.DataDir, "storage", "av", newAvID+".json")
if avBoxID != "" {
newAvPath = filepath.Join(util.DataDir, avBoxID, "storage", "av", newAvID+".json")
var encErr error
data, encErr = av.EncryptAVData(avBoxID, newAvID, data)
if encErr != nil {
logging.LogErrorf("encrypt attribute view [%s] failed: %s", newAvID, encErr)
err = encErr
return
}
av.SetAVBoxID(newAvID, avBoxID)
}
if err = filelock.WriteFile(newAvPath, data); err != nil {
logging.LogErrorf("write attribute view [%s] failed: %s", newAvID, err)
return
}
updateBoundBlockAvsAttribute([]string{newAvID})
return
}
func GetAttributeViewKeysByID(avID string, keyIDs ...string) (ret []*av.Key) {
ret = []*av.Key{}
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
if 1 > len(keyIDs) {
for _, keyValues := range attrView.KeyValues {
key := keyValues.Key
ret = append(ret, key)
}
return
}
for _, keyValues := range attrView.KeyValues {
key := keyValues.Key
for _, keyID := range keyIDs {
if key.ID == keyID {
ret = append(ret, key)
}
}
}
return ret
}
func SetDatabaseBlockView(blockID, avID, viewID string) (err error) {
attrView, err := av.ParseAttributeView(avID)
if nil != err {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
view := attrView.GetView(viewID)
if nil == view {
err = av.ErrViewNotFound
logging.LogErrorf("view [%s] not found in attribute view [%s]", viewID, avID)
return
}
node, tree, err := getNodeByBlockID(nil, blockID)
if err != nil {
return
}
node.AttributeViewType = string(view.LayoutType)
attrs := parse.IAL2Map(node.KramdownIAL)
attrs[av.NodeAttrView] = viewID
err = setNodeAttrs(node, tree, attrs)
if err != nil {
logging.LogWarnf("set node [%s] attrs failed: %s", blockID, err)
return
}
return
}
// normalizeDatabaseBlockView 使数据库载体绑定的视图和布局保持一致。
func normalizeDatabaseBlockView(node *ast.Node, attrView *av.AttributeView) (changed bool) {
if nil == node || nil == attrView || ast.NodeAttributeView != node.Type {
return
}
viewID := node.IALAttr(av.NodeAttrView)
var view *av.View
if "" != viewID {
view = attrView.GetView(viewID)
}
if nil == view {
view, _ = attrView.GetFirstView()
}
if nil == view {
return
}
layout := string(view.LayoutType)
if layout != node.AttributeViewType {
node.AttributeViewType = layout
changed = true
}
if view.ID != viewID {
node.SetIALAttr(av.NodeAttrView, view.ID)
changed = true
}
return
}
func normalizeDatabaseBlockViews(node *ast.Node, boxID string, attrViews map[string]*av.AttributeView) (changed bool) {
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || ast.NodeAttributeView != n.Type {
return ast.WalkContinue
}
attrView := cachedAttributeViewForBox(n.AttributeViewID, boxID, attrViews)
if normalizeDatabaseBlockView(n, attrView) {
changed = true
}
return ast.WalkContinue
})
return
}
func cachedAttributeViewForBox(avID, boxID string, attrViews map[string]*av.AttributeView) (ret *av.AttributeView) {
cacheKey := boxID + "\x00" + avID
if ret, ok := attrViews[cacheKey]; ok {
return ret
}
var err error
if IsEncryptedBox(boxID) {
ret, err = av.ParseAttributeViewInBox(avID, boxID)
} else {
ret, err = av.ParseAttributeView(avID)
}
if nil != err {
ret = nil
}
attrViews[cacheKey] = ret
return
}
func GetAttributeViewPrimaryKeyValues(avID, keyword string, blockIDs []string, page, pageSize int) (attributeViewName string, databaseBlockIDs []string, keyValues *av.KeyValues, total int, err error) {
waitForSyncingStorages()
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
attributeViewName = getAttrViewName(attrView)
databaseBlockIDs = treenode.GetMirrorAttrViewBlockIDs(avID)
keyValues = attrView.GetBlockKeyValues()
if nil == keyValues {
keyValues = &av.KeyValues{}
return
}
var values []*av.Value
valuesByBlockID := map[string]*av.Value{}
for _, kv := range keyValues.Values {
if !kv.IsDetached && !treenode.ExistBlockTree(kv.Block.ID) {
continue
}
valuesByBlockID[kv.BlockID] = kv
if strings.Contains(strings.ToLower(kv.String(true)), strings.ToLower(keyword)) {
values = append(values, kv)
}
}
if 0 < len(blockIDs) {
values = nil
for _, blockID := range blockIDs {
if value := valuesByBlockID[blockID]; nil != value {
values = append(values, value)
}
}
keyValues.Values = values
total = len(values)
return
}
keyValues.Values = values
sort.Slice(keyValues.Values, func(i, j int) bool {
if keyValues.Values[i].Block.Updated == keyValues.Values[j].Block.Updated {
return keyValues.Values[i].BlockID > keyValues.Values[j].BlockID
}
return keyValues.Values[i].Block.Updated > keyValues.Values[j].Block.Updated
})
total = len(keyValues.Values)
if 1 > page {
page = 1
}
if 1 > pageSize {
pageSize = 16
}
start := (page - 1) * pageSize
if len(keyValues.Values) < start {
start = len(keyValues.Values)
}
end := min(len(keyValues.Values), start+pageSize)
keyValues.Values = keyValues.Values[start:end]
return
}
func GetAttributeViewRelationCandidates(srcAvID, relationKeyID, keyword string, selectedBlockIDs []string, page, pageSize int) (
attributeViewName string, databaseBlockIDs []string, columns []*av.TableColumn, selectedRows, rows []*av.TableRow,
total int, err error,
) {
waitForSyncingStorages()
srcAttrView, err := av.ParseAttributeView(srcAvID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", srcAvID, err)
return
}
var relationKey *av.Key
attrView := srcAttrView
if "" != relationKeyID {
relationKey, _ = srcAttrView.GetKey(relationKeyID)
if nil == relationKey || av.KeyTypeRelation != relationKey.Type || nil == relationKey.Relation {
err = av.ErrKeyNotFound
return
}
if relationKey.Relation.AvID != srcAvID {
attrView, err = av.ParseAttributeView(relationKey.Relation.AvID)
}
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", relationKey.Relation.AvID, err)
return
}
}
attributeViewName = getAttrViewName(attrView)
databaseBlockIDs = treenode.GetMirrorAttrViewBlockIDs(attrView.ID)
table := renderAttributeViewRelationCandidates(attrView)
if nil == table {
err = av.ErrViewNotFound
return
}
columns = table.Columns
rowsByID := map[string]*av.TableRow{}
for _, row := range table.Rows {
rowsByID[row.ID] = row
}
for _, blockID := range selectedBlockIDs {
if row := rowsByID[blockID]; nil != row {
selectedRows = append(selectedRows, row)
}
}
if nil == selectedRows {
selectedRows = []*av.TableRow{}
}
if nil != relationKey && hasAttrViewFilterConditions(relationKey.Relation.CandidateFilters) {
validColumns := map[string]bool{}
for _, keyValues := range attrView.KeyValues {
if nil != keyValues && nil != keyValues.Key {
validColumns[keyValues.Key.ID] = true
}
}
table.Filters, _ = av.PruneInvalidColumnFilters(
av.CloneFilters(relationKey.Relation.CandidateFilters), validColumns)
cachedAttrViews := map[string]*av.AttributeView{attrView.ID: attrView}
av.Filter(table, attrView, sql.GetFurtherCollections(attrView, cachedAttrViews), cachedAttrViews)
}
rows, total = filterSortPageRelationCandidates(table.Rows, keyword, page, pageSize)
return
}
func filterSortPageRelationCandidates(tableRows []*av.TableRow, keyword string, page, pageSize int) (
rows []*av.TableRow, total int,
) {
for _, row := range tableRows {
if relationCandidateMatches(row, keyword) {
rows = append(rows, row)
}
}
sort.Slice(rows, func(i, j int) bool {
iCreated := relationCandidateCreatedAt(rows[i])
jCreated := relationCandidateCreatedAt(rows[j])
if iCreated == jCreated {
return rows[i].ID > rows[j].ID
}
return iCreated > jCreated
})
total = len(rows)
if 1 > page {
page = 1
}
if 1 > pageSize {
pageSize = 16
}
start := min(len(rows), (page-1)*pageSize)
end := min(len(rows), start+pageSize)
rows = rows[start:end]
if nil == rows {
rows = []*av.TableRow{}
}
return
}
func renderAttributeViewRelationCandidates(attrView *av.AttributeView) (ret *av.Table) {
if nil == attrView {
return
}
keysByID := map[string]*av.Key{}
for _, keyValues := range attrView.KeyValues {
if nil != keyValues && nil != keyValues.Key {
keysByID[keyValues.Key.ID] = keyValues.Key
}
}
var keys []*av.Key
added := map[string]bool{}
if blockKey := attrView.GetBlockKey(); nil != blockKey {
keys = append(keys, blockKey)
added[blockKey.ID] = true
}
appendKey := func(key *av.Key) {
if nil == key || added[key.ID] || av.KeyTypeLineNumber == key.Type {
return
}
keys = append(keys, key)
added[key.ID] = true
}
for _, keyID := range attrView.KeyIDs {
appendKey(keysByID[keyID])
}
for _, keyValues := range attrView.KeyValues {
if nil != keyValues {
appendKey(keyValues.Key)
}
}
view := &av.View{
ID: ast.NewNodeID(),
Filters: []*av.ViewFilter{},
Sorts: []*av.ViewSort{},
PageSize: av.ViewDefaultPageSize,
LayoutType: av.LayoutTypeTable,
Table: av.NewLayoutTable(),
}
for _, key := range keys {
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{
BaseField: &av.BaseField{ID: key.ID},
})
}
viewable := sql.RenderView(attrView, view, "", false)
ret, _ = viewable.(*av.Table)
return
}
func relationCandidateMatches(row *av.TableRow, keyword string) bool {
keywords := strings.Fields(strings.TrimSpace(keyword))
if 1 > len(keywords) {
return true
}
for _, cell := range row.Cells {
if nil == cell || nil == cell.Value {
continue
}
content := cell.Value.String(true)
allKeywordsHit := true
for _, currentKeyword := range keywords {
if util.SearchCaseSensitive {
if !strings.Contains(content, currentKeyword) {
allKeywordsHit = false
break
}
} else if !strings.Contains(strings.ToLower(content), strings.ToLower(currentKeyword)) {
allKeywordsHit = false
break
}
}
if allKeywordsHit {
return true
}
}
return false
}
func relationCandidateCreatedAt(row *av.TableRow) int64 {
if nil == row {
return 0
}
blockValue := row.GetBlockValue()
if nil == blockValue {
return 0
}
if 0 < blockValue.CreatedAt {
return blockValue.CreatedAt
}
if nil != blockValue.Block {
return blockValue.Block.Created
}
return 0
}
func GetAttributeViewFilterSort(avID, blockID string) (filters []*av.ViewFilter, sorts []*av.ViewSort) {
waitForSyncingStorages()
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if nil == view || err != nil {
logging.LogErrorf("get current view failed: %s", err)
return
}
filters = view.Filters
sorts = view.Sorts
if 1 > len(filters) {
filters = []*av.ViewFilter{}
}
if 1 > len(sorts) {
sorts = []*av.ViewSort{}
}
return
}
func SearchAttributeViewRollupDestKeys(avID, keyword string) (ret []*av.Key) {
waitForSyncingStorages()
ret = []*av.Key{}
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
for _, keyValues := range attrView.KeyValues {
if av.KeyTypeRollup != keyValues.Key.Type && av.KeyTypeLineNumber != keyValues.Key.Type {
if strings.Contains(strings.ToLower(keyValues.Key.Name), strings.ToLower(keyword)) {
ret = append(ret, keyValues.Key)
}
}
}
return
}
func SearchAttributeViewRelationKey(avID, keyword string) (ret []*av.Key) {
waitForSyncingStorages()
ret = []*av.Key{}
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
for _, keyValues := range attrView.KeyValues {
if av.KeyTypeRelation == keyValues.Key.Type && nil != keyValues.Key.Relation {
if strings.Contains(strings.ToLower(keyValues.Key.Name), strings.ToLower(keyword)) {
ret = append(ret, keyValues.Key)
}
}
}
return
}
func GetAttributeView(avID string) (ret *av.AttributeView) {
waitForSyncingStorages()
ret, _ = av.ParseAttributeView(avID)
return
}
// AttributeViewData 是面向外部接口的兼容数据,不参与数据库文件持久化。
type AttributeViewData struct {
Spec int `json:"spec"`
ID string `json:"id"`
Name string `json:"name"`
KeyValues []*av.KeyValues `json:"keyValues"`
KeyIDs []string `json:"keyIDs"`
ViewID string `json:"viewID"`
Views []*av.View `json:"views"`
NewItemTemplates []*av.NewItemTemplate `json:"newItemTemplates,omitempty"`
DefaultTemplateID string `json:"defaultTemplateID,omitempty"`
CardCoverPositions map[string]map[string]*av.CardCoverPosition `json:"cardCoverPositions,omitempty"`
}
func NewAttributeViewData(attrView *av.AttributeView) (ret *AttributeViewData) {
if nil == attrView {
return
}
ret = &AttributeViewData{
Spec: attrView.Spec, ID: attrView.ID, Name: attrView.Name, KeyValues: attrView.KeyValues, KeyIDs: attrView.KeyIDs,
Views: attrView.Views, NewItemTemplates: attrView.NewItemTemplates, DefaultTemplateID: attrView.DefaultTemplateID,
CardCoverPositions: attrView.CardCoverPositions,
}
if view, _ := attrView.GetFirstView(); nil != view {
ret.ViewID = view.ID
}
return
}
type AttributeViewFieldView struct {
ID string `json:"id"`
Icon string `json:"icon"`
Name string `json:"name"`
Type av.LayoutType `json:"type"`
Hidden bool `json:"hidden"`
}
func GetAttributeViewFieldViews(avID, keyID string) (ret []*AttributeViewFieldView, err error) {
waitForSyncingStorages()
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
for _, view := range attrView.Views {
field := getAttributeViewField(view, keyID)
if nil == field {
err = fmt.Errorf("field [%s] not found in view [%s]", keyID, view.ID)
return
}
ret = append(ret, &AttributeViewFieldView{
ID: view.ID,
Icon: view.Icon,
Name: view.Name,
Type: view.LayoutType,
Hidden: field.Hidden,
})
}
return
}
type AvSearchResult struct {
AvID string `json:"avID"`
AvName string `json:"avName"`
ViewName string `json:"viewName"`
ViewID string `json:"viewID"`
ViewLayout av.LayoutType `json:"viewLayout"`
BlockID string `json:"blockID"`
HPath string `json:"hPath"`
Matched bool `json:"matched,omitempty"`
Children []*AvSearchResult `json:"children,omitempty"`
}
type AvSearchTempResult struct {
AvID string
AvName string
AvUpdated int64
Score float64
Matched bool
SearchInfo *av.AttributeViewSearchInfo
MatchedViewIDs map[string]bool
}
type SearchAttributeViewOptions struct {
Keyword string
ExcludeAvIDs []string
CurrentAvID string
CurrentBlockID string
IncludeViewMatches bool
}
type attributeViewSearchCacheWarmup struct {
signature uint64
cancel context.CancelFunc
running bool
}
var attributeViewSearchCacheWarmups = struct {
sync.Mutex
states map[string]*attributeViewSearchCacheWarmup
}{states: map[string]*attributeViewSearchCacheWarmup{}}
var attributeViewSearchCacheWarmupDelay = 500 * time.Millisecond
var loadAttributeViewSearchInfo = av.GetAttributeViewSearchInfoInBox
const attributeViewSearchCacheWarmupHashOffset = uint64(1469598103934665603)
const attributeViewSearchCacheWarmupHashPrime = uint64(1099511628211)
func warmAttributeViewSearchCache(boxID string, avIDs []string, signature uint64) {
if len(avIDs) == 0 {
return
}
attributeViewSearchCacheWarmups.Lock()
if state := attributeViewSearchCacheWarmups.states[boxID]; state != nil && state.signature == signature {
attributeViewSearchCacheWarmups.Unlock()
return
}
if state := attributeViewSearchCacheWarmups.states[boxID]; state != nil && state.cancel != nil {
state.cancel()
}
ctx, cancel := context.WithCancel(context.Background())
state := &attributeViewSearchCacheWarmup{signature: signature, cancel: cancel, running: true}
attributeViewSearchCacheWarmups.states[boxID] = state
attributeViewSearchCacheWarmups.Unlock()
ids := slices.Clone(avIDs)
delay := attributeViewSearchCacheWarmupDelay
loader := loadAttributeViewSearchInfo
go func() {
completed := false
defer func() {
attributeViewSearchCacheWarmups.Lock()
defer attributeViewSearchCacheWarmups.Unlock()
if attributeViewSearchCacheWarmups.states[boxID] != state {
return
}
if completed {
state.cancel = nil
state.running = false
} else {
delete(attributeViewSearchCacheWarmups.states, boxID)
}
}()
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return
case <-timer.C:
}
for _, avID := range ids {
select {
case <-ctx.Done():
return
default:
}
if isSyncingStorages() {
return
}
_, _ = loader(avID, boxID)
}
completed = true
}()
}
func stopAttributeViewSearchCacheWarmup(boxID string) {
attributeViewSearchCacheWarmups.Lock()
defer attributeViewSearchCacheWarmups.Unlock()
state := attributeViewSearchCacheWarmups.states[boxID]
if state == nil || !state.running {
return
}
state.cancel()
delete(attributeViewSearchCacheWarmups.states, boxID)
}
func updateAttributeViewSearchCacheWarmupSignature(signature uint64, avID string, info os.FileInfo) uint64 {
for i := 0; i < len(avID); i++ {
signature ^= uint64(avID[i])
signature *= attributeViewSearchCacheWarmupHashPrime
}
if info != nil {
signature ^= uint64(info.ModTime().UnixNano())
signature *= attributeViewSearchCacheWarmupHashPrime
signature ^= uint64(info.Size())
signature *= attributeViewSearchCacheWarmupHashPrime
}
return signature
}
func matchAttributeViewSearchName(name string, keywords []string) (score float64, hit bool) {
if name == "" || len(keywords) == 0 {
return
}
lowerName := strings.ToLower(name)
hit = true
for _, keyword := range keywords {
lowerKeyword := strings.ToLower(keyword)
if !strings.Contains(lowerName, lowerKeyword) {
return 0, false
}
score += smetrics.JaroWinkler(name, keyword, 0.7, 4)
}
return
}
func sortAndLimitAttributeViewSearchResults(results []*AvSearchTempResult, keyword string) []*AvSearchTempResult {
if keyword == "" {
sort.Slice(results, func(i, j int) bool { return results[i].AvUpdated > results[j].AvUpdated })
} else {
sort.SliceStable(results, func(i, j int) bool {
if results[i].Score == results[j].Score {
return results[i].AvUpdated > results[j].AvUpdated
}
return results[i].Score > results[j].Score
})
}
if 12 < len(results) {
return results[:12]
}
return results
}
func SearchAttributeView(keyword string, excludeAvIDs []string, currentAvID, currentBlockID string) []*AvSearchResult {
return SearchAttributeViewWithOptions(SearchAttributeViewOptions{
Keyword: keyword,
ExcludeAvIDs: excludeAvIDs,
CurrentAvID: currentAvID,
CurrentBlockID: currentBlockID,
})
}
func SearchAttributeViewWithOptions(options SearchAttributeViewOptions) (ret []*AvSearchResult) {
searchStart := time.Now()
var waitSyncElapsed, readDirElapsed, getBlockRelsElapsed, scanElapsed, readNameElapsed time.Duration
var readFileInfoElapsed, sortElapsed, resolveElapsed, loadTreeElapsed, findNodeElapsed time.Duration
var readSearchInfoElapsed, resolveHPathElapsed time.Duration
var directoryEntryCount, validFileCount, eligibleFileCount, matchedCount int
var readNameCount, loadTreeCount, findNodeCount, readSearchInfoCount, resolveHPathCount int
var eligibleFileSize int64
defer func() {
totalElapsed := time.Since(searchStart)
if totalElapsed < 500*time.Millisecond {
return
}
logging.LogInfof("search attribute views [total=%dms, waitSync=%dms, readDir=%dms, getBlockRels=%dms, scan=%dms, readNames=%dms, readFileInfo=%dms, sort=%dms, resolve=%dms, loadTrees=%dms, findNodes=%dms, readSearchInfo=%dms, resolveHPaths=%dms, entries=%d, validFiles=%d, eligibleFiles=%d, matched=%d, results=%d, readNameCount=%d, loadTreeCount=%d, findNodeCount=%d, readSearchInfoCount=%d, resolveHPathCount=%d, eligibleSizeBytes=%d, keywordEmpty=%t, includeViewMatches=%t]",
totalElapsed.Milliseconds(), waitSyncElapsed.Milliseconds(), readDirElapsed.Milliseconds(),
getBlockRelsElapsed.Milliseconds(), scanElapsed.Milliseconds(), readNameElapsed.Milliseconds(),
readFileInfoElapsed.Milliseconds(), sortElapsed.Milliseconds(), resolveElapsed.Milliseconds(),
loadTreeElapsed.Milliseconds(), findNodeElapsed.Milliseconds(), readSearchInfoElapsed.Milliseconds(),
resolveHPathElapsed.Milliseconds(), directoryEntryCount, validFileCount, eligibleFileCount, matchedCount,
len(ret), readNameCount, loadTreeCount, findNodeCount, readSearchInfoCount, resolveHPathCount,
eligibleFileSize, strings.TrimSpace(options.Keyword) == "", options.IncludeViewMatches)
}()
waitSyncStart := time.Now()
waitForSyncingStorages()
waitSyncElapsed = time.Since(waitSyncStart)
ret = []*AvSearchResult{}
keyword := strings.TrimSpace(options.Keyword)
keywords := strings.Fields(keyword)
var avSearchTmpResults []*AvSearchTempResult
var warmupAvIDs []string
warmupSignature := attributeViewSearchCacheWarmupHashOffset
warmupSignature ^= cache.GetAVCacheGeneration()
warmupSignature *= attributeViewSearchCacheWarmupHashPrime
boxID := ""
if options.CurrentBlockID != "" {
if bt := treenode.GetBlockTree(options.CurrentBlockID); nil != bt && IsEncryptedBox(bt.BoxID) {
boxID = bt.BoxID
}
} else if options.CurrentAvID != "" {
_, boxID = av.FindAttributeViewPath(options.CurrentAvID)
}
if keyword != "" {
stopAttributeViewSearchCacheWarmup(boxID)
}
avDir := filepath.Join(util.DataDir, "storage", "av")
if boxID != "" {
avDir = filepath.Join(util.DataDir, boxID, "storage", "av")
}
readDirStart := time.Now()
entries, err := os.ReadDir(avDir)
readDirElapsed = time.Since(readDirStart)
if err != nil {
logging.LogErrorf("read directory [%s] failed: %s", avDir, err)
return
}
directoryEntryCount = len(entries)
getBlockRelsStart := time.Now()
avBlockRels := av.GetBlockRels()
getBlockRelsElapsed = time.Since(getBlockRelsStart)
if 1 > len(avBlockRels) {
return
}
scanStart := time.Now()
for _, entry := range entries {
if entry.IsDir() {
continue
}
id := strings.TrimSuffix(entry.Name(), ".json")
if !ast.IsNodeIDPattern(id) {
continue
}
validFileCount++
if nil == avBlockRels[id] {
continue
}
readFileInfoStart := time.Now()
info, _ := entry.Info()
readFileInfoElapsed += time.Since(readFileInfoStart)
warmupAvIDs = append(warmupAvIDs, id)
warmupSignature = updateAttributeViewSearchCacheWarmupSignature(warmupSignature, id, info)
if gulu.Str.Contains(id, options.ExcludeAvIDs) {
continue
}
eligibleFileCount++
if info != nil {
eligibleFileSize += info.Size()
}
a := &AvSearchTempResult{AvID: id}
if nil != info && !info.ModTime().IsZero() {
a.AvUpdated = info.ModTime().UnixMilli()
}
if keyword == "" {
avSearchTmpResults = append(avSearchTmpResults, a)
continue
}
var searchInfo *av.AttributeViewSearchInfo
readSearchInfoStart := time.Now()
searchInfo, _ = av.GetAttributeViewSearchInfoInBox(id, boxID)
readSearchInfoElapsed += time.Since(readSearchInfoStart)
readSearchInfoCount++
var name string
if searchInfo != nil {
name = searchInfo.Name
}
if searchInfo == nil {
readNameStart := time.Now()
name, _ = av.GetAttributeViewNameInBox(id, boxID)
readNameElapsed += time.Since(readNameStart)
readNameCount++
}
score, hit := matchAttributeViewSearchName(name, keywords)
matchedViewIDs := map[string]bool{}
if options.IncludeViewMatches && searchInfo != nil {
for _, view := range searchInfo.Views {
viewScore, viewHit := matchAttributeViewSearchName(view.Name, keywords)
if viewHit {
matchedViewIDs[view.ID] = true
if viewScore > score {
score = viewScore
}
}
}
}
if hit || len(matchedViewIDs) > 0 {
a.AvName = name
a.Score = score
a.Matched = hit
a.SearchInfo = searchInfo
a.MatchedViewIDs = matchedViewIDs
avSearchTmpResults = append(avSearchTmpResults, a)
}
}
scanElapsed = time.Since(scanStart)
sortStart := time.Now()
matchedCount = len(avSearchTmpResults)
avSearchTmpResults = sortAndLimitAttributeViewSearchResults(avSearchTmpResults, keyword)
sortElapsed = time.Since(sortStart)
resolveStart := time.Now()
for _, tmpResult := range avSearchTmpResults {
bIDs := avBlockRels[tmpResult.AvID]
var node *ast.Node
var treeHPath string
for _, bID := range bIDs {
loadTreeStart := time.Now()
tree, _ := loadTreeByBlockIDInBox(bID, boxID)
loadTreeElapsed += time.Since(loadTreeStart)
loadTreeCount++
if nil == tree {
continue
}
findNodeStart := time.Now()
node = treenode.GetNodeInTree(tree, bID)
findNodeElapsed += time.Since(findNodeStart)
findNodeCount++
if nil == node || "" == node.AttributeViewID || ast.NodeAttributeView != node.Type {
node = nil
continue
}
treeHPath = tree.HPath
break
}
if nil == node {
continue
}
searchInfo := tmpResult.SearchInfo
if searchInfo == nil {
readSearchInfoStart := time.Now()
searchInfo, _ = av.GetAttributeViewSearchInfoInBox(tmpResult.AvID, boxID)
readSearchInfoElapsed += time.Since(readSearchInfoStart)
readSearchInfoCount++
}
if searchInfo == nil {
continue
}
if tmpResult.AvName == "" {
tmpResult.AvName = searchInfo.Name
}
resolveHPathStart := time.Now()
hPath := treeHPath
box := Conf.Box(node.Box)
if nil != box {
hPath = box.Name + hPath
}
resolveHPathElapsed += time.Since(resolveHPathStart)
resolveHPathCount++
parent := &AvSearchResult{
AvID: tmpResult.AvID,
AvName: tmpResult.AvName,
BlockID: node.ID,
HPath: hPath,
Matched: tmpResult.Matched,
}
ret = append(ret, parent)
for _, view := range searchInfo.Views {
child := &AvSearchResult{
AvID: tmpResult.AvID,
AvName: tmpResult.AvName,
ViewName: view.Name,
ViewID: view.ID,
ViewLayout: view.LayoutType,
BlockID: node.ID,
HPath: hPath,
Matched: tmpResult.MatchedViewIDs[view.ID],
}
parent.Children = append(parent.Children, child)
}
}
resolveElapsed = time.Since(resolveStart)
if keyword == "" {
warmAttributeViewSearchCache(boxID, warmupAvIDs, warmupSignature)
}
return
}
type BlockAttributeViewKeys struct {
AvID string `json:"avID"`
AvName string `json:"avName"`
BlockIDs []string `json:"blockIDs"`
KeyValues []*av.KeyValues `json:"keyValues"`
ItemPositions []*AttributeViewItemPosition `json:"itemPositions"`
}
type AttributeViewItemPosition struct {
ViewID string `json:"viewID"`
PreviousID string `json:"previousID"`
Groups []*AttributeViewGroupItemPosition `json:"groups"`
}
type AttributeViewGroupItemPosition struct {
GroupID string `json:"groupID"`
PreviousID string `json:"previousID"`
}
func getAttributeViewItemPositions(attrView *av.AttributeView, itemID string) (ret []*AttributeViewItemPosition) {
for _, view := range attrView.Views {
previousID, found := getAttributeViewPreviousItemID(view.ItemIDs, itemID)
if !found {
continue
}
position := &AttributeViewItemPosition{ViewID: view.ID, PreviousID: previousID}
for _, group := range view.Groups {
groupPreviousID, groupFound := getAttributeViewPreviousItemID(group.GroupItemIDs, itemID)
if groupFound {
position.Groups = append(position.Groups, &AttributeViewGroupItemPosition{
GroupID: group.ID, PreviousID: groupPreviousID,
})
}
}
ret = append(ret, position)
}
return
}
func getAttributeViewPreviousItemID(itemIDs []string, itemID string) (previousID string, found bool) {
for i, currentID := range itemIDs {
if currentID != itemID {
continue
}
if 0 < i {
previousID = itemIDs[i-1]
}
found = true
return
}
return
}
type AttributeViewBacklinkRelation struct {
KeyID string `json:"keyID"`
KeyName string `json:"keyName"`
TargetAvID string `json:"targetAvID"`
TargetItemID string `json:"targetItemID"`
}
type AttributeViewBacklink struct {
AvID string `json:"avID"`
AvName string `json:"avName"`
BlockIDs []string `json:"blockIDs"`
DatabaseBlockID string `json:"databaseBlockID"`
BoxID string `json:"boxID"`
DatabasePath string `json:"databasePath"`
ItemID string `json:"itemID"`
ValueID string `json:"valueID"`
Title string `json:"title"`
Icon string `json:"icon"`
BoundBlockID string `json:"boundBlockID"`
IsDetached bool `json:"isDetached"`
Relations []*AttributeViewBacklinkRelation `json:"relations"`
}
type AttributeViewBacklinks struct {
Total int `json:"total"`
Items []*AttributeViewBacklink `json:"items"`
}
type attributeViewBacklinkTarget struct {
avID string
itemID string
}
func getAttributeViewBacklinkMatches(srcAttrView *av.AttributeView, target *attributeViewBacklinkTarget) (ret map[string][]*AttributeViewBacklinkRelation) {
ret = map[string][]*AttributeViewBacklinkRelation{}
seenRelations := map[string]bool{}
for _, keyValues := range srcAttrView.KeyValues {
key := keyValues.Key
if av.KeyTypeRelation != key.Type || nil == key.Relation || key.Relation.AvID != target.avID {
continue
}
for _, value := range keyValues.Values {
if nil == value.Relation || !slices.Contains(value.Relation.BlockIDs, target.itemID) {
continue
}
relationID := value.BlockID + "\x00" + key.ID + "\x00" + target.avID + "\x00" + target.itemID
if seenRelations[relationID] {
continue
}
seenRelations[relationID] = true
ret[value.BlockID] = append(ret[value.BlockID], &AttributeViewBacklinkRelation{
KeyID: key.ID,
KeyName: key.Name,
TargetAvID: target.avID,
TargetItemID: target.itemID,
})
}
}
return
}
func getAttributeViewBacklinkBlockValues(attrView *av.AttributeView) (ret map[string]*av.Value) {
ret = map[string]*av.Value{}
blockKeyValues := attrView.GetBlockKeyValues()
if nil == blockKeyValues {
return
}
for _, value := range blockKeyValues.Values {
ret[value.BlockID] = value
}
return
}
func resolveAttributeViewBacklinkItemID(attrView *av.AttributeView, itemID, valueID string) string {
if nil != attrView.GetBlockValue(itemID) {
return itemID
}
if "" == valueID {
return ""
}
for _, value := range getAttributeViewBacklinkBlockValues(attrView) {
if value.ID == valueID {
return value.BlockID
}
}
return ""
}
func sortAttributeViewBacklinkBlockIDs(blockIDs []string, blockTrees map[string]*treenode.BlockTree) {
sort.Slice(blockIDs, func(i, j int) bool {
left, right := blockTrees[blockIDs[i]], blockTrees[blockIDs[j]]
if nil == left || nil == right {
if nil == left && nil != right {
return false
}
if nil != left && nil == right {
return true
}
return blockIDs[i] < blockIDs[j]
}
if left.HPath != right.HPath {
return left.HPath < right.HPath
}
if left.BoxID != right.BoxID {
return left.BoxID < right.BoxID
}
return blockIDs[i] < blockIDs[j]
})
}
func GetAttributeViewBacklinks(nodeID, avID, itemID, valueID string) (ret *AttributeViewBacklinks) {
waitForSyncingStorages()
ret = &AttributeViewBacklinks{Items: []*AttributeViewBacklink{}}
targets := getAttributeViewBacklinkTargets(nodeID, avID, itemID, valueID)
if 1 > len(targets) {
return
}
backlinks := map[string]*AttributeViewBacklink{}
relationKeys := map[string]map[string]bool{}
cachedAttrViews := map[string]*av.AttributeView{}
cachedBlockValues := map[string]map[string]*av.Value{}
cachedBlockIDs := map[string][]string{}
cachedBlockTrees := map[string]map[string]*treenode.BlockTree{}
for _, target := range targets {
for _, srcAvID := range av.GetSrcAvIDs(target.avID) {
srcAttrView := cachedAttrViews[srcAvID]
if nil == srcAttrView {
var err error
srcAttrView, err = av.ParseAttributeView(srcAvID)
if nil == srcAttrView {
logging.LogErrorf("parse attribute view [%s] failed: %s", srcAvID, err)
continue
}
cachedAttrViews[srcAvID] = srcAttrView
}
blockValues := cachedBlockValues[srcAvID]
if nil == blockValues {
blockValues = getAttributeViewBacklinkBlockValues(srcAttrView)
cachedBlockValues[srcAvID] = blockValues
}
for sourceItemID, relations := range getAttributeViewBacklinkMatches(srcAttrView, target) {
blockValue := blockValues[sourceItemID]
if nil == blockValue || nil == blockValue.Block {
continue
}
backlinkID := srcAvID + "\x00" + sourceItemID
backlink := backlinks[backlinkID]
if nil == backlink {
blockIDs, cached := cachedBlockIDs[srcAvID]
blockTrees := cachedBlockTrees[srcAvID]
if !cached {
blockIDs = treenode.GetMirrorAttrViewBlockIDs(srcAvID)
blockTrees = treenode.GetBlockTrees(blockIDs)
sortAttributeViewBacklinkBlockIDs(blockIDs, blockTrees)
cachedBlockIDs[srcAvID] = blockIDs
cachedBlockTrees[srcAvID] = blockTrees
}
backlink = &AttributeViewBacklink{
AvID: srcAvID,
AvName: getAttrViewName(srcAttrView),
BlockIDs: blockIDs,
ItemID: sourceItemID,
ValueID: blockValue.ID,
Title: blockValue.Block.Content,
Icon: blockValue.Block.Icon,
BoundBlockID: blockValue.Block.ID,
IsDetached: blockValue.IsDetached || "" == blockValue.Block.ID,
Relations: []*AttributeViewBacklinkRelation{},
}
if 0 < len(blockIDs) {
backlink.DatabaseBlockID = blockIDs[0]
if bt := blockTrees[blockIDs[0]]; nil != bt {
backlink.BoxID = bt.BoxID
backlink.DatabasePath = bt.HPath
}
}
backlinks[backlinkID] = backlink
relationKeys[backlinkID] = map[string]bool{}
}
for _, relation := range relations {
relationID := relation.KeyID + "\x00" + relation.TargetAvID + "\x00" + relation.TargetItemID
if relationKeys[backlinkID][relationID] {
continue
}
relationKeys[backlinkID][relationID] = true
backlink.Relations = append(backlink.Relations, relation)
}
}
}
}
for _, backlink := range backlinks {
ret.Items = append(ret.Items, backlink)
}
sort.Slice(ret.Items, func(i, j int) bool {
if ret.Items[i].AvName != ret.Items[j].AvName {
return ret.Items[i].AvName < ret.Items[j].AvName
}
if ret.Items[i].Title != ret.Items[j].Title {
return ret.Items[i].Title < ret.Items[j].Title
}
return ret.Items[i].ItemID < ret.Items[j].ItemID
})
ret.Total = len(ret.Items)
return
}
func getAttributeViewBacklinkTargets(nodeID, avID, itemID, valueID string) (ret []*attributeViewBacklinkTarget) {
if "" != avID && ("" != itemID || "" != valueID) {
attrView, err := av.ParseAttributeView(avID)
if nil == attrView {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
itemID = resolveAttributeViewBacklinkItemID(attrView, itemID, valueID)
if "" != itemID {
ret = append(ret, &attributeViewBacklinkTarget{avID: avID, itemID: itemID})
}
return
}
attrs := sql.GetBlockAttrs(nodeID)
for targetAvID := range strings.SplitSeq(attrs[av.NodeAttrNameAvs], ",") {
if "" == targetAvID {
continue
}
attrView, err := av.ParseAttributeView(targetAvID)
if nil == attrView {
logging.LogErrorf("parse attribute view [%s] failed: %s", targetAvID, err)
continue
}
blockValue := attrView.GetBlockValueByBoundID(nodeID)
if nil != blockValue {
ret = append(ret, &attributeViewBacklinkTarget{avID: targetAvID, itemID: blockValue.BlockID})
}
}
return
}
func GetAttributeViewItemKeys(avID, itemID, valueID string) (ret []*BlockAttributeViewKeys) {
waitForSyncingStorages()
ret = []*BlockAttributeViewKeys{}
attrView, err := av.ParseAttributeView(avID)
if nil == attrView {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
if nil == attrView.GetBlockValue(itemID) && "" != valueID {
blockKeyValues := attrView.GetBlockKeyValues()
if nil != blockKeyValues {
for _, value := range blockKeyValues.Values {
if value.ID == valueID {
itemID = value.BlockID
break
}
}
}
}
if nil == attrView.GetBlockValue(itemID) {
return
}
view, err := getRenderAttributeViewView(attrView, "", "", "", true)
if nil != err {
return
}
sql.RenderView(attrView, view, "", false)
var keyValues []*av.KeyValues
for _, kv := range attrView.KeyValues {
if av.KeyTypeLineNumber == kv.Key.Type {
continue
}
itemKeyValues := &av.KeyValues{Key: kv.Key}
for _, value := range kv.Values {
if value.BlockID == itemID {
itemKeyValues.Values = append(itemKeyValues.Values, value)
}
}
if 0 < len(itemKeyValues.Values) {
keyValues = append(keyValues, itemKeyValues)
}
}
refreshAttrViewKeyIDs(attrView, true)
sorts := map[string]int{}
for i, keyID := range attrView.KeyIDs {
sorts[keyID] = i
}
sort.Slice(keyValues, func(i, j int) bool {
return sorts[keyValues[i].Key.ID] < sorts[keyValues[j].Key.ID]
})
ret = append(ret, &BlockAttributeViewKeys{
AvID: avID,
AvName: getAttrViewName(attrView),
BlockIDs: treenode.GetMirrorAttrViewBlockIDs(avID),
KeyValues: keyValues,
ItemPositions: getAttributeViewItemPositions(attrView, itemID),
})
return
}
func GetBlockAttributeViewKeys(nodeID string) (ret []*BlockAttributeViewKeys) {
waitForSyncingStorages()
ret = []*BlockAttributeViewKeys{}
attrs := sql.GetBlockAttrs(nodeID)
avs := attrs[av.NodeAttrNameAvs]
if "" == avs {
return
}
cachedAttrViews := map[string]*av.AttributeView{}
avIDs := strings.SplitSeq(avs, ",")
for avID := range avIDs {
attrView := cachedAttrViews[avID]
if nil == attrView {
var err error
attrView, err = av.ParseAttributeView(avID)
if nil == attrView {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
continue
}
cachedAttrViews[avID] = attrView
}
if !attrView.ExistBoundBlock(nodeID) {
// 比如剪切后粘贴,块 ID 会变,但是属性还在块上,这里做一次数据订正
// Auto verify the database name when clicking the block superscript icon https://github.com/siyuan-note/siyuan/issues/10861
unbindBlockAv(nil, avID, nodeID)
return
}
blockVal := attrView.GetBlockValueByBoundID(nodeID)
if nil == blockVal {
continue
}
itemID := blockVal.BlockID
view, err := getRenderAttributeViewView(attrView, "", "", nodeID, true)
if nil != err {
continue
}
// 渲染填充 attrView.KeyValues
sql.RenderView(attrView, view, "", false)
var keyValues []*av.KeyValues
for _, kv := range attrView.KeyValues {
if av.KeyTypeLineNumber == kv.Key.Type {
// 属性面板中不显示行号字段
// The line number field no longer appears in the database attribute panel https://github.com/siyuan-note/siyuan/issues/11319
continue
}
kValues := &av.KeyValues{Key: kv.Key}
for _, v := range kv.Values {
if v.BlockID == itemID {
kValues.Values = append(kValues.Values, v)
}
}
keyValues = append(keyValues, kValues)
}
// 字段排序
refreshAttrViewKeyIDs(attrView, true)
sorts := map[string]int{}
for i, k := range attrView.KeyIDs {
sorts[k] = i
}
sort.Slice(keyValues, func(i, j int) bool {
return sorts[keyValues[i].Key.ID] < sorts[keyValues[j].Key.ID]
})
blockIDs := treenode.GetMirrorAttrViewBlockIDs(avID)
if 1 > len(blockIDs) {
// 老数据兼容处理
boxID := ""
if tree, _ := LoadTreeByBlockID(nodeID); tree != nil {
boxID = tree.Box
}
avBts := treenode.GetBlockTreesByTypeInBox("av", boxID)
for _, avBt := range avBts {
if nil == avBt {
continue
}
tree, _ := LoadTreeByBlockIDInExactBox(avBt.ID, avBt.BoxID)
if nil == tree {
continue
}
node := treenode.GetNodeInTree(tree, avBt.ID)
if nil == node {
continue
}
if avID == node.AttributeViewID {
blockIDs = append(blockIDs, avBt.ID)
}
}
if 1 > len(blockIDs) {
tree, _ := LoadTreeByBlockID(nodeID)
if nil != tree {
node := treenode.GetNodeInTree(tree, nodeID)
if nil != node {
if removeErr := removeNodeAvID(node, avID, nil, tree); nil != removeErr {
logging.LogErrorf("remove node avID [%s] failed: %s", avID, removeErr)
}
}
}
continue
}
blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs)
for _, blockID := range blockIDs {
av.UpsertBlockRel(avID, blockID)
}
}
ret = append(ret, &BlockAttributeViewKeys{
AvID: avID,
AvName: getAttrViewName(attrView),
BlockIDs: blockIDs,
KeyValues: keyValues,
ItemPositions: getAttributeViewItemPositions(attrView, itemID),
})
}
return
}
func genAttrViewGroups(view *av.View, attrView *av.AttributeView) {
if !view.IsGroupView() {
return
}
groupStates := getAttrViewGroupStates(view)
group := view.Group
view.Groups = nil
viewable := sql.RenderView(attrView, view, "", false)
var items []av.Item
for _, item := range viewable.(av.Collection).GetItems() {
items = append(items, item)
}
groupKey := view.GetGroupKey(attrView)
if nil == groupKey {
return
}
var rangeStart, rangeEnd float64
switch group.Method {
case av.GroupMethodValue:
if av.GroupOrderMan != group.Order {
sort.SliceStable(items, func(i, j int) bool {
return items[i].GetValue(group.Field).String(false) < items[j].GetValue(group.Field).String(false)
})
}
case av.GroupMethodRangeNum:
if nil == group.Range {
return
}
rangeStart, rangeEnd = group.Range.NumStart, group.Range.NumStart+group.Range.NumStep
sort.SliceStable(items, func(i, j int) bool {
return items[i].GetValue(group.Field).Number.Content < items[j].GetValue(group.Field).Number.Content
})
case av.GroupMethodDateDay, av.GroupMethodDateWeek, av.GroupMethodDateMonth, av.GroupMethodDateYear, av.GroupMethodDateRelative:
if av.KeyTypeCreated == groupKey.Type {
sort.SliceStable(items, func(i, j int) bool {
return items[i].GetValue(group.Field).Created.Content < items[j].GetValue(group.Field).Created.Content
})
} else if av.KeyTypeUpdated == groupKey.Type {
sort.SliceStable(items, func(i, j int) bool {
return items[i].GetValue(group.Field).Updated.Content < items[j].GetValue(group.Field).Updated.Content
})
} else if av.KeyTypeDate == groupKey.Type {
sort.SliceStable(items, func(i, j int) bool {
return items[i].GetValue(group.Field).Date.Content < items[j].GetValue(group.Field).Date.Content
})
}
}
todayStart := time.Now()
todayStart = time.Date(todayStart.Year(), todayStart.Month(), todayStart.Day(), 0, 0, 0, 0, time.Local)
var relationDestAv *av.AttributeView
if av.KeyTypeRelation == groupKey.Type && nil != groupKey.Relation {
if attrView.ID == groupKey.Relation.AvID {
relationDestAv = attrView
} else {
relationDestAv, _ = av.ParseAttributeView(groupKey.Relation.AvID)
}
}
groupItemsMap := map[string][]av.Item{}
for _, item := range items {
value := item.GetValue(group.Field)
if value.IsBlank() {
groupItemsMap[groupValueDefault] = append(groupItemsMap[groupValueDefault], item)
continue
}
var groupVal string
switch group.Method {
case av.GroupMethodValue:
if av.KeyTypeSelect == groupKey.Type || av.KeyTypeMSelect == groupKey.Type {
for _, s := range value.MSelect {
groupItemsMap[s.Content] = append(groupItemsMap[s.Content], item)
}
continue
} else if av.KeyTypeRelation == groupKey.Type {
if nil == relationDestAv {
continue
}
for _, bID := range value.Relation.BlockIDs {
groupItemsMap[bID] = append(groupItemsMap[bID], item)
}
continue
}
groupVal = value.String(false)
case av.GroupMethodRangeNum:
if group.Range.NumStart > value.Number.Content || group.Range.NumEnd < value.Number.Content {
groupVal = groupValueNotInRange
break
}
for rangeEnd <= group.Range.NumEnd && rangeEnd <= value.Number.Content {
rangeStart += group.Range.NumStep
rangeEnd += group.Range.NumStep
}
if rangeStart <= value.Number.Content && rangeEnd > value.Number.Content {
groupVal = fmt.Sprintf("%s - %s", strconv.FormatFloat(rangeStart, 'f', -1, 64), strconv.FormatFloat(rangeEnd, 'f', -1, 64))
}
case av.GroupMethodDateDay, av.GroupMethodDateWeek, av.GroupMethodDateMonth, av.GroupMethodDateYear, av.GroupMethodDateRelative:
var contentTime time.Time
switch value.Type {
case av.KeyTypeDate:
contentTime = time.UnixMilli(value.Date.Content)
case av.KeyTypeCreated:
contentTime = time.UnixMilli(value.Created.Content)
case av.KeyTypeUpdated:
contentTime = time.UnixMilli(value.Updated.Content)
}
switch group.Method {
case av.GroupMethodDateDay:
groupVal = contentTime.Format("2006-01-02")
case av.GroupMethodDateWeek:
year, week := contentTime.ISOWeek()
groupVal = fmt.Sprintf("%d-W%02d", year, week)
case av.GroupMethodDateMonth:
groupVal = contentTime.Format("2006-01")
case av.GroupMethodDateYear:
groupVal = contentTime.Format("2006")
case av.GroupMethodDateRelative:
// 过去 30 天之前的按月分组
// 过去 30 天、过去 7 天、昨天、今天、明天、未来 7 天、未来 30 天
// 未来 30 天之后的按月分组
if contentTime.Before(todayStart.AddDate(0, 0, -30)) {
groupVal = contentTime.Format("2006-01") // 开头的数字用于排序
} else if contentTime.Before(todayStart.AddDate(0, 0, -7)) {
groupVal = groupValueLast30Days
} else if contentTime.Before(todayStart.AddDate(0, 0, -1)) {
groupVal = groupValueLast7Days
} else if contentTime.Before(todayStart) {
groupVal = groupValueYesterday
} else if (contentTime.After(todayStart) || contentTime.Equal(todayStart)) && contentTime.Before(todayStart.AddDate(0, 0, 1)) {
groupVal = groupValueToday
} else if contentTime.After(todayStart.AddDate(0, 0, 30)) {
groupVal = contentTime.Format("2006-01")
} else if contentTime.After(todayStart.AddDate(0, 0, 7)) {
groupVal = groupValueNext30Days
} else if contentTime.Equal(todayStart.AddDate(0, 0, 2)) || contentTime.After(todayStart.AddDate(0, 0, 2)) {
groupVal = groupValueNext7Days
} else {
groupVal = groupValueTomorrow
}
}
}
groupItemsMap[groupVal] = append(groupItemsMap[groupVal], item)
}
if av.KeyTypeSelect == groupKey.Type || av.KeyTypeMSelect == groupKey.Type {
for _, o := range groupKey.Options {
if _, ok := groupItemsMap[o.Name]; !ok {
groupItemsMap[o.Name] = []av.Item{}
}
}
}
if av.KeyTypeCheckbox != groupKey.Type {
if 1 > len(groupItemsMap[groupValueDefault]) {
// 始终保留默认分组 https://github.com/siyuan-note/siyuan/issues/15587
groupItemsMap[groupValueDefault] = []av.Item{}
}
} else {
// 对于复选框分组,空白分组表示未选中状态,始终保留 https://github.com/siyuan-note/siyuan/issues/15650
if nil == groupItemsMap[""] {
groupItemsMap[""] = []av.Item{}
}
if nil == groupItemsMap[av.CheckboxCheckedStr] {
groupItemsMap[av.CheckboxCheckedStr] = []av.Item{}
}
}
for groupValue, groupItems := range groupItemsMap {
var v *av.View
switch view.LayoutType {
case av.LayoutTypeTable:
v = av.NewTableView()
v.Table = av.NewLayoutTable()
case av.LayoutTypeGallery:
v = av.NewGalleryView()
v.Gallery = av.NewLayoutGallery()
case av.LayoutTypeKanban:
v = av.NewKanbanView()
v.Kanban = av.NewLayoutKanban()
default:
logging.LogWarnf("unknown layout type [%s] for group view", view.LayoutType)
return
}
v.GroupItemIDs = []string{}
for _, item := range groupItems {
v.GroupItemIDs = append(v.GroupItemIDs, item.GetID())
}
v.Name = "" // 分组视图的名称在渲染时才填充
v.GroupHidden = 1 // 默认隐藏空白分组
v.GroupKey = groupKey
v.GroupVal = &av.Value{Type: av.KeyTypeText, Text: &av.ValueText{Content: groupValue}}
if av.KeyTypeSelect == groupKey.Type || av.KeyTypeMSelect == groupKey.Type {
if opt := groupKey.GetOption(groupValue); nil != opt {
v.GroupVal.Text = nil
v.GroupVal.Type = av.KeyTypeSelect
v.GroupVal.MSelect = []*av.ValueSelect{{Content: opt.Name, Color: opt.Color}}
}
} else if av.KeyTypeRelation == groupKey.Type {
if relationDestAv != nil && groupValueDefault != groupValue {
v.GroupVal.Text = nil
v.GroupVal.Type = av.KeyTypeRelation
v.GroupVal.Relation = &av.ValueRelation{BlockIDs: []string{groupValue}}
if destBlock := relationDestAv.GetBlockValue(groupValue); nil != destBlock {
v.GroupVal.Relation.Contents = []*av.Value{destBlock}
}
}
} else if av.KeyTypeCheckbox == groupKey.Type {
v.GroupVal.Text = nil
v.GroupVal.Type = av.KeyTypeCheckbox
v.GroupVal.Checkbox = &av.ValueCheckbox{}
if "" != groupValue {
v.GroupVal.Checkbox.Checked = true
}
}
v.GroupSort = -1
view.Groups = append(view.Groups, v)
}
view.GroupCreated = time.Now().UnixMilli()
setAttrViewGroupStates(view, groupStates)
}
// GroupState 用于临时记录每个分组视图的状态,以便后面重新生成分组后可以恢复这些状态。
type GroupState struct {
ID string
Folded bool
Hidden int
Sort int
ItemIDs []string
}
func getAttrViewGroupStates(view *av.View) (groupStates map[string]*GroupState) {
groupStates = map[string]*GroupState{}
if !view.IsGroupView() {
return
}
for _, groupView := range view.Groups {
if av.LayoutTypeKanban == groupView.LayoutType {
// 看板视图的分组不能折叠
groupView.GroupFolded = false
}
groupStates[groupView.GetGroupValue()] = &GroupState{
ID: groupView.ID,
Folded: groupView.GroupFolded,
Hidden: groupView.GroupHidden,
Sort: groupView.GroupSort,
ItemIDs: groupView.GroupItemIDs,
}
}
return
}
func setAttrViewGroupStates(view *av.View, groupStates map[string]*GroupState) {
for _, groupView := range view.Groups {
if state, ok := groupStates[groupView.GetGroupValue()]; ok {
groupView.ID = state.ID
groupView.GroupFolded = state.Folded
groupView.GroupHidden = state.Hidden
groupView.GroupSort = state.Sort
itemIDsSort := map[string]int{}
for i, itemID := range state.ItemIDs {
itemIDsSort[itemID] = i
}
sort.SliceStable(groupView.GroupItemIDs, func(i, j int) bool {
return itemIDsSort[groupView.GroupItemIDs[i]] < itemIDsSort[groupView.GroupItemIDs[j]]
})
}
}
defaultGroup := view.GetGroupByGroupValue(groupValueDefault)
if nil != defaultGroup {
if -1 == defaultGroup.GroupSort {
view.RemoveGroupByID(defaultGroup.ID)
} else {
defaultGroup = nil
}
}
for i, groupView := range view.Groups {
if i != groupView.GroupSort && -1 == groupView.GroupSort {
groupView.GroupSort = i
}
}
if nil != defaultGroup {
view.Groups = append(view.Groups, defaultGroup)
defaultGroup.GroupSort = len(view.Groups) - 1
}
}
func GetCurrentAttributeViewImages(c *gin.Context, avID, viewID, query string) (ret []string, err error) {
var attrView *av.AttributeView
attrView, err = av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
var view *av.View
view, err = resolveAttributeViewView(attrView, viewID, "", "")
if nil != err {
return
}
cachedAttrViews := map[string]*av.AttributeView{}
rollupFurtherCollections := sql.GetFurtherCollections(attrView, cachedAttrViews)
table := getAttrViewTable(attrView, view, query)
av.Filter(table, attrView, rollupFurtherCollections, cachedAttrViews)
av.Sort(table, attrView)
if IsReadOnlyRoleContext(c) {
table = FilterViewByPublishAccess(c, GetPublishAccess(), table).(*av.Table)
}
ids := map[string]bool{}
for _, column := range table.Columns {
ids[column.ID] = column.Hidden
}
for _, row := range table.Rows {
for _, cell := range row.Cells {
if nil != cell.Value && av.KeyTypeMAsset == cell.Value.Type && nil != cell.Value.MAsset && !ids[cell.Value.KeyID] {
for _, a := range cell.Value.MAsset {
if av.AssetTypeImage == a.Type {
ret = append(ret, a.Content)
}
}
}
}
}
return
}
func (tx *Transaction) doSetAttrViewColDateFillCreated(operation *Operation) (ret *TxErr) {
err := setAttributeViewColDateFillCreated(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColDateFillCreated(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
keyID := operation.ID
key, _ := attrView.GetKey(keyID)
if nil == key || av.KeyTypeDate != key.Type {
return
}
if nil == key.Date {
key.Date = &av.Date{}
}
key.Date.AutoFillNow = operation.Data.(bool)
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewColDateFillSpecificTime(operation *Operation) (ret *TxErr) {
err := setAttrViewColDateFillSpecificTime(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewColDateFillSpecificTime(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
keyID := operation.ID
dateValues, _ := attrView.GetKeyValues(keyID)
if nil == dateValues || av.KeyTypeDate != dateValues.Key.Type {
return
}
if nil == dateValues.Key.Date {
dateValues.Key.Date = &av.Date{}
}
dateValues.Key.Date.FillSpecificTime = operation.Data.(bool)
for _, v := range dateValues.Values {
if !v.IsEmpty() {
continue
}
if nil == v.Date {
v.Date = &av.ValueDate{}
}
v.Date.IsNotTime = !dateValues.Key.Date.FillSpecificTime
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewCreatedIncludeTime(operation *Operation) (ret *TxErr) {
err := setAttrViewCreatedIncludeTime(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewCreatedIncludeTime(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
key, _ := attrView.GetKey(operation.ID)
if nil == key {
return
}
if nil == key.Created {
key.Created = &av.Created{}
}
key.Created.IncludeTime = operation.Data.(bool)
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewUpdatedIncludeTime(operation *Operation) (ret *TxErr) {
err := setAttrViewUpdatedIncludeTime(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttrViewUpdatedIncludeTime(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
key, _ := attrView.GetKey(operation.ID)
if nil == key {
return
}
if nil == key.Updated {
key.Updated = &av.Updated{}
}
key.Updated.IncludeTime = operation.Data.(bool)
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doHideAttrViewName(operation *Operation) (ret *TxErr) {
err := hideAttrViewName(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func hideAttrViewName(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", operation.AvID, err)
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if nil == view {
logging.LogErrorf("get view [%s] failed: %s", operation.BlockID, err)
return
}
view.HideAttrViewName = operation.Data.(bool)
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doUpdateAttrViewColRollup(operation *Operation) (ret *TxErr) {
err := updateAttributeViewColRollup(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func updateAttributeViewColRollup(operation *Operation) (err error) {
// operation.AvID 汇总字段所在 av
// operation.ID 汇总字段 ID
// operation.ParentID 汇总字段基于的关联字段 ID
// operation.KeyID 目标字段 ID
// operation.Data 计算方式
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
rollUpKey, _ := attrView.GetKey(operation.ID)
if nil == rollUpKey {
return
}
var filters []*av.ViewFilter
oldDestAvID := ""
if nil != rollUpKey.Rollup {
filters = rollUpKey.Rollup.Filters
if oldRelationKey, _ := attrView.GetKey(rollUpKey.Rollup.RelationKeyID); nil != oldRelationKey &&
nil != oldRelationKey.Relation {
oldDestAvID = oldRelationKey.Relation.AvID
}
}
newDestAvID := ""
if newRelationKey, _ := attrView.GetKey(operation.ParentID); nil != newRelationKey && nil != newRelationKey.Relation {
newDestAvID = newRelationKey.Relation.AvID
}
if oldDestAvID != newDestAvID {
filters = nil
}
rollUpKey.Rollup = &av.Rollup{
RelationKeyID: operation.ParentID,
KeyID: operation.KeyID,
Filters: filters,
}
if nil == operation.Data {
return
}
data := operation.Data.(map[string]any)
if nil != data["calc"] {
calcData, jsonErr := gulu.JSON.MarshalJSON(data["calc"])
if nil != jsonErr {
err = jsonErr
return
}
if jsonErr = gulu.JSON.UnmarshalJSON(calcData, &rollUpKey.Rollup.Calc); nil != jsonErr {
err = jsonErr
return
}
}
// 如果存在该汇总字段的过滤条件,则移除该过滤条件 https://github.com/siyuan-note/siyuan/issues/15660
for _, view := range attrView.Views {
view.Filters = av.RemoveFiltersByColumn(view.Filters, rollUpKey.ID)
if 0 == len(view.Filters) {
// 保持 spec 5 根组不变量:根组被裁空后补一个空 AND 根组
view.Filters = []*av.ViewFilter{{Combination: av.FilterCombinationAnd}}
}
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doUpdateAttrViewColRelation(operation *Operation) (ret *TxErr) {
err := updateAttributeViewColRelation(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func updateAttributeViewColRelation(operation *Operation) (err error) {
// operation.AvID 源 avID
// operation.ID 目标 avID
// operation.KeyID 源 av 关联字段 ID
// operation.IsTwoWay 是否双向关联
// operation.BackRelationKeyID 双向关联的目标关联字段 ID
// operation.Name 双向关联的目标关联字段名称
// operation.Format 源 av 关联字段名称
srcAv, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
destAv, err := av.ParseAttributeView(operation.ID)
if err != nil {
return
}
isSameAv := srcAv.ID == destAv.ID
if isSameAv {
destAv = srcAv
}
oldDestAvID := ""
for _, keyValues := range srcAv.KeyValues {
if keyValues.Key.ID != operation.KeyID {
continue
}
srcRel := keyValues.Key.Relation
// 已经设置过双向关联的话需要先断开双向关联
if nil != srcRel {
oldDestAvID = srcRel.AvID
if srcRel.IsTwoWay {
oldDestAv, _ := av.ParseAttributeView(srcRel.AvID)
if nil != oldDestAv {
isOldSameAv := oldDestAv.ID == destAv.ID
if isOldSameAv {
oldDestAv = destAv
}
oldDestKey, _ := oldDestAv.GetKey(srcRel.BackKeyID)
if nil != oldDestKey && nil != oldDestKey.Relation && oldDestKey.Relation.AvID == srcAv.ID && oldDestKey.Relation.IsTwoWay {
oldDestKey.Relation.IsTwoWay = false
oldDestKey.Relation.BackKeyID = ""
}
if !isOldSameAv {
err = av.SaveAttributeView(oldDestAv)
if err != nil {
return
}
}
}
}
av.RemoveAvRel(srcAv.ID, srcRel.AvID)
}
var candidateFilters []*av.ViewFilter
if nil != srcRel && srcRel.AvID == operation.ID {
candidateFilters = srcRel.CandidateFilters
}
srcRel = &av.Relation{
AvID: operation.ID,
IsTwoWay: operation.IsTwoWay,
CandidateFilters: candidateFilters,
}
if operation.IsTwoWay {
srcRel.BackKeyID = operation.BackRelationKeyID
} else {
srcRel.BackKeyID = ""
}
keyValues.Key.Relation = srcRel
keyValues.Key.Name = operation.Format
break
}
if oldDestAvID != operation.ID {
srcAv.RemoveNewItemTemplateFieldValue(operation.KeyID)
srcAv.RemoveExactRelationFilters(operation.KeyID)
for _, keyValues := range srcAv.KeyValues {
if nil != keyValues && nil != keyValues.Key && av.KeyTypeRollup == keyValues.Key.Type &&
nil != keyValues.Key.Rollup && keyValues.Key.Rollup.RelationKeyID == operation.KeyID {
keyValues.Key.Rollup.Filters = nil
}
}
}
destAdded := false
backRelKey, _ := destAv.GetKey(operation.BackRelationKeyID)
if nil != backRelKey {
backRelKey.Relation = &av.Relation{
AvID: operation.AvID,
IsTwoWay: operation.IsTwoWay,
BackKeyID: operation.KeyID,
}
destAdded = true
if operation.IsTwoWay {
name := strings.TrimSpace(operation.Name)
if "" == name {
name = srcAv.Name + " " + operation.Format
}
backRelKey.Name = strings.TrimSpace(name)
} else {
backRelKey.Relation.BackKeyID = ""
}
}
if !destAdded && operation.IsTwoWay {
// 新建双向关联目标字段
name := strings.TrimSpace(operation.Name)
if "" == name {
name = srcAv.Name + " " + operation.Format
name = strings.TrimSpace(name)
}
destKeyValues := &av.KeyValues{
Key: &av.Key{
ID: operation.BackRelationKeyID,
Name: name,
Type: av.KeyTypeRelation,
Relation: &av.Relation{AvID: operation.AvID, IsTwoWay: operation.IsTwoWay, BackKeyID: operation.KeyID},
},
}
destAv.KeyValues = append(destAv.KeyValues, destKeyValues)
for _, v := range destAv.Views {
switch v.LayoutType {
case av.LayoutTypeTable:
v.Table.Columns = append(v.Table.Columns, &av.ViewTableColumn{BaseField: &av.BaseField{ID: operation.BackRelationKeyID}})
case av.LayoutTypeGallery:
v.Gallery.CardFields = append(v.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: &av.BaseField{ID: operation.BackRelationKeyID}})
case av.LayoutTypeKanban:
v.Kanban.Fields = append(v.Kanban.Fields, &av.ViewKanbanField{BaseField: &av.BaseField{ID: operation.BackRelationKeyID}})
}
}
now := time.Now().UnixMilli()
// 和现有值进行关联
for _, keyValues := range srcAv.KeyValues {
if keyValues.Key.ID != operation.KeyID {
continue
}
for _, srcVal := range keyValues.Values {
for _, blockID := range srcVal.Relation.BlockIDs {
destVal := destAv.GetValue(destKeyValues.Key.ID, blockID)
if nil == destVal {
destVal = &av.Value{ID: ast.NewNodeID(), KeyID: destKeyValues.Key.ID, BlockID: blockID, Type: keyValues.Key.Type, Relation: &av.ValueRelation{}, CreatedAt: now, UpdatedAt: now + 1000}
} else {
destVal.Type = keyValues.Key.Type
if nil == destVal.Relation {
destVal.Relation = &av.ValueRelation{}
}
destVal.UpdatedAt = now
destVal.IsRenderAutoFill = false
}
destVal.Relation.BlockIDs = append(destVal.Relation.BlockIDs, srcVal.BlockID)
destVal.Relation.BlockIDs = gulu.Str.RemoveDuplicatedElem(destVal.Relation.BlockIDs)
destKeyValues.Values = append(destKeyValues.Values, destVal)
}
}
}
}
regenAttrViewGroups(srcAv)
err = av.SaveAttributeView(srcAv)
if err != nil {
return
}
if !isSameAv {
regenAttrViewGroups(destAv)
err = av.SaveAttributeView(destAv)
ReloadAttrView(destAv.ID)
}
av.UpsertAvBackRel(srcAv.ID, destAv.ID)
if operation.IsTwoWay && !isSameAv {
av.UpsertAvBackRel(destAv.ID, srcAv.ID)
}
if "" != oldDestAvID && oldDestAvID != operation.ID && oldDestAvID != srcAv.ID {
ReloadAttrView(oldDestAvID)
}
return
}
func (tx *Transaction) doSortAttrViewView(operation *Operation) (ret *TxErr) {
avID := operation.AvID
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", operation.AvID, err)
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
view := attrView.GetView(operation.ID)
if nil == view {
logging.LogErrorf("get view failed: %s", operation.BlockID)
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
viewID := view.ID
previousViewID := operation.PreviousID
if viewID == previousViewID {
return
}
var index, previousIndex int
for i, v := range attrView.Views {
if v.ID == viewID {
view = v
index = i
break
}
}
attrView.Views = append(attrView.Views[:index], attrView.Views[index+1:]...)
for i, v := range attrView.Views {
if v.ID == previousViewID {
previousIndex = i + 1
break
}
}
attrView.Views = util.InsertElem(attrView.Views, previousIndex, view)
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: avID}
}
return
}
func (tx *Transaction) doRemoveAttrViewView(operation *Operation) (ret *TxErr) {
var err error
avID := operation.AvID
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: avID}
}
if 1 >= len(attrView.Views) {
logging.LogWarnf("can't remove last view [%s] of attribute view [%s]", operation.AvID, avID)
return
}
view, err := getAttrViewViewToRemove(attrView, operation)
if nil == view {
logging.LogWarnf("get view [%s] to remove failed: %s", operation.ID, err)
if nil == err {
err = av.ErrViewNotFound
}
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
viewID := view.ID
var index int
for i, view := range attrView.Views {
if viewID == view.ID {
attrView.Views = append(attrView.Views[:i], attrView.Views[i+1:]...)
index = i - 1
break
}
}
if 0 > index {
index = 0
}
view = attrView.Views[index]
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: avID}
}
trees, nodes := getMirrorBlocksNodes(avID)
for _, node := range nodes {
attrs := parse.IAL2Map(node.KramdownIAL)
changed := false
visibleViewIDsValue := attrs[av.NodeAttrVisibleViewIDs]
visibleViewIDs := attrView.GetVisibleViewIDs(visibleViewIDsValue)
if "" != visibleViewIDsValue {
normalized := strings.Join(visibleViewIDs, ",")
if normalized != visibleViewIDsValue {
attrs[av.NodeAttrVisibleViewIDs] = normalized
changed = true
}
}
blockViewID := attrs[av.NodeAttrView]
if blockViewID == viewID {
attrs[av.NodeAttrView] = visibleViewIDs[0]
fallbackView := attrView.GetView(visibleViewIDs[0])
node.AttributeViewType = string(fallbackView.LayoutType)
changed = true
}
if !changed {
continue
}
// 镜像块节点未关联 tree,通过 blocktree 解析 boxID 以走加密笔记本守卫与 box-aware 缓存键
boxID := ""
if bt := treenode.GetBlockTree(node.ID); nil != bt {
boxID = bt.BoxID
}
oldAttrs, e := setNodeAttrs0(node, attrs, boxID)
if nil != e {
logging.LogErrorf("set node attrs failed: %s", e)
continue
}
cache.PutBlockIALInBox(node.ID, boxID, parse.IAL2Map(node.KramdownIAL))
pushBlockAttrs(oldAttrs, node)
}
for _, tree := range trees {
if err = indexWriteTreeUpsertQueue(tree); err != nil {
return
}
}
operation.RetData = view.LayoutType
return
}
func getAttrViewViewToRemove(attrView *av.AttributeView, operation *Operation) (ret *av.View, err error) {
if "" != operation.ID {
ret = attrView.GetView(operation.ID)
if nil == ret {
err = av.ErrViewNotFound
}
return
}
return getAttrViewViewByBlockID(attrView, operation.BlockID)
}
func getMirrorBlocksNodes(avID string) (trees []*parse.Tree, nodes []*ast.Node) {
mirrorBlockIDs := treenode.GetMirrorAttrViewBlockIDs(avID)
mirrorBlockTrees := filesys.LoadTrees(mirrorBlockIDs)
for id, tree := range mirrorBlockTrees {
node := treenode.GetNodeInTree(tree, id)
if nil == node {
logging.LogErrorf("get node in tree by block ID [%s] failed", id)
continue
}
nodes = append(nodes, node)
}
for _, tree := range mirrorBlockTrees {
trees = append(trees, tree)
}
return
}
func freezeOtherAttrViewBlockVisibleViews(attrView *av.AttributeView, currentBlockID string, currentTree *parse.Tree) (err error) {
var oldViewIDs []string
for _, view := range attrView.Views {
oldViewIDs = append(oldViewIDs, view.ID)
}
if 1 > len(oldViewIDs) {
return
}
value := strings.Join(oldViewIDs, ",")
var otherBlockIDs []string
for _, blockID := range treenode.GetMirrorAttrViewBlockIDs(attrView.ID) {
if blockID != currentBlockID {
otherBlockIDs = append(otherBlockIDs, blockID)
}
}
mirrorBlockTrees := filesys.LoadTrees(otherBlockIDs)
changedTrees := map[string]*parse.Tree{}
for blockID, tree := range mirrorBlockTrees {
if tree.Root.ID == currentTree.Root.ID {
tree = currentTree
}
node := treenode.GetNodeInTree(tree, blockID)
if nil == node {
logging.LogErrorf("get node in tree by block ID [%s] failed", blockID)
continue
}
if "" != node.IALAttr(av.NodeAttrVisibleViewIDs) {
continue
}
boxID := tree.Box
oldAttrs, setErr := setNodeAttrs0(node, map[string]string{av.NodeAttrVisibleViewIDs: value}, boxID)
if nil != setErr {
return setErr
}
cache.PutBlockIALInBox(node.ID, boxID, parse.IAL2Map(node.KramdownIAL))
pushBlockAttrs(oldAttrs, node)
if tree.Root.ID != currentTree.Root.ID {
changedTrees[tree.Root.ID] = tree
}
}
for _, tree := range changedTrees {
if err = indexWriteTreeUpsertQueue(tree); nil != err {
return
}
}
return
}
func (tx *Transaction) doDuplicateAttrViewRow(operation *Operation) (ret *TxErr) {
srcRowID := ""
if 0 < len(operation.SrcIDs) {
srcRowID = operation.SrcIDs[0]
}
if "" == srcRowID {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: "source row id is empty"}
}
if err := DuplicateAttributeViewRow(tx, operation.AvID, operation.PreviousID, srcRowID, operation.ID); err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func (tx *Transaction) doDuplicateAttrViewView(operation *Operation) (ret *TxErr) {
var err error
avID := operation.AvID
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return &TxErr{code: TxErrHandleAttributeView, id: avID}
}
masterView := attrView.GetView(operation.PreviousID)
if nil == masterView {
logging.LogErrorf("get master view failed: %s", avID)
return &TxErr{code: TxErrHandleAttributeView, id: avID}
}
node, tree, _ := getNodeByBlockID(nil, operation.BlockID)
if nil == node {
logging.LogErrorf("get node by block ID [%s] failed", operation.BlockID)
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID}
}
visibleViewIDs := attrView.GetVisibleViewIDs(node.IALAttr(av.NodeAttrVisibleViewIDs))
visibleViewIDs = append(visibleViewIDs, operation.ID)
if err = freezeOtherAttrViewBlockVisibleViews(attrView, operation.BlockID, tree); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
attrs := parse.IAL2Map(node.KramdownIAL)
attrs[av.NodeAttrView] = operation.ID
attrs[av.NodeAttrVisibleViewIDs] = strings.Join(visibleViewIDs, ",")
node.AttributeViewType = string(masterView.LayoutType)
err = setNodeAttrs(node, tree, attrs)
if err != nil {
logging.LogWarnf("set node [%s] attrs failed: %s", operation.BlockID, err)
return
}
var view *av.View
switch masterView.LayoutType {
case av.LayoutTypeTable:
view = av.NewTableView()
case av.LayoutTypeGallery:
view = av.NewGalleryView()
case av.LayoutTypeKanban:
view = av.NewKanbanView()
}
view.ID = operation.ID
attrView.Views = append(attrView.Views, view)
view.Icon = masterView.Icon
view.Name = util.GetDuplicateName(masterView.Name)
view.HideAttrViewName = masterView.HideAttrViewName
view.Desc = masterView.Desc
view.LayoutType = masterView.LayoutType
view.PageSize = masterView.PageSize
view.Filters = av.CloneFilters(masterView.Filters)
for _, s := range masterView.Sorts {
view.Sorts = append(view.Sorts, &av.ViewSort{
Column: s.Column,
Order: s.Order,
DateEndpoint: s.DateEndpoint,
})
}
switch masterView.LayoutType {
case av.LayoutTypeTable:
for _, col := range masterView.Table.Columns {
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{
BaseField: &av.BaseField{
ID: col.ID,
Wrap: col.Wrap,
Hidden: col.Hidden,
Desc: col.Desc,
},
Pin: col.Pin,
Width: col.Width,
Align: col.Align,
Calc: col.Calc,
})
}
view.Table.ShowIcon = masterView.Table.ShowIcon
view.Table.WrapField = masterView.Table.WrapField
case av.LayoutTypeGallery:
for _, field := range masterView.Gallery.CardFields {
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{
BaseField: &av.BaseField{
ID: field.ID,
Wrap: field.Wrap,
Hidden: field.Hidden,
Desc: field.Desc,
},
FullRow: field.FullRow,
})
}
view.Gallery.CoverFrom = masterView.Gallery.CoverFrom
view.Gallery.CoverFromAssetKeyID = masterView.Gallery.CoverFromAssetKeyID
view.Gallery.CardAspectRatio = masterView.Gallery.CardAspectRatio
view.Gallery.CardAspectRatioValue = masterView.Gallery.CardAspectRatioValue
view.Gallery.CardSize = masterView.Gallery.CardSize
view.Gallery.CardWidth = masterView.Gallery.CardWidth
view.Gallery.CardLayout = masterView.Gallery.CardLayout
view.Gallery.FitImage = masterView.Gallery.FitImage
view.Gallery.DisplayFieldName = masterView.Gallery.DisplayFieldName
view.Gallery.DisplayEmptyFields = masterView.Gallery.DisplayEmptyFields
view.Gallery.ShowIcon = masterView.Gallery.ShowIcon
view.Gallery.WrapField = masterView.Gallery.WrapField
case av.LayoutTypeKanban:
for _, field := range masterView.Kanban.Fields {
view.Kanban.Fields = append(view.Kanban.Fields, &av.ViewKanbanField{
BaseField: &av.BaseField{
ID: field.ID,
Wrap: field.Wrap,
Hidden: field.Hidden,
Desc: field.Desc,
},
FullRow: field.FullRow,
})
}
view.Kanban.CoverFrom = masterView.Kanban.CoverFrom
view.Kanban.CoverFromAssetKeyID = masterView.Kanban.CoverFromAssetKeyID
view.Kanban.CardAspectRatio = masterView.Kanban.CardAspectRatio
view.Kanban.CardAspectRatioValue = masterView.Kanban.CardAspectRatioValue
view.Kanban.CardSize = masterView.Kanban.CardSize
view.Kanban.CardWidth = masterView.Kanban.CardWidth
view.Kanban.CardLayout = masterView.Kanban.CardLayout
view.Kanban.FitImage = masterView.Kanban.FitImage
view.Kanban.DisplayFieldName = masterView.Kanban.DisplayFieldName
view.Kanban.DisplayEmptyFields = masterView.Kanban.DisplayEmptyFields
view.Kanban.FillColBackgroundColor = masterView.Kanban.FillColBackgroundColor
view.Kanban.ShowIcon = masterView.Kanban.ShowIcon
view.Kanban.WrapField = masterView.Kanban.WrapField
}
view.ItemIDs = masterView.ItemIDs
if nil != masterView.Group {
view.Group = &av.ViewGroup{}
if copyErr := copier.Copy(view.Group, masterView.Group); nil != copyErr {
logging.LogErrorf("copy group failed: %s", copyErr)
return &TxErr{code: TxErrHandleAttributeView, id: avID, msg: copyErr.Error()}
}
view.GroupItemIDs = masterView.GroupItemIDs
regenAttrViewGroups(attrView)
}
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return &TxErr{code: TxErrHandleAttributeView, msg: err.Error(), id: avID}
}
return
}
func (tx *Transaction) doAddAttrViewView(operation *Operation) (ret *TxErr) {
err := addAttrViewView(operation.AvID, operation.ID, operation.BlockID, operation.Layout)
if nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func addAttrViewView(avID, viewID, blockID string, layout av.LayoutType) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
if 1 > len(attrView.Views) {
logging.LogErrorf("no view in attribute view [%s]", avID)
return
}
firstView := attrView.Views[0]
if nil == firstView {
logging.LogErrorf("get first view failed: %s", avID)
return
}
if "" == layout {
layout = av.LayoutTypeTable
}
var view *av.View
switch layout {
case av.LayoutTypeTable:
view = av.NewTableView()
switch firstView.LayoutType {
case av.LayoutTypeTable:
for _, col := range firstView.Table.Columns {
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{
BaseField: &av.BaseField{ID: col.ID}, Width: col.Width, Align: col.Align,
})
}
case av.LayoutTypeGallery:
for _, field := range firstView.Gallery.CardFields {
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: &av.BaseField{ID: field.ID}})
}
case av.LayoutTypeKanban:
for _, field := range firstView.Kanban.Fields {
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: &av.BaseField{ID: field.ID}})
}
}
case av.LayoutTypeGallery:
view = av.NewGalleryView()
switch firstView.LayoutType {
case av.LayoutTypeTable:
for _, col := range firstView.Table.Columns {
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: &av.BaseField{ID: col.ID}})
}
case av.LayoutTypeGallery:
for _, field := range firstView.Gallery.CardFields {
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: &av.BaseField{ID: field.ID}})
}
case av.LayoutTypeKanban:
for _, field := range firstView.Kanban.Fields {
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: &av.BaseField{ID: field.ID}})
}
}
case av.LayoutTypeKanban:
view = av.NewKanbanView()
switch firstView.LayoutType {
case av.LayoutTypeTable:
for _, col := range firstView.Table.Columns {
view.Kanban.Fields = append(view.Kanban.Fields, &av.ViewKanbanField{BaseField: &av.BaseField{ID: col.ID}})
}
case av.LayoutTypeGallery:
for _, field := range firstView.Gallery.CardFields {
view.Kanban.Fields = append(view.Kanban.Fields, &av.ViewKanbanField{BaseField: &av.BaseField{ID: field.ID}})
}
case av.LayoutTypeKanban:
for _, field := range firstView.Kanban.Fields {
view.Kanban.Fields = append(view.Kanban.Fields, &av.ViewKanbanField{BaseField: &av.BaseField{ID: field.ID}})
}
}
default:
err = av.ErrWrongLayoutType
logging.LogErrorf("wrong layout type [%s] for attribute view [%s]", layout, avID)
return
}
node, tree, _ := getNodeByBlockID(nil, blockID)
if nil == node {
logging.LogErrorf("get node by block ID [%s] failed", blockID)
return
}
visibleViewIDs := attrView.GetVisibleViewIDs(node.IALAttr(av.NodeAttrVisibleViewIDs))
visibleViewIDs = append(visibleViewIDs, viewID)
if err = freezeOtherAttrViewBlockVisibleViews(attrView, blockID, tree); nil != err {
return
}
view.ItemIDs = firstView.ItemIDs
view.ID = viewID
attrView.Views = append(attrView.Views, view)
if av.LayoutTypeKanban == layout {
preferredGroupKey := getKanbanPreferredGroupKey(attrView)
group := &av.ViewGroup{Field: preferredGroupKey.ID}
setAttributeViewGroup(attrView, view, group)
}
node.AttributeViewType = string(view.LayoutType)
attrs := parse.IAL2Map(node.KramdownIAL)
attrs[av.NodeAttrView] = viewID
attrs[av.NodeAttrVisibleViewIDs] = strings.Join(visibleViewIDs, ",")
err = setNodeAttrs(node, tree, attrs)
if err != nil {
logging.LogWarnf("set node [%s] attrs failed: %s", blockID, err)
return
}
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
return
}
func getKanbanPreferredGroupKey(attrView *av.AttributeView) (ret *av.Key) {
for _, kv := range attrView.KeyValues {
if av.KeyTypeSelect == kv.Key.Type {
ret = kv.Key
break
}
}
if nil == ret {
name := av.GetAttributeViewI18n("select")
ret = av.NewKey(ast.NewNodeID(), name, "", av.KeyTypeSelect)
attrView.KeyValues = append(attrView.KeyValues, &av.KeyValues{Key: ret})
for _, view := range attrView.Views {
newField := &av.BaseField{ID: ret.ID}
if nil != view.Table {
newField.Wrap = view.Table.WrapField
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: newField})
}
if nil != view.Gallery {
newField.Wrap = view.Gallery.WrapField
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: newField})
}
if nil != view.Kanban {
newField.Wrap = view.Kanban.WrapField
view.Kanban.Fields = append(view.Kanban.Fields, &av.ViewKanbanField{BaseField: newField})
}
}
}
return
}
func (tx *Transaction) doSetAttrViewViewName(operation *Operation) (ret *TxErr) {
var err error
avID := operation.AvID
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return &TxErr{code: TxErrHandleAttributeView, id: avID}
}
viewID := operation.ID
view := attrView.GetView(viewID)
if nil == view {
logging.LogErrorf("get view [%s] failed: %s", viewID, err)
return &TxErr{code: TxErrHandleAttributeView, id: viewID}
}
view.Name = strings.TrimSpace(operation.Data.(string))
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return &TxErr{code: TxErrHandleAttributeView, msg: err.Error(), id: avID}
}
return
}
func (tx *Transaction) doSetAttrViewViewIcon(operation *Operation) (ret *TxErr) {
var err error
avID := operation.AvID
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return &TxErr{code: TxErrHandleAttributeView, id: avID}
}
viewID := operation.ID
view := attrView.GetView(viewID)
if nil == view {
logging.LogErrorf("get view [%s] failed: %s", viewID, err)
return &TxErr{code: TxErrHandleAttributeView, id: viewID}
}
view.Icon = filterAttrViewIconValue(operation.Data.(string))
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return &TxErr{code: TxErrHandleAttributeView, msg: err.Error(), id: avID}
}
return
}
// filterAttrViewIconValue 过滤属性视图图标值,非法值置空,防止存储可执行标记
// https://github.com/siyuan-note/siyuan/security/advisories/GHSA-vx5w-qrvp-mmcq
func filterAttrViewIconValue(icon string) string {
if filtered, valid := util.FilterIconValue(icon); valid {
return filtered
}
return ""
}
func (tx *Transaction) doSetAttrViewViewDesc(operation *Operation) (ret *TxErr) {
var err error
avID := operation.AvID
attrView, err := av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return &TxErr{code: TxErrHandleAttributeView, id: avID}
}
viewID := operation.ID
view := attrView.GetView(viewID)
if nil == view {
logging.LogErrorf("get view [%s] failed: %s", viewID, err)
return &TxErr{code: TxErrHandleAttributeView, id: viewID}
}
view.Desc = strings.TrimSpace(operation.Data.(string))
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return &TxErr{code: TxErrHandleAttributeView, msg: err.Error(), id: avID}
}
return
}
func (tx *Transaction) doSetAttrViewName(operation *Operation) (ret *TxErr) {
err := tx.setAttributeViewName(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func (tx *Transaction) doSetAttrViewNewItemTemplates(operation *Operation) (ret *TxErr) {
attrView, err := av.ParseAttributeView(operation.AvID)
if nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
data, err := gulu.JSON.MarshalJSON(operation.Data)
if nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
config := &av.NewItemTemplatesConfig{}
if err = gulu.JSON.UnmarshalJSON(data, config); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
if err = attrView.SetNewItemTemplates(config); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
if err = av.SaveAttributeView(attrView); nil != err {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
ReloadAttrView(operation.AvID)
return
}
const attrAvNameTpl = `${avName}`
func (tx *Transaction) setAttributeViewName(operation *Operation) (err error) {
avID := operation.ID
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
attrView.Name = strings.TrimSpace(operation.Data.(string))
attrView.Name = strings.ReplaceAll(attrView.Name, "\n", " ")
if 512 < utf8.RuneCountInString(attrView.Name) {
attrView.Name = gulu.Str.SubStr(attrView.Name, 512)
}
err = av.SaveAttributeView(attrView)
_, nodes := tx.getAttrViewBoundNodes(attrView)
for _, node := range nodes {
avNames := getAvNames(node.IALAttr(av.NodeAttrNameAvs))
oldAttrs := parse.IAL2Map(node.KramdownIAL)
node.SetIALAttr(av.NodeAttrViewNames, avNames)
pushBlockAttrs(oldAttrs, node)
}
return
}
func getAvNames(avIDs string) (ret string) {
if "" == avIDs {
return
}
avNames := bytes.Buffer{}
nodeAvIDs := strings.SplitSeq(avIDs, ",")
for nodeAvID := range nodeAvIDs {
nodeAvName, getErr := av.GetAttributeViewName(nodeAvID)
if nil != getErr {
continue
}
if "" == nodeAvName {
nodeAvName = Conf.language(105)
}
tpl := strings.ReplaceAll(attrAvNameTpl, "${avID}", nodeAvID)
tpl = strings.ReplaceAll(tpl, "${avName}", util.EscapeHTML(nodeAvName))
avNames.WriteString(tpl)
avNames.WriteString(" ")
}
if 0 < avNames.Len() {
avNames.Truncate(avNames.Len() - 6)
ret = avNames.String()
}
return
}
func (tx *Transaction) getAttrViewBoundNodes(attrView *av.AttributeView) (trees map[string]*parse.Tree, nodes []*ast.Node) {
blockKeyValues := attrView.GetBlockKeyValues()
if nil == blockKeyValues || nil == blockKeyValues.Values {
return
}
trees = map[string]*parse.Tree{}
for _, blockKeyValue := range blockKeyValues.Values {
if blockKeyValue.IsDetached || nil == blockKeyValue.Block {
continue
}
blockID := blockKeyValue.Block.ID
if "" == blockID {
continue
}
var tree *parse.Tree
tree = trees[blockID]
if nil == tree {
if nil == tx {
tree, _ = LoadTreeByBlockID(blockID)
} else {
tree, _ = tx.loadTree(blockID)
}
}
if nil == tree {
continue
}
trees[blockID] = tree
node := treenode.GetNodeInTree(tree, blockID)
if nil == node {
continue
}
nodes = append(nodes, node)
}
return
}
func (tx *Transaction) doSetAttrViewFilters(operation *Operation) (ret *TxErr) {
err := SetAttrViewFilters(operation.AvID, operation.BlockID, operation.Data.([]any))
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func (tx *Transaction) doSetAttrViewColRelationFilters(operation *Operation) (ret *TxErr) {
err := setAttrViewColFilters(operation.AvID, operation.BlockID, operation.KeyID, operation.Data.([]any), true)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func (tx *Transaction) doSetAttrViewColRollupFilters(operation *Operation) (ret *TxErr) {
err := setAttrViewColFilters(operation.AvID, operation.BlockID, operation.KeyID, operation.Data.([]any), false)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
// avParseView 根据 blockID 推导 box 上下文,使用 box-aware 或全局 AV 解析。
func avParseView(avID, blockID string) (*av.AttributeView, error) {
boxID := deriveAVBoxID(blockID)
if boxID != "" {
return av.ParseAttributeViewInBox(avID, boxID)
}
return av.ParseAttributeView(avID)
}
// avSaveView 根据 blockID 推导 box 上下文,使用 box-aware 或全局 AV 保存。
func avSaveView(attrView *av.AttributeView, blockID string) error {
boxID := deriveAVBoxID(blockID)
if boxID != "" {
_, parseErr := av.ParseAttributeViewInBox(attrView.ID, boxID)
if parseErr == nil {
return av.SaveAttributeView(attrView)
}
}
return av.SaveAttributeView(attrView)
}
// deriveAVBoxID 通过 blockID 反查所在 box。blockID 为空或不是加密 box 时返回空串。
func deriveAVBoxID(blockID string) string {
if blockID == "" {
return ""
}
bt := treenode.GetBlockTree(blockID)
if bt == nil {
for _, encBoxID := range treenode.GetOpenedEncryptedBoxIDs() {
if encBT := treenode.GetBlockTreeInBox(blockID, encBoxID); encBT != nil {
bt = encBT
break
}
}
}
if bt == nil || !IsEncryptedBox(bt.BoxID) {
return ""
}
return bt.BoxID
}
// SetAttrViewFilters 用新的过滤规则数组整体替换指定视图的过滤规则,并持久化。
// data 为 JSON 反序列化前的 []any(通常是前端传来的过滤节点树)。
func SetAttrViewFilters(avID, blockID string, data []any) (err error) {
attrView, err := avParseView(avID, blockID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return
}
jsonData, err := gulu.JSON.MarshalJSON(data)
if err != nil {
return
}
var newFilters []*av.ViewFilter
if err = gulu.JSON.UnmarshalJSON(jsonData, &newFilters); err != nil {
return
}
view.Filters = newFilters
// 归一化为单一根组:spec 5 起顶层应为一个根组。
// 兜底旧前端/异常数据发来的扁平叶子数组,在内存里包成 AND 根组后再持久化。
if 1 != len(view.Filters) || nil == view.Filters[0] || !view.Filters[0].IsGroup() {
view.Filters = []*av.ViewFilter{{Combination: av.FilterCombinationAnd, Filters: view.Filters}}
}
// 限制筛选嵌套深度,防止异常数据创建过深的嵌套分组。
if err = av.ValidateFilterDepth(view.Filters); nil != err {
return
}
err = avSaveView(attrView, blockID)
return
}
func setAttrViewColFilters(avID, blockID, keyID string, data []any, relation bool) (err error) {
attrView, err := avParseView(avID, blockID)
if err != nil {
return
}
key, _ := attrView.GetKey(keyID)
if nil == key {
return av.ErrKeyNotFound
}
destAvID := ""
if relation {
if av.KeyTypeRelation != key.Type || nil == key.Relation {
return av.ErrKeyNotFound
}
destAvID = key.Relation.AvID
} else {
if av.KeyTypeRollup != key.Type || nil == key.Rollup {
return av.ErrKeyNotFound
}
relationKey, _ := attrView.GetKey(key.Rollup.RelationKeyID)
if nil == relationKey || nil == relationKey.Relation {
return av.ErrKeyNotFound
}
destAvID = relationKey.Relation.AvID
}
destAttrView := attrView
if destAvID != avID {
destAttrView, err = av.ParseAttributeView(destAvID)
if err != nil {
return
}
}
jsonData, err := gulu.JSON.MarshalJSON(data)
if err != nil {
return
}
var filters []*av.ViewFilter
if err = gulu.JSON.UnmarshalJSON(jsonData, &filters); err != nil {
return
}
if 0 < len(filters) && (1 != len(filters) || nil == filters[0] || !filters[0].IsGroup()) {
filters = []*av.ViewFilter{{Combination: av.FilterCombinationAnd, Filters: filters}}
}
if err = av.ValidateFilterDepth(filters); nil != err {
return
}
validColumns := map[string]bool{}
for _, keyValues := range destAttrView.KeyValues {
if nil != keyValues && nil != keyValues.Key {
validColumns[keyValues.Key.ID] = true
}
}
filters, _ = av.PruneInvalidColumnFilters(filters, validColumns)
if !hasAttrViewFilterConditions(filters) {
filters = nil
}
if relation {
key.Relation.CandidateFilters = filters
} else {
key.Rollup.Filters = filters
}
err = avSaveView(attrView, blockID)
return
}
func hasAttrViewFilterConditions(filters []*av.ViewFilter) bool {
for _, filter := range filters {
if nil == filter {
continue
}
if filter.IsGroup() {
if hasAttrViewFilterConditions(filter.Filters) {
return true
}
continue
}
return true
}
return false
}
func attrViewFiltersContainColumn(filters []*av.ViewFilter, column string) bool {
for _, filter := range filters {
if nil == filter {
continue
}
if filter.IsGroup() {
if attrViewFiltersContainColumn(filter.Filters, column) {
return true
}
continue
}
if filter.Column == column {
return true
}
}
return false
}
func attrViewFiltersContainOption(filters []*av.ViewFilter, column, optionContent string) bool {
for _, filter := range filters {
if nil == filter {
continue
}
if filter.IsGroup() {
if attrViewFiltersContainOption(filter.Filters, column, optionContent) {
return true
}
continue
}
if filter.Column != column || nil == filter.Value {
continue
}
for _, option := range filter.Value.MSelect {
if nil != option && option.Content == optionContent {
return true
}
}
}
return false
}
func removeAttrViewColumnFromFieldFilters(attrView *av.AttributeView, targetAvID, column string) (changed bool) {
for _, keyValues := range attrView.KeyValues {
if nil == keyValues || nil == keyValues.Key {
continue
}
key := keyValues.Key
if av.KeyTypeRelation == key.Type && nil != key.Relation && key.Relation.AvID == targetAvID &&
attrViewFiltersContainColumn(key.Relation.CandidateFilters, column) {
key.Relation.CandidateFilters = av.RemoveFiltersByColumn(key.Relation.CandidateFilters, column)
if !hasAttrViewFilterConditions(key.Relation.CandidateFilters) {
key.Relation.CandidateFilters = nil
}
changed = true
continue
}
if av.KeyTypeRollup != key.Type || nil == key.Rollup ||
!attrViewFiltersContainColumn(key.Rollup.Filters, column) {
continue
}
relationKey, _ := attrView.GetKey(key.Rollup.RelationKeyID)
if nil == relationKey || nil == relationKey.Relation || relationKey.Relation.AvID != targetAvID {
continue
}
key.Rollup.Filters = av.RemoveFiltersByColumn(key.Rollup.Filters, column)
if !hasAttrViewFilterConditions(key.Rollup.Filters) {
key.Rollup.Filters = nil
}
changed = true
}
return
}
func removeAttrViewOptionFromFieldFilters(attrView *av.AttributeView, targetAvID, column,
optionContent string) (changed bool) {
for _, keyValues := range attrView.KeyValues {
if nil == keyValues || nil == keyValues.Key {
continue
}
key := keyValues.Key
if av.KeyTypeRelation == key.Type && nil != key.Relation && key.Relation.AvID == targetAvID &&
attrViewFiltersContainOption(key.Relation.CandidateFilters, column, optionContent) {
key.Relation.CandidateFilters = av.RemoveSelectOptionFromFilters(
key.Relation.CandidateFilters, column, optionContent)
if !hasAttrViewFilterConditions(key.Relation.CandidateFilters) {
key.Relation.CandidateFilters = nil
}
changed = true
continue
}
if av.KeyTypeRollup != key.Type || nil == key.Rollup ||
!attrViewFiltersContainOption(key.Rollup.Filters, column, optionContent) {
continue
}
relationKey, _ := attrView.GetKey(key.Rollup.RelationKeyID)
if nil == relationKey || nil == relationKey.Relation || relationKey.Relation.AvID != targetAvID {
continue
}
key.Rollup.Filters = av.RemoveSelectOptionFromFilters(key.Rollup.Filters, column, optionContent)
if !hasAttrViewFilterConditions(key.Rollup.Filters) {
key.Rollup.Filters = nil
}
changed = true
}
return
}
func renameAttrViewOptionInFieldFilters(attrView *av.AttributeView, targetAvID, column,
oldContent, newContent, newColor string) (changed bool) {
for _, keyValues := range attrView.KeyValues {
if nil == keyValues || nil == keyValues.Key {
continue
}
key := keyValues.Key
if av.KeyTypeRelation == key.Type && nil != key.Relation && key.Relation.AvID == targetAvID &&
attrViewFiltersContainOption(key.Relation.CandidateFilters, column, oldContent) {
av.RenameSelectOptionInFilters(key.Relation.CandidateFilters, column, oldContent, newContent, newColor)
changed = true
continue
}
if av.KeyTypeRollup != key.Type || nil == key.Rollup ||
!attrViewFiltersContainOption(key.Rollup.Filters, column, oldContent) {
continue
}
relationKey, _ := attrView.GetKey(key.Rollup.RelationKeyID)
if nil == relationKey || nil == relationKey.Relation || relationKey.Relation.AvID != targetAvID {
continue
}
av.RenameSelectOptionInFilters(key.Rollup.Filters, column, oldContent, newContent, newColor)
changed = true
}
return
}
func (tx *Transaction) doSetAttrViewSorts(operation *Operation) (ret *TxErr) {
err := SetAttrViewSorts(operation.AvID, operation.BlockID, operation.Data.([]any))
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
// SetAttrViewSorts 用新的排序规则数组整体替换指定视图的排序规则,并持久化。
// data 为 JSON 反序列化前的 []any。
func SetAttrViewSorts(avID, blockID string, data []any) (err error) {
attrView, err := avParseView(avID, blockID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return
}
jsonData, err := gulu.JSON.MarshalJSON(data)
if err != nil {
return
}
var newSorts []*av.ViewSort
if err = gulu.JSON.UnmarshalJSON(jsonData, &newSorts); err != nil {
return
}
view.Sorts = newSorts
err = avSaveView(attrView, blockID)
return
}
func (tx *Transaction) doSetAttrViewPageSize(operation *Operation) (ret *TxErr) {
err := setAttributeViewPageSize(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewPageSize(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
view.PageSize = int(operation.Data.(float64))
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewColCalc(operation *Operation) (ret *TxErr) {
err := setAttributeViewColumnCalc(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColumnCalc(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
operationData := operation.Data.(any)
data, err := gulu.JSON.MarshalJSON(operationData)
if err != nil {
return
}
calc := &av.FieldCalc{}
switch view.LayoutType {
case av.LayoutTypeTable:
if err = gulu.JSON.UnmarshalJSON(data, calc); err != nil {
return
}
for _, column := range view.Table.Columns {
if column.ID == operation.ID {
column.Calc = calc
break
}
}
case av.LayoutTypeGallery, av.LayoutTypeKanban:
return
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doInsertAttrViewBlock(operation *Operation) (ret *TxErr) {
result, err := addAttributeViewBlocks(tx, operation.Srcs, operation.AvID, operation.BlockID, operation.ViewID, operation.GroupID, operation.PreviousID, operation.IgnoreDefaultFill)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
operation.RetData = result
return
}
func AddAttributeViewBlock(tx *Transaction, srcs []map[string]any, avID, dbBlockID, viewID, groupID, previousItemID string, ignoreDefaultFill bool) (err error) {
_, err = addAttributeViewBlocks(tx, srcs, avID, dbBlockID, viewID, groupID, previousItemID, ignoreDefaultFill)
return
}
type insertAttrViewBlockResult struct {
InsertedItemIDs []string `json:"insertedItemIDs"`
ExistingItemIDs []string `json:"existingItemIDs"`
}
func addAttributeViewBlocks(tx *Transaction, srcs []map[string]any, avID, dbBlockID, viewID, groupID, previousItemID string, ignoreDefaultFill bool) (result *insertAttrViewBlockResult, err error) {
result = &insertAttrViewBlockResult{}
if 0 == len(srcs) {
return
}
slices.Reverse(srcs) // https://github.com/siyuan-note/siyuan/issues/11286
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
now := time.Now().UnixMilli()
for _, src := range srcs {
boundBlockID := ""
srcItemID := ast.NewNodeID()
if nil != src["itemID"] {
srcItemID = src["itemID"].(string)
}
isDetached := src["isDetached"].(bool)
var tree *parse.Tree
if !isDetached {
boundBlockID = src["id"].(string)
if !ast.IsNodeIDPattern(boundBlockID) {
continue
}
var loadErr error
if nil != tx {
tree, loadErr = tx.loadTree(boundBlockID)
} else {
tree, loadErr = LoadTreeByBlockID(boundBlockID)
}
if nil != loadErr {
logging.LogErrorf("load tree [%s] failed: %s", boundBlockID, loadErr)
err = loadErr
return
}
}
var srcContent string
if nil != src["content"] {
srcContent = src["content"].(string)
}
if avErr := addAttributeViewBlock0(attrView, now, avID, dbBlockID, viewID, groupID, previousItemID, srcItemID, boundBlockID, srcContent, src, isDetached, ignoreDefaultFill, tree, tx, result); nil != avErr {
err = avErr
return
}
}
regenAttrViewGroups(attrView)
err = av.SaveAttributeView(attrView)
return
}
func addAttributeViewBlock(now int64, avID, dbBlockID, viewID, groupID, previousItemID, addingItemID, addingBoundBlockID, addingBlockContent string, src map[string]any, isDetached, ignoreDefaultFill bool, tree *parse.Tree, tx *Transaction, result *insertAttrViewBlockResult) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
if err = addAttributeViewBlock0(attrView, now, avID, dbBlockID, viewID, groupID, previousItemID, addingItemID, addingBoundBlockID, addingBlockContent, src, isDetached, ignoreDefaultFill, tree, tx, result); nil != err {
return
}
regenAttrViewGroups(attrView)
err = av.SaveAttributeView(attrView)
return
}
func addAttributeViewBlock0(attrView *av.AttributeView, now int64, avID, dbBlockID, viewID, groupID, previousItemID, addingItemID, addingBoundBlockID, addingBlockContent string, src map[string]any, isDetached, ignoreDefaultFill bool, tree *parse.Tree, tx *Transaction, result *insertAttrViewBlockResult) (err error) {
var node *ast.Node
if !isDetached {
node = treenode.GetNodeInTree(tree, addingBoundBlockID)
if nil == node {
err = ErrBlockNotFound
return
}
} else {
if "" == addingItemID {
addingItemID = ast.NewNodeID()
logging.LogWarnf("detached block id is empty, generate a new one [%s]", addingItemID)
}
}
var blockIcon string
if !isDetached {
blockIcon, addingBlockContent = getNodeAvBlockText(node, "")
addingBlockContent = util.UnescapeHTML(addingBlockContent)
}
// 检查是否重复添加相同的块
blockValues := attrView.GetBlockKeyValues()
if nil == blockValues || nil == blockValues.Key {
return fmt.Errorf("attribute view [%s] has no block key", avID)
}
for _, blockValue := range blockValues.Values {
if nil == blockValue || nil == blockValue.Block {
continue
}
if "" != addingBoundBlockID && blockValue.Block.ID == addingBoundBlockID {
if !isDetached {
// 重复绑定一下,比如剪切数据库块、取消绑定块后再次添加的场景需要
bindBlockAv0(tx, avID, node, tree)
blockValue.IsDetached = isDetached
blockValue.Block.Icon = blockIcon
blockValue.Block.Content = addingBlockContent
blockValue.UpdatedAt = now
}
msg := fmt.Sprintf(Conf.language(269), util.EscapeHTML(getAttrViewName(attrView)))
util.PushMsg(msg, 5000)
src["itemID"] = blockValue.BlockID
if nil == err {
result.ExistingItemIDs = append(result.ExistingItemIDs, blockValue.BlockID)
}
return
}
}
blockValue := &av.Value{
ID: ast.NewNodeID(),
KeyID: blockValues.Key.ID,
BlockID: addingItemID,
Type: av.KeyTypeBlock,
IsDetached: isDetached,
CreatedAt: now,
UpdatedAt: now,
Block: &av.ValueBlock{Icon: blockIcon, Content: addingBlockContent, Created: now, Updated: now}}
if !isDetached {
blockValue.Block.ID = addingBoundBlockID
}
blockValues.Values = append(blockValues.Values, blockValue)
view, err := getAttrViewViewByBlockID(attrView, dbBlockID)
if nil != err {
logging.LogErrorf("get view by block ID [%s] failed: %s", dbBlockID, err)
return
}
if "" != viewID {
view = attrView.GetView(viewID)
if nil == view {
logging.LogErrorf("get view by view ID [%s] failed", viewID)
return av.ErrViewNotFound
}
}
groupView := view
if "" != groupID {
groupView = view.GetGroupByID(groupID)
}
useGroupDefault := "" != groupID
if !ignoreDefaultFill {
fillDefaultValue(attrView, view, groupView, previousItemID, addingItemID, true, useGroupDefault)
}
// 处理日期字段默认填充当前创建时间
// The database date field supports filling the current time by default https://github.com/siyuan-note/siyuan/issues/10823
fillAttrViewAutoFillNowValues(attrView, addingItemID, isDetached, now)
if !isDetached {
bindBlockAv0(tx, avID, node, tree)
}
// 在所有视图上添加项目
for _, v := range attrView.Views {
if "" != previousItemID {
changed := false
for i, id := range v.ItemIDs {
if id == previousItemID {
v.ItemIDs = append(v.ItemIDs[:i+1], append([]string{addingItemID}, v.ItemIDs[i+1:]...)...)
changed = true
break
}
}
if !changed {
v.ItemIDs = append(v.ItemIDs, addingItemID)
}
} else {
v.ItemIDs = append([]string{addingItemID}, v.ItemIDs...)
}
// 在所有分组视图中添加,目的是为了在重新分组的过程中保住排序状态 https://github.com/siyuan-note/siyuan/issues/15560
for _, g := range v.Groups {
if "" != previousItemID {
changed := false
for i, id := range g.GroupItemIDs {
if id == previousItemID {
g.GroupItemIDs = append(g.GroupItemIDs[:i+1], append([]string{addingItemID}, g.GroupItemIDs[i+1:]...)...)
changed = true
break
}
}
if !changed {
g.GroupItemIDs = append(g.GroupItemIDs, addingItemID)
}
} else {
g.GroupItemIDs = append([]string{addingItemID}, g.GroupItemIDs...)
}
}
}
result.InsertedItemIDs = append(result.InsertedItemIDs, addingItemID)
return
}
func fillAttrViewAutoFillNowValues(attrView *av.AttributeView, addingItemID string, isDetached bool, now int64) {
for _, keyValues := range attrView.KeyValues {
if av.KeyTypeDate == keyValues.Key.Type && nil != keyValues.Key.Date && keyValues.Key.Date.AutoFillNow {
val := keyValues.GetValue(addingItemID)
if nil == val {
dateVal := &av.Value{
ID: ast.NewNodeID(), KeyID: keyValues.Key.ID, BlockID: addingItemID, Type: av.KeyTypeDate, IsDetached: isDetached, CreatedAt: now, UpdatedAt: now + 1000,
Date: &av.ValueDate{Content: now, IsNotEmpty: true, IsNotTime: !keyValues.Key.Date.FillSpecificTime},
}
keyValues.Values = append(keyValues.Values, dateVal)
} else {
if nil == val.Date {
val.Date = &av.ValueDate{}
}
val.CreatedAt, val.UpdatedAt = now, now+1000
val.Date.Content, val.Date.IsNotEmpty, val.Date.IsNotTime = now, true, !keyValues.Key.Date.FillSpecificTime
val.IsRenderAutoFill = false
}
}
}
}
func fillDefaultValue(attrView *av.AttributeView, view, groupView *av.View, previousItemID, addingItemID string, isCreate, useGroupDefault bool) {
defaultValues := getAttrViewAddingBlockDefaultValues(attrView, view, groupView, previousItemID, addingItemID, isCreate, useGroupDefault)
for keyID, newValue := range defaultValues {
newValue.BlockID = addingItemID
keyValues, getErr := attrView.GetKeyValues(keyID)
if nil != getErr {
continue
}
if av.KeyTypeRollup == newValue.Type {
// 汇总字段的值是渲染时计算的,不需要添加到数据存储中
continue
}
if (av.KeyTypeSelect == newValue.Type || av.KeyTypeMSelect == newValue.Type) && 1 > len(newValue.MSelect) && groupValueDefault != groupView.GetGroupValue() {
// 单选或多选类型的值可能需要从分组条件中获取默认值
if opt := keyValues.Key.GetOption(groupView.GetGroupValue()); nil != opt {
newValue.MSelect = append(newValue.MSelect, &av.ValueSelect{Content: opt.Name, Color: opt.Color})
}
}
if av.KeyTypeRelation == newValue.Type && nil != keyValues.Key.Relation && keyValues.Key.Relation.IsTwoWay {
// 双向关联需要同时更新目标字段的值
updateTwoWayRelationDestAttrView(attrView, keyValues.Key, newValue, 1, []string{})
}
existingVal := keyValues.GetValue(addingItemID)
if nil == existingVal {
newValue.IsRenderAutoFill = false
keyValues.Values = append(keyValues.Values, newValue)
} else {
newValueRaw := newValue.GetValByType(keyValues.Key.Type)
if av.KeyTypeBlock != existingVal.Type || (av.KeyTypeBlock == existingVal.Type && existingVal.IsDetached) {
// 非主键的值直接覆盖,主键的值只覆盖非绑定块
existingVal.IsRenderAutoFill = false
existingVal.SetValByType(keyValues.Key.Type, newValueRaw)
}
}
}
}
func getNewValueByNearItem(nearItem av.Item, key *av.Key, addingBlockID string) (ret *av.Value) {
if nil == nearItem {
return
}
defaultVal := nearItem.GetValue(key.ID)
if nil == defaultVal {
return
}
ret = defaultVal.Clone()
if nil == ret {
return
}
ret.ID = ast.NewNodeID()
ret.KeyID = key.ID
ret.BlockID = addingBlockID
ret.CreatedAt = util.CurrentTimeMillis()
ret.UpdatedAt = ret.CreatedAt + 1000
return
}
func getNearItem(attrView *av.AttributeView, view, groupView *av.View, previousItemID string) (ret av.Item) {
cachedAttrViews := map[string]*av.AttributeView{}
rollupFurtherCollections := sql.GetFurtherCollections(attrView, cachedAttrViews)
viewable := sql.RenderGroupView(attrView, view, groupView, "")
av.Filter(viewable, attrView, rollupFurtherCollections, cachedAttrViews)
av.Sort(viewable, attrView)
items := viewable.(av.Collection).GetItems()
if 0 < len(items) {
if "" != previousItemID {
for _, row := range items {
if row.GetID() == previousItemID {
ret = row
return
}
}
} else {
if 0 < len(items) {
ret = items[0]
return
}
}
}
return
}
func (tx *Transaction) doRemoveAttrViewBlock(operation *Operation) (ret *TxErr) {
err := removeAttributeViewBlock(operation.SrcIDs, operation.AvID, tx)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID}
}
return
}
func RemoveAttributeViewBlock(srcIDs []string, avID string) (err error) {
err = removeAttributeViewBlock(srcIDs, avID, nil)
return
}
func removeAttributeViewBlock(srcIDs []string, avID string, tx *Transaction) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
relationDestAvIDs := map[string]bool{}
for _, keyValues := range attrView.KeyValues {
if av.KeyTypeRelation != keyValues.Key.Type || nil == keyValues.Key.Relation {
continue
}
for _, value := range keyValues.Values {
if gulu.Str.Contains(value.BlockID, srcIDs) {
relationDestAvIDs[keyValues.Key.Relation.AvID] = true
break
}
}
}
trees := map[string]*parse.Tree{}
for _, keyValues := range attrView.KeyValues {
tmp := keyValues.Values[:0]
for i, val := range keyValues.Values {
if !gulu.Str.Contains(val.BlockID, srcIDs) {
tmp = append(tmp, keyValues.Values[i])
} else {
// Remove av block also remove node attr https://github.com/siyuan-note/siyuan/issues/9091#issuecomment-1709824006
if !val.IsDetached && nil != val.Block {
bt := treenode.GetBlockTree(val.Block.ID)
if nil == bt {
for _, encBoxID := range treenode.GetOpenedEncryptedBoxIDs() {
if encBT := treenode.GetBlockTreeInBox(val.Block.ID, encBoxID); nil != encBT {
bt = encBT
break
}
}
}
if nil != bt {
tree := trees[bt.RootID]
if nil == tree {
tree, _ = LoadTreeByBlockID(val.Block.ID)
}
if nil != tree {
trees[bt.RootID] = tree
if node := treenode.GetNodeInTree(tree, val.Block.ID); nil != node {
if err = removeNodeAvID(node, avID, tx, tree); err != nil {
return
}
}
}
}
}
}
}
keyValues.Values = tmp
}
for _, view := range attrView.Views {
for _, blockID := range srcIDs {
view.ItemIDs = gulu.Str.RemoveElem(view.ItemIDs, blockID)
}
}
for _, itemID := range srcIDs {
attrView.RemoveCardCoverPositions(itemID)
}
attrView.RemoveNewItemTemplateRelationItems(avID, srcIDs)
attrView.RemoveRelationFilterItems(avID, srcIDs)
regenAttrViewGroups(attrView)
err = av.SaveAttributeView(attrView)
if nil != err {
return
}
if err = removeRelatedRelationItems(avID, srcIDs); nil != err {
return
}
refreshRelatedSrcAvs(avID, tx)
for destAvID := range relationDestAvIDs {
if "" != destAvID && destAvID != avID {
ReloadAttrView(destAvID)
}
}
historyDir, err := getHistoryDir(HistoryOpUpdate)
if err != nil {
logging.LogErrorf("get history dir failed: %s", err)
return
}
blockIDs := treenode.GetMirrorAttrViewBlockIDs(avID)
for _, blockID := range blockIDs {
tree := trees[blockID]
if nil == tree {
tree, _ = LoadTreeByBlockID(blockID)
}
if nil == tree {
continue
}
historyPath := filepath.Join(historyDir, tree.Box, tree.Path)
absPath := filepath.Join(util.DataDir, tree.Box, tree.Path)
if err = filelock.Copy(absPath, historyPath); err != nil {
logging.LogErrorf("backup [path=%s] to history [%s] failed: %s", absPath, historyPath, err)
return
}
}
srcAvPath, _ := av.FindAttributeViewPath(avID)
if srcAvPath == "" {
return
}
destAvPath := filepath.Join(historyDir, "storage", "av", avID+".json")
if copyErr := filelock.Copy(srcAvPath, destAvPath); nil != copyErr {
logging.LogErrorf("copy av [%s] failed: %s", srcAvPath, copyErr)
}
indexHistoryDir(filepath.Base(historyDir), util.NewLute())
return
}
func removeRelatedRelationItems(avID string, itemIDs []string) (err error) {
for _, relatedAvID := range av.GetSrcAvIDs(avID) {
if relatedAvID == avID {
continue
}
relatedAv, parseErr := av.ParseAttributeView(relatedAvID)
if nil != parseErr || nil == relatedAv {
continue
}
templateChanged := relatedAv.RemoveNewItemTemplateRelationItems(avID, itemIDs)
filterChanged := relatedAv.RemoveRelationFilterItems(avID, itemIDs)
if !templateChanged && !filterChanged {
continue
}
if err = av.SaveAttributeView(relatedAv); nil != err {
return
}
ReloadAttrView(relatedAvID)
}
return
}
func removeNodeAvID(node *ast.Node, avID string, tx *Transaction, tree *parse.Tree) (err error) {
attrs := removeNodeAvIDAttrs(node, avID)
if nil != tx {
if err = setNodeAttrsWithTx(tx, node, tree, attrs); err != nil {
return
}
} else {
if err = setNodeAttrs(node, tree, attrs); err != nil {
return
}
}
return
}
func removeNodeAvIDAttrs(node *ast.Node, avID string) map[string]string {
attrs := parse.IAL2Map(node.KramdownIAL)
if avs := attrs[av.NodeAttrNameAvs]; "" != avs {
avIDs := strings.Split(avs, ",")
avIDs = gulu.Str.RemoveElem(avIDs, avID)
var existAvIDs []string
for _, attributeViewID := range avIDs {
if av.IsAttributeViewExist(attributeViewID) {
existAvIDs = append(existAvIDs, attributeViewID)
}
}
avIDs = existAvIDs
if 0 == len(avIDs) {
attrs[av.NodeAttrNameAvs] = ""
} else {
attrs[av.NodeAttrNameAvs] = strings.Join(avIDs, ",")
node.SetIALAttr(av.NodeAttrNameAvs, strings.Join(avIDs, ","))
avNames := getAvNames(node.IALAttr(av.NodeAttrNameAvs))
attrs[av.NodeAttrViewNames] = avNames
}
}
return attrs
}
func (tx *Transaction) doDuplicateAttrViewKey(operation *Operation) (ret *TxErr) {
err := duplicateAttributeViewKey(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func duplicateAttributeViewKey(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
key, _ := attrView.GetKey(operation.KeyID)
if nil == key {
return
}
if av.KeyTypeBlock == key.Type || av.KeyTypeRelation == key.Type {
return
}
copyKey := &av.Key{}
if err = copier.Copy(copyKey, key); err != nil {
logging.LogErrorf("clone key failed: %s", err)
}
copyKey.ID = operation.NextID
copyKey.Name = util.GetDuplicateName(key.Name)
attrView.KeyValues = append(attrView.KeyValues, &av.KeyValues{Key: copyKey})
for _, view := range attrView.Views {
switch view.LayoutType {
case av.LayoutTypeTable:
for i, column := range view.Table.Columns {
if column.ID == key.ID {
view.Table.Columns = append(view.Table.Columns[:i+1], append([]*av.ViewTableColumn{
{
BaseField: &av.BaseField{
ID: copyKey.ID,
Wrap: column.Wrap,
Hidden: column.Hidden,
Desc: column.Desc,
},
Pin: column.Pin,
Width: column.Width,
Align: column.Align,
},
}, view.Table.Columns[i+1:]...)...)
break
}
}
case av.LayoutTypeGallery:
for i, field := range view.Gallery.CardFields {
if field.ID == key.ID {
view.Gallery.CardFields = append(view.Gallery.CardFields[:i+1], append([]*av.ViewGalleryCardField{
{
BaseField: &av.BaseField{
ID: copyKey.ID,
Wrap: field.Wrap,
Hidden: field.Hidden,
Desc: field.Desc,
},
FullRow: field.FullRow,
},
}, view.Gallery.CardFields[i+1:]...)...)
break
}
}
case av.LayoutTypeKanban:
for i, field := range view.Kanban.Fields {
if field.ID == key.ID {
view.Kanban.Fields = append(view.Kanban.Fields[:i+1], append([]*av.ViewKanbanField{
{
BaseField: &av.BaseField{
ID: copyKey.ID,
Wrap: field.Wrap,
Hidden: field.Hidden,
Desc: field.Desc,
},
FullRow: field.FullRow,
},
}, view.Kanban.Fields[i+1:]...)...)
break
}
}
}
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewColumnWidth(operation *Operation) (ret *TxErr) {
err := setAttributeViewColWidth(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColWidth(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
for _, column := range view.Table.Columns {
if column.ID == operation.ID {
column.Width = av.FilterWidthValue(operation.Data.(string))
break
}
}
case av.LayoutTypeGallery, av.LayoutTypeKanban:
return
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewColumnsWidth(operation *Operation) (ret *TxErr) {
err := setAttributeViewColsWidth(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColsWidth(operation *Operation) (err error) {
widthData, ok := operation.Data.(map[string]any)
if !ok {
return fmt.Errorf("invalid attribute view column widths")
}
widths := map[string]string{}
for id, value := range widthData {
width, valueOK := value.(string)
if !valueOK {
return fmt.Errorf("invalid width for attribute view column [%s]", id)
}
widths[id] = width
}
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return err
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return err
}
if av.LayoutTypeTable != view.LayoutType {
return nil
}
for _, column := range view.Table.Columns {
if width, found := widths[column.ID]; found {
column.Width = av.FilterWidthValue(width)
}
}
return av.SaveAttributeView(attrView)
}
func (tx *Transaction) doSetAttrViewColumnAlign(operation *Operation) (ret *TxErr) {
err := setAttributeViewColAlign(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColAlign(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
if av.LayoutTypeTable != view.LayoutType {
return
}
alignValue, ok := operation.Data.(string)
align := av.TableColumnAlign(alignValue)
if !ok || !align.IsValid() {
return av.ErrInvalidColumnAlign
}
for _, column := range view.Table.Columns {
if column.ID == operation.ID {
column.Align = align
break
}
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewColumnWrap(operation *Operation) (ret *TxErr) {
err := setAttributeViewColWrap(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColWrap(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
newWrap := operation.Data.(bool)
allFieldWrap := true
switch view.LayoutType {
case av.LayoutTypeTable:
for _, column := range view.Table.Columns {
if column.ID == operation.ID {
column.Wrap = newWrap
}
allFieldWrap = allFieldWrap && column.Wrap
}
view.Table.WrapField = allFieldWrap
case av.LayoutTypeGallery:
for _, field := range view.Gallery.CardFields {
if field.ID == operation.ID {
field.Wrap = newWrap
}
allFieldWrap = allFieldWrap && field.Wrap
}
view.Gallery.WrapField = allFieldWrap
case av.LayoutTypeKanban:
for _, field := range view.Kanban.Fields {
if field.ID == operation.ID {
field.Wrap = newWrap
}
allFieldWrap = allFieldWrap && field.Wrap
}
view.Kanban.WrapField = allFieldWrap
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewColumnHidden(operation *Operation) (ret *TxErr) {
err := setAttributeViewColHidden(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColHidden(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
viewIDs := operation.ViewIDs
if 1 > len(viewIDs) {
if "" != operation.ViewID {
viewIDs = []string{operation.ViewID}
} else {
var view *av.View
view, err = getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
viewIDs = []string{view.ID}
}
}
err = setAttributeViewFieldsHidden(attrView, operation.ID, viewIDs, operation.Data.(bool))
if err != nil {
return
}
err = av.SaveAttributeView(attrView)
return
}
func setAttributeViewFieldsHidden(attrView *av.AttributeView, keyID string, viewIDs []string, hidden bool) (err error) {
var fields []*av.BaseField
seen := map[string]bool{}
for _, viewID := range viewIDs {
if seen[viewID] {
continue
}
seen[viewID] = true
view := attrView.GetView(viewID)
if nil == view {
return fmt.Errorf("view [%s] not found", viewID)
}
field := getAttributeViewField(view, keyID)
if nil == field {
return fmt.Errorf("field [%s] not found in view [%s]", keyID, viewID)
}
fields = append(fields, field)
}
if 1 > len(fields) {
return errors.New("view IDs is empty")
}
for _, field := range fields {
field.Hidden = hidden
}
return
}
func getAttributeViewField(view *av.View, keyID string) (ret *av.BaseField) {
switch view.LayoutType {
case av.LayoutTypeTable:
if nil == view.Table {
return
}
for _, column := range view.Table.Columns {
if column.ID == keyID {
return column.BaseField
}
}
case av.LayoutTypeGallery:
if nil == view.Gallery {
return
}
for _, field := range view.Gallery.CardFields {
if field.ID == keyID {
return field.BaseField
}
}
case av.LayoutTypeKanban:
if nil == view.Kanban {
return
}
for _, field := range view.Kanban.Fields {
if field.ID == keyID {
return field.BaseField
}
}
}
return
}
func (tx *Transaction) doSetAttrViewColumnPin(operation *Operation) (ret *TxErr) {
err := setAttributeViewColPin(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColPin(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
switch view.LayoutType {
case av.LayoutTypeTable:
pin := operation.Data.(bool)
for _, column := range view.Table.Columns {
column.Pin = pin && column.ID == operation.ID
}
case av.LayoutTypeGallery, av.LayoutTypeKanban:
return
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewColumnIcon(operation *Operation) (ret *TxErr) {
err := setAttributeViewColIcon(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColIcon(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
for _, keyValues := range attrView.KeyValues {
if keyValues.Key.ID == operation.ID {
keyValues.Key.Icon = filterAttrViewIconValue(operation.Data.(string))
break
}
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewColumnDesc(operation *Operation) (ret *TxErr) {
err := setAttributeViewColDesc(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColDesc(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
for _, keyValues := range attrView.KeyValues {
if keyValues.Key.ID == operation.ID {
keyValues.Key.Desc = operation.Data.(string)
break
}
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSortAttrViewRow(operation *Operation) (ret *TxErr) {
err := sortAttributeViewRow(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func sortAttributeViewRow(operation *Operation) (err error) {
if operation.ID == operation.PreviousID {
// 拖拽到自己的下方,不做任何操作 https://github.com/siyuan-note/siyuan/issues/11048
return
}
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
var view *av.View
if "" != operation.ViewID {
view = attrView.GetView(operation.ViewID)
if nil == view {
return av.ErrViewNotFound
}
} else {
view, err = getAttrViewViewByBlockID(attrView, operation.BlockID)
if err != nil {
return
}
}
var itemID string
var idx, previousIndex int
if nil != view.Group && "" != operation.GroupID {
if groupView := view.GetGroupByID(operation.GroupID); nil != groupView {
groupKey := view.GetGroupKey(attrView)
isAcrossGroup := operation.GroupID != operation.TargetGroupID
if isAcrossGroup && (av.KeyTypeTemplate == groupKey.Type || av.KeyTypeCreated == groupKey.Type || av.KeyTypeUpdated == groupKey.Type) {
// 这些字段类型不支持跨分组移动,因为它们的值是自动计算生成的
return
}
for i, id := range groupView.GroupItemIDs {
if id == operation.ID {
itemID = id
idx = i
break
}
}
if "" == itemID {
itemID = operation.ID
groupView.GroupItemIDs = append(groupView.GroupItemIDs, itemID)
idx = len(groupView.GroupItemIDs) - 1
}
groupView.GroupItemIDs = append(groupView.GroupItemIDs[:idx], groupView.GroupItemIDs[idx+1:]...)
if isAcrossGroup {
if targetGroupView := view.GetGroupByID(operation.TargetGroupID); nil != targetGroupView && !gulu.Str.Contains(itemID, targetGroupView.GroupItemIDs) {
fillDefaultValue(attrView, view, targetGroupView, operation.PreviousID, itemID, false, true)
if val := attrView.GetValue(groupKey.ID, itemID); nil != val {
if av.MSelectExistOption(val.MSelect, groupView.GetGroupValue()) {
// 移除旧分组的值
val.MSelect = av.MSelectRemoveOption(val.MSelect, groupView.GetGroupValue())
}
now := time.Now().UnixMilli()
val.SetUpdatedAt(now)
if blockVal := attrView.GetBlockValue(itemID); nil != blockVal {
blockVal.Block.Updated = now
blockVal.SetUpdatedAt(now)
}
}
for i, r := range targetGroupView.GroupItemIDs {
if r == operation.PreviousID {
previousIndex = i + 1
break
}
}
targetGroupView.GroupItemIDs = util.InsertElem(targetGroupView.GroupItemIDs, previousIndex, itemID)
}
regenAttrViewGroups(attrView)
} else { // 同分组内排序
for i, r := range groupView.GroupItemIDs {
if r == operation.PreviousID {
previousIndex = i + 1
break
}
}
groupView.GroupItemIDs = util.InsertElem(groupView.GroupItemIDs, previousIndex, itemID)
}
}
} else {
for i, id := range view.ItemIDs {
if id == operation.ID {
itemID = id
idx = i
break
}
}
if "" == itemID {
itemID = operation.ID
view.ItemIDs = append(view.ItemIDs, itemID)
idx = len(view.ItemIDs) - 1
}
view.ItemIDs = append(view.ItemIDs[:idx], view.ItemIDs[idx+1:]...)
for i, r := range view.ItemIDs {
if r == operation.PreviousID {
previousIndex = i + 1
break
}
}
view.ItemIDs = util.InsertElem(view.ItemIDs, previousIndex, itemID)
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSortAttrViewColumn(operation *Operation) (ret *TxErr) {
err := SortAttributeViewViewKey(operation.AvID, operation.BlockID, operation.ID, operation.PreviousID)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func SortAttributeViewViewKey(avID, blockID, keyID, previousKeyID string) (err error) {
if keyID == previousKeyID {
// 拖拽到自己的右侧,不做任何操作 https://github.com/siyuan-note/siyuan/issues/11048
return
}
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return
}
var curIndex, previousIndex int
switch view.LayoutType {
case av.LayoutTypeTable:
var col *av.ViewTableColumn
for i, column := range view.Table.Columns {
if column.ID == keyID {
col = column
curIndex = i
break
}
}
if nil == col {
return
}
view.Table.Columns = append(view.Table.Columns[:curIndex], view.Table.Columns[curIndex+1:]...)
for i, column := range view.Table.Columns {
if column.ID == previousKeyID {
previousIndex = i + 1
break
}
}
view.Table.Columns = util.InsertElem(view.Table.Columns, previousIndex, col)
case av.LayoutTypeGallery:
var field *av.ViewGalleryCardField
for i, cardField := range view.Gallery.CardFields {
if cardField.ID == keyID {
field = cardField
curIndex = i
break
}
}
if nil == field {
return
}
view.Gallery.CardFields = append(view.Gallery.CardFields[:curIndex], view.Gallery.CardFields[curIndex+1:]...)
for i, cardField := range view.Gallery.CardFields {
if cardField.ID == previousKeyID {
previousIndex = i + 1
break
}
}
view.Gallery.CardFields = util.InsertElem(view.Gallery.CardFields, previousIndex, field)
case av.LayoutTypeKanban:
var field *av.ViewKanbanField
for i, kanbanField := range view.Kanban.Fields {
if kanbanField.ID == keyID {
field = kanbanField
curIndex = i
break
}
}
if nil == field {
return
}
view.Kanban.Fields = append(view.Kanban.Fields[:curIndex], view.Kanban.Fields[curIndex+1:]...)
for i, kanbanField := range view.Kanban.Fields {
if kanbanField.ID == previousKeyID {
previousIndex = i + 1
break
}
}
view.Kanban.Fields = util.InsertElem(view.Kanban.Fields, previousIndex, field)
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSortAttrViewKey(operation *Operation) (ret *TxErr) {
err := SortAttributeViewKey(operation.AvID, operation.ID, operation.PreviousID)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func SortAttributeViewKey(avID, keyID, previousKeyID string) (err error) {
if keyID == previousKeyID {
return
}
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
refreshAttrViewKeyIDs(attrView, false)
var currentKeyID string
var idx, previousIndex int
for i, k := range attrView.KeyIDs {
if k == keyID {
currentKeyID = k
idx = i
break
}
}
if "" == currentKeyID {
return
}
attrView.KeyIDs = append(attrView.KeyIDs[:idx], attrView.KeyIDs[idx+1:]...)
for i, k := range attrView.KeyIDs {
if k == previousKeyID {
previousIndex = i + 1
break
}
}
attrView.KeyIDs = util.InsertElem(attrView.KeyIDs, previousIndex, currentKeyID)
err = av.SaveAttributeView(attrView)
return
}
func refreshAttrViewKeyIDs(attrView *av.AttributeView, needSave bool) {
// 订正 keyIDs 数据
existKeyIDs := map[string]bool{}
for _, keyValues := range attrView.KeyValues {
existKeyIDs[keyValues.Key.ID] = true
}
for k := range existKeyIDs {
if !gulu.Str.Contains(k, attrView.KeyIDs) {
attrView.KeyIDs = append(attrView.KeyIDs, k)
}
}
var tmp []string
for _, k := range attrView.KeyIDs {
if ok := existKeyIDs[k]; ok {
tmp = append(tmp, k)
}
}
attrView.KeyIDs = tmp
if needSave {
av.SaveAttributeView(attrView)
}
}
func (tx *Transaction) doAddAttrViewColumn(operation *Operation) (ret *TxErr) {
var icon string
if nil != operation.Data {
icon = operation.Data.(string)
}
err := AddAttributeViewKey(operation.AvID, operation.BlockID, operation.ID, operation.Name, operation.Typ, icon, operation.PreviousID,
av.DateDisplayFormat(operation.Format))
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func AddAttributeViewKey(avID, blockID, keyID, keyName, keyType, keyIcon, previousKeyID string, dateFormat av.DateDisplayFormat) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
currentView, err := getAttrViewViewByBlockID(attrView, blockID)
if nil != err {
return
}
keyTyp := av.KeyType(keyType)
switch keyTyp {
case av.KeyTypeBlock:
return errors.New("cannot add an attribute view block key")
case av.KeyTypeText, av.KeyTypeNumber, av.KeyTypeDate, av.KeyTypeSelect, av.KeyTypeMSelect, av.KeyTypeURL, av.KeyTypeEmail,
av.KeyTypePhone, av.KeyTypeMAsset, av.KeyTypeTemplate, av.KeyTypeCreated, av.KeyTypeUpdated, av.KeyTypeCheckbox,
av.KeyTypeRelation, av.KeyTypeRollup, av.KeyTypeLineNumber:
key := av.NewKey(keyID, keyName, filterAttrViewIconValue(keyIcon), keyTyp)
if av.KeyTypeDate == keyTyp || av.KeyTypeCreated == keyTyp || av.KeyTypeUpdated == keyTyp {
if !dateFormat.IsValid() {
return errors.New("invalid date display format")
}
key.DateFormat = dateFormat
}
if av.KeyTypeRollup == keyTyp {
key.Rollup = &av.Rollup{Calc: &av.RollupCalc{Operator: av.CalcOperatorNone}}
}
attrView.KeyValues = append(attrView.KeyValues, &av.KeyValues{Key: key})
for _, view := range attrView.Views {
newField := &av.BaseField{ID: key.ID}
if nil != view.Table {
newField.Wrap = view.Table.WrapField
if "" == previousKeyID {
if av.LayoutTypeGallery == currentView.LayoutType || av.LayoutTypeKanban == currentView.LayoutType {
// 如果当前视图是卡片或看板视图则添加到最后
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: newField})
} else {
view.Table.Columns = append([]*av.ViewTableColumn{{BaseField: newField}}, view.Table.Columns...)
}
} else {
added := false
for i, column := range view.Table.Columns {
if column.ID == previousKeyID {
view.Table.Columns = append(view.Table.Columns[:i+1], append([]*av.ViewTableColumn{{BaseField: newField}}, view.Table.Columns[i+1:]...)...)
added = true
break
}
}
if !added {
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: newField})
}
}
}
if nil != view.Gallery {
newField.Wrap = view.Gallery.WrapField
if "" == previousKeyID {
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: newField})
} else {
added := false
for i, field := range view.Gallery.CardFields {
if field.ID == previousKeyID {
view.Gallery.CardFields = append(view.Gallery.CardFields[:i+1], append([]*av.ViewGalleryCardField{{BaseField: newField}}, view.Gallery.CardFields[i+1:]...)...)
added = true
break
}
}
if !added {
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: newField})
}
}
}
if nil != view.Kanban {
newField.Wrap = view.Kanban.WrapField
if "" == previousKeyID {
view.Kanban.Fields = append(view.Kanban.Fields, &av.ViewKanbanField{BaseField: newField})
} else {
added := false
for i, field := range view.Kanban.Fields {
if field.ID == previousKeyID {
view.Kanban.Fields = append(view.Kanban.Fields[:i+1], append([]*av.ViewKanbanField{{BaseField: newField}}, view.Kanban.Fields[i+1:]...)...)
added = true
break
}
}
if !added {
view.Kanban.Fields = append(view.Kanban.Fields, &av.ViewKanbanField{BaseField: newField})
}
}
}
}
default:
return fmt.Errorf("unsupported attribute view key type [%s]", keyType)
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doUpdateAttrViewColTemplate(operation *Operation) (ret *TxErr) {
err := updateAttributeViewColTemplate(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func updateAttributeViewColTemplate(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
colType := av.KeyType(operation.Typ)
switch colType {
case av.KeyTypeTemplate:
for _, keyValues := range attrView.KeyValues {
if keyValues.Key.ID == operation.ID && av.KeyTypeTemplate == keyValues.Key.Type {
keyValues.Key.Template = operation.Data.(string)
break
}
}
}
regenAttrViewGroups(attrView)
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doUpdateAttrViewColNumberFormat(operation *Operation) (ret *TxErr) {
err := updateAttributeViewColNumberFormat(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func updateAttributeViewColNumberFormat(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
colType := av.KeyType(operation.Typ)
switch colType {
case av.KeyTypeNumber:
for _, keyValues := range attrView.KeyValues {
if keyValues.Key.ID == operation.ID && av.KeyTypeNumber == keyValues.Key.Type {
keyValues.Key.NumberFormat = av.NumberFormat(operation.Format)
break
}
}
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewColDateFormat(operation *Operation) (ret *TxErr) {
err := setAttributeViewColDateFormat(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColDateFormat(operation *Operation) (err error) {
format := av.DateDisplayFormat(operation.Format)
if !format.IsValid() {
return errors.New("invalid date display format")
}
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
colType := av.KeyType(operation.Typ)
if av.KeyTypeDate != colType && av.KeyTypeCreated != colType && av.KeyTypeUpdated != colType {
return errors.New("date display format is only available for date fields")
}
for _, keyValues := range attrView.KeyValues {
if keyValues.Key.ID == operation.ID && keyValues.Key.Type == colType {
keyValues.Key.DateFormat = format
break
}
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doUpdateAttrViewColumn(operation *Operation) (ret *TxErr) {
err := updateAttributeViewColumn(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func updateAttributeViewColumn(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
colType := av.KeyType(operation.Typ)
changeType := false
switch colType {
case av.KeyTypeBlock, av.KeyTypeText, av.KeyTypeNumber, av.KeyTypeDate, av.KeyTypeSelect, av.KeyTypeMSelect, av.KeyTypeURL, av.KeyTypeEmail,
av.KeyTypePhone, av.KeyTypeMAsset, av.KeyTypeTemplate, av.KeyTypeCreated, av.KeyTypeUpdated, av.KeyTypeCheckbox,
av.KeyTypeRelation, av.KeyTypeRollup, av.KeyTypeLineNumber:
for _, keyValues := range attrView.KeyValues {
if keyValues.Key.ID == operation.ID {
isPrimaryKey := av.KeyTypeBlock == keyValues.Key.Type
if isPrimaryKey != (av.KeyTypeBlock == colType) {
if isPrimaryKey {
err = errors.New("cannot change type of primary key field")
} else {
err = errors.New("cannot change field type to primary key")
}
return
}
keyValues.Key.Name = strings.TrimSpace(operation.Name)
changeType = keyValues.Key.Type != colType
keyValues.Key.Type = colType
for _, value := range keyValues.Values {
value.Type = colType
}
break
}
}
}
if changeType {
attrView.RemoveNewItemTemplateFieldValue(operation.ID)
for _, view := range attrView.Views {
if nil != view.Group {
if groupKey := view.GetGroupKey(attrView); nil != groupKey && groupKey.ID == operation.ID {
removeAttributeViewGroup0(view)
}
}
}
removeAttrViewColumnFromFieldFilters(attrView, attrView.ID, operation.ID)
}
if err = av.SaveAttributeView(attrView); nil != err {
return
}
if changeType {
relatedAvIDs := av.GetSrcAvIDs(attrView.ID)
for _, relatedAvID := range relatedAvIDs {
if relatedAvID == attrView.ID {
continue
}
destAv, _ := av.ParseAttributeView(relatedAvID)
if nil == destAv {
continue
}
for _, keyValues := range destAv.KeyValues {
if av.KeyTypeRollup == keyValues.Key.Type && nil != keyValues.Key.Rollup &&
keyValues.Key.Rollup.KeyID == operation.ID {
// 置空关联过来的汇总
for _, val := range keyValues.Values {
val.Rollup.Contents = nil
}
keyValues.Key.Rollup.Calc = &av.RollupCalc{Operator: av.CalcOperatorNone}
}
}
removeAttrViewColumnFromFieldFilters(destAv, attrView.ID, operation.ID)
regenAttrViewGroups(destAv)
av.SaveAttributeView(destAv)
ReloadAttrView(destAv.ID)
}
}
return
}
func (tx *Transaction) doRemoveAttrViewColumn(operation *Operation) (ret *TxErr) {
err := RemoveAttributeViewKey(operation.AvID, operation.ID, operation.RemoveDest)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func RemoveAttributeViewKey(avID, keyID string, removeRelationDest bool) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
key, keyErr := attrView.GetKey(keyID)
if nil != keyErr {
err = keyErr
return
}
if av.KeyTypeBlock == key.Type {
err = errors.New("cannot remove primary key field")
return
}
var removedKey *av.Key
for i, keyValues := range attrView.KeyValues {
if keyValues.Key.ID == keyID {
attrView.KeyValues = append(attrView.KeyValues[:i], attrView.KeyValues[i+1:]...)
removedKey = keyValues.Key
attrView.RemoveNewItemTemplateFieldValue(keyID)
break
}
}
if nil != removedKey && av.KeyTypeRelation == removedKey.Type && nil != removedKey.Relation {
if removedKey.Relation.IsTwoWay {
var destAv *av.AttributeView
if avID == removedKey.Relation.AvID {
destAv = attrView
} else {
destAv, _ = av.ParseAttributeView(removedKey.Relation.AvID)
}
if nil != destAv {
oldDestKey, _ := destAv.GetKey(removedKey.Relation.BackKeyID)
if nil != oldDestKey && nil != oldDestKey.Relation && oldDestKey.Relation.AvID == attrView.ID && oldDestKey.Relation.IsTwoWay {
oldDestKey.Relation.IsTwoWay = false
oldDestKey.Relation.BackKeyID = ""
}
destAvRelSrcAv := false
for i, keyValues := range destAv.KeyValues {
if keyValues.Key.ID == removedKey.Relation.BackKeyID {
if removeRelationDest { // 删除双向关联的目标字段
destAv.KeyValues = append(destAv.KeyValues[:i], destAv.KeyValues[i+1:]...)
destAv.RemoveNewItemTemplateFieldValue(removedKey.Relation.BackKeyID)
}
continue
}
if av.KeyTypeRelation == keyValues.Key.Type && keyValues.Key.Relation.AvID == attrView.ID {
destAvRelSrcAv = true
}
}
if removeRelationDest {
for _, view := range destAv.Views {
switch view.LayoutType {
case av.LayoutTypeTable:
for i, column := range view.Table.Columns {
if column.ID == removedKey.Relation.BackKeyID {
view.Table.Columns = append(view.Table.Columns[:i], view.Table.Columns[i+1:]...)
break
}
}
case av.LayoutTypeGallery:
for i, field := range view.Gallery.CardFields {
if field.ID == removedKey.Relation.BackKeyID {
view.Gallery.CardFields = append(view.Gallery.CardFields[:i], view.Gallery.CardFields[i+1:]...)
break
}
}
case av.LayoutTypeKanban:
for i, field := range view.Kanban.Fields {
if field.ID == removedKey.Relation.BackKeyID {
view.Kanban.Fields = append(view.Kanban.Fields[:i], view.Kanban.Fields[i+1:]...)
break
}
}
}
}
}
if destAv != attrView {
av.SaveAttributeView(destAv)
ReloadAttrView(destAv.ID)
}
if !destAvRelSrcAv {
av.RemoveAvRel(destAv.ID, attrView.ID)
}
}
srcAvRelDestAv := false
for _, keyValues := range attrView.KeyValues {
if av.KeyTypeRelation == keyValues.Key.Type && nil != keyValues.Key.Relation && keyValues.Key.Relation.AvID == removedKey.Relation.AvID {
srcAvRelDestAv = true
}
}
if !srcAvRelDestAv {
av.RemoveAvRel(attrView.ID, removedKey.Relation.AvID)
}
}
}
attrView.RemoveCardCoverPositionsBySource(av.CardCoverSource(av.CoverFromAssetField, keyID))
for _, view := range attrView.Views {
if nil != view.Table {
for i, column := range view.Table.Columns {
if column.ID == keyID {
view.Table.Columns = append(view.Table.Columns[:i], view.Table.Columns[i+1:]...)
break
}
}
}
if nil != view.Gallery {
for i, field := range view.Gallery.CardFields {
if field.ID == keyID {
view.Gallery.CardFields = append(view.Gallery.CardFields[:i], view.Gallery.CardFields[i+1:]...)
break
}
}
}
if nil != view.Kanban {
for i, field := range view.Kanban.Fields {
if field.ID == keyID {
view.Kanban.Fields = append(view.Kanban.Fields[:i], view.Kanban.Fields[i+1:]...)
break
}
}
}
}
for _, view := range attrView.Views {
if nil != view.Group {
if groupKey := view.GetGroupKey(attrView); nil != groupKey && groupKey.ID == keyID {
removeAttributeViewGroup0(view)
}
}
}
removeAttrViewColumnFromFieldFilters(attrView, avID, keyID)
if err = av.SaveAttributeView(attrView); nil != err {
return
}
if nil != removedKey && av.KeyTypeRelation == removedKey.Type && nil != removedKey.Relation &&
"" != removedKey.Relation.AvID && removedKey.Relation.AvID != avID {
ReloadAttrView(removedKey.Relation.AvID)
}
relatedAvIDs := av.GetSrcAvIDs(avID)
for _, relatedAvID := range relatedAvIDs {
if relatedAvID == avID {
continue
}
destAv, _ := av.ParseAttributeView(relatedAvID)
if nil == destAv {
continue
}
for _, keyValues := range destAv.KeyValues {
if av.KeyTypeRollup == keyValues.Key.Type && nil != keyValues.Key.Rollup &&
keyValues.Key.Rollup.KeyID == keyID {
// 置空关联过来的汇总
for _, val := range keyValues.Values {
val.Rollup.Contents = nil
}
}
}
removeAttrViewColumnFromFieldFilters(destAv, avID, keyID)
regenAttrViewGroups(destAv)
av.SaveAttributeView(destAv)
ReloadAttrView(destAv.ID)
}
return
}
func (tx *Transaction) doReplaceAttrViewBlock(operation *Operation) (ret *TxErr) {
targetItemID, duplicate, err := replaceAttributeViewBlock(operation.AvID, operation.PreviousID, operation.NextID, operation.IsDetached, tx)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID}
}
operation.RetData = map[string]any{"targetItemID": targetItemID, "duplicate": duplicate}
return
}
func replaceAttributeViewBlock(avID, oldBlockID, newBlockID string, isDetached bool, tx *Transaction) (targetItemID string, duplicate bool, err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
if targetItemID, duplicate, err = replaceAttributeViewBlock0(attrView, oldBlockID, newBlockID, isDetached, tx); nil != err {
return
}
if err = av.SaveAttributeView(attrView); nil != err {
return
}
return
}
func replaceAttributeViewBlock0(attrView *av.AttributeView, oldBlockID, newNodeID string, isDetached bool, tx *Transaction) (targetItemID string, duplicate bool, err error) {
avID := attrView.ID
targetItemID = oldBlockID
var tree *parse.Tree
var node *ast.Node
if !isDetached {
node, tree, _ = getNodeByBlockID(tx, newNodeID)
}
now := util.CurrentTimeMillis()
// 检查是否已经存在绑定块,如果存在的话则重新绑定
for _, blockVal := range attrView.GetBlockKeyValues().Values {
if !isDetached && blockVal.Block.ID == newNodeID && nil != node && nil != tree {
bindBlockAv0(tx, avID, node, tree)
blockVal.IsDetached = false
icon, content := getNodeAvBlockText(node, "")
content = util.UnescapeHTML(content)
blockVal.Block.Icon, blockVal.Block.Content = icon, content
blockVal.UpdatedAt = now
regenAttrViewGroups(attrView)
targetItemID = blockVal.BlockID
duplicate = blockVal.BlockID != oldBlockID
return
}
}
for _, blockVal := range attrView.GetBlockKeyValues().Values {
if blockVal.BlockID != oldBlockID {
continue
}
if av.KeyTypeBlock == blockVal.Type {
blockVal.IsDetached = isDetached
if !isDetached {
if "" != blockVal.Block.ID && blockVal.Block.ID != newNodeID {
unbindBlockAv(tx, avID, blockVal.Block.ID)
}
bindBlockAv(tx, avID, newNodeID)
blockVal.Block.ID = newNodeID
icon, content := getNodeAvBlockText(node, "")
content = util.UnescapeHTML(content)
blockVal.Block.Icon, blockVal.Block.Content = icon, content
refreshRelatedSrcAvs(avID, tx)
} else {
blockVal.Block.ID = ""
}
}
}
regenAttrViewGroups(attrView)
return
}
func BatchReplaceAttributeViewBlocks(avID string, isDetached bool, oldNew []map[string]string) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
for _, oldNewMap := range oldNew {
for oldBlockID, newNodeID := range oldNewMap {
if _, _, err = replaceAttributeViewBlock0(attrView, oldBlockID, newNodeID, isDetached, nil); nil != err {
return
}
}
}
if err = av.SaveAttributeView(attrView); nil != err {
return
}
return
}
func (tx *Transaction) doUpdateAttrViewCell(operation *Operation) (ret *TxErr) {
_, err := UpdateAttributeViewCell(tx, operation.AvID, operation.KeyID, operation.RowID, operation.Data)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func (tx *Transaction) doBatchUpdateAttrViewCells(operations []*Operation) (ret *TxErr) {
attrView, err := av.ParseAttributeView(operations[0].AvID)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operations[0].AvID, msg: err.Error()}
}
for _, operation := range operations {
if _, err = updateAttributeViewValue(tx, attrView, operation.KeyID, operation.RowID, operation.Data, false); err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
}
regenAttrViewGroups(attrView)
if err = av.SaveAttributeView(attrView); err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: attrView.ID, msg: err.Error()}
}
refreshRelatedSrcAvs(attrView.ID, tx)
return
}
func (tx *Transaction) doUpdateAttrViewCells(operation *Operation) (ret *TxErr) {
if "" == operation.AvID || 0 == len(operation.CellUpdates) {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: "invalid attribute view cell updates"}
}
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
context := newAttrViewValueUpdateContext(attrView)
for _, cell := range operation.CellUpdates {
if nil == cell || "" == cell.KeyID || "" == cell.RowID {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: "invalid attribute view cell update"}
}
if _, err = updateAttributeViewValue0(tx, attrView, cell.KeyID, cell.RowID, cell.Data, false, context); err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
}
regenAttrViewGroups(attrView)
if err = av.SaveAttributeView(attrView); err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: attrView.ID, msg: err.Error()}
}
refreshRelatedSrcAvs(attrView.ID, tx)
return
}
func BatchUpdateAttributeViewCells(tx *Transaction, avID string, values []any) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
for _, value := range values {
v := value.(map[string]any)
keyID := v["keyID"].(string)
var itemID string
if _, ok := v["itemID"]; ok {
itemID = v["itemID"].(string)
} else if _, ok := v["rowID"]; ok {
// TODO 该参数将于 2026 年 12 月 1 日后删除
itemID = v["rowID"].(string)
msg := fmt.Sprintf("[%s] parameter [%s] is deprecated, visit [https://github.com/siyuan-note/siyuan/issues/15727] for details",
"/api/av/batchSetAttributeViewBlockAttrs", "rowID")
logging.LogWarn(msg)
err = errors.New(msg)
return
}
valueData := v["value"]
_, err = updateAttributeViewValue(tx, attrView, keyID, itemID, valueData, false)
if err != nil {
return
}
}
regenAttrViewGroups(attrView)
if err = av.SaveAttributeView(attrView); err != nil {
return
}
refreshRelatedSrcAvs(avID, tx)
return
}
func UpdateAttributeViewCell(tx *Transaction, avID, keyID, itemID string, valueData any) (val *av.Value, err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
val, err = updateAttributeViewValue(tx, attrView, keyID, itemID, valueData, true)
if nil != err {
return
}
return
}
func updateAttributeViewValue(tx *Transaction, attrView *av.AttributeView, keyID, itemID string, valueData any, save bool) (val *av.Value, err error) {
return updateAttributeViewValue0(tx, attrView, keyID, itemID, valueData, save, nil)
}
type attrViewValueUpdateContext struct {
keyValues map[string]*av.KeyValues
blockValues map[string]*av.Value
values map[string]map[string]*av.Value
}
func newAttrViewValueUpdateContext(attrView *av.AttributeView) *attrViewValueUpdateContext {
context := &attrViewValueUpdateContext{
keyValues: map[string]*av.KeyValues{},
blockValues: map[string]*av.Value{},
values: map[string]map[string]*av.Value{},
}
for _, keyValues := range attrView.KeyValues {
if nil == keyValues || nil == keyValues.Key {
continue
}
context.keyValues[keyValues.Key.ID] = keyValues
values := map[string]*av.Value{}
for _, value := range keyValues.Values {
if nil == value {
continue
}
if _, exists := values[value.BlockID]; !exists {
values[value.BlockID] = value
}
if av.KeyTypeBlock == keyValues.Key.Type {
if _, exists := context.blockValues[value.BlockID]; !exists {
context.blockValues[value.BlockID] = value
}
}
}
context.values[keyValues.Key.ID] = values
}
return context
}
func updateAttributeViewValue0(tx *Transaction, attrView *av.AttributeView, keyID, itemID string, valueData any, save bool, context *attrViewValueUpdateContext) (val *av.Value, err error) {
avID := attrView.ID
var keyValues *av.KeyValues
var blockVal *av.Value
if nil == context {
keyValues, err = attrView.GetKeyValues(keyID)
if nil != err {
return
}
blockVal = attrView.GetBlockValue(itemID)
} else {
keyValues = context.keyValues[keyID]
if nil == keyValues {
err = av.ErrKeyNotFound
return
}
blockVal = context.blockValues[itemID]
}
if nil == blockVal {
err = av.ErrItemNotFound
return
}
now := time.Now().UnixMilli()
oldIsDetached := blockVal.IsDetached
oldBoundBlockID := blockVal.Block.ID
if nil == context {
for _, value := range keyValues.Values {
if itemID == value.BlockID {
val = value
break
}
}
} else {
val = context.values[keyID][itemID]
}
if nil != val {
val.Type = keyValues.Key.Type
}
if nil == val {
val = &av.Value{ID: ast.NewNodeID(), KeyID: keyID, BlockID: itemID, Type: keyValues.Key.Type, CreatedAt: now, UpdatedAt: now}
keyValues.Values = append(keyValues.Values, val)
if nil != context {
context.values[keyID][itemID] = val
}
}
valueID := val.ID
valueType := val.Type
valueCreatedAt := val.CreatedAt
isUpdatingBlockKey := av.KeyTypeBlock == val.Type
var oldRelationBlockIDs []string
if av.KeyTypeRelation == val.Type {
if nil != val.Relation {
for _, bID := range val.Relation.BlockIDs {
oldRelationBlockIDs = append(oldRelationBlockIDs, bID)
}
}
}
data, err := gulu.JSON.MarshalJSON(valueData)
if err != nil {
logging.LogErrorf("marshal value [%+v] failed: %s", valueData, err)
return
}
if err = gulu.JSON.UnmarshalJSON(data, val); err != nil {
logging.LogErrorf("unmarshal data [%s] failed: %s", data, err)
return
}
val.ID = valueID
val.KeyID = keyID
val.BlockID = itemID
val.Type = valueType
val.CreatedAt = valueCreatedAt
key, _ := attrView.GetKey(keyID)
if av.KeyTypeNumber == val.Type {
if nil != val.Number {
if !val.Number.IsNotEmpty {
val.Number.Content = 0
val.Number.FormattedContent = ""
} else {
val.Number.FormatNumber()
}
}
} else if av.KeyTypeDate == val.Type {
if nil != val.Date {
if !val.Date.IsNotEmpty {
val.Date.Content = 0
val.Date.FormattedContent = ""
} else if nil != key {
val.Date.FormatDate(key.DateFormat)
}
}
} else if av.KeyTypeSelect == val.Type || av.KeyTypeMSelect == val.Type {
if nil != key && 0 < len(val.MSelect) {
var tmp []*av.ValueSelect
// 移除空选项 https://github.com/siyuan-note/siyuan/issues/15533
for _, v := range val.MSelect {
if "" != v.Content {
tmp = append(tmp, v)
}
}
val.MSelect = tmp
if 1 > len(val.MSelect) {
return
}
// The selection options are inconsistent after pasting data into the database https://github.com/siyuan-note/siyuan/issues/11409
for _, valOpt := range val.MSelect {
if opt := key.GetOption(valOpt.Content); nil == opt {
// 不存在的选项新建保存
color := av.FilterColorValue(valOpt.Color)
if "" == color {
color = fmt.Sprintf("%d", 1+rand.Intn(14))
}
opt = &av.SelectOption{Name: valOpt.Content, Color: color}
key.Options = append(key.Options, opt)
} else {
// 已经存在的选项颜色需要保持不变
valOpt.Color = opt.Color
}
}
}
}
relationChangeMode := 0 // 0:不变(仅排序),1:增加,2:减少
if av.KeyTypeRelation == val.Type {
// 关联字段得 content 是自动渲染的,所以不需要保存
val.Relation.Contents = nil
val.Relation.BlockIDs = gulu.Str.RemoveDuplicatedElem(val.Relation.BlockIDs)
// 计算关联变更模式
if !slices.Equal(oldRelationBlockIDs, val.Relation.BlockIDs) {
if len(oldRelationBlockIDs) > len(val.Relation.BlockIDs) {
relationChangeMode = 2
} else {
relationChangeMode = 1
}
}
}
// val.IsDetached 只有更新主键的时候才会传入,所以下面需要结合 isUpdatingBlockKey 来判断
if isUpdatingBlockKey {
if oldIsDetached {
// 之前是非绑定块
if !val.IsDetached { // 现在绑定了块
bindBlockAv(tx, avID, val.Block.ID)
}
} else {
// 之前绑定了块
if val.IsDetached { // 现在是非绑定块
unbindBlockAv(tx, avID, val.Block.ID)
val.Block.ID = ""
} else {
// 现在也绑定了块
if oldBoundBlockID != val.Block.ID { // 之前绑定的块和现在绑定的块不一样
// 换绑块
unbindBlockAv(tx, avID, oldBoundBlockID)
bindBlockAv(tx, avID, val.Block.ID)
val.Block.Content = util.UnescapeHTML(val.Block.Content)
} else { // 之前绑定的块和现在绑定的块一样
content := strings.TrimSpace(val.Block.Content)
node, tree, _ := getNodeByBlockID(tx, val.Block.ID)
_, blockText := getNodeAvBlockText(node, "")
if "" == content {
// 使用动态锚文本
val.Block.Content = util.UnescapeHTML(blockText)
updateBlockValueStaticText(tx, node, tree, avID, "")
} else {
val.Block.Content = content
updateBlockValueStaticText(tx, node, tree, avID, content)
}
}
}
}
}
if nil != blockVal {
blockVal.Block.Updated = now
blockVal.SetUpdatedAt(now)
if isUpdatingBlockKey {
blockVal.IsDetached = val.IsDetached
}
}
val.SetUpdatedAt(now)
if nil != key && av.KeyTypeRelation == key.Type && nil != key.Relation && key.Relation.IsTwoWay {
// 双向关联需要同时更新目标字段的值
updateTwoWayRelationDestAttrView(attrView, key, val, relationChangeMode, oldRelationBlockIDs)
}
if save {
regenAttrViewGroups(attrView)
if err = av.SaveAttributeView(attrView); nil != err {
return
}
}
if 0 != relationChangeMode && nil != key && nil != key.Relation && "" != key.Relation.AvID && key.Relation.AvID != avID {
ReloadAttrView(key.Relation.AvID)
}
if save {
refreshRelatedSrcAvs(avID, tx)
}
return
}
func refreshRelatedSrcAvs(destAvID string, tx *Transaction) {
relatedAvIDs := av.GetSrcAvIDs(destAvID)
var tmp []string
for _, relatedAvID := range relatedAvIDs {
if relatedAvID == destAvID {
// 目标和源相同则跳过
continue
}
tmp = append(tmp, relatedAvID)
}
relatedAvIDs = tmp
if nil != tx {
tx.relatedAvIDs = append(tx.relatedAvIDs, relatedAvIDs...)
} else {
for _, relatedAvID := range relatedAvIDs {
destAv, _ := av.ParseAttributeView(relatedAvID)
if nil == destAv {
continue
}
regenAttrViewGroups(destAv)
av.SaveAttributeView(destAv)
ReloadAttrView(relatedAvID)
}
}
}
// relationChangeMode
// 0:关联字段值不变(仅排序),不影响目标值
// 1:关联字段值增加,增加目标值
// 2:关联字段值减少,减少目标值
func updateTwoWayRelationDestAttrView(attrView *av.AttributeView, relKey *av.Key, val *av.Value, relationChangeMode int, oldRelationBlockIDs []string) {
var destAv *av.AttributeView
if attrView.ID == relKey.Relation.AvID {
destAv = attrView
} else {
destAv, _ = av.ParseAttributeView(relKey.Relation.AvID)
}
if nil == destAv {
return
}
now := util.CurrentTimeMillis()
if 1 == relationChangeMode {
addBlockIDs := val.Relation.BlockIDs
for _, bID := range oldRelationBlockIDs {
addBlockIDs = gulu.Str.RemoveElem(addBlockIDs, bID)
}
for _, blockID := range addBlockIDs {
for _, keyValues := range destAv.KeyValues {
if keyValues.Key.ID != relKey.Relation.BackKeyID {
continue
}
destVal := keyValues.GetValue(blockID)
if nil == destVal {
destVal = &av.Value{ID: ast.NewNodeID(), KeyID: keyValues.Key.ID, BlockID: blockID, Type: keyValues.Key.Type, Relation: &av.ValueRelation{}, CreatedAt: now, UpdatedAt: now + 1000}
keyValues.Values = append(keyValues.Values, destVal)
}
destVal.Relation.BlockIDs = append(destVal.Relation.BlockIDs, val.BlockID)
destVal.Relation.BlockIDs = gulu.Str.RemoveDuplicatedElem(destVal.Relation.BlockIDs)
break
}
}
} else if 2 == relationChangeMode {
removeBlockIDs := oldRelationBlockIDs
for _, bID := range val.Relation.BlockIDs {
removeBlockIDs = gulu.Str.RemoveElem(removeBlockIDs, bID)
}
for _, blockID := range removeBlockIDs {
for _, keyValues := range destAv.KeyValues {
if keyValues.Key.ID != relKey.Relation.BackKeyID {
continue
}
for _, value := range keyValues.Values {
if value.BlockID == blockID {
value.Relation.BlockIDs = gulu.Str.RemoveElem(value.Relation.BlockIDs, val.BlockID)
value.SetUpdatedAt(now)
break
}
}
}
}
}
if destAv != attrView {
regenAttrViewGroups(destAv)
av.SaveAttributeView(destAv)
}
}
// regenAttrViewGroups 重新生成分组视图。
func regenAttrViewGroups(attrView *av.AttributeView) {
for _, view := range attrView.Views {
groupKey := view.GetGroupKey(attrView)
if nil == groupKey {
continue
}
genAttrViewGroups(view, attrView)
}
}
func unbindBlockAv(tx *Transaction, avID, nodeID string) {
node, tree, err := getNodeByBlockID(tx, nodeID)
if err != nil {
return
}
attrs := parse.IAL2Map(node.KramdownIAL)
if "" == attrs[av.NodeAttrNameAvs] {
return
}
avIDs := strings.Split(attrs[av.NodeAttrNameAvs], ",")
avIDs = gulu.Str.RemoveElem(avIDs, avID)
if 0 == len(avIDs) {
attrs[av.NodeAttrNameAvs] = ""
} else {
attrs[av.NodeAttrNameAvs] = strings.Join(avIDs, ",")
}
avNames := getAvNames(attrs[av.NodeAttrNameAvs])
if "" != avNames {
attrs[av.NodeAttrViewNames] = avNames
}
if nil != tx {
err = setNodeAttrsWithTx(tx, node, tree, attrs)
} else {
err = setNodeAttrs(node, tree, attrs)
}
if err != nil {
logging.LogWarnf("set node [%s] attrs failed: %s", nodeID, err)
return
}
return
}
func bindBlockAv(tx *Transaction, avID, blockID string) {
node, tree, err := getNodeByBlockID(tx, blockID)
if err != nil {
return
}
bindBlockAv0(tx, avID, node, tree)
return
}
func bindBlockAv0(tx *Transaction, avID string, node *ast.Node, tree *parse.Tree) {
attrs := parse.IAL2Map(node.KramdownIAL)
if "" == attrs[av.NodeAttrNameAvs] {
attrs[av.NodeAttrNameAvs] = avID
} else {
avIDs := strings.Split(attrs[av.NodeAttrNameAvs], ",")
avIDs = append(avIDs, avID)
avIDs = gulu.Str.RemoveDuplicatedElem(avIDs)
attrs[av.NodeAttrNameAvs] = strings.Join(avIDs, ",")
}
avNames := getAvNames(attrs[av.NodeAttrNameAvs])
if "" != avNames {
attrs[av.NodeAttrViewNames] = avNames
}
var err error
if nil != tx {
err = setNodeAttrsWithTx(tx, node, tree, attrs)
} else {
err = setNodeAttrs(node, tree, attrs)
}
if err != nil {
logging.LogWarnf("set node [%s] attrs failed: %s", node.ID, err)
return
}
return
}
func updateBlockValueStaticText(tx *Transaction, node *ast.Node, tree *parse.Tree, avID, text string) {
// 设置静态锚文本 Database-bound block primary key supports setting static anchor text https://github.com/siyuan-note/siyuan/issues/10049
if nil == node {
return
}
attrs := parse.IAL2Map(node.KramdownIAL)
attrs[av.NodeAttrViewStaticText+"-"+avID] = text
var err error
if nil != tx {
err = setNodeAttrsWithTx(tx, node, tree, attrs)
} else {
err = setNodeAttrs(node, tree, attrs)
}
if err != nil {
logging.LogWarnf("set node [%s] attrs failed: %s", node.ID, err)
return
}
}
func getNodeByBlockID(tx *Transaction, blockID string) (node *ast.Node, tree *parse.Tree, err error) {
if nil != tx {
tree, err = tx.loadTree(blockID)
} else {
tree, err = LoadTreeByBlockID(blockID)
}
if err != nil {
return
}
node = treenode.GetNodeInTree(tree, blockID)
if nil == node {
logging.LogWarnf("node [%s] not found in tree [%s]", blockID, tree.ID)
return
}
return
}
func (tx *Transaction) doUpdateAttrViewColOptions(operation *Operation) (ret *TxErr) {
err := updateAttributeViewColumnOptions(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func updateAttributeViewColumnOptions(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
jsonData, err := gulu.JSON.MarshalJSON(operation.Data)
if err != nil {
return
}
options := []*av.SelectOption{}
if err = gulu.JSON.UnmarshalJSON(jsonData, &options); err != nil {
return
}
// 移除空选项 https://github.com/siyuan-note/siyuan/issues/15533
var tmp []*av.SelectOption
for _, opt := range options {
if "" != opt.Name {
tmp = append(tmp, opt)
}
}
options = tmp
if 1 > len(options) {
return
}
optionSorts := map[string]int{}
for i, opt := range options {
optionSorts[opt.Name] = i
}
selectKey, _ := attrView.GetKey(operation.ID)
if nil == selectKey {
return
}
existingOptions := map[string]*av.SelectOption{}
for _, opt := range selectKey.Options {
existingOptions[opt.Name] = opt
}
for _, opt := range options {
if existingOpt, exists := existingOptions[opt.Name]; exists {
// 如果选项已经存在则更新颜色和描述
existingOpt.Color = av.FilterColorValue(opt.Color)
existingOpt.Desc = opt.Desc
} else {
// 如果选项不存在则添加新选项
selectKey.Options = append(selectKey.Options, &av.SelectOption{
Name: opt.Name,
Color: av.FilterColorValue(opt.Color),
Desc: opt.Desc,
})
}
}
sortAttributeViewColumnOptions(selectKey.Options, optionSorts)
regenAttrViewGroups(attrView)
err = av.SaveAttributeView(attrView)
return
}
func sortAttributeViewColumnOptions(options []*av.SelectOption, optionSorts map[string]int) {
var sortableOptions []*av.SelectOption
for _, opt := range options {
if _, ok := optionSorts[opt.Name]; ok {
sortableOptions = append(sortableOptions, opt)
}
}
sort.SliceStable(sortableOptions, func(i, j int) bool {
return optionSorts[sortableOptions[i].Name] < optionSorts[sortableOptions[j].Name]
})
// 仅重排请求中包含的选项,避免过期请求扰动其他请求新增的选项。
sortableIndex := 0
for i, opt := range options {
if _, ok := optionSorts[opt.Name]; ok {
options[i] = sortableOptions[sortableIndex]
sortableIndex++
}
}
}
func (tx *Transaction) doRemoveAttrViewColOption(operation *Operation) (ret *TxErr) {
err := removeAttributeViewColumnOption(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func removeAttributeViewColumnOption(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
optName := operation.Data.(string)
key, err := attrView.GetKey(operation.ID)
if err != nil {
return
}
for i, opt := range key.Options {
if optName == opt.Name {
key.Options = append(key.Options[:i], key.Options[i+1:]...)
break
}
}
attrView.RemoveNewItemTemplateSelectOption(operation.ID, optName)
for _, keyValues := range attrView.KeyValues {
if keyValues.Key.ID != operation.ID {
continue
}
for _, value := range keyValues.Values {
if nil == value || nil == value.MSelect {
continue
}
for i, opt := range value.MSelect {
if optName == opt.Content {
value.MSelect = append(value.MSelect[:i], value.MSelect[i+1:]...)
break
}
}
}
break
}
// 如果存在选项对应的过滤条件,则删除过滤条件中设置的选项值 https://github.com/siyuan-note/siyuan/issues/15536
for _, view := range attrView.Views {
view.Filters = av.RemoveSelectOptionFromFilters(view.Filters, operation.ID, optName)
if 0 == len(view.Filters) {
// 保持 spec 5 根组不变量
view.Filters = []*av.ViewFilter{{Combination: av.FilterCombinationAnd}}
}
}
removeAttrViewOptionFromFieldFilters(attrView, attrView.ID, operation.ID, optName)
regenAttrViewGroups(attrView)
if err = av.SaveAttributeView(attrView); nil != err {
return
}
for _, relatedAvID := range av.GetSrcAvIDs(attrView.ID) {
if relatedAvID == attrView.ID {
continue
}
relatedAv, parseErr := av.ParseAttributeView(relatedAvID)
if nil != parseErr || nil == relatedAv ||
!removeAttrViewOptionFromFieldFilters(relatedAv, attrView.ID, operation.ID, optName) {
continue
}
if err = av.SaveAttributeView(relatedAv); nil != err {
return
}
ReloadAttrView(relatedAvID)
}
return
}
func (tx *Transaction) doUpdateAttrViewColOption(operation *Operation) (ret *TxErr) {
err := updateAttributeViewColumnOption(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func updateAttributeViewColumnOption(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
key, err := attrView.GetKey(operation.ID)
if err != nil {
return
}
data := operation.Data.(map[string]any)
rename := false
oldName := strings.TrimSpace(data["oldName"].(string))
newName := strings.TrimSpace(data["newName"].(string))
newDesc := strings.TrimSpace(data["newDesc"].(string))
newColor := av.FilterColorValue(data["newColor"].(string))
found := false
if oldName != newName {
rename = true
for _, opt := range key.Options {
if newName == opt.Name { // 如果选项已经存在则直接使用
found = true
newColor = opt.Color
newDesc = opt.Desc
break
}
}
}
if rename {
attrView.RenameNewItemTemplateSelectOption(operation.ID, oldName, newName, newColor)
}
if !found {
for i, opt := range key.Options {
if oldName == opt.Name {
key.Options[i].Name = newName
key.Options[i].Color = newColor
key.Options[i].Desc = newDesc
break
}
}
}
// 如果存在选项对应的值,需要更新值中的选项
for _, keyValues := range attrView.KeyValues {
if keyValues.Key.ID != operation.ID {
continue
}
for _, value := range keyValues.Values {
if nil == value || nil == value.MSelect {
continue
}
found = false
for _, opt := range value.MSelect {
if newName == opt.Content {
found = true
break
}
}
if found && rename {
idx := -1
for i, opt := range value.MSelect {
if oldName == opt.Content {
idx = i
break
}
}
if 0 <= idx {
value.MSelect = util.RemoveElem(value.MSelect, idx)
}
} else {
for i, opt := range value.MSelect {
if oldName == opt.Content {
value.MSelect[i].Content = newName
value.MSelect[i].Color = newColor
break
}
}
}
}
break
}
// 如果存在选项对应的过滤条件,需要更新过滤条件中设置的选项值
// Database select field filters follow option editing changes https://github.com/siyuan-note/siyuan/issues/10881
for _, view := range attrView.Views {
av.RenameSelectOptionInFilters(view.Filters, key.ID, oldName, newName, newColor)
}
renameAttrViewOptionInFieldFilters(attrView, attrView.ID, key.ID, oldName, newName, newColor)
regenAttrViewGroups(attrView)
if err = av.SaveAttributeView(attrView); nil != err {
return
}
for _, relatedAvID := range av.GetSrcAvIDs(attrView.ID) {
if relatedAvID == attrView.ID {
continue
}
relatedAv, parseErr := av.ParseAttributeView(relatedAvID)
if nil != parseErr || nil == relatedAv ||
!renameAttrViewOptionInFieldFilters(relatedAv, attrView.ID, key.ID, oldName, newName, newColor) {
continue
}
if err = av.SaveAttributeView(relatedAv); nil != err {
return
}
ReloadAttrView(relatedAvID)
}
return
}
func (tx *Transaction) doSetAttrViewColOptionDesc(operation *Operation) (ret *TxErr) {
err := setAttributeViewColumnOptionDesc(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColumnOptionDesc(operation *Operation) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
key, err := attrView.GetKey(operation.ID)
if err != nil {
return
}
data := operation.Data.(map[string]any)
name := data["name"].(string)
desc := data["desc"].(string)
for i, opt := range key.Options {
if name == opt.Name {
key.Options[i].Desc = desc
break
}
}
err = av.SaveAttributeView(attrView)
return
}
func resolveAttributeViewView(attrView *av.AttributeView, viewID, carrierViewID, blockID string) (ret *av.View, err error) {
if "" != viewID {
ret = attrView.GetView(viewID)
if nil == ret {
return nil, av.ErrViewNotFound
}
return
}
if "" != carrierViewID {
if ret = attrView.GetView(carrierViewID); nil != ret {
return
}
}
if "" != blockID {
node, _, _ := getNodeByBlockID(nil, blockID)
if nil != node && ast.NodeAttributeView == node.Type && attrView.ID == node.AttributeViewID {
if ret = attrView.GetView(node.IALAttr(av.NodeAttrView)); nil != ret {
return
}
}
}
return attrView.GetFirstView()
}
func getAttrViewViewByBlockID(attrView *av.AttributeView, blockID string) (ret *av.View, err error) {
return resolveAttributeViewView(attrView, "", "", blockID)
}
func getAttrViewName(attrView *av.AttributeView) string {
ret := strings.TrimSpace(attrView.Name)
if "" == ret {
ret = Conf.language(105)
}
return ret
}
func updateBoundBlockAvsAttribute(avIDs []string) {
// 更新指定 avIDs 中绑定块的 avs 属性
cachedTrees, saveTrees := map[string]*parse.Tree{}, map[string]*parse.Tree{}
luteEngine := util.NewLute()
for _, avID := range avIDs {
attrView, _ := av.ParseAttributeView(avID)
if nil == attrView {
continue
}
blockKeyValues := attrView.GetBlockKeyValues()
if nil == blockKeyValues || nil == blockKeyValues.Values {
continue
}
for _, blockValue := range blockKeyValues.Values {
if blockValue.IsDetached || nil == blockValue.Block {
continue
}
boundBlockID := blockValue.Block.ID
if "" == boundBlockID {
continue
}
bt := treenode.GetBlockTree(boundBlockID)
if nil == bt {
for _, encBoxID := range treenode.GetOpenedEncryptedBoxIDs() {
if encBT := treenode.GetBlockTreeInBox(boundBlockID, encBoxID); nil != encBT {
bt = encBT
break
}
}
}
if nil == bt {
continue
}
tree := cachedTrees[bt.RootID]
if nil == tree {
tree, _ = filesys.LoadTree(bt.BoxID, bt.Path, luteEngine)
if nil == tree {
continue
}
cachedTrees[bt.RootID] = tree
}
node := treenode.GetNodeInTree(tree, boundBlockID)
if nil == node {
continue
}
attrs := parse.IAL2Map(node.KramdownIAL)
if "" == attrs[av.NodeAttrNameAvs] {
attrs[av.NodeAttrNameAvs] = avID
} else {
nodeAvIDs := strings.Split(attrs[av.NodeAttrNameAvs], ",")
nodeAvIDs = append(nodeAvIDs, avID)
nodeAvIDs = gulu.Str.RemoveDuplicatedElem(nodeAvIDs)
attrs[av.NodeAttrNameAvs] = strings.Join(nodeAvIDs, ",")
saveTrees[bt.RootID] = tree
}
avNames := getAvNames(attrs[av.NodeAttrNameAvs])
if "" != avNames {
attrs[av.NodeAttrViewNames] = avNames
}
oldAttrs, setErr := setNodeAttrs0(node, attrs, bt.BoxID)
if nil != setErr {
continue
}
cache.PutBlockIALInBox(node.ID, bt.BoxID, parse.IAL2Map(node.KramdownIAL))
pushBlockAttrs(oldAttrs, node)
if "" != avNames {
node.RemoveIALAttr(av.NodeAttrViewNames)
}
}
}
for _, saveTree := range saveTrees {
if treeErr := indexWriteTreeUpsertQueue(saveTree); nil != treeErr {
logging.LogErrorf("index write tree upsert queue failed: %s", treeErr)
}
avNodes := saveTree.Root.ChildrenByType(ast.NodeAttributeView)
av.BatchUpsertBlockRel(avNodes)
}
}