diff --git a/kernel/av/av.go b/kernel/av/av.go index 9fb1adb80..fe7e5f764 100644 --- a/kernel/av/av.go +++ b/kernel/av/av.go @@ -43,10 +43,10 @@ type AttributeView struct { Views []*View `json:"views"` // 视图 } -// KeyValues 描述了属性视图属性列值的结构。 +// KeyValues 描述了属性视图属性键值列表的结构。 type KeyValues struct { - Key *Key `json:"key"` // 属性视图属性列 - Values []*Value `json:"values,omitempty"` // 属性视图属性列值 + Key *Key `json:"key"` // 属性视图属性键 + Values []*Value `json:"values,omitempty"` // 属性视图属性值列表 } func (kValues *KeyValues) GetValue(blockID string) (ret *Value) { @@ -197,6 +197,11 @@ const ( LayoutTypeGallery LayoutType = "gallery" // 属性视图类型 - 画廊 ) +const ( + TableViewDefaultPageSize = 50 // 表格视图默认分页大小 + GalleryViewDefaultPageSize = 50 // 画廊视图默认分页大小 +) + func NewTableView() (ret *View) { ret = &View{ ID: ast.NewNodeID(), @@ -207,7 +212,7 @@ func NewTableView() (ret *View) { ID: ast.NewNodeID(), Filters: []*ViewFilter{}, Sorts: []*ViewSort{}, - PageSize: 50, + PageSize: TableViewDefaultPageSize, }, } return @@ -224,7 +229,7 @@ func NewTableViewWithBlockKey(blockKeyID string) (view *View, blockKey, selectKe ID: ast.NewNodeID(), Filters: []*ViewFilter{}, Sorts: []*ViewSort{}, - PageSize: 50, + PageSize: TableViewDefaultPageSize, }, } blockKey = NewKey(blockKeyID, getI18nName("key"), "", KeyTypeBlock) @@ -245,7 +250,7 @@ func NewGalleryView() (ret *View) { ID: ast.NewNodeID(), Filters: []*ViewFilter{}, Sorts: []*ViewSort{}, - PageSize: 50, + PageSize: GalleryViewDefaultPageSize, }, } return @@ -411,7 +416,7 @@ func SaveAttributeView(av *AttributeView) (err error) { view.Table.RowIDs = gulu.Str.RemoveDuplicatedElem(view.Table.RowIDs) // 分页大小 if 1 > view.Table.PageSize { - view.Table.PageSize = 50 + view.Table.PageSize = TableViewDefaultPageSize } } } diff --git a/kernel/av/gallery.go b/kernel/av/gallery.go deleted file mode 100644 index 9e1aca7a7..000000000 --- a/kernel/av/gallery.go +++ /dev/null @@ -1,77 +0,0 @@ -// 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 . - -package av - -// LayoutGallery 描述了画廊布局的结构。 -type LayoutGallery struct { - Spec int `json:"spec"` // 布局格式版本 - ID string `json:"id"` // 布局 ID - - CoverFrom int `json:"coverFrom"` // 封面来源,0:无,1:内容图,2:资源字段 - CoverFromAssetKeyID string `json:"coverFromAssetKeyId,omitempty"` // 资源字段 ID,CoverFrom 为 2 时有效 - CardFields []*ViewGalleryCardField `json:"fields"` // 画廊卡片字段 - CardIDs []string `json:"cardIds"` // 卡片 ID,用于自定义排序 - Filters []*ViewFilter `json:"filters"` // 过滤规则 - Sorts []*ViewSort `json:"sorts"` // 排序规则 - PageSize int `json:"pageSize"` // 每页卡片数 -} - -// ViewGalleryCardField 描述了画廊卡片字段的结构。 -type ViewGalleryCardField struct { - ID string `json:"id"` // 字段 ID - - Hidden bool `json:"hidden"` // 是否隐藏 - Desc string `json:"desc,omitempty"` // 字段描述 -} - -// Gallery 描述了画廊实例的结构。 -type Gallery struct { - ID string `json:"id"` // 画廊布局 ID - Icon string `json:"icon"` // 画廊图标 - Name string `json:"name"` // 画廊名称 - Desc string `json:"desc"` // 画廊描述 - HideAttrViewName bool `json:"hideAttrViewName"` // 是否隐藏属性视图名称 - Filters []*ViewFilter `json:"filters"` // 过滤规则 - Sorts []*ViewSort `json:"sorts"` // 排序规则 - Cards []*GalleryCard `json:"cards"` // 画廊卡片 - CardCount int `json:"cardCount"` // 画廊总卡片数 - Limit int `json:"limit"` // 每页卡片数 -} - -// GalleryCard 描述了画廊实例卡片的结构。 -type GalleryCard struct { - ID string `json:"id"` // 卡片 ID - Fields []*GalleryField `json:"fields"` // 卡片字段 - - CoverURL string `json:"coverURL"` // 卡片封面超链接 -} - -// GalleryField 描述了画廊实例卡片字段的结构。 -type GalleryField struct { - ID string `json:"id"` // 字段 ID - Value *Value `json:"value"` // 字段值 - ValueType KeyType `json:"valueType"` // 字段值类型 - - // 以下是某些字段类型的特有属性 - - Options []*SelectOption `json:"options,omitempty"` // 选项字段表 - NumberFormat NumberFormat `json:"numberFormat"` // 数字字段格式化 - Template string `json:"template"` // 模板字段内容 - Relation *Relation `json:"relation,omitempty"` // 关联字段 - Rollup *Rollup `json:"rollup,omitempty"` // 汇总字段 - Date *Date `json:"date,omitempty"` // 日期设置 -} diff --git a/kernel/av/layout_gallery.go b/kernel/av/layout_gallery.go new file mode 100644 index 000000000..0ee061d63 --- /dev/null +++ b/kernel/av/layout_gallery.go @@ -0,0 +1,270 @@ +// 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 . + +package av + +import "sort" + +// LayoutGallery 描述了画廊布局的结构。 +type LayoutGallery struct { + Spec int `json:"spec"` // 布局格式版本 + ID string `json:"id"` // 布局 ID + + CoverFrom int `json:"coverFrom"` // 封面来源,0:无,1:内容图,2:资源字段 + CoverFromAssetKeyID string `json:"coverFromAssetKeyId,omitempty"` // 资源字段 ID,CoverFrom 为 2 时有效 + CardFields []*ViewGalleryCardField `json:"fields"` // 画廊卡片字段 + CardIDs []string `json:"cardIds"` // 卡片 ID,用于自定义排序 + Filters []*ViewFilter `json:"filters"` // 过滤规则 + Sorts []*ViewSort `json:"sorts"` // 排序规则 + PageSize int `json:"pageSize"` // 每页卡片数 +} + +// ViewGalleryCardField 描述了画廊卡片字段的结构。 +type ViewGalleryCardField struct { + ID string `json:"id"` // 字段 ID + + Hidden bool `json:"hidden"` // 是否隐藏 + Desc string `json:"desc,omitempty"` // 字段描述 +} + +// Gallery 描述了画廊实例的结构。 +type Gallery struct { + ID string `json:"id"` // 画廊布局 ID + Icon string `json:"icon"` // 画廊图标 + Name string `json:"name"` // 画廊名称 + Desc string `json:"desc"` // 画廊描述 + HideAttrViewName bool `json:"hideAttrViewName"` // 是否隐藏属性视图名称 + Filters []*ViewFilter `json:"filters"` // 过滤规则 + Sorts []*ViewSort `json:"sorts"` // 排序规则 + Fields []*GalleryField `json:"fields"` // 画廊字段 + Cards []*GalleryCard `json:"cards"` // 画廊卡片 + CardCount int `json:"cardCount"` // 画廊总卡片数 + PageSize int `json:"pageSize"` // 每页卡片数 +} + +// GalleryCard 描述了画廊实例卡片的结构。 +type GalleryCard struct { + ID string `json:"id"` // 卡片 ID + Values []*GalleryFieldValue `json:"values"` // 卡片字段值 + + CoverURL string `json:"coverURL"` // 卡片封面超链接 +} + +// GalleryField 描述了画廊实例卡片字段的结构。 +type GalleryField struct { + ID string `json:"id"` // 字段 ID + Name string `json:"name"` // 字段名 + Type KeyType `json:"type"` // 字段类型 + Icon string `json:"icon"` // 字段图标 + Hidden bool `json:"hidden"` // 是否隐藏 + Desc string `json:"desc"` // 字段描述 + + // 以下是某些字段类型的特有属性 + + Options []*SelectOption `json:"options,omitempty"` // 选项字段表 + NumberFormat NumberFormat `json:"numberFormat"` // 数字字段格式化 + Template string `json:"template"` // 模板字段内容 + Relation *Relation `json:"relation,omitempty"` // 关联字段 + Rollup *Rollup `json:"rollup,omitempty"` // 汇总字段 + Date *Date `json:"date,omitempty"` // 日期设置 +} + +// GalleryFieldValue 描述了画廊实例字段值的结构。 +type GalleryFieldValue struct { + ID string `json:"id"` // 字段值 ID + Value *Value `json:"value"` // 字段值 + ValueType KeyType `json:"valueType"` // 字段值类型 +} + +func (card *GalleryCard) GetBlockValue() (ret *Value) { + for _, v := range card.Values { + if KeyTypeBlock == v.ValueType { + ret = v.Value + break + } + } + return +} + +func (gallery *Gallery) GetType() LayoutType { + return LayoutTypeGallery +} + +func (gallery *Gallery) GetID() string { + return gallery.ID +} + +func (gallery *Gallery) Sort(attrView *AttributeView) { + if 1 > len(gallery.Sorts) { + return + } + + type FieldIndexSort struct { + Index int + Order SortOrder + } + + var fieldIndexSorts []*FieldIndexSort + for _, s := range gallery.Sorts { + for i, c := range gallery.Fields { + if c.ID == s.Column { + fieldIndexSorts = append(fieldIndexSorts, &FieldIndexSort{Index: i, Order: s.Order}) + break + } + } + } + + editedValCards := map[string]bool{} + for i, card := range gallery.Cards { + for _, fieldIndexSort := range fieldIndexSorts { + val := gallery.Cards[i].Values[fieldIndexSort.Index].Value + if KeyTypeCheckbox == val.Type { + if block := card.GetBlockValue(); nil != block && block.IsEdited() { + // 如果主键编辑过,则勾选框也算作编辑过,参与排序 https://github.com/siyuan-note/siyuan/issues/11016 + editedValCards[card.ID] = true + break + } + } + + if val.IsEdited() { + // 如果该卡片某字段的值已经编辑过,则该卡片可参与排序 + editedValCards[card.ID] = true + break + } + } + } + + // 将未编辑的卡片和已编辑的卡片分开排序 + var uneditedCards, editedCards []*GalleryCard + for _, card := range gallery.Cards { + if _, ok := editedValCards[card.ID]; ok { + editedCards = append(editedCards, card) + } else { + uneditedCards = append(uneditedCards, card) + } + } + + sort.Slice(uneditedCards, func(i, j int) bool { + val1 := uneditedCards[i].GetBlockValue() + if nil == val1 { + return true + } + val2 := uneditedCards[j].GetBlockValue() + if nil == val2 { + return false + } + return val1.CreatedAt < val2.CreatedAt + }) + + sort.Slice(editedCards, func(i, j int) bool { + sorted := true + for _, fieldIndexSort := range fieldIndexSorts { + val1 := editedCards[i].Values[fieldIndexSort.Index].Value + val2 := editedCards[j].Values[fieldIndexSort.Index].Value + if nil == val1 || val1.IsEmpty() { + if nil != val2 && !val2.IsEmpty() { + return false + } + sorted = false + continue + } else { + if nil == val2 || val2.IsEmpty() { + return true + } + } + + result := val1.Compare(val2, attrView) + if 0 == result { + sorted = false + continue + } + sorted = true + + if fieldIndexSort.Order == SortOrderAsc { + return 0 > result + } + return 0 < result + } + + if !sorted { + key1 := editedCards[i].GetBlockValue() + if nil == key1 { + return false + } + key2 := editedCards[j].GetBlockValue() + if nil == key2 { + return false + } + return key1.CreatedAt < key2.CreatedAt + } + return false + }) + + // 将包含未编辑的卡片放在最后 + gallery.Cards = append(editedCards, uneditedCards...) + if 1 > len(gallery.Cards) { + gallery.Cards = []*GalleryCard{} + } +} + +func (gallery *Gallery) Filter(attrView *AttributeView) { + if 1 > len(gallery.Filters) { + return + } + + var fieldIndexes []int + for _, f := range gallery.Filters { + for i, c := range gallery.Cards { + if c.ID == f.Column { + fieldIndexes = append(fieldIndexes, i) + break + } + } + } + + cards := []*GalleryCard{} + attrViewCache := map[string]*AttributeView{} + attrViewCache[attrView.ID] = attrView + for _, card := range gallery.Cards { + pass := true + for j, index := range fieldIndexes { + operator := gallery.Filters[j].Operator + + if nil == card.Values[index].Value { + if FilterOperatorIsNotEmpty == operator { + pass = false + } else if FilterOperatorIsEmpty == operator { + pass = true + break + } + + if KeyTypeText != card.Values[index].ValueType { + pass = false + } + break + } + + if !card.Values[index].Value.Filter(gallery.Filters[j], attrView, card.ID, &attrViewCache) { + pass = false + break + } + } + if pass { + cards = append(cards, card) + } + } + gallery.Cards = cards +} diff --git a/kernel/av/layout_gallery_calc.go b/kernel/av/layout_gallery_calc.go new file mode 100644 index 000000000..96383ea7b --- /dev/null +++ b/kernel/av/layout_gallery_calc.go @@ -0,0 +1,21 @@ +// 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 . + +package av + +func (gallery *Gallery) Calc() { + // 画廊视图不支持计算 +} diff --git a/kernel/model/attribute_view.go b/kernel/model/attribute_view.go index f31ca7f1b..a09845bb7 100644 --- a/kernel/model/attribute_view.go +++ b/kernel/model/attribute_view.go @@ -676,7 +676,7 @@ func GetBlockAttributeViewKeys(blockID string) (ret []*BlockAttributeViewKeys) { } var renderErr error - kv.Values[0].Template.Content, renderErr = sql.RenderTemplateCol(ial, keyValues, kv.Key.Template) + 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) } @@ -904,11 +904,36 @@ func renderAttributeView(attrView *av.AttributeView, viewID, query string, page, view.Table.Sorts = tmpSorts viewable = sql.RenderAttributeViewTable(attrView, view, query) + case av.LayoutTypeGallery: + // 字段删除以后需要删除设置的过滤和排序 + tmpFilters := []*av.ViewFilter{} + for _, f := range view.Gallery.Filters { + if k, _ := attrView.GetKey(f.Column); nil != k { + tmpFilters = append(tmpFilters, f) + } + } + view.Gallery.Filters = tmpFilters + + tmpSorts := []*av.ViewSort{} + for _, s := range view.Gallery.Sorts { + if k, _ := attrView.GetKey(s.Column); nil != k { + tmpSorts = append(tmpSorts, s) + } + } + view.Gallery.Sorts = tmpSorts + + viewable = sql.RenderAttributeViewGallery(attrView, view, query) } - viewable.FilterRows(attrView) - viewable.SortRows(attrView) - viewable.CalcCols() + if nil == viewable { + err = av.ErrViewNotFound + logging.LogErrorf("render attribute view [%s] failed", attrView.ID) + return + } + + viewable.Filter(attrView) + viewable.Sort(attrView) + viewable.Calc() // 分页 switch viewable.GetType() { @@ -916,19 +941,34 @@ func renderAttributeView(attrView *av.AttributeView, viewID, query string, page, table := viewable.(*av.Table) table.RowCount = len(table.Rows) if 1 > view.Table.PageSize { - view.Table.PageSize = 50 + view.Table.PageSize = av.TableViewDefaultPageSize } table.PageSize = view.Table.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) + if 1 > view.Gallery.PageSize { + view.Gallery.PageSize = av.GalleryViewDefaultPageSize + } + gallery.PageSize = view.Gallery.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 } @@ -949,8 +989,8 @@ func GetCurrentAttributeViewImages(avID, viewID, query string) (ret []string, er } table := sql.RenderAttributeViewTable(attrView, view, query) - table.FilterRows(attrView) - table.SortRows(attrView) + table.Filter(attrView) + table.Sort(attrView) for _, row := range table.Rows { for _, cell := range row.Cells { @@ -2021,8 +2061,8 @@ func addAttributeViewBlock(now int64, avID, blockID, previousBlockID, addingBloc view, _ := getAttrViewViewByBlockID(attrView, blockID) if nil != view && 0 < len(view.Table.Filters) && !ignoreFillFilter { viewable := sql.RenderAttributeViewTable(attrView, view, "") - viewable.FilterRows(attrView) - viewable.SortRows(attrView) + viewable.Filter(attrView) + viewable.Sort(attrView) var nearRow *av.TableRow if 0 < len(viewable.Rows) { diff --git a/kernel/sql/av.go b/kernel/sql/av.go index d6f8af03f..f3a607486 100644 --- a/kernel/sql/av.go +++ b/kernel/sql/av.go @@ -18,412 +18,25 @@ package sql import ( "bytes" - "fmt" - "sort" "strings" "text/template" "time" - "github.com/88250/lute/ast" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/av" "github.com/siyuan-note/siyuan/kernel/filesys" - "github.com/siyuan-note/siyuan/kernel/treenode" "github.com/siyuan-note/siyuan/kernel/util" ) -func RenderAttributeViewTable(attrView *av.AttributeView, view *av.View, query string) (ret *av.Table) { - ret = &av.Table{ - ID: view.ID, - Icon: view.Icon, - Name: view.Name, - Desc: view.Desc, - HideAttrViewName: view.HideAttrViewName, - Columns: []*av.TableColumn{}, - Rows: []*av.TableRow{}, - Filters: view.Table.Filters, - Sorts: view.Table.Sorts, - } - - // 组装列 - for _, col := range view.Table.Columns { - key, getErr := attrView.GetKey(col.ID) - if nil != getErr { - // 找不到字段则在视图中删除 - - switch view.LayoutType { - case av.LayoutTypeTable: - for i, column := range view.Table.Columns { - if column.ID == col.ID { - view.Table.Columns = append(view.Table.Columns[:i], view.Table.Columns[i+1:]...) - break - } - } - } - - logging.LogWarnf("get key [%s] failed: %s", col.ID, getErr) - av.SaveAttributeView(attrView) - continue - } - - ret.Columns = append(ret.Columns, &av.TableColumn{ - ID: key.ID, - Name: key.Name, - Type: key.Type, - Icon: key.Icon, - Options: key.Options, - NumberFormat: key.NumberFormat, - Template: key.Template, - Relation: key.Relation, - Rollup: key.Rollup, - Date: key.Date, - Wrap: col.Wrap, - Hidden: col.Hidden, - Width: col.Width, - Desc: key.Desc, - Pin: col.Pin, - Calc: col.Calc, - }) - } - - // 生成行 - rows := map[string][]*av.KeyValues{} - for _, keyValues := range attrView.KeyValues { - for _, val := range keyValues.Values { - values := rows[val.BlockID] - if nil == values { - values = []*av.KeyValues{{Key: keyValues.Key, Values: []*av.Value{val}}} - } else { - values = append(values, &av.KeyValues{Key: keyValues.Key, Values: []*av.Value{val}}) - } - rows[val.BlockID] = values - } - } - - // 过滤掉不存在的行 - var notFound []string - var toCheckBlockIDs []string - for blockID, keyValues := range rows { - blockValue := getRowBlockValue(keyValues) - if nil == blockValue { - notFound = append(notFound, blockID) - continue - } - - if blockValue.IsDetached { - continue - } - - if nil != blockValue.Block && "" == blockValue.Block.ID { - notFound = append(notFound, blockID) - continue - } - - toCheckBlockIDs = append(toCheckBlockIDs, blockID) - } - checkRet := treenode.ExistBlockTrees(toCheckBlockIDs) - for blockID, exist := range checkRet { - if !exist { - notFound = append(notFound, blockID) - } - } - for _, blockID := range notFound { - delete(rows, blockID) - } - - // 生成行单元格 - for rowID, row := range rows { - var tableRow av.TableRow - for _, col := range ret.Columns { - var tableCell *av.TableCell - for _, keyValues := range row { - if keyValues.Key.ID == col.ID { - tableCell = &av.TableCell{ - ID: keyValues.Values[0].ID, - Value: keyValues.Values[0], - ValueType: col.Type, - } - break - } - } - if nil == tableCell { - tableCell = &av.TableCell{ - ID: ast.NewNodeID(), - ValueType: col.Type, - } - } - tableRow.ID = rowID - - switch tableCell.ValueType { - case av.KeyTypeNumber: // 格式化数字 - if nil != tableCell.Value && nil != tableCell.Value.Number && tableCell.Value.Number.IsNotEmpty { - tableCell.Value.Number.Format = col.NumberFormat - tableCell.Value.Number.FormatNumber() - } - case av.KeyTypeTemplate: // 渲染模板列 - tableCell.Value = &av.Value{ID: tableCell.ID, KeyID: col.ID, BlockID: rowID, Type: av.KeyTypeTemplate, Template: &av.ValueTemplate{Content: col.Template}} - case av.KeyTypeCreated: // 填充创建时间列值,后面再渲染 - tableCell.Value = &av.Value{ID: tableCell.ID, KeyID: col.ID, BlockID: rowID, Type: av.KeyTypeCreated} - case av.KeyTypeUpdated: // 填充更新时间列值,后面再渲染 - tableCell.Value = &av.Value{ID: tableCell.ID, KeyID: col.ID, BlockID: rowID, Type: av.KeyTypeUpdated} - case av.KeyTypeRelation: // 清空关联列值,后面再渲染 https://ld246.com/article/1703831044435 - if nil != tableCell.Value && nil != tableCell.Value.Relation { - tableCell.Value.Relation.Contents = nil - } - } - - FillAttributeViewTableCellNilValue(tableCell, rowID, col.ID) - - tableRow.Cells = append(tableRow.Cells, tableCell) - } - ret.Rows = append(ret.Rows, &tableRow) - } - - // 批量获取块属性以提升性能 - var ialIDs []string - for _, row := range ret.Rows { - block := row.GetBlockValue() - if nil != block && !block.IsDetached { - ialIDs = append(ialIDs, row.ID) - } - } - ials := BatchGetBlockAttrs(ialIDs) - - // 渲染自动生成的列值,比如关联列、汇总列、创建时间列和更新时间列 - avCache := map[string]*av.AttributeView{} - avCache[attrView.ID] = attrView - for _, row := range ret.Rows { - for _, cell := range row.Cells { - switch cell.ValueType { - case av.KeyTypeBlock: // 对于主键可能需要填充静态锚文本 Database-bound block primary key supports setting static anchor text https://github.com/siyuan-note/siyuan/issues/10049 - if nil != cell.Value.Block { - for k, v := range ials[row.ID] { - if k == av.NodeAttrViewStaticText+"-"+attrView.ID { - cell.Value.Block.Content = v - break - } - } - } - case av.KeyTypeRollup: // 渲染汇总列 - rollupKey, _ := attrView.GetKey(cell.Value.KeyID) - if nil == rollupKey || nil == rollupKey.Rollup { - break - } - - relKey, _ := attrView.GetKey(rollupKey.Rollup.RelationKeyID) - if nil == relKey || nil == relKey.Relation { - break - } - - relVal := attrView.GetValue(relKey.ID, row.ID) - if nil == relVal || nil == relVal.Relation { - break - } - - destAv := avCache[relKey.Relation.AvID] - if nil == destAv { - destAv, _ = av.ParseAttributeView(relKey.Relation.AvID) - if nil != destAv { - avCache[relKey.Relation.AvID] = destAv - } - } - if nil == destAv { - break - } - - destKey, _ := destAv.GetKey(rollupKey.Rollup.KeyID) - if nil == destKey { - continue - } - - for _, blockID := range relVal.Relation.BlockIDs { - destVal := destAv.GetValue(rollupKey.Rollup.KeyID, blockID) - if nil == destVal { - if destAv.ExistBlock(blockID) { // 数据库中存在行但是列值不存在是数据未初始化,这里补一个默认值 - destVal = av.GetAttributeViewDefaultValue(ast.NewNodeID(), rollupKey.Rollup.KeyID, blockID, destKey.Type) - } - if nil == destVal { - continue - } - } - if av.KeyTypeNumber == destKey.Type { - destVal.Number.Format = destKey.NumberFormat - destVal.Number.FormatNumber() - } - - cell.Value.Rollup.Contents = append(cell.Value.Rollup.Contents, destVal.Clone()) - } - - cell.Value.Rollup.RenderContents(rollupKey.Rollup.Calc, destKey) - - // 将汇总列的值保存到 rows 中,后续渲染模板列的时候会用到,下同 - // Database table view template columns support reading relation, rollup, created and updated columns https://github.com/siyuan-note/siyuan/issues/10442 - keyValues := rows[row.ID] - keyValues = append(keyValues, &av.KeyValues{Key: rollupKey, Values: []*av.Value{{ID: cell.Value.ID, KeyID: rollupKey.ID, BlockID: row.ID, Type: av.KeyTypeRollup, Rollup: cell.Value.Rollup}}}) - rows[row.ID] = keyValues - case av.KeyTypeRelation: // 渲染关联列 - relKey, _ := attrView.GetKey(cell.Value.KeyID) - if nil != relKey && nil != relKey.Relation { - destAv := avCache[relKey.Relation.AvID] - if nil == destAv { - destAv, _ = av.ParseAttributeView(relKey.Relation.AvID) - if nil != destAv { - avCache[relKey.Relation.AvID] = destAv - } - } - if nil != destAv { - blocks := map[string]*av.Value{} - blockValues := destAv.GetBlockKeyValues() - if nil != blockValues { - for _, blockValue := range blockValues.Values { - blocks[blockValue.BlockID] = blockValue - } - for _, blockID := range cell.Value.Relation.BlockIDs { - if val := blocks[blockID]; nil != val { - cell.Value.Relation.Contents = append(cell.Value.Relation.Contents, val) - } - } - } - } - } - - keyValues := rows[row.ID] - keyValues = append(keyValues, &av.KeyValues{Key: relKey, Values: []*av.Value{{ID: cell.Value.ID, KeyID: relKey.ID, BlockID: row.ID, Type: av.KeyTypeRelation, Relation: cell.Value.Relation}}}) - rows[row.ID] = keyValues - case av.KeyTypeCreated: // 渲染创建时间 - createdStr := row.ID[:len("20060102150405")] - created, parseErr := time.ParseInLocation("20060102150405", createdStr, time.Local) - if nil == parseErr { - cell.Value.Created = av.NewFormattedValueCreated(created.UnixMilli(), 0, av.CreatedFormatNone) - cell.Value.Created.IsNotEmpty = true - } else { - cell.Value.Created = av.NewFormattedValueCreated(time.Now().UnixMilli(), 0, av.CreatedFormatNone) - } - - keyValues := rows[row.ID] - createdKey, _ := attrView.GetKey(cell.Value.KeyID) - keyValues = append(keyValues, &av.KeyValues{Key: createdKey, Values: []*av.Value{{ID: cell.Value.ID, KeyID: createdKey.ID, BlockID: row.ID, Type: av.KeyTypeCreated, Created: cell.Value.Created}}}) - rows[row.ID] = keyValues - case av.KeyTypeUpdated: // 渲染更新时间 - ial := ials[row.ID] - if nil == ial { - ial = map[string]string{} - } - block := row.GetBlockValue() - updatedStr := ial["updated"] - if "" == updatedStr && nil != block { - cell.Value.Updated = av.NewFormattedValueUpdated(block.Block.Updated, 0, av.UpdatedFormatNone) - cell.Value.Updated.IsNotEmpty = true - } else { - updated, parseErr := time.ParseInLocation("20060102150405", updatedStr, time.Local) - if nil == parseErr { - cell.Value.Updated = av.NewFormattedValueUpdated(updated.UnixMilli(), 0, av.UpdatedFormatNone) - cell.Value.Updated.IsNotEmpty = true - } else { - cell.Value.Updated = av.NewFormattedValueUpdated(time.Now().UnixMilli(), 0, av.UpdatedFormatNone) - } - } - - keyValues := rows[row.ID] - updatedKey, _ := attrView.GetKey(cell.Value.KeyID) - keyValues = append(keyValues, &av.KeyValues{Key: updatedKey, Values: []*av.Value{{ID: cell.Value.ID, KeyID: updatedKey.ID, BlockID: row.ID, Type: av.KeyTypeUpdated, Updated: cell.Value.Updated}}}) - rows[row.ID] = keyValues - } - } - } - - // 最后单独渲染模板列,这样模板列就可以使用汇总、关联、创建时间和更新时间列的值了 - // Database table view template columns support reading relation, rollup, created and updated columns https://github.com/siyuan-note/siyuan/issues/10442 - - var renderTemplateErr error - for _, row := range ret.Rows { - for _, cell := range row.Cells { - switch cell.ValueType { - case av.KeyTypeTemplate: // 渲染模板列 - keyValues := rows[row.ID] - ial := ials[row.ID] - if nil == ial { - ial = map[string]string{} - } - content, renderErr := RenderTemplateCol(ial, keyValues, cell.Value.Template.Content) - cell.Value.Template.Content = content - if nil != renderErr { - key, _ := attrView.GetKey(cell.Value.KeyID) - keyName := "" - if nil != key { - keyName = key.Name - } - renderTemplateErr = fmt.Errorf("database [%s] template field [%s] rendering failed: %s", getAttrViewName(attrView), keyName, renderErr) - } - } - } - } - if nil != renderTemplateErr { - util.PushErrMsg(fmt.Sprintf(util.Langs[util.Lang][44], util.EscapeHTML(renderTemplateErr.Error())), 30000) - } - - // 根据搜索条件过滤 - query = strings.TrimSpace(query) - if "" != query { - // 将连续空格转换为一个空格 - query = strings.Join(strings.Fields(query), " ") - // 按空格分割关键字 - keywords := strings.Split(query, " ") - // 使用 AND 逻辑 https://github.com/siyuan-note/siyuan/issues/11535 - var hitRows []*av.TableRow - for _, row := range ret.Rows { - hit := false - for _, cell := range row.Cells { - allKeywordsHit := true - for _, keyword := range keywords { - if !strings.Contains(strings.ToLower(cell.Value.String(true)), strings.ToLower(keyword)) { - allKeywordsHit = false - break - } - } - if allKeywordsHit { - hit = true - break - } - } - if hit { - hitRows = append(hitRows, row) - } - } - ret.Rows = hitRows - if 1 > len(ret.Rows) { - ret.Rows = []*av.TableRow{} - } - } - - // 自定义排序 - sortRowIDs := map[string]int{} - if 0 < len(view.Table.RowIDs) { - for i, rowID := range view.Table.RowIDs { - sortRowIDs[rowID] = i - } - } - - sort.Slice(ret.Rows, func(i, j int) bool { - iv := sortRowIDs[ret.Rows[i].ID] - jv := sortRowIDs[ret.Rows[j].ID] - if iv == jv { - return ret.Rows[i].ID < ret.Rows[j].ID - } - return iv < jv - }) - return -} - -func RenderTemplateCol(ial map[string]string, rowValues []*av.KeyValues, tplContent string) (ret string, err error) { +func RenderTemplateField(ial map[string]string, rowValues []*av.KeyValues, tplContent string) (ret string, err error) { if "" == ial["id"] { - block := getRowBlockValue(rowValues) + block := getBlockValue(rowValues) if nil != block && nil != block.Block { ial["id"] = block.Block.ID } } if "" == ial["updated"] { - block := getRowBlockValue(rowValues) + block := getBlockValue(rowValues) if nil != block && nil != block.Block { ial["updated"] = time.UnixMilli(block.Block.Updated).Format("20060102150405") } @@ -560,73 +173,68 @@ func RenderTemplateCol(ial map[string]string, rowValues []*av.KeyValues, tplCont return } -func FillAttributeViewTableCellNilValue(tableCell *av.TableCell, rowID, colID string) { - if nil == tableCell.Value { - tableCell.Value = av.GetAttributeViewDefaultValue(tableCell.ID, colID, rowID, tableCell.ValueType) - return - } - - tableCell.Value.Type = tableCell.ValueType - switch tableCell.ValueType { +func fillAttributeViewNilValue(value *av.Value, rowID, colID string, typ av.KeyType) { + value.Type = typ + switch typ { case av.KeyTypeText: - if nil == tableCell.Value.Text { - tableCell.Value.Text = &av.ValueText{} + if nil == value.Text { + value.Text = &av.ValueText{} } case av.KeyTypeNumber: - if nil == tableCell.Value.Number { - tableCell.Value.Number = &av.ValueNumber{} + if nil == value.Number { + value.Number = &av.ValueNumber{} } case av.KeyTypeDate: - if nil == tableCell.Value.Date { - tableCell.Value.Date = &av.ValueDate{} + if nil == value.Date { + value.Date = &av.ValueDate{} } case av.KeyTypeSelect: - if 1 > len(tableCell.Value.MSelect) { - tableCell.Value.MSelect = []*av.ValueSelect{} + if 1 > len(value.MSelect) { + value.MSelect = []*av.ValueSelect{} } case av.KeyTypeMSelect: - if 1 > len(tableCell.Value.MSelect) { - tableCell.Value.MSelect = []*av.ValueSelect{} + if 1 > len(value.MSelect) { + value.MSelect = []*av.ValueSelect{} } case av.KeyTypeURL: - if nil == tableCell.Value.URL { - tableCell.Value.URL = &av.ValueURL{} + if nil == value.URL { + value.URL = &av.ValueURL{} } case av.KeyTypeEmail: - if nil == tableCell.Value.Email { - tableCell.Value.Email = &av.ValueEmail{} + if nil == value.Email { + value.Email = &av.ValueEmail{} } case av.KeyTypePhone: - if nil == tableCell.Value.Phone { - tableCell.Value.Phone = &av.ValuePhone{} + if nil == value.Phone { + value.Phone = &av.ValuePhone{} } case av.KeyTypeMAsset: - if 1 > len(tableCell.Value.MAsset) { - tableCell.Value.MAsset = []*av.ValueAsset{} + if 1 > len(value.MAsset) { + value.MAsset = []*av.ValueAsset{} } case av.KeyTypeTemplate: - if nil == tableCell.Value.Template { - tableCell.Value.Template = &av.ValueTemplate{} + if nil == value.Template { + value.Template = &av.ValueTemplate{} } case av.KeyTypeCreated: - if nil == tableCell.Value.Created { - tableCell.Value.Created = &av.ValueCreated{} + if nil == value.Created { + value.Created = &av.ValueCreated{} } case av.KeyTypeUpdated: - if nil == tableCell.Value.Updated { - tableCell.Value.Updated = &av.ValueUpdated{} + if nil == value.Updated { + value.Updated = &av.ValueUpdated{} } case av.KeyTypeCheckbox: - if nil == tableCell.Value.Checkbox { - tableCell.Value.Checkbox = &av.ValueCheckbox{} + if nil == value.Checkbox { + value.Checkbox = &av.ValueCheckbox{} } case av.KeyTypeRelation: - if nil == tableCell.Value.Relation { - tableCell.Value.Relation = &av.ValueRelation{} + if nil == value.Relation { + value.Relation = &av.ValueRelation{} } case av.KeyTypeRollup: - if nil == tableCell.Value.Rollup { - tableCell.Value.Rollup = &av.ValueRollup{} + if nil == value.Rollup { + value.Rollup = &av.ValueRollup{} } } } @@ -687,7 +295,7 @@ func getAttributeViewContent(avID string) (content string) { return } -func getRowBlockValue(keyValues []*av.KeyValues) (ret *av.Value) { +func getBlockValue(keyValues []*av.KeyValues) (ret *av.Value) { for _, kv := range keyValues { if av.KeyTypeBlock == kv.Key.Type && 0 < len(kv.Values) { ret = kv.Values[0] @@ -704,3 +312,31 @@ func getAttrViewName(attrView *av.AttributeView) string { } return ret } + +func removeMissingField(attrView *av.AttributeView, view *av.View, missingKeyID string) { + logging.LogWarnf("key [%s] is missing", missingKeyID) + + changed := false + switch view.LayoutType { + case av.LayoutTypeTable: + for i, column := range view.Table.Columns { + if column.ID == missingKeyID { + view.Table.Columns = append(view.Table.Columns[:i], view.Table.Columns[i+1:]...) + changed = true + break + } + } + case av.LayoutTypeGallery: + for i, cardField := range view.Gallery.CardFields { + if cardField.ID == missingKeyID { + view.Gallery.CardFields = append(view.Gallery.CardFields[:i], view.Gallery.CardFields[i+1:]...) + changed = true + break + } + } + } + + if changed { + av.SaveAttributeView(attrView) + } +} diff --git a/kernel/sql/av_gallery.go b/kernel/sql/av_gallery.go new file mode 100644 index 000000000..3842cb319 --- /dev/null +++ b/kernel/sql/av_gallery.go @@ -0,0 +1,382 @@ +package sql + +import ( + "fmt" + "sort" + "strings" + "time" + + "github.com/88250/lute/ast" + "github.com/siyuan-note/siyuan/kernel/av" + "github.com/siyuan-note/siyuan/kernel/treenode" + "github.com/siyuan-note/siyuan/kernel/util" +) + +func RenderAttributeViewGallery(attrView *av.AttributeView, view *av.View, query string) (ret *av.Gallery) { + ret = &av.Gallery{ + ID: view.ID, + Icon: view.Icon, + Name: view.Name, + Desc: view.Desc, + HideAttrViewName: view.HideAttrViewName, + Filters: view.Gallery.Filters, + Sorts: view.Gallery.Sorts, + Fields: []*av.GalleryField{}, + Cards: []*av.GalleryCard{}, + } + + // 组装字段 + for _, field := range view.Gallery.CardFields { + key, getErr := attrView.GetKey(field.ID) + if nil != getErr { + // 找不到字段则在视图中删除 + removeMissingField(attrView, view, field.ID) + continue + } + + ret.Fields = append(ret.Fields, &av.GalleryField{ + ID: key.ID, + Name: key.Name, + Type: key.Type, + Icon: key.Icon, + Hidden: field.Hidden, + Desc: key.Desc, + Options: key.Options, + NumberFormat: key.NumberFormat, + Template: key.Template, + Relation: key.Relation, + Rollup: key.Rollup, + Date: key.Date, + }) + } + + // 生成卡片 + cards := map[string][]*av.KeyValues{} + for _, keyValues := range attrView.KeyValues { + for _, val := range keyValues.Values { + values := cards[val.BlockID] + if nil == values { + values = []*av.KeyValues{{Key: keyValues.Key, Values: []*av.Value{val}}} + } else { + values = append(values, &av.KeyValues{Key: keyValues.Key, Values: []*av.Value{val}}) + } + cards[val.BlockID] = values + } + } + + // 过滤掉不存在的卡片 + var notFound []string + var toCheckBlockIDs []string + for blockID, keyValues := range cards { + blockValue := getBlockValue(keyValues) + if nil == blockValue { + notFound = append(notFound, blockID) + continue + } + + if blockValue.IsDetached { + continue + } + + if nil != blockValue.Block && "" == blockValue.Block.ID { + notFound = append(notFound, blockID) + continue + } + + toCheckBlockIDs = append(toCheckBlockIDs, blockID) + } + checkRet := treenode.ExistBlockTrees(toCheckBlockIDs) + for blockID, exist := range checkRet { + if !exist { + notFound = append(notFound, blockID) + } + } + for _, blockID := range notFound { + delete(cards, blockID) + } + + // 生成卡片字段值 + for cardID, card := range cards { + var galleryCard av.GalleryCard + for _, field := range ret.Fields { + var fieldValue *av.GalleryFieldValue + for _, keyValues := range card { + if keyValues.Key.ID == field.ID { + fieldValue = &av.GalleryFieldValue{ + ID: keyValues.Values[0].ID, + Value: keyValues.Values[0], + ValueType: field.Type, + } + break + } + } + if nil == fieldValue { + fieldValue = &av.GalleryFieldValue{ + ID: ast.NewNodeID(), + ValueType: field.Type, + } + } + galleryCard.ID = cardID + + switch fieldValue.ValueType { + case av.KeyTypeNumber: // 格式化数字 + if nil != fieldValue.Value && nil != fieldValue.Value.Number && fieldValue.Value.Number.IsNotEmpty { + fieldValue.Value.Number.Format = field.NumberFormat + fieldValue.Value.Number.FormatNumber() + } + case av.KeyTypeTemplate: // 渲染模板字段 + fieldValue.Value = &av.Value{ID: fieldValue.ID, KeyID: field.ID, BlockID: cardID, Type: av.KeyTypeTemplate, Template: &av.ValueTemplate{Content: field.Template}} + case av.KeyTypeCreated: // 填充创建时间字段值,后面再渲染 + fieldValue.Value = &av.Value{ID: fieldValue.ID, KeyID: field.ID, BlockID: cardID, Type: av.KeyTypeCreated} + case av.KeyTypeUpdated: // 填充更新时间字段值,后面再渲染 + fieldValue.Value = &av.Value{ID: fieldValue.ID, KeyID: field.ID, BlockID: cardID, Type: av.KeyTypeUpdated} + case av.KeyTypeRelation: // 清空关联字段值,后面再渲染 https://ld246.com/article/1703831044435 + if nil != fieldValue.Value && nil != fieldValue.Value.Relation { + fieldValue.Value.Relation.Contents = nil + } + } + + if nil == fieldValue.Value { + fieldValue.Value = av.GetAttributeViewDefaultValue(fieldValue.ID, field.ID, cardID, fieldValue.ValueType) + } else { + fillAttributeViewNilValue(fieldValue.Value, cardID, field.ID, fieldValue.ValueType) + } + + galleryCard.Values = append(galleryCard.Values, fieldValue) + } + ret.Cards = append(ret.Cards, &galleryCard) + } + + // 批量获取块属性以提升性能 + var ialIDs []string + for _, card := range ret.Cards { + block := card.GetBlockValue() + if nil != block && !block.IsDetached { + ialIDs = append(ialIDs, card.ID) + } + } + ials := BatchGetBlockAttrs(ialIDs) + + // 渲染自动生成的字段值,比如关联字段、汇总字段、创建时间字段和更新时间字段 + avCache := map[string]*av.AttributeView{} + avCache[attrView.ID] = attrView + for _, card := range ret.Cards { + for _, value := range card.Values { + switch value.ValueType { + case av.KeyTypeBlock: // 对于主键可能需要填充静态锚文本 Database-bound block primary key supports setting static anchor text https://github.com/siyuan-note/siyuan/issues/10049 + if nil != value.Value.Block { + for k, v := range ials[card.ID] { + if k == av.NodeAttrViewStaticText+"-"+attrView.ID { + value.Value.Block.Content = v + break + } + } + } + case av.KeyTypeRollup: // 渲染汇总字段 + rollupKey, _ := attrView.GetKey(value.Value.KeyID) + if nil == rollupKey || nil == rollupKey.Rollup { + break + } + + relKey, _ := attrView.GetKey(rollupKey.Rollup.RelationKeyID) + if nil == relKey || nil == relKey.Relation { + break + } + + relVal := attrView.GetValue(relKey.ID, card.ID) + if nil == relVal || nil == relVal.Relation { + break + } + + destAv := avCache[relKey.Relation.AvID] + if nil == destAv { + destAv, _ = av.ParseAttributeView(relKey.Relation.AvID) + if nil != destAv { + avCache[relKey.Relation.AvID] = destAv + } + } + if nil == destAv { + break + } + + destKey, _ := destAv.GetKey(rollupKey.Rollup.KeyID) + if nil == destKey { + continue + } + + for _, blockID := range relVal.Relation.BlockIDs { + destVal := destAv.GetValue(rollupKey.Rollup.KeyID, blockID) + if nil == destVal { + if destAv.ExistBlock(blockID) { // 数据库中存在但是值不存在是数据未初始化,这里补一个默认值 + destVal = av.GetAttributeViewDefaultValue(ast.NewNodeID(), rollupKey.Rollup.KeyID, blockID, destKey.Type) + } + if nil == destVal { + continue + } + } + if av.KeyTypeNumber == destKey.Type { + destVal.Number.Format = destKey.NumberFormat + destVal.Number.FormatNumber() + } + + value.Value.Rollup.Contents = append(value.Value.Rollup.Contents, destVal.Clone()) + } + + value.Value.Rollup.RenderContents(rollupKey.Rollup.Calc, destKey) + + // 将汇总字段的值保存到 cards 中,后续渲染模板字段的时候会用到,下同 + keyValues := cards[card.ID] + keyValues = append(keyValues, &av.KeyValues{Key: rollupKey, Values: []*av.Value{{ID: value.Value.ID, KeyID: rollupKey.ID, BlockID: card.ID, Type: av.KeyTypeRollup, Rollup: value.Value.Rollup}}}) + cards[card.ID] = keyValues + case av.KeyTypeRelation: // 渲染关联字段 + relKey, _ := attrView.GetKey(value.Value.KeyID) + if nil != relKey && nil != relKey.Relation { + destAv := avCache[relKey.Relation.AvID] + if nil == destAv { + destAv, _ = av.ParseAttributeView(relKey.Relation.AvID) + if nil != destAv { + avCache[relKey.Relation.AvID] = destAv + } + } + if nil != destAv { + blocks := map[string]*av.Value{} + blockValues := destAv.GetBlockKeyValues() + if nil != blockValues { + for _, blockValue := range blockValues.Values { + blocks[blockValue.BlockID] = blockValue + } + for _, blockID := range value.Value.Relation.BlockIDs { + if val := blocks[blockID]; nil != val { + value.Value.Relation.Contents = append(value.Value.Relation.Contents, val) + } + } + } + } + } + + keyValues := cards[card.ID] + keyValues = append(keyValues, &av.KeyValues{Key: relKey, Values: []*av.Value{{ID: value.Value.ID, KeyID: relKey.ID, BlockID: card.ID, Type: av.KeyTypeRelation, Relation: value.Value.Relation}}}) + cards[card.ID] = keyValues + case av.KeyTypeCreated: // 渲染创建时间 + createdStr := card.ID[:len("20060102150405")] + created, parseErr := time.ParseInLocation("20060102150405", createdStr, time.Local) + if nil == parseErr { + value.Value.Created = av.NewFormattedValueCreated(created.UnixMilli(), 0, av.CreatedFormatNone) + value.Value.Created.IsNotEmpty = true + } else { + value.Value.Created = av.NewFormattedValueCreated(time.Now().UnixMilli(), 0, av.CreatedFormatNone) + } + + keyValues := cards[card.ID] + createdKey, _ := attrView.GetKey(value.Value.KeyID) + keyValues = append(keyValues, &av.KeyValues{Key: createdKey, Values: []*av.Value{{ID: value.Value.ID, KeyID: createdKey.ID, BlockID: card.ID, Type: av.KeyTypeCreated, Created: value.Value.Created}}}) + cards[card.ID] = keyValues + case av.KeyTypeUpdated: // 渲染更新时间 + ial := ials[card.ID] + if nil == ial { + ial = map[string]string{} + } + block := card.GetBlockValue() + updatedStr := ial["updated"] + if "" == updatedStr && nil != block { + value.Value.Updated = av.NewFormattedValueUpdated(block.Block.Updated, 0, av.UpdatedFormatNone) + value.Value.Updated.IsNotEmpty = true + } else { + updated, parseErr := time.ParseInLocation("20060102150405", updatedStr, time.Local) + if nil == parseErr { + value.Value.Updated = av.NewFormattedValueUpdated(updated.UnixMilli(), 0, av.UpdatedFormatNone) + value.Value.Updated.IsNotEmpty = true + } else { + value.Value.Updated = av.NewFormattedValueUpdated(time.Now().UnixMilli(), 0, av.UpdatedFormatNone) + } + } + + keyValues := cards[card.ID] + updatedKey, _ := attrView.GetKey(value.Value.KeyID) + keyValues = append(keyValues, &av.KeyValues{Key: updatedKey, Values: []*av.Value{{ID: value.Value.ID, KeyID: updatedKey.ID, BlockID: card.ID, Type: av.KeyTypeUpdated, Updated: value.Value.Updated}}}) + cards[card.ID] = keyValues + } + } + } + + // 最后单独渲染模板字段,这样模板字段就可以使用汇总、关联、创建时间和更新时间字段的值了 + + var renderTemplateErr error + for _, card := range ret.Cards { + for _, value := range card.Values { + switch value.ValueType { + case av.KeyTypeTemplate: // 渲染模板字段 + keyValues := cards[card.ID] + ial := ials[card.ID] + if nil == ial { + ial = map[string]string{} + } + content, renderErr := RenderTemplateField(ial, keyValues, value.Value.Template.Content) + value.Value.Template.Content = content + if nil != renderErr { + key, _ := attrView.GetKey(value.Value.KeyID) + keyName := "" + if nil != key { + keyName = key.Name + } + renderTemplateErr = fmt.Errorf("database [%s] template field [%s] rendering failed: %s", getAttrViewName(attrView), keyName, renderErr) + } + } + } + } + if nil != renderTemplateErr { + util.PushErrMsg(fmt.Sprintf(util.Langs[util.Lang][44], util.EscapeHTML(renderTemplateErr.Error())), 30000) + } + + // 根据搜索条件过滤 + query = strings.TrimSpace(query) + if "" != query { + // 将连续空格转换为一个空格 + query = strings.Join(strings.Fields(query), " ") + // 按空格分割关键字 + keywords := strings.Split(query, " ") + // 使用 AND 逻辑 https://github.com/siyuan-note/siyuan/issues/11535 + var hitCards []*av.GalleryCard + for _, card := range ret.Cards { + hit := false + for _, value := range card.Values { + allKeywordsHit := true + for _, keyword := range keywords { + if !strings.Contains(strings.ToLower(value.Value.String(true)), strings.ToLower(keyword)) { + allKeywordsHit = false + break + } + } + if allKeywordsHit { + hit = true + break + } + } + if hit { + hitCards = append(hitCards, card) + } + } + ret.Cards = hitCards + if 1 > len(ret.Cards) { + ret.Cards = []*av.GalleryCard{} + } + } + + // 自定义排序 + sortCardIDs := map[string]int{} + if 0 < len(view.Gallery.CardIDs) { + for i, cardID := range view.Gallery.CardIDs { + sortCardIDs[cardID] = i + } + } + + sort.Slice(ret.Fields, func(i, j int) bool { + iv := sortCardIDs[ret.Fields[i].ID] + jv := sortCardIDs[ret.Fields[j].ID] + if iv == jv { + return ret.Fields[i].ID < ret.Fields[j].ID + } + return iv < jv + }) + return +} diff --git a/kernel/sql/av_table.go b/kernel/sql/av_table.go new file mode 100644 index 000000000..db16b0782 --- /dev/null +++ b/kernel/sql/av_table.go @@ -0,0 +1,402 @@ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package sql + +import ( + "fmt" + "sort" + "strings" + "time" + + "github.com/88250/lute/ast" + "github.com/siyuan-note/siyuan/kernel/av" + "github.com/siyuan-note/siyuan/kernel/treenode" + "github.com/siyuan-note/siyuan/kernel/util" +) + +func RenderAttributeViewTable(attrView *av.AttributeView, view *av.View, query string) (ret *av.Table) { + ret = &av.Table{ + ID: view.ID, + Icon: view.Icon, + Name: view.Name, + Desc: view.Desc, + HideAttrViewName: view.HideAttrViewName, + Columns: []*av.TableColumn{}, + Rows: []*av.TableRow{}, + Filters: view.Table.Filters, + Sorts: view.Table.Sorts, + } + + // 组装列 + for _, col := range view.Table.Columns { + key, getErr := attrView.GetKey(col.ID) + if nil != getErr { + // 找不到字段则在视图中删除 + removeMissingField(attrView, view, col.ID) + continue + } + + ret.Columns = append(ret.Columns, &av.TableColumn{ + ID: key.ID, + Name: key.Name, + Type: key.Type, + Icon: key.Icon, + Options: key.Options, + NumberFormat: key.NumberFormat, + Template: key.Template, + Relation: key.Relation, + Rollup: key.Rollup, + Date: key.Date, + Wrap: col.Wrap, + Hidden: col.Hidden, + Width: col.Width, + Desc: key.Desc, + Pin: col.Pin, + Calc: col.Calc, + }) + } + + // 生成行 + rows := map[string][]*av.KeyValues{} + for _, keyValues := range attrView.KeyValues { + for _, val := range keyValues.Values { + values := rows[val.BlockID] + if nil == values { + values = []*av.KeyValues{{Key: keyValues.Key, Values: []*av.Value{val}}} + } else { + values = append(values, &av.KeyValues{Key: keyValues.Key, Values: []*av.Value{val}}) + } + rows[val.BlockID] = values + } + } + + // 过滤掉不存在的行 + var notFound []string + var toCheckBlockIDs []string + for blockID, keyValues := range rows { + blockValue := getBlockValue(keyValues) + if nil == blockValue { + notFound = append(notFound, blockID) + continue + } + + if blockValue.IsDetached { + continue + } + + if nil != blockValue.Block && "" == blockValue.Block.ID { + notFound = append(notFound, blockID) + continue + } + + toCheckBlockIDs = append(toCheckBlockIDs, blockID) + } + checkRet := treenode.ExistBlockTrees(toCheckBlockIDs) + for blockID, exist := range checkRet { + if !exist { + notFound = append(notFound, blockID) + } + } + for _, blockID := range notFound { + delete(rows, blockID) + } + + // 生成行单元格 + for rowID, row := range rows { + var tableRow av.TableRow + for _, col := range ret.Columns { + var tableCell *av.TableCell + for _, keyValues := range row { + if keyValues.Key.ID == col.ID { + tableCell = &av.TableCell{ + ID: keyValues.Values[0].ID, + Value: keyValues.Values[0], + ValueType: col.Type, + } + break + } + } + if nil == tableCell { + tableCell = &av.TableCell{ + ID: ast.NewNodeID(), + ValueType: col.Type, + } + } + tableRow.ID = rowID + + switch tableCell.ValueType { + case av.KeyTypeNumber: // 格式化数字 + if nil != tableCell.Value && nil != tableCell.Value.Number && tableCell.Value.Number.IsNotEmpty { + tableCell.Value.Number.Format = col.NumberFormat + tableCell.Value.Number.FormatNumber() + } + case av.KeyTypeTemplate: // 渲染模板列 + tableCell.Value = &av.Value{ID: tableCell.ID, KeyID: col.ID, BlockID: rowID, Type: av.KeyTypeTemplate, Template: &av.ValueTemplate{Content: col.Template}} + case av.KeyTypeCreated: // 填充创建时间列值,后面再渲染 + tableCell.Value = &av.Value{ID: tableCell.ID, KeyID: col.ID, BlockID: rowID, Type: av.KeyTypeCreated} + case av.KeyTypeUpdated: // 填充更新时间列值,后面再渲染 + tableCell.Value = &av.Value{ID: tableCell.ID, KeyID: col.ID, BlockID: rowID, Type: av.KeyTypeUpdated} + case av.KeyTypeRelation: // 清空关联列值,后面再渲染 https://ld246.com/article/1703831044435 + if nil != tableCell.Value && nil != tableCell.Value.Relation { + tableCell.Value.Relation.Contents = nil + } + } + + if nil == tableCell.Value { + tableCell.Value = av.GetAttributeViewDefaultValue(tableCell.ID, col.ID, rowID, tableCell.ValueType) + } else { + fillAttributeViewNilValue(tableCell.Value, rowID, col.ID, tableCell.ValueType) + } + + tableRow.Cells = append(tableRow.Cells, tableCell) + } + ret.Rows = append(ret.Rows, &tableRow) + } + + // 批量获取块属性以提升性能 + var ialIDs []string + for _, row := range ret.Rows { + block := row.GetBlockValue() + if nil != block && !block.IsDetached { + ialIDs = append(ialIDs, row.ID) + } + } + ials := BatchGetBlockAttrs(ialIDs) + + // 渲染自动生成的列值,比如关联列、汇总列、创建时间列和更新时间列 + avCache := map[string]*av.AttributeView{} + avCache[attrView.ID] = attrView + for _, row := range ret.Rows { + for _, cell := range row.Cells { + switch cell.ValueType { + case av.KeyTypeBlock: // 对于主键可能需要填充静态锚文本 Database-bound block primary key supports setting static anchor text https://github.com/siyuan-note/siyuan/issues/10049 + if nil != cell.Value.Block { + for k, v := range ials[row.ID] { + if k == av.NodeAttrViewStaticText+"-"+attrView.ID { + cell.Value.Block.Content = v + break + } + } + } + case av.KeyTypeRollup: // 渲染汇总列 + rollupKey, _ := attrView.GetKey(cell.Value.KeyID) + if nil == rollupKey || nil == rollupKey.Rollup { + break + } + + relKey, _ := attrView.GetKey(rollupKey.Rollup.RelationKeyID) + if nil == relKey || nil == relKey.Relation { + break + } + + relVal := attrView.GetValue(relKey.ID, row.ID) + if nil == relVal || nil == relVal.Relation { + break + } + + destAv := avCache[relKey.Relation.AvID] + if nil == destAv { + destAv, _ = av.ParseAttributeView(relKey.Relation.AvID) + if nil != destAv { + avCache[relKey.Relation.AvID] = destAv + } + } + if nil == destAv { + break + } + + destKey, _ := destAv.GetKey(rollupKey.Rollup.KeyID) + if nil == destKey { + continue + } + + for _, blockID := range relVal.Relation.BlockIDs { + destVal := destAv.GetValue(rollupKey.Rollup.KeyID, blockID) + if nil == destVal { + if destAv.ExistBlock(blockID) { // 数据库中存在行但是列值不存在是数据未初始化,这里补一个默认值 + destVal = av.GetAttributeViewDefaultValue(ast.NewNodeID(), rollupKey.Rollup.KeyID, blockID, destKey.Type) + } + if nil == destVal { + continue + } + } + if av.KeyTypeNumber == destKey.Type { + destVal.Number.Format = destKey.NumberFormat + destVal.Number.FormatNumber() + } + + cell.Value.Rollup.Contents = append(cell.Value.Rollup.Contents, destVal.Clone()) + } + + cell.Value.Rollup.RenderContents(rollupKey.Rollup.Calc, destKey) + + // 将汇总列的值保存到 rows 中,后续渲染模板列的时候会用到,下同 + // Database table view template columns support reading relation, rollup, created and updated columns https://github.com/siyuan-note/siyuan/issues/10442 + keyValues := rows[row.ID] + keyValues = append(keyValues, &av.KeyValues{Key: rollupKey, Values: []*av.Value{{ID: cell.Value.ID, KeyID: rollupKey.ID, BlockID: row.ID, Type: av.KeyTypeRollup, Rollup: cell.Value.Rollup}}}) + rows[row.ID] = keyValues + case av.KeyTypeRelation: // 渲染关联列 + relKey, _ := attrView.GetKey(cell.Value.KeyID) + if nil != relKey && nil != relKey.Relation { + destAv := avCache[relKey.Relation.AvID] + if nil == destAv { + destAv, _ = av.ParseAttributeView(relKey.Relation.AvID) + if nil != destAv { + avCache[relKey.Relation.AvID] = destAv + } + } + if nil != destAv { + blocks := map[string]*av.Value{} + blockValues := destAv.GetBlockKeyValues() + if nil != blockValues { + for _, blockValue := range blockValues.Values { + blocks[blockValue.BlockID] = blockValue + } + for _, blockID := range cell.Value.Relation.BlockIDs { + if val := blocks[blockID]; nil != val { + cell.Value.Relation.Contents = append(cell.Value.Relation.Contents, val) + } + } + } + } + } + + keyValues := rows[row.ID] + keyValues = append(keyValues, &av.KeyValues{Key: relKey, Values: []*av.Value{{ID: cell.Value.ID, KeyID: relKey.ID, BlockID: row.ID, Type: av.KeyTypeRelation, Relation: cell.Value.Relation}}}) + rows[row.ID] = keyValues + case av.KeyTypeCreated: // 渲染创建时间 + createdStr := row.ID[:len("20060102150405")] + created, parseErr := time.ParseInLocation("20060102150405", createdStr, time.Local) + if nil == parseErr { + cell.Value.Created = av.NewFormattedValueCreated(created.UnixMilli(), 0, av.CreatedFormatNone) + cell.Value.Created.IsNotEmpty = true + } else { + cell.Value.Created = av.NewFormattedValueCreated(time.Now().UnixMilli(), 0, av.CreatedFormatNone) + } + + keyValues := rows[row.ID] + createdKey, _ := attrView.GetKey(cell.Value.KeyID) + keyValues = append(keyValues, &av.KeyValues{Key: createdKey, Values: []*av.Value{{ID: cell.Value.ID, KeyID: createdKey.ID, BlockID: row.ID, Type: av.KeyTypeCreated, Created: cell.Value.Created}}}) + rows[row.ID] = keyValues + case av.KeyTypeUpdated: // 渲染更新时间 + ial := ials[row.ID] + if nil == ial { + ial = map[string]string{} + } + block := row.GetBlockValue() + updatedStr := ial["updated"] + if "" == updatedStr && nil != block { + cell.Value.Updated = av.NewFormattedValueUpdated(block.Block.Updated, 0, av.UpdatedFormatNone) + cell.Value.Updated.IsNotEmpty = true + } else { + updated, parseErr := time.ParseInLocation("20060102150405", updatedStr, time.Local) + if nil == parseErr { + cell.Value.Updated = av.NewFormattedValueUpdated(updated.UnixMilli(), 0, av.UpdatedFormatNone) + cell.Value.Updated.IsNotEmpty = true + } else { + cell.Value.Updated = av.NewFormattedValueUpdated(time.Now().UnixMilli(), 0, av.UpdatedFormatNone) + } + } + + keyValues := rows[row.ID] + updatedKey, _ := attrView.GetKey(cell.Value.KeyID) + keyValues = append(keyValues, &av.KeyValues{Key: updatedKey, Values: []*av.Value{{ID: cell.Value.ID, KeyID: updatedKey.ID, BlockID: row.ID, Type: av.KeyTypeUpdated, Updated: cell.Value.Updated}}}) + rows[row.ID] = keyValues + } + } + } + + // 最后单独渲染模板列,这样模板列就可以使用汇总、关联、创建时间和更新时间列的值了 + // Database table view template columns support reading relation, rollup, created and updated columns https://github.com/siyuan-note/siyuan/issues/10442 + + var renderTemplateErr error + for _, row := range ret.Rows { + for _, cell := range row.Cells { + switch cell.ValueType { + case av.KeyTypeTemplate: // 渲染模板列 + keyValues := rows[row.ID] + ial := ials[row.ID] + if nil == ial { + ial = map[string]string{} + } + content, renderErr := RenderTemplateField(ial, keyValues, cell.Value.Template.Content) + cell.Value.Template.Content = content + if nil != renderErr { + key, _ := attrView.GetKey(cell.Value.KeyID) + keyName := "" + if nil != key { + keyName = key.Name + } + renderTemplateErr = fmt.Errorf("database [%s] template field [%s] rendering failed: %s", getAttrViewName(attrView), keyName, renderErr) + } + } + } + } + if nil != renderTemplateErr { + util.PushErrMsg(fmt.Sprintf(util.Langs[util.Lang][44], util.EscapeHTML(renderTemplateErr.Error())), 30000) + } + + // 根据搜索条件过滤 + query = strings.TrimSpace(query) + if "" != query { + // 将连续空格转换为一个空格 + query = strings.Join(strings.Fields(query), " ") + // 按空格分割关键字 + keywords := strings.Split(query, " ") + // 使用 AND 逻辑 https://github.com/siyuan-note/siyuan/issues/11535 + var hitRows []*av.TableRow + for _, row := range ret.Rows { + hit := false + for _, cell := range row.Cells { + allKeywordsHit := true + for _, keyword := range keywords { + if !strings.Contains(strings.ToLower(cell.Value.String(true)), strings.ToLower(keyword)) { + allKeywordsHit = false + break + } + } + if allKeywordsHit { + hit = true + break + } + } + if hit { + hitRows = append(hitRows, row) + } + } + ret.Rows = hitRows + if 1 > len(ret.Rows) { + ret.Rows = []*av.TableRow{} + } + } + + // 自定义排序 + sortRowIDs := map[string]int{} + if 0 < len(view.Table.RowIDs) { + for i, rowID := range view.Table.RowIDs { + sortRowIDs[rowID] = i + } + } + + sort.Slice(ret.Rows, func(i, j int) bool { + iv := sortRowIDs[ret.Rows[i].ID] + jv := sortRowIDs[ret.Rows[j].ID] + if iv == jv { + return ret.Rows[i].ID < ret.Rows[j].ID + } + return iv < jv + }) + return +}