mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-09-22 00:20:47 +02:00
🎨 Database grouping by field https://github.com/siyuan-note/siyuan/issues/10964
This commit is contained in:
parent
759c12be06
commit
cff71aa720
7 changed files with 139 additions and 72 deletions
|
@ -292,10 +292,14 @@ func addAttributeViewBlocks(c *gin.Context) {
|
|||
}
|
||||
|
||||
avID := arg["avID"].(string)
|
||||
blockID := ""
|
||||
var blockID string
|
||||
if blockIDArg := arg["blockID"]; nil != blockIDArg {
|
||||
blockID = blockIDArg.(string)
|
||||
}
|
||||
var groupID string
|
||||
if groupIDArg := arg["groupID"]; nil != groupIDArg {
|
||||
groupID = groupIDArg.(string)
|
||||
}
|
||||
var previousID string
|
||||
if nil != arg["previousID"] {
|
||||
previousID = arg["previousID"].(string)
|
||||
|
@ -310,7 +314,7 @@ func addAttributeViewBlocks(c *gin.Context) {
|
|||
src := v.(map[string]interface{})
|
||||
srcs = append(srcs, src)
|
||||
}
|
||||
err := model.AddAttributeViewBlock(nil, srcs, avID, blockID, previousID, ignoreFillFilter)
|
||||
err := model.AddAttributeViewBlock(nil, srcs, avID, blockID, groupID, previousID, ignoreFillFilter)
|
||||
if err != nil {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
|
|
|
@ -197,11 +197,28 @@ type View struct {
|
|||
Groups []*View `json:"groups,omitempty"` // 分组视图列表
|
||||
GroupItemIDs []string `json:"groupItemIds"` // 分组项目 ID 列表,用于维护分组中的所有项目
|
||||
GroupCalc *GroupCalc `json:"groupCalc,omitempty"` // 分组计算规则
|
||||
GroupName string `json:"groupName,omitempty"` // 分组名称
|
||||
GroupValue string `json:"groupValue,omitempty"` // 分组值
|
||||
GroupFolded bool `json:"groupFolded"` // 分组是否折叠
|
||||
GroupHidden int `json:"groupHidden"` // 分组是否隐藏,0:显示,1:空白隐藏,2:手动隐藏
|
||||
}
|
||||
|
||||
const (
|
||||
GroupValueDefault = "_@default@_" // 默认分组值(值为空的默认分组)
|
||||
GroupValueNotInRange = "_@notInRange@_" // 不再范围内的分组值(只有数字类型的分组才可能是该值)
|
||||
)
|
||||
|
||||
// GetGroup 获取指定分组 ID 的分组视图。
|
||||
func (view *View) GetGroup(groupID string) *View {
|
||||
if nil == view.Groups {
|
||||
return nil
|
||||
}
|
||||
for _, group := range view.Groups {
|
||||
if group.ID == groupID {
|
||||
return group
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GroupCalc 描述了分组计算规则和结果的结构。
|
||||
type GroupCalc struct {
|
||||
Field string `json:"field"` // 字段 ID
|
||||
|
@ -283,9 +300,6 @@ type Viewable interface {
|
|||
// GetGroupCalc 获取视图分组计算规则和结果。
|
||||
GetGroupCalc() *GroupCalc
|
||||
|
||||
// SetGroupName 设置分组名称。
|
||||
SetGroupName(name string)
|
||||
|
||||
// SetGroupFolded 设置分组是否折叠。
|
||||
SetGroupFolded(folded bool)
|
||||
|
||||
|
|
|
@ -866,8 +866,8 @@ func (filter *ViewFilter) GetAffectValue(key *Key, defaultVal *Value) (ret *Valu
|
|||
return
|
||||
}
|
||||
}
|
||||
// 没有默认值则使用过滤条件的值
|
||||
|
||||
// 没有默认值则使用过滤条件的值
|
||||
switch filter.Value.Type {
|
||||
case KeyTypeBlock:
|
||||
switch filter.Operator {
|
||||
|
|
|
@ -66,7 +66,6 @@ type BaseInstance struct {
|
|||
|
||||
Groups []Viewable `json:"groups,omitempty"` // 分组实例列表
|
||||
GroupCalc *GroupCalc `json:"groupCalc,omitempty"` // 分组计算规则和结果
|
||||
GroupName string `json:"groupName,omitempty"` // 分组名称
|
||||
GroupFolded bool `json:"groupFolded"` // 分组是否折叠
|
||||
GroupHidden int `json:"groupHidden"` // 分组是否隐藏,0:显示,1:空白隐藏,2:手动隐藏
|
||||
}
|
||||
|
@ -91,7 +90,6 @@ func NewViewBaseInstance(view *View) *BaseInstance {
|
|||
Sorts: view.Sorts,
|
||||
Group: view.Group,
|
||||
GroupCalc: view.GroupCalc,
|
||||
GroupName: view.GroupName,
|
||||
GroupFolded: view.GroupFolded,
|
||||
GroupHidden: view.GroupHidden,
|
||||
ShowIcon: showIcon,
|
||||
|
@ -119,10 +117,6 @@ func (baseInstance *BaseInstance) GetGroupCalc() *GroupCalc {
|
|||
return baseInstance.GroupCalc
|
||||
}
|
||||
|
||||
func (baseInstance *BaseInstance) SetGroupName(name string) {
|
||||
baseInstance.GroupName = name
|
||||
}
|
||||
|
||||
func (baseInstance *BaseInstance) SetGroupFolded(folded bool) {
|
||||
baseInstance.GroupFolded = folded
|
||||
}
|
||||
|
|
|
@ -1539,12 +1539,8 @@ func renderAttributeView(attrView *av.AttributeView, blockID, viewID, query stri
|
|||
// 如果存在分组的话渲染分组视图
|
||||
var groups []av.Viewable
|
||||
for _, groupView := range view.Groups {
|
||||
switch groupView.LayoutType {
|
||||
case av.LayoutTypeTable:
|
||||
groupView.Table.Columns = view.Table.Columns
|
||||
case av.LayoutTypeGallery:
|
||||
groupView.Gallery.CardFields = view.Gallery.CardFields
|
||||
}
|
||||
groupView.Filters = view.Filters
|
||||
groupView.Sorts = view.Sorts
|
||||
|
||||
groupViewable := sql.RenderView(attrView, groupView, query)
|
||||
err = renderViewableInstance(groupViewable, view, attrView, page, pageSize)
|
||||
|
@ -1622,13 +1618,12 @@ func genAttrViewViewGroups(view *av.View, attrView *av.AttributeView) {
|
|||
})
|
||||
}
|
||||
|
||||
const defaultGroupName, notInRange = "_@default@_", "_@notInRange@_"
|
||||
var groupName string
|
||||
groupItemsMap := map[string][]av.Item{}
|
||||
for _, item := range items {
|
||||
value := item.GetValue(group.Field)
|
||||
if value.IsEmpty() {
|
||||
groupName = defaultGroupName
|
||||
groupName = av.GroupValueDefault
|
||||
groupItemsMap[groupName] = append(groupItemsMap[groupName], item)
|
||||
continue
|
||||
}
|
||||
|
@ -1638,16 +1633,16 @@ func genAttrViewViewGroups(view *av.View, attrView *av.AttributeView) {
|
|||
groupName = value.String(false)
|
||||
case av.GroupMethodRangeNum:
|
||||
if group.Range.NumStart > value.Number.Content || group.Range.NumEnd < value.Number.Content {
|
||||
groupName = notInRange
|
||||
groupName = av.GroupValueNotInRange
|
||||
break
|
||||
}
|
||||
|
||||
for rangeEnd <= group.Range.NumEnd && rangeEnd < value.Number.Content {
|
||||
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 {
|
||||
if rangeStart <= value.Number.Content && rangeEnd > value.Number.Content {
|
||||
groupName = 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:
|
||||
|
@ -1727,9 +1722,10 @@ func genAttrViewViewGroups(view *av.View, attrView *av.AttributeView) {
|
|||
v.GroupItemIDs = append(v.GroupItemIDs, item.GetID())
|
||||
}
|
||||
|
||||
if defaultGroupName == name {
|
||||
v.GroupValue = name
|
||||
if av.GroupValueDefault == name {
|
||||
name = fmt.Sprintf(Conf.language(264), groupKey.Name)
|
||||
} else if notInRange == name {
|
||||
} else if av.GroupValueNotInRange == name {
|
||||
name = fmt.Sprintf(Conf.language(265))
|
||||
}
|
||||
v.Name = name
|
||||
|
@ -2838,14 +2834,14 @@ func setAttributeViewColumnCalc(operation *Operation) (err error) {
|
|||
}
|
||||
|
||||
func (tx *Transaction) doInsertAttrViewBlock(operation *Operation) (ret *TxErr) {
|
||||
err := AddAttributeViewBlock(tx, operation.Srcs, operation.AvID, operation.BlockID, operation.PreviousID, operation.IgnoreFillFilterVal)
|
||||
err := AddAttributeViewBlock(tx, operation.Srcs, operation.AvID, operation.BlockID, operation.GroupID, operation.PreviousID, operation.IgnoreFillFilterVal)
|
||||
if err != nil {
|
||||
return &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func AddAttributeViewBlock(tx *Transaction, srcs []map[string]interface{}, avID, blockID, previousBlockID string, ignoreFillFilter bool) (err error) {
|
||||
func AddAttributeViewBlock(tx *Transaction, srcs []map[string]interface{}, avID, blockID, groupID, previousBlockID string, ignoreFillFilter bool) (err error) {
|
||||
slices.Reverse(srcs) // https://github.com/siyuan-note/siyuan/issues/11286
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
|
@ -2874,14 +2870,14 @@ func AddAttributeViewBlock(tx *Transaction, srcs []map[string]interface{}, avID,
|
|||
if nil != src["content"] {
|
||||
srcContent = src["content"].(string)
|
||||
}
|
||||
if avErr := addAttributeViewBlock(now, avID, blockID, previousBlockID, srcID, srcContent, isDetached, ignoreFillFilter, tree, tx); nil != avErr {
|
||||
if avErr := addAttributeViewBlock(now, avID, blockID, groupID, previousBlockID, srcID, srcContent, isDetached, ignoreFillFilter, tree, tx); nil != avErr {
|
||||
return avErr
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func addAttributeViewBlock(now int64, avID, blockID, previousBlockID, addingBlockID, addingBlockContent string, isDetached, ignoreFillFilter bool, tree *parse.Tree, tx *Transaction) (err error) {
|
||||
func addAttributeViewBlock(now int64, avID, blockID, groupID, previousBlockID, addingBlockID, addingBlockContent string, isDetached, ignoreFillFilter bool, tree *parse.Tree, tx *Transaction) (err error) {
|
||||
var node *ast.Node
|
||||
if !isDetached {
|
||||
node = treenode.GetNodeInTree(tree, addingBlockID)
|
||||
|
@ -2935,17 +2931,14 @@ func addAttributeViewBlock(now int64, avID, blockID, previousBlockID, addingBloc
|
|||
Block: &av.ValueBlock{ID: addingBlockID, Icon: blockIcon, Content: addingBlockContent, Created: now, Updated: now}}
|
||||
blockValues.Values = append(blockValues.Values, blockValue)
|
||||
|
||||
// 如果存在过滤条件,则将过滤条件应用到新添加的块上
|
||||
view, _ := getAttrViewViewByBlockID(attrView, blockID)
|
||||
if nil != view && 0 < len(view.Filters) && !ignoreFillFilter {
|
||||
view, _ := getAttrViewViewByBlockID(attrView, blockID) // blockID 可能不传,所以这里的 view 可能为空,后面使用需要判空
|
||||
var nearItem av.Item // 临近项
|
||||
if nil != view && ((0 < len(view.Filters) && !ignoreFillFilter) || "" != groupID) {
|
||||
// 存在过滤条件或者指定分组视图时,先获取临近项备用
|
||||
viewable := sql.RenderView(attrView, view, "")
|
||||
av.Filter(viewable, attrView)
|
||||
av.Sort(viewable, attrView)
|
||||
|
||||
collection := viewable.(av.Collection)
|
||||
items := collection.GetItems()
|
||||
|
||||
var nearItem av.Item
|
||||
items := viewable.(av.Collection).GetItems()
|
||||
if 0 < len(items) {
|
||||
if "" != previousBlockID {
|
||||
for _, row := range items {
|
||||
|
@ -2960,19 +2953,26 @@ func addAttributeViewBlock(now int64, avID, blockID, previousBlockID, addingBloc
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterKeyIDs := map[string]bool{}
|
||||
if nil != view {
|
||||
for _, f := range view.Filters {
|
||||
filterKeyIDs[f.Column] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 如果存在过滤条件,则将过滤条件应用到新添加的块上
|
||||
if nil != view && 0 < len(view.Filters) && !ignoreFillFilter {
|
||||
sameKeyFilterSort := false // 是否在同一个字段上同时存在过滤和排序
|
||||
if 0 < len(view.Sorts) {
|
||||
filterKeys, sortKeys := map[string]bool{}, map[string]bool{}
|
||||
for _, f := range view.Filters {
|
||||
filterKeys[f.Column] = true
|
||||
}
|
||||
sortKeys := map[string]bool{}
|
||||
for _, s := range view.Sorts {
|
||||
sortKeys[s.Column] = true
|
||||
}
|
||||
|
||||
for key := range filterKeys {
|
||||
if sortKeys[key] {
|
||||
for k := range filterKeyIDs {
|
||||
if sortKeys[k] {
|
||||
sameKeyFilterSort = true
|
||||
break
|
||||
}
|
||||
|
@ -3028,6 +3028,7 @@ func addAttributeViewBlock(now int64, avID, blockID, previousBlockID, addingBloc
|
|||
bindBlockAv0(tx, avID, node, tree)
|
||||
}
|
||||
|
||||
// 在所有视图上添加项目
|
||||
for _, v := range attrView.Views {
|
||||
if "" != previousBlockID {
|
||||
changed := false
|
||||
|
@ -3046,6 +3047,50 @@ func addAttributeViewBlock(now int64, avID, blockID, previousBlockID, addingBloc
|
|||
}
|
||||
}
|
||||
|
||||
// 如果存在分组条件,则将分组条件应用到新添加的块上
|
||||
groupKey := getViewGroupKey(view, attrView)
|
||||
if nil != view && nil != groupKey {
|
||||
if !filterKeyIDs[groupKey.ID] /* 过滤条件应用过的话就不重复处理了 */ && "" != groupID {
|
||||
if groupView := view.GetGroup(groupID); nil != groupView {
|
||||
if keyValues, _ := attrView.GetKeyValues(groupKey.ID); nil != keyValues {
|
||||
var newValue, defaultVal *av.Value
|
||||
if nil != nearItem {
|
||||
defaultVal = nearItem.GetValue(groupKey.ID)
|
||||
}
|
||||
if nil != defaultVal {
|
||||
newValue = defaultVal.Clone()
|
||||
} else {
|
||||
newValue = av.GetAttributeViewDefaultValue(ast.NewNodeID(), groupKey.ID, blockID, groupKey.Type)
|
||||
}
|
||||
|
||||
if av.KeyTypeBlock == newValue.Type {
|
||||
// 如果是主键的话前面已经添加过了,这里仅修改内容
|
||||
blockValue.Block.Content = newValue.Block.Content
|
||||
} else {
|
||||
newValue.ID = ast.NewNodeID()
|
||||
newValue.CreatedAt = util.CurrentTimeMillis()
|
||||
newValue.UpdatedAt = newValue.CreatedAt + 1000
|
||||
newValue.KeyID = keyValues.Key.ID
|
||||
newValue.BlockID = addingBlockID
|
||||
newValue.IsDetached = isDetached
|
||||
|
||||
if av.KeyTypeSelect == groupKey.Type || av.KeyTypeMSelect == groupKey.Type {
|
||||
// 因为单选或多选只能按选项分组,并且可能存在空白分组(前面可能找不到临近项) ,所以单选或多选类型的分组字段使用分组值内容对应的选项
|
||||
if opt := groupKey.GetOption(groupView.GroupValue); nil != opt {
|
||||
newValue.MSelect[0].Content = opt.Name
|
||||
newValue.MSelect[0].Color = opt.Color
|
||||
}
|
||||
}
|
||||
|
||||
keyValues.Values = append(keyValues.Values, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
regenAttrViewViewGroups(attrView, groupKey.ID)
|
||||
}
|
||||
|
||||
err = av.SaveAttributeView(attrView)
|
||||
return
|
||||
}
|
||||
|
@ -4428,8 +4473,7 @@ func updateAttributeViewValue(tx *Transaction, attrView *av.AttributeView, keyID
|
|||
|
||||
func regenAttrViewViewGroups(attrView *av.AttributeView, keyID string) {
|
||||
for _, view := range attrView.Views {
|
||||
if nil != view.Group {
|
||||
groupKey, _ := attrView.GetKey(view.Group.Field)
|
||||
groupKey := getViewGroupKey(view, attrView)
|
||||
if nil == groupKey {
|
||||
continue
|
||||
}
|
||||
|
@ -4453,6 +4497,16 @@ func regenAttrViewViewGroups(attrView *av.AttributeView, keyID string) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getViewGroupKey(view *av.View, attrView *av.AttributeView) *av.Key {
|
||||
if nil == view.Group {
|
||||
return nil
|
||||
}
|
||||
if "" == view.Group.Field {
|
||||
return nil
|
||||
}
|
||||
ret, _ := attrView.GetKey(view.Group.Field)
|
||||
return ret
|
||||
}
|
||||
|
||||
func unbindBlockAv(tx *Transaction, avID, blockID string) {
|
||||
|
|
|
@ -986,7 +986,7 @@ func DuplicateDoc(tree *parse.Tree) {
|
|||
AddAttributeViewBlock(nil, []map[string]interface{}{{
|
||||
"id": n.ID,
|
||||
"isDetached": false,
|
||||
}}, avID, "", "", false)
|
||||
}}, avID, "", "", "", false)
|
||||
ReloadAttrView(avID)
|
||||
}
|
||||
return ast.WalkContinue
|
||||
|
|
|
@ -1096,7 +1096,7 @@ func (tx *Transaction) doLargeInsert(previousID string) (ret *TxErr) {
|
|||
AddAttributeViewBlock(tx, []map[string]interface{}{{
|
||||
"id": insertedNode.ID,
|
||||
"isDetached": false,
|
||||
}}, avID, "", previousID, false)
|
||||
}}, avID, "", "", previousID, false)
|
||||
ReloadAttrView(avID)
|
||||
}
|
||||
|
||||
|
@ -1275,7 +1275,7 @@ func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) {
|
|||
AddAttributeViewBlock(tx, []map[string]interface{}{{
|
||||
"id": insertedNode.ID,
|
||||
"isDetached": false,
|
||||
}}, avID, "", previousID, false)
|
||||
}}, avID, "", "", previousID, false)
|
||||
ReloadAttrView(avID)
|
||||
}
|
||||
|
||||
|
@ -1692,6 +1692,7 @@ type Operation struct {
|
|||
BackRelationKeyID string `json:"backRelationKeyID"` // 属性视图关联列回链关联列的 ID
|
||||
RemoveDest bool `json:"removeDest"` // 属性视图删除关联目标
|
||||
Layout av.LayoutType `json:"layout"` // 属性视图布局类型
|
||||
GroupID string `json:"groupID"` // 属性视图分组视图 ID
|
||||
}
|
||||
|
||||
type Transaction struct {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue