siyuan/kernel/model/attribute_view.go

5201 lines
140 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package model
import (
"bytes"
"fmt"
"math/rand"
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
"time"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/jinzhu/copier"
"github.com/siyuan-note/dejavu/entity"
"github.com/siyuan-note/filelock"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/av"
"github.com/siyuan-note/siyuan/kernel/cache"
"github.com/siyuan-note/siyuan/kernel/filesys"
"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 GetAttrViewAddingBlockDefaultValues(avID, viewID, groupID, previousBlockID, addingBlockID string) (ret map[string]*av.Value) {
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 := attrView.GetView(viewID)
if nil == view {
logging.LogErrorf("view [%s] not found in attribute view [%s]", viewID, avID)
return
}
groupView := view
if "" != groupID {
groupView = view.GetGroup(groupID)
}
if nil == groupView {
logging.LogErrorf("group [%s] not found in view [%s] of attribute view [%s]", groupID, viewID, avID)
return
}
return getAttrViewAddingBlockDefaultValues(attrView, view, groupView, previousBlockID, addingBlockID)
}
func getAttrViewAddingBlockDefaultValues(attrView *av.AttributeView, view, groupView *av.View, previousBlockID, addingBlockID string) (ret map[string]*av.Value) {
ret = map[string]*av.Value{}
nearItem := getNearItem(attrView, view, groupView, previousBlockID)
filterKeyIDs := map[string]bool{}
for _, f := range view.Filters {
filterKeyIDs[f.Column] = true
}
for _, filter := range view.Filters {
keyValues, _ := attrView.GetKeyValues(filter.Column)
if nil == keyValues {
continue
}
var defaultVal *av.Value
if nil != nearItem {
defaultVal = nearItem.GetValue(filter.Column)
}
newValue := filter.GetAffectValue(keyValues.Key, defaultVal, addingBlockID)
if nil != newValue {
ret[keyValues.Key.ID] = newValue
}
}
groupKey := view.GetGroupKey(attrView)
if nil != groupKey && !filterKeyIDs[groupKey.ID] /* 命中了过滤条件的话就不重复处理了 */ {
if keyValues, _ := attrView.GetKeyValues(groupKey.ID); nil != keyValues {
newValue := getNewValueByNearItem(nearItem, groupKey, addingBlockID)
if av.KeyTypeSelect == groupKey.Type || av.KeyTypeMSelect == groupKey.Type {
// 因为单选或多选只能按选项分组,并且可能存在空白分组(前面可能找不到临近项) ,所以单选或多选类型的分组字段使用分组值内容对应的选项
if opt := groupKey.GetOption(groupView.GroupValue); nil != opt && groupValueDefault != groupView.GroupValue {
newValue.MSelect = append(newValue.MSelect, &av.ValueSelect{Content: opt.Name, Color: opt.Color})
}
}
ret[groupKey.ID] = newValue
}
}
return
}
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
}
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)
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.GroupUpdated = 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:
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)
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) 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 nil == view.Group {
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) 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
}
view.Group = group
regenAttrViewViewGroups(attrView, "force")
err = av.SaveAttributeView(attrView)
ReloadAttrView(avID)
return
}
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) {
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 = av.CardAspectRatio(operation.Data.(float64))
}
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) 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, layout av.LayoutType) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
view, err := getAttrViewViewByBlockID(attrView, blockID)
if err != nil {
return
}
newLayout := layout
if newLayout == view.LayoutType {
return
}
switch newLayout {
case av.LayoutTypeTable:
if view.Name == av.GetAttributeViewI18n("gallery") {
view.Name = av.GetAttributeViewI18n("table")
}
if nil != view.Table {
break
}
view.Table = av.NewLayoutTable()
switch view.LayoutType {
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.LayoutTypeGallery:
if view.Name == av.GetAttributeViewI18n("table") {
view.Name = av.GetAttributeViewI18n("gallery")
}
if nil != view.Gallery {
break
}
view.Gallery = av.NewLayoutGallery()
switch view.LayoutType {
case av.LayoutTypeTable:
for _, col := range view.Table.Columns {
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: &av.BaseField{ID: col.ID}})
}
}
}
view.LayoutType = newLayout
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)
attrView.ViewID = view.ID
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
}
}
}
if err = av.SaveAttributeView(attrView); nil != err {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
ReloadAttrView(avID)
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
}
}
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)
}
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)
}
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 setAttrViewDisplayFieldName(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.DisplayFieldName = 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) {
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 = av.CardSize(operation.Data.(float64))
}
err = av.SaveAttributeView(attrView)
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
}
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))
}
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()
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.ID = blockID
v.Block.Created = now
v.Block.Updated = now
}
v.IsDetached = true
v.CreatedAt = now
v.UpdatedAt = now
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: 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)
}
}
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
ReloadAttrView(avID)
return
}
func DuplicateDatabaseBlock(avID string) (newAvID, newBlockID string, err error) {
storageAvDir := filepath.Join(util.DataDir, "storage", "av")
oldAvPath := filepath.Join(storageAvDir, avID+".json")
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
}
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
}
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
}
newAvPath := filepath.Join(storageAvDir, newAvID+".json")
if err = filelock.WriteFile(newAvPath, data); err != nil {
logging.LogErrorf("write attribute view [%s] failed: %s", newAvID, err)
return
}
updateBoundBlockAvsAttribute([]string{newAvID})
return
}
func GetAttributeViewKeysByAvID(avID 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
}
for _, keyValues := range attrView.KeyValues {
key := keyValues.Key
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
}
if attrView.ViewID != viewID {
attrView.ViewID = viewID
if err = av.SaveAttributeView(attrView); err != nil {
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
}
func GetAttributeViewPrimaryKeyValues(avID, keyword string, page, pageSize int) (attributeViewName string, databaseBlockIDs []string, keyValues *av.KeyValues, 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()
var values []*av.Value
for _, kv := range keyValues.Values {
if !kv.IsDetached && !treenode.ExistBlockTree(kv.BlockID) {
continue
}
if strings.Contains(strings.ToLower(kv.String(true)), strings.ToLower(keyword)) {
values = append(values, kv)
}
}
keyValues.Values = values
if 1 > pageSize {
pageSize = 16
}
start := (page - 1) * pageSize
end := start + pageSize
if len(keyValues.Values) < end {
end = len(keyValues.Values)
}
keyValues.Values = keyValues.Values[start:end]
sort.Slice(keyValues.Values, func(i, j int) bool {
return keyValues.Values[i].Block.Updated > keyValues.Values[j].Block.Updated
})
return
}
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 {
view, err = attrView.GetCurrentView(attrView.ViewID)
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 SearchAttributeViewNonRelationKey(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 && av.KeyTypeRollup != keyValues.Key.Type && av.KeyTypeTemplate != keyValues.Key.Type && av.KeyTypeCreated != keyValues.Key.Type && av.KeyTypeUpdated != 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
}
type SearchAttributeViewResult struct {
AvID string `json:"avID"`
AvName string `json:"avName"`
BlockID string `json:"blockID"`
HPath string `json:"hPath"`
}
func SearchAttributeView(keyword string, excludeAvIDs []string) (ret []*SearchAttributeViewResult) {
waitForSyncingStorages()
ret = []*SearchAttributeViewResult{}
keyword = strings.TrimSpace(keyword)
keywords := strings.Fields(keyword)
type result struct {
AvID string
AvName string
AvUpdated int64
Score float64
}
var avs []*result
avDir := filepath.Join(util.DataDir, "storage", "av")
entries, err := os.ReadDir(avDir)
if err != nil {
logging.LogErrorf("read directory [%s] failed: %s", avDir, err)
return
}
avBlockRels := av.GetBlockRels()
for _, entry := range entries {
if entry.IsDir() {
continue
}
id := strings.TrimSuffix(entry.Name(), ".json")
if !ast.IsNodeIDPattern(id) {
continue
}
if nil == avBlockRels[id] {
continue
}
name, _ := av.GetAttributeViewNameByPath(filepath.Join(avDir, entry.Name()))
info, _ := entry.Info()
if "" != keyword {
score := 0.0
hit := false
for _, k := range keywords {
if strings.Contains(strings.ToLower(name), strings.ToLower(k)) {
score += smetrics.JaroWinkler(name, k, 0.7, 4)
hit = true
} else {
hit = false
break
}
}
if hit {
a := &result{AvID: id, AvName: name, Score: score}
if nil != info && !info.ModTime().IsZero() {
a.AvUpdated = info.ModTime().UnixMilli()
}
avs = append(avs, a)
}
} else {
a := &result{AvID: id, AvName: name}
if nil != info && !info.ModTime().IsZero() {
a.AvUpdated = info.ModTime().UnixMilli()
}
avs = append(avs, a)
}
}
if "" == keyword {
sort.Slice(avs, func(i, j int) bool { return avs[i].AvUpdated > avs[j].AvUpdated })
} else {
sort.SliceStable(avs, func(i, j int) bool {
if avs[i].Score == avs[j].Score {
return avs[i].AvUpdated > avs[j].AvUpdated
}
return avs[i].Score > avs[j].Score
})
}
if 12 <= len(avs) {
avs = avs[:12]
}
var avIDs []string
for _, a := range avs {
avIDs = append(avIDs, a.AvID)
}
avBlocks := treenode.BatchGetMirrorAttrViewBlocks(avIDs)
var blockIDs []string
for _, avBlock := range avBlocks {
blockIDs = append(blockIDs, avBlock.BlockIDs...)
}
blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs)
trees := filesys.LoadTrees(blockIDs)
for _, blockID := range blockIDs {
tree := trees[blockID]
if nil == tree {
continue
}
node := treenode.GetNodeInTree(tree, blockID)
if nil == node {
continue
}
if "" == node.AttributeViewID {
continue
}
avID := node.AttributeViewID
var existAv *result
for _, av := range avs {
if av.AvID == avID {
existAv = av
break
}
}
if nil == existAv {
continue
}
var hPath string
baseBlock := treenode.GetBlockTreeRootByPath(node.Box, node.Path)
if nil != baseBlock {
hPath = baseBlock.HPath
}
box := Conf.Box(node.Box)
if nil != box {
hPath = box.Name + hPath
}
if !gulu.Str.Contains(avID, excludeAvIDs) {
ret = append(ret, &SearchAttributeViewResult{
AvID: avID,
AvName: existAv.AvName,
BlockID: blockID,
HPath: hPath,
})
}
}
return
}
type BlockAttributeViewKeys struct {
AvID string `json:"avID"`
AvName string `json:"avName"`
BlockIDs []string `json:"blockIDs"`
KeyValues []*av.KeyValues `json:"keyValues"`
}
func GetBlockAttributeViewKeys(blockID string) (ret []*BlockAttributeViewKeys) {
waitForSyncingStorages()
ret = []*BlockAttributeViewKeys{}
attrs := sql.GetBlockAttrs(blockID)
avs := attrs[av.NodeAttrNameAvs]
if "" == avs {
return
}
attrViewCache := map[string]*av.AttributeView{}
avIDs := strings.Split(avs, ",")
for _, avID := range avIDs {
attrView := attrViewCache[avID]
if nil == attrView {
attrView, _ = av.ParseAttributeView(avID)
if nil == attrView {
unbindBlockAv(nil, avID, blockID)
return
}
attrViewCache[avID] = attrView
}
if 1 > len(attrView.Views) {
unbindBlockAv(nil, avID, blockID)
return
}
if !attrView.ExistBlock(blockID) {
// 比如剪切后粘贴,块 ID 会变,但是属性还在块上,这里做一次数据订正
// Auto verify the database name when clicking the block superscript icon https://github.com/siyuan-note/siyuan/issues/10861
unbindBlockAv(nil, avID, blockID)
return
}
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 == blockID {
kValues.Values = append(kValues.Values, v)
}
}
switch kValues.Key.Type {
case av.KeyTypeRollup:
kValues.Values = append(kValues.Values, &av.Value{ID: ast.NewNodeID(), KeyID: kValues.Key.ID, BlockID: blockID, Type: av.KeyTypeRollup, Rollup: &av.ValueRollup{Contents: []*av.Value{}}})
case av.KeyTypeTemplate:
kValues.Values = append(kValues.Values, &av.Value{ID: ast.NewNodeID(), KeyID: kValues.Key.ID, BlockID: blockID, Type: av.KeyTypeTemplate, Template: &av.ValueTemplate{Content: ""}})
case av.KeyTypeCreated:
kValues.Values = append(kValues.Values, &av.Value{ID: ast.NewNodeID(), KeyID: kValues.Key.ID, BlockID: blockID, Type: av.KeyTypeCreated})
case av.KeyTypeUpdated:
kValues.Values = append(kValues.Values, &av.Value{ID: ast.NewNodeID(), KeyID: kValues.Key.ID, BlockID: blockID, Type: av.KeyTypeUpdated})
case av.KeyTypeNumber:
for _, v := range kValues.Values {
if nil != v.Number {
v.Number.Format = kValues.Key.NumberFormat
v.Number.FormatNumber()
}
}
}
if 0 < len(kValues.Values) {
for _, v := range kValues.Values {
sql.FillAttributeViewNilValue(v, v.Type)
}
keyValues = append(keyValues, kValues)
} else {
// 如果没有值,那么就补一个默认值
kValues.Values = append(kValues.Values, av.GetAttributeViewDefaultValue(ast.NewNodeID(), kv.Key.ID, blockID, kv.Key.Type))
keyValues = append(keyValues, kValues)
}
}
// 渲染自动生成的列值,比如模板列、关联列、汇总列、创建时间列和更新时间列
// 先处理关联列、汇总列、创建时间列和更新时间列
for _, kv := range keyValues {
switch kv.Key.Type {
case av.KeyTypeBlock: // 对于主键可能需要填充静态锚文本 Database-bound block primary key supports setting static anchor text https://github.com/siyuan-note/siyuan/issues/10049
if nil != kv.Values[0].Block {
ial := sql.GetBlockAttrs(blockID)
if v := ial[av.NodeAttrViewStaticText+"-"+attrView.ID]; "" != v {
kv.Values[0].Block.Content = v
}
}
case av.KeyTypeRollup:
if nil == kv.Key.Rollup {
break
}
relKey, _ := attrView.GetKey(kv.Key.Rollup.RelationKeyID)
if nil == relKey {
break
}
relVal := attrView.GetValue(kv.Key.Rollup.RelationKeyID, kv.Values[0].BlockID)
if nil != relVal && nil != relVal.Relation {
destAv := attrViewCache[relKey.Relation.AvID]
if nil == destAv {
destAv, _ = av.ParseAttributeView(relKey.Relation.AvID)
if nil == destAv {
break
}
attrViewCache[relKey.Relation.AvID] = destAv
}
destKey, _ := destAv.GetKey(kv.Key.Rollup.KeyID)
if nil != destKey {
for _, bID := range relVal.Relation.BlockIDs {
destVal := destAv.GetValue(kv.Key.Rollup.KeyID, bID)
if nil == destVal {
if destAv.ExistBlock(bID) { // 数据库中存在行但是列值不存在是数据未初始化,这里补一个默认值
destVal = av.GetAttributeViewDefaultValue(ast.NewNodeID(), kv.Key.Rollup.KeyID, bID, destKey.Type)
}
if nil == destVal {
continue
}
}
if av.KeyTypeNumber == destKey.Type {
destVal.Number.Format = destKey.NumberFormat
destVal.Number.FormatNumber()
}
kv.Values[0].Rollup.Contents = append(kv.Values[0].Rollup.Contents, destVal.Clone())
}
kv.Values[0].Rollup.RenderContents(kv.Key.Rollup.Calc, destKey)
}
}
case av.KeyTypeRelation:
if nil == kv.Key.Relation {
break
}
destAv := attrViewCache[kv.Key.Relation.AvID]
if nil == destAv {
destAv, _ = av.ParseAttributeView(kv.Key.Relation.AvID)
if nil == destAv {
break
}
attrViewCache[kv.Key.Relation.AvID] = destAv
}
blocks := map[string]*av.Value{}
for _, blockValue := range destAv.GetBlockKeyValues().Values {
blocks[blockValue.BlockID] = blockValue
}
kv.Values[0].Relation.Contents = nil // 先清空 https://github.com/siyuan-note/siyuan/issues/10670
for _, bID := range kv.Values[0].Relation.BlockIDs {
kv.Values[0].Relation.Contents = append(kv.Values[0].Relation.Contents, blocks[bID])
}
case av.KeyTypeCreated:
createdStr := blockID[:len("20060102150405")]
created, parseErr := time.ParseInLocation("20060102150405", createdStr, time.Local)
if nil == parseErr {
kv.Values[0].Created = av.NewFormattedValueCreated(created.UnixMilli(), 0, av.CreatedFormatNone)
kv.Values[0].Created.IsNotEmpty = true
} else {
logging.LogWarnf("parse created [%s] failed: %s", createdStr, parseErr)
kv.Values[0].Created = av.NewFormattedValueCreated(time.Now().UnixMilli(), 0, av.CreatedFormatNone)
}
case av.KeyTypeUpdated:
ial := sql.GetBlockAttrs(blockID)
updatedStr := ial["updated"]
updated, parseErr := time.ParseInLocation("20060102150405", updatedStr, time.Local)
if nil == parseErr {
kv.Values[0].Updated = av.NewFormattedValueUpdated(updated.UnixMilli(), 0, av.UpdatedFormatNone)
kv.Values[0].Updated.IsNotEmpty = true
} else {
logging.LogWarnf("parse updated [%s] failed: %s", updatedStr, parseErr)
kv.Values[0].Updated = av.NewFormattedValueUpdated(time.Now().UnixMilli(), 0, av.UpdatedFormatNone)
}
}
}
// 再处理模板列
// 渲染模板
var renderTemplateErr error
for _, kv := range keyValues {
switch kv.Key.Type {
case av.KeyTypeTemplate:
if 0 < len(kv.Values) {
ial := map[string]string{}
block := av.GetKeyBlockValue(keyValues)
if nil != block && !block.IsDetached {
ial = sql.GetBlockAttrs(block.BlockID)
}
if nil == kv.Values[0].Template {
kv.Values[0] = av.GetAttributeViewDefaultValue(kv.Values[0].ID, kv.Key.ID, blockID, kv.Key.Type)
}
var renderErr error
kv.Values[0].Template.Content, renderErr = sql.RenderTemplateField(ial, keyValues, kv.Key.Template)
if nil != renderErr {
renderTemplateErr = fmt.Errorf("database [%s] template field [%s] rendering failed: %s", getAttrViewName(attrView), kv.Key.Name, renderErr)
}
}
}
}
if nil != renderTemplateErr {
util.PushErrMsg(fmt.Sprintf(Conf.Language(44), util.EscapeHTML(renderTemplateErr.Error())), 30000)
}
// 字段排序
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) {
// 老数据兼容处理
avBts := treenode.GetBlockTreesByType("av")
for _, avBt := range avBts {
if nil == avBt {
continue
}
tree, _ := LoadTreeByBlockID(avBt.ID)
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(blockID)
if nil != tree {
node := treenode.GetNodeInTree(tree, blockID)
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,
})
}
return
}
func RenderRepoSnapshotAttributeView(indexID, avID string) (viewable av.Viewable, attrView *av.AttributeView, err error) {
repo, err := newRepository()
if err != nil {
return
}
index, err := repo.GetIndex(indexID)
if err != nil {
return
}
files, err := repo.GetFiles(index)
if err != nil {
return
}
var avFile *entity.File
for _, f := range files {
if "/storage/av/"+avID+".json" == f.Path {
avFile = f
break
}
}
if nil == avFile {
attrView = av.NewAttributeView(avID)
} else {
data, readErr := repo.OpenFile(avFile)
if nil != readErr {
logging.LogErrorf("read attribute view [%s] failed: %s", avID, readErr)
return
}
attrView = &av.AttributeView{}
if err = gulu.JSON.UnmarshalJSON(data, attrView); err != nil {
logging.LogErrorf("unmarshal attribute view [%s] failed: %s", avID, err)
return
}
}
viewable, err = renderAttributeView(attrView, "", "", "", 1, -1, nil)
return
}
func RenderHistoryAttributeView(avID, created string) (viewable av.Viewable, attrView *av.AttributeView, err error) {
createdUnix, parseErr := strconv.ParseInt(created, 10, 64)
if nil != parseErr {
logging.LogErrorf("parse created [%s] failed: %s", created, parseErr)
return
}
dirPrefix := time.Unix(createdUnix, 0).Format("2006-01-02-150405")
globPath := filepath.Join(util.HistoryDir, dirPrefix+"*")
matches, err := filepath.Glob(globPath)
if err != nil {
logging.LogErrorf("glob [%s] failed: %s", globPath, err)
return
}
if 1 > len(matches) {
return
}
historyDir := matches[0]
avJSONPath := filepath.Join(historyDir, "storage", "av", avID+".json")
if !gulu.File.IsExist(avJSONPath) {
avJSONPath = filepath.Join(util.DataDir, "storage", "av", avID+".json")
}
if !gulu.File.IsExist(avJSONPath) {
attrView = av.NewAttributeView(avID)
} else {
data, readErr := os.ReadFile(avJSONPath)
if nil != readErr {
logging.LogErrorf("read attribute view [%s] failed: %s", avID, readErr)
return
}
attrView = &av.AttributeView{}
if err = gulu.JSON.UnmarshalJSON(data, attrView); err != nil {
logging.LogErrorf("unmarshal attribute view [%s] failed: %s", avID, err)
return
}
}
viewable, err = renderAttributeView(attrView, "", "", "", 1, -1, nil)
return
}
func RenderAttributeView(blockID, avID, viewID, query string, page, pageSize int, groupPaging map[string]interface{}) (viewable av.Viewable, attrView *av.AttributeView, err error) {
waitForSyncingStorages()
if avJSONPath := av.GetAttributeViewDataPath(avID); !filelock.IsExist(avJSONPath) {
attrView = av.NewAttributeView(avID)
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
}
attrView, err = av.ParseAttributeView(avID)
if err != nil {
logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
return
}
viewable, err = renderAttributeView(attrView, blockID, viewID, query, page, pageSize, groupPaging)
return
}
const (
groupValueDefault = "_@default@_" // 默认分组值(值为空的默认分组)
groupValueNotInRange = "_@notInRange@_" // 不再范围内的分组值(只有数字类型的分组才可能是该值)
groupValueLast30Days, groupValueLast7Days = "_@last30Days@_", "_@last7Days@_"
groupValueYesterday, groupValueToday, groupValueTomorrow = "_@yesterday@_", "_@today@_", "_@tomorrow@_"
groupValueNext7Days, groupValueNext30Days = "_@next7Days@_", "_@next30Days@_"
)
func renderAttributeView(attrView *av.AttributeView, blockID, viewID, query string, page, pageSize int, groupPaging map[string]interface{}) (viewable av.Viewable, err error) {
if 1 > len(attrView.Views) {
view, _, _ := av.NewTableViewWithBlockKey(ast.NewNodeID())
attrView.Views = append(attrView.Views, view)
attrView.ViewID = view.ID
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", attrView.ID, err)
return
}
}
if "" == viewID && "" != blockID {
node, _, getErr := getNodeByBlockID(nil, blockID)
if nil != getErr {
logging.LogWarnf("get node by block ID [%s] failed: %s", blockID, getErr)
} else {
viewID = node.IALAttr(av.NodeAttrView)
}
}
var view *av.View
if "" != viewID {
view, _ = attrView.GetCurrentView(viewID)
if nil != view && view.ID != attrView.ViewID {
attrView.ViewID = view.ID
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", attrView.ID, err)
return
}
}
} else {
view = attrView.GetView(attrView.ViewID)
}
if nil == view {
view = attrView.Views[0]
}
// 做一些数据兼容和订正处理
checkAttrView(attrView, view)
upgradeAttributeViewSpec(attrView)
viewable = sql.RenderView(attrView, view, query)
err = renderViewableInstance(viewable, view, attrView, page, pageSize)
if nil != err {
return
}
// 当前日期可能会变,所以如果是按日期分组则需要重新生成分组
if isGroupByDate(view) {
updatedDate := time.UnixMilli(view.GroupUpdated).Format("2006-01-02")
if time.Now().Format("2006-01-02") != updatedDate {
regenAttrViewViewGroups(attrView, "force")
av.SaveAttributeView(attrView)
}
}
// 如果存在分组的话渲染分组视图
if groupKey := view.GetGroupKey(attrView); nil != groupKey {
for _, groupView := range view.Groups {
switch groupView.GroupValue {
case groupValueDefault:
groupView.Name = fmt.Sprintf(Conf.language(264), groupKey.Name)
case groupValueNotInRange:
groupView.Name = Conf.language(265)
case groupValueLast30Days:
groupView.Name = fmt.Sprintf(Conf.language(259), 30)
case groupValueLast7Days:
groupView.Name = fmt.Sprintf(Conf.language(259), 7)
case groupValueYesterday:
groupView.Name = Conf.language(260)
case groupValueToday:
groupView.Name = Conf.language(261)
case groupValueTomorrow:
groupView.Name = Conf.language(262)
case groupValueNext7Days:
groupView.Name = fmt.Sprintf(Conf.language(263), 7)
case groupValueNext30Days:
groupView.Name = fmt.Sprintf(Conf.language(263), 30)
default:
groupView.Name = groupView.GroupValue
}
}
var groups []av.Viewable
for _, groupView := range view.Groups {
groupViewable := sql.RenderGroupView(attrView, view, groupView, query)
groupPage, groupPageSize := page, pageSize
if nil != groupPaging {
if paging := groupPaging[groupView.ID]; nil != paging {
pagingMap := paging.(map[string]interface{})
if nil != pagingMap["page"] {
groupPage = int(pagingMap["page"].(float64))
}
if nil != pagingMap["pageSize"] {
groupPageSize = int(pagingMap["pageSize"].(float64))
}
}
}
err = renderViewableInstance(groupViewable, view, attrView, groupPage, groupPageSize)
if nil != err {
return
}
groups = append(groups, groupViewable)
// 将分组视图的分组字段清空,减少冗余(字段信息可以在总的视图 view 对象上获取到)
switch groupView.LayoutType {
case av.LayoutTypeTable:
groupView.Table.Columns = nil
case av.LayoutTypeGallery:
groupView.Gallery.CardFields = nil
}
}
viewable.SetGroups(groups)
// 将总的视图上的项目清空,减少冗余
viewable.(av.Collection).SetItems(nil)
}
return
}
func genAttrViewViewGroups(view *av.View, attrView *av.AttributeView) {
if nil == view.Group {
return
}
// 临时记录每个分组视图的状态,以便后面重新生成分组后可以恢复这些状态
type GroupState struct {
Folded bool
Hidden int
Sort int
}
groupStates := map[string]*GroupState{}
for i, groupView := range view.Groups {
groupStates[groupView.GroupValue] = &GroupState{
Folded: groupView.GroupFolded,
Hidden: groupView.GroupHidden,
Sort: i,
}
}
group := view.Group
view.Groups = nil
viewable := sql.RenderView(attrView, view, "")
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 groupVal string
groupItemsMap := map[string][]av.Item{}
for _, item := range items {
value := item.GetValue(group.Field)
if value.IsEmpty() {
groupVal = groupValueDefault
groupItemsMap[groupVal] = append(groupItemsMap[groupVal], item)
continue
}
switch group.Method {
case av.GroupMethodValue:
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{}
}
}
}
for name, 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()
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.GroupValue = name
view.Groups = append(view.Groups, v)
}
view.GroupUpdated = time.Now().UnixMilli()
// 恢复分组视图状态
for _, groupView := range view.Groups {
if state, ok := groupStates[groupView.GroupValue]; ok {
groupView.GroupFolded = state.Folded
groupView.GroupHidden = state.Hidden
}
}
if av.GroupOrderMan == view.Group.Order {
// 恢复分组视图的自定义顺序
if len(groupStates) > 0 {
sort.SliceStable(view.Groups, func(i, j int) bool {
if stateI, ok := groupStates[view.Groups[i].GroupValue]; ok {
if stateJ, ok := groupStates[view.Groups[j].GroupValue]; ok {
return stateI.Sort < stateJ.Sort
}
}
return false
})
}
} else {
if av.GroupMethodDateRelative == view.Group.Method {
var relativeDateGroups []*av.View
var last30Days, last7Days, yesterday, today, tomorrow, next7Days, next30Days, defaultGroup *av.View
for _, groupView := range view.Groups {
_, err := time.Parse("2006-01", groupView.GroupValue)
if nil == err { // 如果能解析出来说明是 30 天之前或 30 天之后的分组形式
relativeDateGroups = append(relativeDateGroups, groupView)
} else { // 否则是相对日期分组形式
switch groupView.GroupValue {
case groupValueLast30Days:
last30Days = groupView
case groupValueLast7Days:
last7Days = groupView
case groupValueYesterday:
yesterday = groupView
case groupValueToday:
today = groupView
case groupValueTomorrow:
tomorrow = groupView
case groupValueNext7Days:
next7Days = groupView
case groupValueNext30Days:
next30Days = groupView
case groupValueDefault:
defaultGroup = groupView
}
}
}
sort.SliceStable(relativeDateGroups, func(i, j int) bool {
return relativeDateGroups[i].GroupValue < relativeDateGroups[j].GroupValue
})
var lastNext30Days []*av.View
if nil != next30Days {
lastNext30Days = append(lastNext30Days, next30Days)
}
if nil != next7Days {
lastNext30Days = append(lastNext30Days, next7Days)
}
if nil != tomorrow {
lastNext30Days = append(lastNext30Days, tomorrow)
}
if nil != today {
lastNext30Days = append(lastNext30Days, today)
}
if nil != yesterday {
lastNext30Days = append(lastNext30Days, yesterday)
}
if nil != last7Days {
lastNext30Days = append(lastNext30Days, last7Days)
}
if nil != last30Days {
lastNext30Days = append(lastNext30Days, last30Days)
}
startIdx := -1
thisMonth := todayStart.Format("2006-01")
for i, monthGroup := range relativeDateGroups {
if monthGroup.GroupValue < thisMonth {
startIdx = i + 1
}
}
if -1 == startIdx {
startIdx = 0
}
for _, g := range lastNext30Days {
relativeDateGroups = util.InsertElem(relativeDateGroups, startIdx, g)
}
if nil != defaultGroup {
relativeDateGroups = append([]*av.View{defaultGroup}, relativeDateGroups...)
}
if av.GroupOrderDesc == view.Group.Order {
slices.Reverse(relativeDateGroups)
}
view.Groups = relativeDateGroups
} else {
sort.SliceStable(view.Groups, func(i, j int) bool {
iVal, jVal := view.Groups[i].GroupValue, view.Groups[j].GroupValue
if av.GroupOrderAsc == view.Group.Order {
return util.NaturalCompare(iVal, jVal)
}
return util.NaturalCompare(jVal, iVal)
})
}
}
}
func isGroupByDate(view *av.View) bool {
if nil == view.Group {
return false
}
return av.GroupMethodDateDay == view.Group.Method || av.GroupMethodDateWeek == view.Group.Method || av.GroupMethodDateMonth == view.Group.Method || av.GroupMethodDateYear == view.Group.Method || av.GroupMethodDateRelative == view.Group.Method
}
func renderViewableInstance(viewable av.Viewable, view *av.View, attrView *av.AttributeView, page, pageSize int) (err error) {
if nil == viewable {
err = av.ErrViewNotFound
logging.LogErrorf("render attribute view [%s] failed", attrView.ID)
return
}
av.Filter(viewable, attrView)
av.Sort(viewable, attrView)
av.Calc(viewable, attrView)
// 分页
switch viewable.GetType() {
case av.LayoutTypeTable:
table := viewable.(*av.Table)
table.RowCount = len(table.Rows)
table.PageSize = view.PageSize
if 1 > pageSize {
pageSize = table.PageSize
}
start := (page - 1) * pageSize
end := start + pageSize
if len(table.Rows) < end {
end = len(table.Rows)
}
table.Rows = table.Rows[start:end]
case av.LayoutTypeGallery:
gallery := viewable.(*av.Gallery)
gallery.CardCount = len(gallery.Cards)
gallery.PageSize = view.PageSize
if 1 > pageSize {
pageSize = gallery.PageSize
}
start := (page - 1) * pageSize
end := start + pageSize
if len(gallery.Cards) < end {
end = len(gallery.Cards)
}
gallery.Cards = gallery.Cards[start:end]
}
return
}
func GetCurrentAttributeViewImages(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
if "" != viewID {
view, _ = attrView.GetCurrentView(viewID)
} else {
view = attrView.GetView(attrView.ViewID)
}
table := getAttrViewTable(attrView, view, query)
av.Filter(table, attrView)
av.Sort(table, attrView)
for _, row := range table.Rows {
for _, cell := range row.Cells {
if nil != cell.Value && av.KeyTypeMAsset == cell.Value.Type && nil != cell.Value.MAsset {
for _, a := range cell.Value.MAsset {
if av.AssetTypeImage == a.Type {
ret = append(ret, a.Content)
}
}
}
}
}
return
}
func (tx *Transaction) doUnbindAttrViewBlock(operation *Operation) (ret *TxErr) {
err := unbindAttributeViewBlock(operation, tx)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID}
}
return
}
func unbindAttributeViewBlock(operation *Operation, tx *Transaction) (err error) {
attrView, err := av.ParseAttributeView(operation.AvID)
if err != nil {
return
}
node, _, _ := getNodeByBlockID(tx, operation.ID)
if nil == node {
return
}
var changedAvIDs []string
for _, keyValues := range attrView.KeyValues {
for _, value := range keyValues.Values {
if av.KeyTypeRelation == value.Type {
if nil != value.Relation {
for i, relBlockID := range value.Relation.BlockIDs {
if relBlockID == operation.ID {
value.Relation.BlockIDs[i] = operation.NextID
changedAvIDs = append(changedAvIDs, attrView.ID)
}
}
}
}
if value.BlockID != operation.ID {
continue
}
if av.KeyTypeBlock == value.Type {
unbindBlockAv(tx, operation.AvID, value.BlockID)
}
value.BlockID = operation.NextID
value.IsDetached = true
if nil != value.Block {
value.Block.ID = operation.NextID
}
avIDs := replaceRelationAvValues(operation.AvID, operation.ID, operation.NextID)
changedAvIDs = append(changedAvIDs, avIDs...)
}
}
replacedRowID := false
for _, v := range attrView.Views {
for i, itemID := range v.ItemIDs {
if itemID == operation.ID {
v.ItemIDs[i] = operation.NextID
replacedRowID = true
break
}
}
if !replacedRowID {
v.ItemIDs = append(v.ItemIDs, operation.NextID)
}
}
err = av.SaveAttributeView(attrView)
changedAvIDs = gulu.Str.RemoveDuplicatedElem(changedAvIDs)
for _, avID := range changedAvIDs {
ReloadAttrView(avID)
}
return
}
func (tx *Transaction) doSetAttrViewColDate(operation *Operation) (ret *TxErr) {
err := setAttributeViewColDate(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewColDate(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) 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
}
rollUpKey.Rollup = &av.Rollup{
RelationKeyID: operation.ParentID,
KeyID: operation.KeyID,
}
if nil != operation.Data {
data := operation.Data.(map[string]interface{})
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
}
}
}
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
}
for _, keyValues := range srcAv.KeyValues {
if keyValues.Key.ID != operation.KeyID {
continue
}
srcRel := keyValues.Key.Relation
// 已经设置过双向关联的话需要先断开双向关联
if nil != srcRel {
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)
}
srcRel = &av.Relation{
AvID: operation.ID,
IsTwoWay: operation.IsTwoWay,
}
if operation.IsTwoWay {
srcRel.BackKeyID = operation.BackRelationKeyID
} else {
srcRel.BackKeyID = ""
}
keyValues.Key.Relation = srcRel
keyValues.Key.Name = operation.Format
break
}
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}})
}
}
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.Relation.BlockIDs = append(destVal.Relation.BlockIDs, srcVal.BlockID)
destVal.Relation.BlockIDs = gulu.Str.RemoveDuplicatedElem(destVal.Relation.BlockIDs)
regenAttrViewViewGroups(srcAv, destVal.KeyID)
destKeyValues.Values = append(destKeyValues.Values, destVal)
}
}
}
}
err = av.SaveAttributeView(srcAv)
if err != nil {
return
}
if !isSameAv {
err = av.SaveAttributeView(destAv)
ReloadAttrView(destAv.ID)
}
av.UpsertAvBackRel(srcAv.ID, destAv.ID)
if operation.IsTwoWay && !isSameAv {
av.UpsertAvBackRel(destAv.ID, srcAv.ID)
}
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 := getAttrViewViewByBlockID(attrView, operation.BlockID)
if nil == view {
logging.LogWarnf("get view failed: %s", operation.BlockID)
return
}
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]
attrView.ViewID = view.ID
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)
blockViewID := attrs[av.NodeAttrView]
if blockViewID == viewID {
attrs[av.NodeAttrView] = attrView.ViewID
node.AttributeViewType = string(view.LayoutType)
oldAttrs, e := setNodeAttrs0(node, attrs)
if nil != e {
logging.LogErrorf("set node attrs failed: %s", e)
continue
}
cache.PutBlockIAL(node.ID, parse.IAL2Map(node.KramdownIAL))
pushBroadcastAttrTransactions(oldAttrs, node)
}
}
for _, tree := range trees {
if err = indexWriteTreeUpsertQueue(tree); err != nil {
return
}
}
operation.RetData = view.LayoutType
return
}
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 (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}
}
attrs := parse.IAL2Map(node.KramdownIAL)
attrs[av.NodeAttrView] = operation.ID
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()
}
view.ID = operation.ID
attrView.Views = append(attrView.Views, view)
attrView.ViewID = view.ID
view.Icon = masterView.Icon
view.Name = util.GetDuplicateName(masterView.Name)
view.HideAttrViewName = masterView.HideAttrViewName
view.Desc = masterView.Desc
view.LayoutType = masterView.LayoutType
for _, filter := range masterView.Filters {
view.Filters = append(view.Filters, &av.ViewFilter{
Column: filter.Column,
Operator: filter.Operator,
Value: filter.Value,
RelativeDate: filter.RelativeDate,
RelativeDate2: filter.RelativeDate2,
})
}
for _, s := range masterView.Sorts {
view.Sorts = append(view.Sorts, &av.ViewSort{
Column: s.Column,
Order: s.Order,
})
}
if nil != masterView.Group {
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.PageSize = masterView.PageSize
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,
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,
},
})
}
view.Gallery.CoverFrom = masterView.Gallery.CoverFrom
view.Gallery.CoverFromAssetKeyID = masterView.Gallery.CoverFromAssetKeyID
view.Gallery.CardSize = masterView.Gallery.CardSize
view.Gallery.FitImage = masterView.Gallery.FitImage
view.Gallery.DisplayFieldName = masterView.Gallery.DisplayFieldName
view.Gallery.ShowIcon = masterView.Gallery.ShowIcon
view.Gallery.WrapField = masterView.Gallery.WrapField
}
view.ItemIDs = masterView.ItemIDs
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})
}
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.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}})
}
}
default:
err = av.ErrWrongLayoutType
logging.LogErrorf("wrong layout type [%s] for attribute view [%s]", layout, avID)
return
}
view.ItemIDs = firstView.ItemIDs
attrView.ViewID = viewID
view.ID = viewID
attrView.Views = append(attrView.Views, view)
node, tree, _ := getNodeByBlockID(nil, blockID)
if nil == node {
logging.LogErrorf("get node by block ID [%s] failed", blockID)
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
}
if err = av.SaveAttributeView(attrView); err != nil {
logging.LogErrorf("save attribute view [%s] failed: %s", avID, err)
return
}
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 = 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) 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
}
const attrAvNameTpl = `<span data-av-id="${avID}" data-popover-url="/api/av/getMirrorDatabaseBlocks" class="popover__block">${avName}</span>`
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))
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)
pushBroadcastAttrTransactions(oldAttrs, node)
}
return
}
func getAvNames(avIDs string) (ret string) {
if "" == avIDs {
return
}
avNames := bytes.Buffer{}
nodeAvIDs := strings.Split(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}", nodeAvName)
avNames.WriteString(tpl)
avNames.WriteString("&nbsp;")
}
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()
trees = map[string]*parse.Tree{}
for _, blockKeyValue := range blockKeyValues.Values {
if blockKeyValue.IsDetached {
continue
}
var tree *parse.Tree
tree = trees[blockKeyValue.BlockID]
if nil == tree {
if nil == tx {
tree, _ = LoadTreeByBlockID(blockKeyValue.BlockID)
} else {
tree, _ = tx.loadTree(blockKeyValue.BlockID)
}
}
if nil == tree {
continue
}
trees[blockKeyValue.BlockID] = tree
node := treenode.GetNodeInTree(tree, blockKeyValue.BlockID)
if nil == node {
continue
}
nodes = append(nodes, node)
}
return
}
func (tx *Transaction) doSetAttrViewFilters(operation *Operation) (ret *TxErr) {
err := setAttributeViewFilters(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewFilters(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.([]interface{})
data, err := gulu.JSON.MarshalJSON(operationData)
if err != nil {
return
}
if err = gulu.JSON.UnmarshalJSON(data, &view.Filters); err != nil {
return
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doSetAttrViewSorts(operation *Operation) (ret *TxErr) {
err := setAttributeViewSorts(operation)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func setAttributeViewSorts(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.([]interface{})
data, err := gulu.JSON.MarshalJSON(operationData)
if err != nil {
return
}
if err = gulu.JSON.UnmarshalJSON(data, &view.Sorts); err != nil {
return
}
err = av.SaveAttributeView(attrView)
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.(interface{})
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:
return
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doInsertAttrViewBlock(operation *Operation) (ret *TxErr) {
err := AddAttributeViewBlock(tx, operation.Srcs, operation.AvID, operation.BlockID, operation.GroupID, operation.PreviousID)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func AddAttributeViewBlock(tx *Transaction, srcs []map[string]interface{}, avID, blockID, groupID, previousBlockID string) (err error) {
slices.Reverse(srcs) // https://github.com/siyuan-note/siyuan/issues/11286
now := time.Now().UnixMilli()
for _, src := range srcs {
srcID := src["id"].(string)
if !ast.IsNodeIDPattern(srcID) {
continue
}
isDetached := src["isDetached"].(bool)
var tree *parse.Tree
if !isDetached {
var loadErr error
if nil != tx {
tree, loadErr = tx.loadTree(srcID)
} else {
tree, loadErr = LoadTreeByBlockID(srcID)
}
if nil != loadErr {
logging.LogErrorf("load tree [%s] failed: %s", srcID, loadErr)
return loadErr
}
}
var srcContent string
if nil != src["content"] {
srcContent = src["content"].(string)
}
if avErr := addAttributeViewBlock(now, avID, blockID, groupID, previousBlockID, srcID, srcContent, isDetached, tree, tx); nil != avErr {
return avErr
}
}
return
}
func addAttributeViewBlock(now int64, avID, blockID, groupID, previousBlockID, addingBlockID, addingBlockContent string, isDetached bool, tree *parse.Tree, tx *Transaction) (err error) {
var node *ast.Node
if !isDetached {
node = treenode.GetNodeInTree(tree, addingBlockID)
if nil == node {
err = ErrBlockNotFound
return
}
} else {
if "" == addingBlockID {
addingBlockID = ast.NewNodeID()
logging.LogWarnf("detached block id is empty, generate a new one [%s]", addingBlockID)
}
}
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
var blockIcon string
if !isDetached {
blockIcon, addingBlockContent = getNodeAvBlockText(node)
addingBlockContent = util.UnescapeHTML(addingBlockContent)
}
// 检查是否重复添加相同的块
blockValues := attrView.GetBlockKeyValues()
for _, blockValue := range blockValues.Values {
if blockValue.Block.ID == addingBlockID {
if !isDetached {
// 重复绑定一下,比如剪切数据库块、取消绑定块后再次添加的场景需要
bindBlockAv0(tx, avID, node, tree)
blockValue.IsDetached = isDetached
blockValue.Block.Icon = blockIcon
blockValue.Block.Content = addingBlockContent
blockValue.UpdatedAt = now
err = av.SaveAttributeView(attrView)
}
return
}
}
blockValue := &av.Value{
ID: ast.NewNodeID(),
KeyID: blockValues.Key.ID,
BlockID: addingBlockID,
Type: av.KeyTypeBlock,
IsDetached: isDetached,
CreatedAt: now,
UpdatedAt: now,
Block: &av.ValueBlock{ID: addingBlockID, Icon: blockIcon, Content: addingBlockContent, Created: now, Updated: now}}
blockValues.Values = append(blockValues.Values, blockValue)
view, err := getAttrViewViewByBlockID(attrView, blockID)
if nil != err {
logging.LogErrorf("get view by block ID [%s] failed: %s", blockID, err)
return
}
groupView := view
if "" != groupID {
groupView = view.GetGroup(groupID)
}
defaultValues := getAttrViewAddingBlockDefaultValues(attrView, view, groupView, previousBlockID, addingBlockID)
for keyID, newValue := range defaultValues {
keyValues, getErr := attrView.GetKeyValues(keyID)
if nil != getErr {
continue
}
if av.KeyTypeBlock == newValue.Type {
// 如果是主键的话前面已经添加过了,这里仅修改内容
blockValue.Block.Content = newValue.Block.Content
continue
}
if (av.KeyTypeSelect == newValue.Type || av.KeyTypeMSelect == newValue.Type) && 1 > len(newValue.MSelect) {
// 单选或多选类型的值可能需要从分组条件中获取默认值
if opt := keyValues.Key.GetOption(groupView.GroupValue); nil != opt && groupValueDefault != groupView.GroupValue {
newValue.MSelect = append(newValue.MSelect, &av.ValueSelect{Content: opt.Name, Color: opt.Color})
}
}
newValue.BlockID = addingBlockID
keyValues.Values = append(keyValues.Values, newValue)
}
// 处理日期字段默认填充当前创建时间
// The database date field supports filling the current time by default https://github.com/siyuan-note/siyuan/issues/10823
for _, keyValues := range attrView.KeyValues {
if av.KeyTypeDate == keyValues.Key.Type && nil != keyValues.Key.Date && keyValues.Key.Date.AutoFillNow {
dateVal := &av.Value{
ID: ast.NewNodeID(), KeyID: keyValues.Key.ID, BlockID: addingBlockID, Type: av.KeyTypeDate, IsDetached: isDetached, CreatedAt: now, UpdatedAt: now + 1000,
Date: &av.ValueDate{Content: now, IsNotEmpty: true},
}
keyValues.Values = append(keyValues.Values, dateVal)
}
}
if !isDetached {
bindBlockAv0(tx, avID, node, tree)
}
// 在所有视图上添加项目
for _, v := range attrView.Views {
if "" != previousBlockID {
changed := false
for i, id := range v.ItemIDs {
if id == previousBlockID {
v.ItemIDs = append(v.ItemIDs[:i+1], append([]string{addingBlockID}, v.ItemIDs[i+1:]...)...)
changed = true
break
}
}
if !changed {
v.ItemIDs = append(v.ItemIDs, addingBlockID)
}
} else {
v.ItemIDs = append([]string{addingBlockID}, v.ItemIDs...)
}
for _, g := range v.Groups {
if "" != previousBlockID {
changed := false
for i, id := range g.GroupItemIDs {
if id == previousBlockID {
g.GroupItemIDs = append(g.GroupItemIDs[:i+1], append([]string{addingBlockID}, g.GroupItemIDs[i+1:]...)...)
changed = true
break
}
}
if !changed {
g.GroupItemIDs = append(g.GroupItemIDs, addingBlockID)
}
} else {
g.GroupItemIDs = append([]string{addingBlockID}, g.GroupItemIDs...)
}
}
}
groupKey := view.GetGroupKey(attrView)
if nil != groupKey {
regenAttrViewViewGroups(attrView, groupKey.ID)
}
err = av.SaveAttributeView(attrView)
return
}
func getNewValueByNearItem(nearItem av.Item, key *av.Key, addingBlockID string) (ret *av.Value) {
if nil != nearItem {
defaultVal := nearItem.GetValue(key.ID)
ret = defaultVal.Clone()
ret.ID = ast.NewNodeID()
ret.KeyID = key.ID
ret.BlockID = addingBlockID
ret.CreatedAt = util.CurrentTimeMillis()
ret.UpdatedAt = ret.CreatedAt + 1000
return
}
return av.GetAttributeViewDefaultValue(ast.NewNodeID(), key.ID, addingBlockID, key.Type)
}
func getNearItem(attrView *av.AttributeView, view, groupView *av.View, previousItemID string) (ret av.Item) {
viewable := sql.RenderGroupView(attrView, view, groupView, "")
av.Filter(viewable, attrView)
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
}
trees := map[string]*parse.Tree{}
for _, keyValues := range attrView.KeyValues {
tmp := keyValues.Values[:0]
for i, values := range keyValues.Values {
if !gulu.Str.Contains(values.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 bt := treenode.GetBlockTree(values.BlockID); nil != bt {
tree := trees[bt.RootID]
if nil == tree {
tree, _ = LoadTreeByBlockID(values.BlockID)
}
if nil != tree {
trees[bt.RootID] = tree
if node := treenode.GetNodeInTree(tree, values.BlockID); 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)
}
}
regenAttrViewViewGroups(attrView, "force")
relatedAvIDs := av.GetSrcAvIDs(avID)
for _, relatedAvID := range relatedAvIDs {
ReloadAttrView(relatedAvID)
}
err = av.SaveAttributeView(attrView)
if nil != err {
return
}
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 := filepath.Join(util.DataDir, "storage", "av", avID+".json")
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 removeNodeAvID(node *ast.Node, avID string, tx *Transaction, tree *parse.Tree) (err error) {
attrs := parse.IAL2Map(node.KramdownIAL)
if ast.NodeDocument == node.Type {
delete(attrs, "custom-hidden")
node.RemoveIALAttr("custom-hidden")
}
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
}
}
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 (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 || av.KeyTypeRollup == 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,
},
}, 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,
},
},
}, view.Gallery.CardFields[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 = operation.Data.(string)
break
}
}
case av.LayoutTypeGallery:
return
}
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
}
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
}
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.Hidden = operation.Data.(bool)
break
}
}
case av.LayoutTypeGallery:
for _, field := range view.Gallery.CardFields {
if field.ID == operation.ID {
field.Hidden = operation.Data.(bool)
break
}
}
}
err = av.SaveAttributeView(attrView)
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:
for _, column := range view.Table.Columns {
if column.ID == operation.ID {
column.Pin = operation.Data.(bool)
break
}
}
case av.LayoutTypeGallery:
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 = 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
}
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.GetGroup(operation.GroupID); nil != groupView {
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 operation.GroupID != operation.TargetGroupID { // 跨分组排序
if targetGroupView := view.GetGroup(operation.TargetGroupID); nil != targetGroupView {
groupKey := view.GetGroupKey(attrView)
nearItem := getNearItem(attrView, view, targetGroupView, operation.PreviousID)
newValue := getNewValueByNearItem(nearItem, groupKey, operation.ID)
val := attrView.GetValue(groupKey.ID, operation.ID)
newValueRaw := newValue.GetValByType(groupKey.Type)
val.SetValByType(groupKey.Type, newValueRaw)
for i, r := range targetGroupView.GroupItemIDs {
if r == operation.PreviousID {
previousIndex = i + 1
break
}
}
targetGroupView.GroupItemIDs = util.InsertElem(targetGroupView.GroupItemIDs, previousIndex, itemID)
}
} 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)
}
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.ID, operation.Name, operation.Typ, icon, operation.PreviousID)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func AddAttributeViewKey(avID, keyID, keyName, keyType, keyIcon, previousKeyID string) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
currentView, err := attrView.GetCurrentView(attrView.ViewID)
if nil != err {
return
}
keyTyp := av.KeyType(keyType)
switch keyTyp {
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, keyIcon, keyTyp)
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 {
if nil != view.Table {
if "" == previousKeyID {
if av.LayoutTypeGallery == currentView.LayoutType {
// 如果当前视图是卡片视图则添加到最后
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: &av.BaseField{ID: key.ID}})
} else {
view.Table.Columns = append([]*av.ViewTableColumn{{BaseField: &av.BaseField{ID: key.ID}}}, 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: &av.BaseField{ID: key.ID}}}, view.Table.Columns[i+1:]...)...)
added = true
break
}
}
if !added {
view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: &av.BaseField{ID: key.ID}})
}
}
}
if nil != view.Gallery {
if "" == previousKeyID {
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: &av.BaseField{ID: key.ID}})
} 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: &av.BaseField{ID: key.ID}}}, view.Gallery.CardFields[i+1:]...)...)
added = true
break
}
}
if !added {
view.Gallery.CardFields = append(view.Gallery.CardFields, &av.ViewGalleryCardField{BaseField: &av.BaseField{ID: key.ID}})
}
}
}
}
}
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
}
}
}
regenAttrViewViewGroups(attrView, operation.ID)
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) 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 {
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 {
for _, view := range attrView.Views {
if nil != view.Group {
if groupKey := view.GetGroupKey(attrView); nil != groupKey && groupKey.ID == operation.ID {
removeAttributeViewGroup0(view)
}
}
}
}
err = av.SaveAttributeView(attrView)
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
}
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
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:]...)
}
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
}
}
}
}
}
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)
}
}
}
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
}
}
}
}
for _, view := range attrView.Views {
if nil != view.Group {
if groupKey := view.GetGroupKey(attrView); nil != groupKey && groupKey.ID == keyID {
removeAttributeViewGroup0(view)
}
}
}
err = av.SaveAttributeView(attrView)
return
}
func (tx *Transaction) doReplaceAttrViewBlock(operation *Operation) (ret *TxErr) {
err := replaceAttributeViewBlock(operation.AvID, operation.PreviousID, operation.NextID, operation.IsDetached, tx)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID}
}
return
}
func replaceAttributeViewBlock(avID, oldBlockID, newBlockID string, isDetached bool, tx *Transaction) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
if 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, newBlockID string, isDetached bool, tx *Transaction) (err error) {
avID := attrView.ID
var node *ast.Node
var tree *parse.Tree
if !isDetached {
node, tree, _ = getNodeByBlockID(tx, newBlockID)
}
now := util.CurrentTimeMillis()
// 检查是否已经存在绑定块,如果存在的话则重新绑定
for _, keyValues := range attrView.KeyValues {
for _, value := range keyValues.Values {
if av.KeyTypeBlock == value.Type && nil != value.Block && value.BlockID == newBlockID {
if !isDetached {
bindBlockAv0(tx, avID, node, tree)
value.IsDetached = false
icon, content := getNodeAvBlockText(node)
content = util.UnescapeHTML(content)
value.Block.Icon, value.Block.Content = icon, content
value.UpdatedAt = now
regenAttrViewViewGroups(attrView, value.KeyID)
err = av.SaveAttributeView(attrView)
}
return
}
}
}
var changedAvIDs []string
for _, keyValues := range attrView.KeyValues {
for _, value := range keyValues.Values {
if av.KeyTypeRelation == value.Type {
if nil != value.Relation {
for i, relBlockID := range value.Relation.BlockIDs {
if relBlockID == oldBlockID {
value.Relation.BlockIDs[i] = newBlockID
changedAvIDs = append(changedAvIDs, attrView.ID)
}
}
}
}
if value.BlockID != oldBlockID {
continue
}
if av.KeyTypeBlock == value.Type && value.BlockID != newBlockID {
// 换绑
unbindBlockAv(tx, avID, value.BlockID)
}
value.BlockID = newBlockID
if av.KeyTypeBlock == value.Type && nil != value.Block {
value.Block.ID = newBlockID
value.IsDetached = isDetached
if !isDetached {
icon, content := getNodeAvBlockText(node)
content = util.UnescapeHTML(content)
value.Block.Icon, value.Block.Content = icon, content
}
}
if av.KeyTypeBlock == value.Type && !isDetached {
bindBlockAv(tx, avID, newBlockID)
avIDs := replaceRelationAvValues(avID, oldBlockID, newBlockID)
changedAvIDs = append(changedAvIDs, avIDs...)
}
}
}
replacedRowID := false
for _, v := range attrView.Views {
for i, itemID := range v.ItemIDs {
if itemID == oldBlockID {
v.ItemIDs[i] = newBlockID
replacedRowID = true
break
}
}
if !replacedRowID {
v.ItemIDs = append(v.ItemIDs, newBlockID)
}
}
changedAvIDs = gulu.Str.RemoveDuplicatedElem(changedAvIDs)
for _, id := range changedAvIDs {
ReloadAttrView(id)
}
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, newBlockID := range oldNewMap {
if err = replaceAttributeViewBlock0(attrView, oldBlockID, newBlockID, isDetached, nil); nil != err {
return
}
}
}
if err = av.SaveAttributeView(attrView); nil != err {
return
}
return
}
func (tx *Transaction) doUpdateAttrViewCell(operation *Operation) (ret *TxErr) {
err := updateAttributeViewCell(operation, tx)
if err != nil {
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
}
return
}
func updateAttributeViewCell(operation *Operation, tx *Transaction) (err error) {
_, err = UpdateAttributeViewCell(tx, operation.AvID, operation.KeyID, operation.RowID, operation.Data)
return
}
func BatchUpdateAttributeViewCells(tx *Transaction, avID string, values []interface{}) (err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
for _, value := range values {
v := value.(map[string]interface{})
keyID := v["keyID"].(string)
rowID := v["rowID"].(string)
valueData := v["value"]
_, err = updateAttributeViewValue(tx, attrView, keyID, rowID, valueData)
if err != nil {
return
}
}
if err = av.SaveAttributeView(attrView); err != nil {
return
}
relatedAvIDs := av.GetSrcAvIDs(avID)
for _, relatedAvID := range relatedAvIDs {
ReloadAttrView(relatedAvID)
}
return
}
func UpdateAttributeViewCell(tx *Transaction, avID, keyID, rowID string, valueData interface{}) (val *av.Value, err error) {
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return
}
val, err = updateAttributeViewValue(tx, attrView, keyID, rowID, valueData)
if nil != err {
return
}
if err = av.SaveAttributeView(attrView); err != nil {
return
}
relatedAvIDs := av.GetSrcAvIDs(avID)
for _, relatedAvID := range relatedAvIDs {
ReloadAttrView(relatedAvID)
}
return
}
func updateAttributeViewValue(tx *Transaction, attrView *av.AttributeView, keyID, rowID string, valueData interface{}) (val *av.Value, err error) {
avID := attrView.ID
var blockVal *av.Value
for _, kv := range attrView.KeyValues {
if av.KeyTypeBlock == kv.Key.Type {
for _, v := range kv.Values {
if rowID == v.Block.ID {
blockVal = v
break
}
}
break
}
}
now := time.Now().UnixMilli()
oldIsDetached := true
if nil != blockVal {
oldIsDetached = blockVal.IsDetached
}
for _, keyValues := range attrView.KeyValues {
if keyID != keyValues.Key.ID {
continue
}
for _, value := range keyValues.Values {
if rowID == value.BlockID {
val = value
val.Type = keyValues.Key.Type
break
}
}
if nil == val {
val = &av.Value{ID: ast.NewNodeID(), KeyID: keyID, BlockID: rowID, Type: keyValues.Key.Type, CreatedAt: now, UpdatedAt: now}
keyValues.Values = append(keyValues.Values, val)
}
break
}
isUpdatingBlockKey := av.KeyTypeBlock == val.Type
oldBoundBlockID := val.BlockID
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
}
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 && !val.Date.IsNotEmpty {
val.Date.Content = 0
val.Date.FormattedContent = ""
}
} else if av.KeyTypeSelect == val.Type || av.KeyTypeMSelect == val.Type {
if nil != key && 0 < len(val.MSelect) {
// 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 := 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 len(oldRelationBlockIDs) == len(val.Relation.BlockIDs) {
relationChangeMode = 0
} else {
if len(oldRelationBlockIDs) > len(val.Relation.BlockIDs) {
relationChangeMode = 2
} else {
relationChangeMode = 1
}
}
}
// val.IsDetached 只有更新主键的时候才会传入,所以下面需要结合 isUpdatingBlockKey 来判断
if oldIsDetached {
// 之前是游离行
if !val.IsDetached { // 现在绑定了块
// 将游离行绑定到新建的块上
bindBlockAv(tx, avID, rowID)
if nil != val.Block {
val.BlockID = val.Block.ID
}
}
} else {
// 之前绑定了块
if isUpdatingBlockKey { // 正在更新主键
if val.IsDetached { // 现在是游离行
// 将绑定的块从属性视图中移除
unbindBlockAv(tx, avID, rowID)
} else {
// 现在也绑定了块
if oldBoundBlockID != val.BlockID { // 之前绑定的块和现在绑定的块不一样
// 换绑块
unbindBlockAv(tx, avID, oldBoundBlockID)
bindBlockAv(tx, avID, val.BlockID)
val.Block.Content = util.UnescapeHTML(val.Block.Content)
} else { // 之前绑定的块和现在绑定的块一样
content := strings.TrimSpace(val.Block.Content)
node, tree, _ := getNodeByBlockID(tx, val.BlockID)
updateStaticText := true
_, blockText := getNodeAvBlockText(node)
if "" == content {
val.Block.Content = util.UnescapeHTML(blockText)
} else {
if blockText == content {
updateStaticText = false
} else {
val.Block.Content = content
}
}
if updateStaticText {
// 设置静态锚文本 Database-bound block primary key supports setting static anchor text https://github.com/siyuan-note/siyuan/issues/10049
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)
regenAttrViewViewGroups(attrView, keyID)
if nil != key && av.KeyTypeRelation == key.Type && nil != key.Relation && key.Relation.IsTwoWay {
// 双向关联需要同时更新目标字段的值
var destAv *av.AttributeView
if avID == key.Relation.AvID {
destAv = attrView
} else {
destAv, _ = av.ParseAttributeView(key.Relation.AvID)
}
if nil != destAv {
// relationChangeMode
// 0关联列值不变仅排序不影响目标值
// 1关联列值增加增加目标值
// 2关联列值减少减少目标值
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 != key.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, rowID)
destVal.Relation.BlockIDs = gulu.Str.RemoveDuplicatedElem(destVal.Relation.BlockIDs)
regenAttrViewViewGroups(destAv, key.Relation.BackKeyID)
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 != key.Relation.BackKeyID {
continue
}
for _, value := range keyValues.Values {
if value.BlockID == blockID {
value.Relation.BlockIDs = gulu.Str.RemoveElem(value.Relation.BlockIDs, rowID)
value.SetUpdatedAt(now)
regenAttrViewViewGroups(destAv, key.Relation.BackKeyID)
break
}
}
}
}
}
if destAv != attrView {
av.SaveAttributeView(destAv)
}
}
}
return
}
func regenAttrViewViewGroups(attrView *av.AttributeView, keyID string) {
for _, view := range attrView.Views {
groupKey := view.GetGroupKey(attrView)
if nil == groupKey {
continue
}
if "force" != keyID {
if av.KeyTypeTemplate != groupKey.Type && av.KeyTypeCreated != groupKey.Type && av.KeyTypeUpdated != groupKey.Type &&
view.Group.Field != keyID {
continue
}
}
genAttrViewViewGroups(view, attrView)
for _, g := range view.Groups {
if view.Group.HideEmpty {
if 1 > len(g.GroupItemIDs) {
if 0 == g.GroupHidden {
g.GroupHidden = 1
}
} else {
g.GroupHidden = 0
}
} else {
if 2 != g.GroupHidden {
g.GroupHidden = 0
}
}
}
}
}
func unbindBlockAv(tx *Transaction, avID, blockID string) {
node, tree, err := getNodeByBlockID(tx, blockID)
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", blockID, 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) {
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
}
for _, keyValues := range attrView.KeyValues {
if keyValues.Key.ID == operation.ID {
keyValues.Key.Options = options
err = av.SaveAttributeView(attrView)
return
}
}
return
}
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
}
}
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
}
regenAttrViewViewGroups(attrView, operation.ID)
err = av.SaveAttributeView(attrView)
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]interface{})
rename := false
oldName := strings.TrimSpace(data["oldName"].(string))
newName := strings.TrimSpace(data["newName"].(string))
newDesc := strings.TrimSpace(data["newDesc"].(string))
newColor := 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 !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 {
for _, filter := range view.Filters {
if filter.Column != key.ID {
continue
}
if nil != filter.Value && (av.KeyTypeSelect == filter.Value.Type || av.KeyTypeMSelect == filter.Value.Type) {
for i, opt := range filter.Value.MSelect {
if oldName == opt.Content {
filter.Value.MSelect[i].Content = newName
filter.Value.MSelect[i].Color = newColor
break
}
}
}
}
}
regenAttrViewViewGroups(attrView, operation.ID)
err = av.SaveAttributeView(attrView)
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]interface{})
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 getAttrViewViewByBlockID(attrView *av.AttributeView, blockID string) (ret *av.View, err error) {
var viewID string
var node *ast.Node
if "" != blockID {
node, _, _ = getNodeByBlockID(nil, blockID)
}
if nil != node {
viewID = node.IALAttr(av.NodeAttrView)
}
return attrView.GetCurrentView(viewID)
}
func getAttrViewName(attrView *av.AttributeView) string {
ret := strings.TrimSpace(attrView.Name)
if "" == ret {
ret = Conf.language(105)
}
return ret
}
func replaceRelationAvValues(avID, previousID, nextID string) (changedSrcAvID []string) {
// The database relation fields follow the change after the primary key field is changed https://github.com/siyuan-note/siyuan/issues/11117
srcAvIDs := av.GetSrcAvIDs(avID)
for _, srcAvID := range srcAvIDs {
srcAv, parseErr := av.ParseAttributeView(srcAvID)
changed := false
if nil != parseErr {
continue
}
for _, srcKeyValues := range srcAv.KeyValues {
if av.KeyTypeRelation != srcKeyValues.Key.Type {
continue
}
if nil == srcKeyValues.Key.Relation || avID != srcKeyValues.Key.Relation.AvID {
continue
}
for _, srcValue := range srcKeyValues.Values {
if nil == srcValue.Relation {
continue
}
srcAvChanged := false
srcValue.Relation.BlockIDs, srcAvChanged = util.ReplaceStr(srcValue.Relation.BlockIDs, previousID, nextID)
if srcAvChanged {
regenAttrViewViewGroups(srcAv, srcValue.KeyID)
changed = true
}
}
}
if changed {
av.SaveAttributeView(srcAv)
changedSrcAvID = append(changedSrcAvID, srcAvID)
}
}
return
}
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()
for _, blockValue := range blockKeyValues.Values {
if blockValue.IsDetached {
continue
}
bt := treenode.GetBlockTree(blockValue.BlockID)
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, blockValue.BlockID)
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)
if nil != setErr {
continue
}
cache.PutBlockIAL(node.ID, parse.IAL2Map(node.KramdownIAL))
pushBroadcastAttrTransactions(oldAttrs, node)
}
}
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)
}
}