From fca3bf68554140c7831b9cfd797bf417b394640e Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Fri, 15 Dec 2023 20:05:14 +0800 Subject: [PATCH] :art: Add Relation and Rollup column to database table view https://github.com/siyuan-note/siyuan/issues/9888 --- kernel/av/av.go | 22 ++++- kernel/av/table.go | 161 +++++++++++++++++++++++++++++++++ kernel/av/value.go | 21 +++++ kernel/model/attribute_view.go | 8 +- kernel/treenode/node.go | 8 ++ 5 files changed, 215 insertions(+), 5 deletions(-) diff --git a/kernel/av/av.go b/kernel/av/av.go index 550287788..929492583 100644 --- a/kernel/av/av.go +++ b/kernel/av/av.go @@ -66,6 +66,8 @@ const ( KeyTypeCreated KeyType = "created" KeyTypeUpdated KeyType = "updated" KeyTypeCheckbox KeyType = "checkbox" + KeyTypeRelation KeyType = "relation" + KeyTypeRollup KeyType = "rollup" ) // Key 描述了属性视图属性列的基础结构。 @@ -77,9 +79,23 @@ type Key struct { // 以下是某些列类型的特有属性 - Options []*KeySelectOption `json:"options,omitempty"` // 选项列表 - NumberFormat NumberFormat `json:"numberFormat"` // 列数字格式化 - Template string `json:"template"` // 模板内容 + // 单选/多选列 + Options []*KeySelectOption `json:"options,omitempty"` // 选项列表 + + // 数字列 + NumberFormat NumberFormat `json:"numberFormat"` // 列数字格式化 + + // 模板列 + Template string `json:"template"` // 模板内容 + + // 关联列 + RelationAvID string `json:"relationAvID"` // 关联的属性视图 ID + RelationKeyID string `json:"relationKeyID"` // 关联列 ID + IsBiRelation bool `json:"isBiRelation"` // 是否双向关联 + BackRelationKeyID string `json:"backRelationKeyID"` // 双向关联时回链关联列的 ID + + // 汇总列 + RollupKeyID string `json:"rollupKeyID"` // 汇总列 ID } func NewKey(id, name, icon string, keyType KeyType) *Key { diff --git a/kernel/av/table.go b/kernel/av/table.go index 878e1e6d0..c518ad4c0 100644 --- a/kernel/av/table.go +++ b/kernel/av/table.go @@ -186,6 +186,10 @@ func (value *Value) Compare(other *Value) int { } return 0 } + case KeyTypeRelation: + // TODO: relation compare + case KeyTypeRollup: + // TODO: rollup compare } return 0 } @@ -567,6 +571,29 @@ func (value *Value) CompareOperator(other *Value, operator FilterOperator) bool return !value.Checkbox.Checked } } + + if nil != value.Relation && nil != other.Relation { + switch operator { + case FilterOperatorContains: + if "" == strings.TrimSpace(other.Relation.Content) { + return true + } + return strings.Contains(value.Relation.Content, other.Relation.Content) + case FilterOperatorDoesNotContain: + if "" == strings.TrimSpace(other.Relation.Content) { + return true + } + return !strings.Contains(value.Relation.Content, other.Relation.Content) + case FilterOperatorIsEmpty: + return "" == strings.TrimSpace(value.Relation.Content) + case FilterOperatorIsNotEmpty: + return "" != strings.TrimSpace(value.Relation.Content) + } + } + + if nil != value.Rollup && nil != other.Rollup { + // TODO: rollup filter + } return false } @@ -760,6 +787,10 @@ func (table *Table) CalcCols() { table.calcColUpdated(col, i) case KeyTypeCheckbox: table.calcColCheckbox(col, i) + case KeyTypeRelation: + table.calcColRelation(col, i) + case KeyTypeRollup: + table.calcColRollup(col, i) } } } @@ -1912,3 +1943,133 @@ func (table *Table) calcColCheckbox(col *TableColumn, colIndex int) { } } } + +func (table *Table) calcColRelation(col *TableColumn, colIndex int) { + switch col.Calc.Operator { + case CalcOperatorCountAll: + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(len(table.Rows)), NumberFormatNone)} + case CalcOperatorCountValues: + countValues := 0 + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Relation { + countValues++ + } + } + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countValues), NumberFormatNone)} + case CalcOperatorCountUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Relation { + for _, id := range row.Cells[colIndex].Value.Relation.BlockIDs { + if !uniqueValues[id] { + uniqueValues[id] = true + countUniqueValues++ + } + } + } + } + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues), NumberFormatNone)} + case CalcOperatorCountEmpty: + countEmpty := 0 + for _, row := range table.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Relation || 0 == len(row.Cells[colIndex].Value.Relation.BlockIDs) { + countEmpty++ + } + } + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countEmpty), NumberFormatNone)} + case CalcOperatorCountNotEmpty: + countNotEmpty := 0 + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Relation && 0 < len(row.Cells[colIndex].Value.Relation.BlockIDs) { + countNotEmpty++ + } + } + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty), NumberFormatNone)} + case CalcOperatorPercentEmpty: + countEmpty := 0 + for _, row := range table.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Relation || 0 == len(row.Cells[colIndex].Value.Relation.BlockIDs) { + countEmpty++ + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countEmpty)/float64(len(table.Rows)), NumberFormatPercent)} + } + case CalcOperatorPercentNotEmpty: + countNotEmpty := 0 + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Relation && 0 < len(row.Cells[colIndex].Value.Relation.BlockIDs) { + countNotEmpty++ + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} + } + } +} + +func (table *Table) calcColRollup(col *TableColumn, colIndex int) { + switch col.Calc.Operator { + case CalcOperatorCountAll: + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(len(table.Rows)), NumberFormatNone)} + case CalcOperatorCountValues: + countValues := 0 + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Rollup { + countValues++ + } + } + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countValues), NumberFormatNone)} + case CalcOperatorCountUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Rollup { + for _, content := range row.Cells[colIndex].Value.Rollup.Contents { + if !uniqueValues[content] { + uniqueValues[content] = true + countUniqueValues++ + } + } + } + } + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues), NumberFormatNone)} + case CalcOperatorCountEmpty: + countEmpty := 0 + for _, row := range table.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Rollup || 0 == len(row.Cells[colIndex].Value.Rollup.Contents) { + countEmpty++ + } + } + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countEmpty), NumberFormatNone)} + case CalcOperatorCountNotEmpty: + countNotEmpty := 0 + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Rollup && 0 < len(row.Cells[colIndex].Value.Rollup.Contents) { + countNotEmpty++ + } + } + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty), NumberFormatNone)} + case CalcOperatorPercentEmpty: + countEmpty := 0 + for _, row := range table.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Rollup || 0 == len(row.Cells[colIndex].Value.Rollup.Contents) { + countEmpty++ + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countEmpty)/float64(len(table.Rows)), NumberFormatPercent)} + } + case CalcOperatorPercentNotEmpty: + countNotEmpty := 0 + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Rollup && 0 < len(row.Cells[colIndex].Value.Rollup.Contents) { + countNotEmpty++ + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} + } + } +} diff --git a/kernel/av/value.go b/kernel/av/value.go index dfb8cf658..b86f6a8a6 100644 --- a/kernel/av/value.go +++ b/kernel/av/value.go @@ -50,6 +50,8 @@ type Value struct { Created *ValueCreated `json:"created,omitempty"` Updated *ValueUpdated `json:"updated,omitempty"` Checkbox *ValueCheckbox `json:"checkbox,omitempty"` + Relation *ValueRelation `json:"relation,omitempty"` + Rollup *ValueRollup `json:"rollup,omitempty"` } func (value *Value) String() string { @@ -135,6 +137,16 @@ func (value *Value) String() string { return "√" } return "" + case KeyTypeRelation: + if nil == value.Relation { + return "" + } + return value.Relation.Content + case KeyTypeRollup: + if nil == value.Rollup { + return "" + } + return strings.Join(value.Rollup.Contents, " ") default: return "" } @@ -433,3 +445,12 @@ func NewFormattedValueUpdated(content, content2 int64, format UpdatedFormat) (re type ValueCheckbox struct { Checked bool `json:"checked"` } + +type ValueRelation struct { + Content string `json:"content"` + BlockIDs []string `json:"blockIDs"` +} + +type ValueRollup struct { + Contents []string `json:"contents"` +} diff --git a/kernel/model/attribute_view.go b/kernel/model/attribute_view.go index 88287910d..0b21e1910 100644 --- a/kernel/model/attribute_view.go +++ b/kernel/model/attribute_view.go @@ -1490,7 +1490,9 @@ func addAttributeViewColumn(operation *Operation) (err error) { keyType := av.KeyType(operation.Typ) switch keyType { - 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: + 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: var icon string if nil != operation.Data { icon = operation.Data.(string) @@ -1584,7 +1586,9 @@ func updateAttributeViewColumn(operation *Operation) (err error) { colType := av.KeyType(operation.Typ) 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: + 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: for _, keyValues := range attrView.KeyValues { if keyValues.Key.ID == operation.ID { keyValues.Key.Name = strings.TrimSpace(operation.Name) diff --git a/kernel/treenode/node.go b/kernel/treenode/node.go index 2cf77b6b8..0312927f0 100644 --- a/kernel/treenode/node.go +++ b/kernel/treenode/node.go @@ -812,6 +812,14 @@ func FillAttributeViewTableCellNilValue(tableCell *av.TableCell, rowID, colID st if nil == tableCell.Value.Checkbox { tableCell.Value.Checkbox = &av.ValueCheckbox{} } + case av.KeyTypeRelation: + if nil == tableCell.Value.Relation { + tableCell.Value.Relation = &av.ValueRelation{} + } + case av.KeyTypeRollup: + if nil == tableCell.Value.Rollup { + tableCell.Value.Rollup = &av.ValueRollup{} + } } }