浏览代码

:art: Add Relation and Rollup column to database table view https://github.com/siyuan-note/siyuan/issues/9888

Daniel 1 年之前
父节点
当前提交
fca3bf6855
共有 5 个文件被更改,包括 215 次插入5 次删除
  1. 19 3
      kernel/av/av.go
  2. 161 0
      kernel/av/table.go
  3. 21 0
      kernel/av/value.go
  4. 6 2
      kernel/model/attribute_view.go
  5. 8 0
      kernel/treenode/node.go

+ 19 - 3
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 {

+ 161 - 0
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)}
+		}
+	}
+}

+ 21 - 0
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"`
+}

+ 6 - 2
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)

+ 8 - 0
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{}
+		}
 	}
 }