Forráskód Böngészése

Merge remote-tracking branch 'origin/dev' into dev

Vanessa 1 éve
szülő
commit
d1009f2333

+ 3 - 1
app/src/protyle/hint/extend.ts

@@ -447,7 +447,9 @@ export const hintRenderTemplate = (value: string, protyle: IProtyle, nodeElement
 
 export const hintRenderWidget = (value: string, protyle: IProtyle) => {
     focusByRange(protyle.toolbar.range);
-    insertHTML(protyle.lute.SpinBlockDOM(`<iframe src="/widgets/${value}" data-subtype="widget" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>`), protyle, true);
+    // src 地址以 / 结尾
+    // Use the path ending with `/` when loading the widget https://github.com/siyuan-note/siyuan/issues/10520
+    insertHTML(protyle.lute.SpinBlockDOM(`<iframe src="/widgets/${value}/" data-subtype="widget" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>`), protyle, true);
     hideElements(["util"], protyle);
 };
 

+ 5 - 1
kernel/api/av.go

@@ -122,8 +122,12 @@ func addAttributeViewValues(c *gin.Context) {
 		previousID = arg["previousID"].(string)
 	}
 	isDetached := arg["isDetached"].(bool)
+	ignoreFillFilter := true
+	if nil != arg["ignoreFillFilter"] {
+		ignoreFillFilter = arg["ignoreFillFilter"].(bool)
+	}
 
-	err := model.AddAttributeViewBlock(nil, srcIDs, avID, blockID, previousID, isDetached)
+	err := model.AddAttributeViewBlock(nil, srcIDs, avID, blockID, previousID, isDetached, ignoreFillFilter)
 	if nil != err {
 		ret.Code = -1
 		ret.Msg = err.Error()

+ 554 - 0
kernel/av/filter.go

@@ -17,6 +17,9 @@
 package av
 
 import (
+	"strings"
+	"time"
+
 	"github.com/siyuan-note/siyuan/kernel/util"
 )
 
@@ -75,6 +78,557 @@ const (
 	FilterOperatorIsFalse          FilterOperator = "Is false"
 )
 
+func (value *Value) Filter(filter *ViewFilter, attrView *AttributeView, rowID string) bool {
+	if nil == filter || (nil == filter.Value && nil == filter.RelativeDate) {
+		return true
+	}
+
+	if nil != filter.Value && value.Type != filter.Value.Type {
+		// 由于字段类型被用户编辑过导致和过滤器值类型不匹配,该情况下不过滤
+		return true
+	}
+
+	if nil != value.Rollup && KeyTypeRollup == value.Type && nil != filter && nil != filter.Value && KeyTypeRollup == filter.Value.Type &&
+		nil != filter.Value.Rollup && 0 < len(filter.Value.Rollup.Contents) {
+		// 单独处理汇总类型的比较
+
+		// 处理为空和不为空
+		switch filter.Operator {
+		case FilterOperatorIsEmpty:
+			return 0 == len(value.Rollup.Contents)
+		case FilterOperatorIsNotEmpty:
+			return 0 != len(value.Rollup.Contents)
+		}
+
+		// 处理值比较
+		key, _ := attrView.GetKey(value.KeyID)
+		if nil == key {
+			return false
+		}
+
+		relKey, _ := attrView.GetKey(key.Rollup.RelationKeyID)
+		if nil == relKey {
+			return false
+		}
+
+		relVal := attrView.GetValue(relKey.ID, rowID)
+		if nil == relVal || nil == relVal.Relation {
+			return false
+		}
+
+		destAv, _ := ParseAttributeView(relKey.Relation.AvID)
+		if nil == destAv {
+			return false
+		}
+
+		for _, blockID := range relVal.Relation.BlockIDs {
+			destVal := destAv.GetValue(key.Rollup.KeyID, blockID)
+			if nil == destVal {
+				continue
+			}
+
+			if destVal.filter(filter.Value.Rollup.Contents[0], filter.RelativeDate, filter.RelativeDate2, filter.Operator) {
+				return true
+			}
+		}
+		return false
+	}
+
+	if nil != value.Relation && KeyTypeRelation == value.Type && 0 < len(value.Relation.Contents) && nil != filter && nil != filter.Value && KeyTypeRelation == filter.Value.Type &&
+		nil != filter.Value.Relation && 0 < len(filter.Value.Relation.BlockIDs) {
+		// 单独处理关联类型的比较
+
+		for _, relationValue := range value.Relation.Contents {
+			filterValue := &Value{Type: KeyTypeBlock, Block: &ValueBlock{Content: filter.Value.Relation.BlockIDs[0]}}
+			if relationValue.filter(filterValue, filter.RelativeDate, filter.RelativeDate2, filter.Operator) {
+				return true
+			}
+		}
+		return false
+	}
+	return value.filter(filter.Value, filter.RelativeDate, filter.RelativeDate2, filter.Operator)
+}
+
+func (value *Value) filter(other *Value, relativeDate, relativeDate2 *RelativeDate, operator FilterOperator) bool {
+	switch value.Type {
+	case KeyTypeBlock:
+		if nil != value.Block && nil != other && nil != other.Block {
+			switch operator {
+			case FilterOperatorIsEqual:
+				return value.Block.Content == other.Block.Content
+			case FilterOperatorIsNotEqual:
+				return value.Block.Content != other.Block.Content
+			case FilterOperatorContains:
+				return strings.Contains(value.Block.Content, other.Block.Content)
+			case FilterOperatorDoesNotContain:
+				return !strings.Contains(value.Block.Content, other.Block.Content)
+			case FilterOperatorStartsWith:
+				return strings.HasPrefix(value.Block.Content, other.Block.Content)
+			case FilterOperatorEndsWith:
+				return strings.HasSuffix(value.Block.Content, other.Block.Content)
+			case FilterOperatorIsEmpty:
+				return "" == strings.TrimSpace(value.Block.Content)
+			case FilterOperatorIsNotEmpty:
+				return "" != strings.TrimSpace(value.Block.Content)
+			}
+		}
+	case KeyTypeText:
+		if nil != value.Text && nil != other && nil != other.Text {
+			switch operator {
+			case FilterOperatorIsEqual:
+				if "" == strings.TrimSpace(other.Text.Content) {
+					return true
+				}
+				return value.Text.Content == other.Text.Content
+			case FilterOperatorIsNotEqual:
+				if "" == strings.TrimSpace(other.Text.Content) {
+					return true
+				}
+				return value.Text.Content != other.Text.Content
+			case FilterOperatorContains:
+				if "" == strings.TrimSpace(other.Text.Content) {
+					return true
+				}
+				return strings.Contains(value.Text.Content, other.Text.Content)
+			case FilterOperatorDoesNotContain:
+				if "" == strings.TrimSpace(other.Text.Content) {
+					return true
+				}
+				return !strings.Contains(value.Text.Content, other.Text.Content)
+			case FilterOperatorStartsWith:
+				if "" == strings.TrimSpace(other.Text.Content) {
+					return true
+				}
+				return strings.HasPrefix(value.Text.Content, other.Text.Content)
+			case FilterOperatorEndsWith:
+				if "" == strings.TrimSpace(other.Text.Content) {
+					return true
+				}
+				return strings.HasSuffix(value.Text.Content, other.Text.Content)
+			case FilterOperatorIsEmpty:
+				return "" == strings.TrimSpace(value.Text.Content)
+			case FilterOperatorIsNotEmpty:
+				return "" != strings.TrimSpace(value.Text.Content)
+			}
+		}
+	case KeyTypeNumber:
+		if nil != value.Number && nil != other && nil != other.Number {
+			switch operator {
+			case FilterOperatorIsEqual:
+				if !other.Number.IsNotEmpty {
+					return true
+				}
+				return value.Number.Content == other.Number.Content
+			case FilterOperatorIsNotEqual:
+				if !other.Number.IsNotEmpty {
+					return true
+				}
+				return value.Number.Content != other.Number.Content
+			case FilterOperatorIsGreater:
+				return value.Number.Content > other.Number.Content
+			case FilterOperatorIsGreaterOrEqual:
+				return value.Number.Content >= other.Number.Content
+			case FilterOperatorIsLess:
+				return value.Number.Content < other.Number.Content
+			case FilterOperatorIsLessOrEqual:
+				return value.Number.Content <= other.Number.Content
+			case FilterOperatorIsEmpty:
+				return !value.Number.IsNotEmpty
+			case FilterOperatorIsNotEmpty:
+				return value.Number.IsNotEmpty
+			}
+		}
+	case KeyTypeDate:
+		if nil != value.Date {
+			if nil != relativeDate {
+				// 使用相对时间比较
+
+				count := relativeDate.Count
+				unit := relativeDate.Unit
+				direction := relativeDate.Direction
+				relativeTimeStart, relativeTimeEnd := calcRelativeTimeRegion(count, unit, direction)
+				_, relativeTimeEnd2 := calcRelativeTimeRegion(relativeDate2.Count, relativeDate2.Unit, relativeDate2.Direction)
+				return filterTime(value.Date.Content, value.Date.IsNotEmpty, relativeTimeStart, relativeTimeEnd, relativeTimeEnd2, operator)
+			} else { // 使用具体时间比较
+				if nil == other.Date {
+					return true
+				}
+
+				otherTime := time.UnixMilli(other.Date.Content)
+				otherStart := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 0, 0, 0, 0, otherTime.Location())
+				otherEnd := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 23, 59, 59, 999999999, otherTime.Location())
+				return filterTime(value.Date.Content, value.Date.IsNotEmpty, otherStart, otherEnd, time.Now(), operator)
+			}
+		}
+	case KeyTypeCreated:
+		if nil != value.Created {
+			if nil != relativeDate {
+				// 使用相对时间比较
+
+				count := relativeDate.Count
+				unit := relativeDate.Unit
+				direction := relativeDate.Direction
+				relativeTimeStart, relativeTimeEnd := calcRelativeTimeRegion(count, unit, direction)
+				return filterTime(value.Created.Content, true, relativeTimeStart, relativeTimeEnd, time.Now(), operator)
+			} else { // 使用具体时间比较
+				if nil == other.Created {
+					return true
+				}
+
+				otherTime := time.UnixMilli(other.Created.Content)
+				otherStart := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 0, 0, 0, 0, otherTime.Location())
+				otherEnd := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 23, 59, 59, 999999999, otherTime.Location())
+				return filterTime(value.Created.Content, value.Created.IsNotEmpty, otherStart, otherEnd, time.Now(), operator)
+			}
+		}
+	case KeyTypeUpdated:
+		if nil != value.Updated {
+			if nil != relativeDate {
+				// 使用相对时间比较
+
+				count := relativeDate.Count
+				unit := relativeDate.Unit
+				direction := relativeDate.Direction
+				relativeTimeStart, relativeTimeEnd := calcRelativeTimeRegion(count, unit, direction)
+				return filterTime(value.Updated.Content, true, relativeTimeStart, relativeTimeEnd, time.Now(), operator)
+			} else { // 使用具体时间比较
+				if nil == other.Updated {
+					return true
+				}
+
+				otherTime := time.UnixMilli(other.Updated.Content)
+				otherStart := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 0, 0, 0, 0, otherTime.Location())
+				otherEnd := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 23, 59, 59, 999999999, otherTime.Location())
+				return filterTime(value.Updated.Content, value.Updated.IsNotEmpty, otherStart, otherEnd, time.Now(), operator)
+			}
+		}
+	case KeyTypeSelect, KeyTypeMSelect:
+		if nil != value.MSelect {
+			if nil != other && nil != other.MSelect {
+				switch operator {
+				case FilterOperatorIsEqual, FilterOperatorContains:
+					contains := false
+					for _, v := range value.MSelect {
+						for _, v2 := range other.MSelect {
+							if v.Content == v2.Content {
+								contains = true
+								break
+							}
+						}
+					}
+					return contains
+				case FilterOperatorIsNotEqual, FilterOperatorDoesNotContain:
+					contains := false
+					for _, v := range value.MSelect {
+						for _, v2 := range other.MSelect {
+							if v.Content == v2.Content {
+								contains = true
+								break
+							}
+						}
+					}
+					return !contains
+				case FilterOperatorIsEmpty:
+					return 0 == len(value.MSelect) || 1 == len(value.MSelect) && "" == value.MSelect[0].Content
+				case FilterOperatorIsNotEmpty:
+					return 0 != len(value.MSelect) && !(1 == len(value.MSelect) && "" == value.MSelect[0].Content)
+				}
+				return false
+			}
+
+			// 没有设置比较值
+
+			switch operator {
+			case FilterOperatorIsEqual, FilterOperatorIsNotEqual, FilterOperatorContains, FilterOperatorDoesNotContain:
+				return true
+			case FilterOperatorIsEmpty:
+				return 0 == len(value.MSelect) || 1 == len(value.MSelect) && "" == value.MSelect[0].Content
+			case FilterOperatorIsNotEmpty:
+				return 0 != len(value.MSelect) && !(1 == len(value.MSelect) && "" == value.MSelect[0].Content)
+			}
+		}
+	case KeyTypeURL:
+		if nil != value.URL && nil != other && nil != other.URL {
+			switch operator {
+			case FilterOperatorIsEqual:
+				return value.URL.Content == other.URL.Content
+			case FilterOperatorIsNotEqual:
+				return value.URL.Content != other.URL.Content
+			case FilterOperatorContains:
+				return strings.Contains(value.URL.Content, other.URL.Content)
+			case FilterOperatorDoesNotContain:
+				return !strings.Contains(value.URL.Content, other.URL.Content)
+			case FilterOperatorStartsWith:
+				return strings.HasPrefix(value.URL.Content, other.URL.Content)
+			case FilterOperatorEndsWith:
+				return strings.HasSuffix(value.URL.Content, other.URL.Content)
+			case FilterOperatorIsEmpty:
+				return "" == strings.TrimSpace(value.URL.Content)
+			case FilterOperatorIsNotEmpty:
+				return "" != strings.TrimSpace(value.URL.Content)
+			}
+		}
+	case KeyTypeEmail:
+		if nil != value.Email && nil != other && nil != other.Email {
+			switch operator {
+			case FilterOperatorIsEqual:
+				return value.Email.Content == other.Email.Content
+			case FilterOperatorIsNotEqual:
+				return value.Email.Content != other.Email.Content
+			case FilterOperatorContains:
+				return strings.Contains(value.Email.Content, other.Email.Content)
+			case FilterOperatorDoesNotContain:
+				return !strings.Contains(value.Email.Content, other.Email.Content)
+			case FilterOperatorStartsWith:
+				return strings.HasPrefix(value.Email.Content, other.Email.Content)
+			case FilterOperatorEndsWith:
+				return strings.HasSuffix(value.Email.Content, other.Email.Content)
+			case FilterOperatorIsEmpty:
+				return "" == strings.TrimSpace(value.Email.Content)
+			case FilterOperatorIsNotEmpty:
+				return "" != strings.TrimSpace(value.Email.Content)
+			}
+		}
+	case KeyTypePhone:
+		if nil != value.Phone && nil != other && nil != other.Phone {
+			switch operator {
+			case FilterOperatorIsEqual:
+				return value.Phone.Content == other.Phone.Content
+			case FilterOperatorIsNotEqual:
+				return value.Phone.Content != other.Phone.Content
+			case FilterOperatorContains:
+				return strings.Contains(value.Phone.Content, other.Phone.Content)
+			case FilterOperatorDoesNotContain:
+				return !strings.Contains(value.Phone.Content, other.Phone.Content)
+			case FilterOperatorStartsWith:
+				return strings.HasPrefix(value.Phone.Content, other.Phone.Content)
+			case FilterOperatorEndsWith:
+				return strings.HasSuffix(value.Phone.Content, other.Phone.Content)
+			case FilterOperatorIsEmpty:
+				return "" == strings.TrimSpace(value.Phone.Content)
+			case FilterOperatorIsNotEmpty:
+				return "" != strings.TrimSpace(value.Phone.Content)
+			}
+		}
+	case KeyTypeMAsset:
+		if nil != value.MAsset && nil != other && nil != other.MAsset && 0 < len(value.MAsset) && 0 < len(other.MAsset) {
+			switch operator {
+			case FilterOperatorIsEqual, FilterOperatorContains:
+				contains := false
+				for _, v := range value.MAsset {
+					for _, v2 := range other.MAsset {
+						if v.Content == v2.Content {
+							contains = true
+							break
+						}
+					}
+				}
+				return contains
+			case FilterOperatorIsNotEqual, FilterOperatorDoesNotContain:
+				contains := false
+				for _, v := range value.MAsset {
+					for _, v2 := range other.MAsset {
+						if v.Content == v2.Content {
+							contains = true
+							break
+						}
+					}
+				}
+				return !contains
+			case FilterOperatorIsEmpty:
+				return 0 == len(value.MAsset) || 1 == len(value.MAsset) && "" == value.MAsset[0].Content
+			case FilterOperatorIsNotEmpty:
+				return 0 != len(value.MAsset) && !(1 == len(value.MAsset) && "" == value.MAsset[0].Content)
+			}
+		}
+	case KeyTypeTemplate:
+		if nil != value.Template && nil != other && nil != other.Template {
+			switch operator {
+			case FilterOperatorIsEqual:
+				if "" == strings.TrimSpace(other.Template.Content) {
+					return true
+				}
+				return value.Template.Content == other.Template.Content
+			case FilterOperatorIsNotEqual:
+				if "" == strings.TrimSpace(other.Template.Content) {
+					return true
+				}
+				return value.Template.Content != other.Template.Content
+			case FilterOperatorIsGreater:
+				if "" == strings.TrimSpace(other.Template.Content) {
+					return true
+				}
+				return value.Template.Content > other.Template.Content
+			case FilterOperatorIsGreaterOrEqual:
+				if "" == strings.TrimSpace(other.Template.Content) {
+					return true
+				}
+				return value.Template.Content >= other.Template.Content
+			case FilterOperatorIsLess:
+				if "" == strings.TrimSpace(other.Template.Content) {
+					return true
+				}
+				return value.Template.Content < other.Template.Content
+			case FilterOperatorIsLessOrEqual:
+				if "" == strings.TrimSpace(other.Template.Content) {
+					return true
+				}
+				return value.Template.Content <= other.Template.Content
+			case FilterOperatorContains:
+				if "" == strings.TrimSpace(other.Template.Content) {
+					return true
+				}
+				return strings.Contains(value.Template.Content, other.Template.Content)
+			case FilterOperatorDoesNotContain:
+				if "" == strings.TrimSpace(other.Template.Content) {
+					return true
+				}
+				return !strings.Contains(value.Template.Content, other.Template.Content)
+			case FilterOperatorStartsWith:
+				if "" == strings.TrimSpace(other.Template.Content) {
+					return true
+				}
+				return strings.HasPrefix(value.Template.Content, other.Template.Content)
+			case FilterOperatorEndsWith:
+				if "" == strings.TrimSpace(other.Template.Content) {
+					return true
+				}
+				return strings.HasSuffix(value.Template.Content, other.Template.Content)
+			case FilterOperatorIsEmpty:
+				return "" == strings.TrimSpace(value.Template.Content)
+			case FilterOperatorIsNotEmpty:
+				return "" != strings.TrimSpace(value.Template.Content)
+			}
+		}
+	case KeyTypeCheckbox:
+		if nil != value.Checkbox {
+			switch operator {
+			case FilterOperatorIsTrue:
+				return value.Checkbox.Checked
+			case FilterOperatorIsFalse:
+				return !value.Checkbox.Checked
+			}
+		}
+	}
+
+	switch operator {
+	case FilterOperatorIsEmpty:
+		return value.IsEmpty()
+	case FilterOperatorIsNotEmpty:
+		return !value.IsEmpty()
+	}
+	return false
+}
+
+func filterTime(valueMills int64, valueIsNotEmpty bool, otherValueStart, otherValueEnd, otherValueEnd2 time.Time, operator FilterOperator) bool {
+	valueTime := time.UnixMilli(valueMills)
+	switch operator {
+	case FilterOperatorIsEqual:
+		return (valueTime.After(otherValueStart) || valueTime.Equal(otherValueStart)) && valueTime.Before(otherValueEnd)
+	case FilterOperatorIsNotEqual:
+		return valueTime.Before(otherValueStart) || valueTime.After(otherValueEnd)
+	case FilterOperatorIsGreater:
+		return valueTime.After(otherValueEnd) || valueTime.Equal(otherValueEnd)
+	case FilterOperatorIsGreaterOrEqual:
+		return valueTime.After(otherValueStart) || valueTime.Equal(otherValueStart)
+	case FilterOperatorIsLess:
+		return valueTime.Before(otherValueStart)
+	case FilterOperatorIsLessOrEqual:
+		return valueTime.Before(otherValueEnd) || valueTime.Equal(otherValueEnd)
+	case FilterOperatorIsBetween:
+		return (valueTime.After(otherValueStart) || valueTime.Equal(otherValueStart)) && (valueTime.Before(otherValueEnd2) || valueTime.Equal(otherValueEnd2))
+	case FilterOperatorIsEmpty:
+		return !valueIsNotEmpty
+	case FilterOperatorIsNotEmpty:
+		return valueIsNotEmpty
+	}
+	return false
+}
+
+// 根据 Count、Unit 和 Direction 计算相对当前时间的开始时间和结束时间
+func calcRelativeTimeRegion(count int, unit RelativeDateUnit, direction RelativeDateDirection) (start, end time.Time) {
+	now := time.Now()
+	switch unit {
+	case RelativeDateUnitDay:
+		switch direction {
+		case RelativeDateDirectionBefore:
+			// 结束时间使用今天的开始时间
+			end = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
+			// 开始时间使用结束时间减去 count 天
+			start = end.AddDate(0, 0, -count)
+		case RelativeDateDirectionThis:
+			// 开始时间使用今天的开始时间
+			start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
+			// 结束时间使用开始时间加上 count 天
+			end = start.AddDate(0, 0, count)
+		case RelativeDateDirectionAfter:
+			// 开始时间使用今天的结束时间
+			start = time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 999999999, now.Location())
+			// 结束时间使用开始时间加上 count 天
+			end = start.AddDate(0, 0, count)
+		}
+	case RelativeDateUnitWeek:
+		weekday := int(now.Weekday())
+		if 0 == weekday {
+			weekday = 7
+		}
+		switch direction {
+		case RelativeDateDirectionBefore:
+			// 结束时间使用本周的开始时间
+			end = time.Date(now.Year(), now.Month(), now.Day()-weekday, 0, 0, 0, 0, now.Location())
+			// 开始时间使用结束时间减去 count*7 天
+			start = end.AddDate(0, 0, -count*7)
+		case RelativeDateDirectionThis:
+			// 开始时间使用本周的开始时间
+			start = time.Date(now.Year(), now.Month(), now.Day()-weekday, 0, 0, 0, 0, now.Location())
+			// 结束时间使用开始时间加上 count*7 天
+			end = start.AddDate(0, 0, count*7)
+		case RelativeDateDirectionAfter:
+			//  开始时间使用本周的结束时间
+			start = time.Date(now.Year(), now.Month(), now.Day()-weekday+7, 23, 59, 59, 999999999, now.Location())
+			// 结束时间使用开始时间加上 count*7 天
+			end = start.AddDate(0, 0, count*7)
+		}
+	case RelativeDateUnitMonth:
+		switch direction {
+		case RelativeDateDirectionBefore:
+			// 结束时间使用本月的开始时间
+			end = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
+			// 开始时间使用结束时间减去 count 个月
+			start = end.AddDate(0, -count, 0)
+		case RelativeDateDirectionThis:
+			// 开始时间使用本月的开始时间
+			start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
+			// 结束时间使用开始时间加上 count 个月
+			end = start.AddDate(0, count, 0)
+		case RelativeDateDirectionAfter:
+			// 开始时间使用本月的结束时间
+			start = time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()).Add(-time.Nanosecond)
+			// 结束时间使用开始时间加上 count 个月
+			end = start.AddDate(0, count, 0)
+		}
+	case RelativeDateUnitYear:
+		switch direction {
+		case RelativeDateDirectionBefore:
+			// 结束时间使用今年的开始时间
+			end = time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location())
+			// 开始时间使用结束时间减去 count 年
+			start = end.AddDate(-count, 0, 0)
+		case RelativeDateDirectionThis:
+			// 开始时间使用今年的开始时间
+			start = time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location())
+			// 结束时间使用开始时间加上 count 年
+			end = start.AddDate(count, 0, 0)
+		case RelativeDateDirectionAfter:
+			// 开始时间使用今年的结束时间
+			start = time.Date(now.Year()+1, 1, 1, 0, 0, 0, 0, now.Location()).Add(-time.Nanosecond)
+			// 结束时间使用开始时间加上 count 年
+			end = start.AddDate(count, 0, 0)
+		}
+	}
+	return
+}
+
 func (filter *ViewFilter) GetAffectValue(key *Key, defaultVal *Value) (ret *Value) {
 	if nil != filter.Value {
 		if KeyTypeRelation == filter.Value.Type || KeyTypeTemplate == filter.Value.Type || KeyTypeRollup == filter.Value.Type || KeyTypeUpdated == filter.Value.Type || KeyTypeCreated == filter.Value.Type {

+ 243 - 0
kernel/av/sort.go

@@ -16,6 +16,14 @@
 
 package av
 
+import (
+	"bytes"
+	"strconv"
+	"strings"
+
+	"github.com/siyuan-note/siyuan/kernel/util"
+)
+
 type Sortable interface {
 	SortRows()
 }
@@ -31,3 +39,238 @@ const (
 	SortOrderAsc  SortOrder = "ASC"
 	SortOrderDesc SortOrder = "DESC"
 )
+
+func (value *Value) Compare(other *Value) int {
+	switch value.Type {
+	case KeyTypeBlock:
+		if nil != value.Block && nil != other.Block {
+			ret := strings.Compare(value.Block.Content, other.Block.Content)
+			if 0 == ret {
+				ret = int(value.CreatedAt - other.CreatedAt)
+			}
+			return ret
+		}
+	case KeyTypeText:
+		if nil != value.Text && nil != other.Text {
+			ret := strings.Compare(value.Text.Content, other.Text.Content)
+			if 0 == ret {
+				ret = int(value.CreatedAt - other.CreatedAt)
+			}
+			return ret
+		}
+	case KeyTypeNumber:
+		if nil != value.Number && nil != other.Number {
+			if value.Number.IsNotEmpty {
+				if !other.Number.IsNotEmpty {
+					return 1
+				}
+
+				if value.Number.Content > other.Number.Content {
+					return 1
+				} else if value.Number.Content < other.Number.Content {
+					return -1
+				} else {
+					return int(value.CreatedAt - other.CreatedAt)
+				}
+			} else {
+				if other.Number.IsNotEmpty {
+					return -1
+				}
+				return int(value.CreatedAt - other.CreatedAt)
+			}
+		}
+	case KeyTypeDate:
+		if nil != value.Date && nil != other.Date {
+			if value.Date.IsNotEmpty {
+				if !other.Date.IsNotEmpty {
+					return 1
+				}
+				if value.Date.Content > other.Date.Content {
+					return 1
+				} else if value.Date.Content < other.Date.Content {
+					return -1
+				} else {
+					return int(value.CreatedAt - other.CreatedAt)
+				}
+			} else {
+				if other.Date.IsNotEmpty {
+					return -1
+				}
+				return int(value.CreatedAt - other.CreatedAt)
+			}
+		}
+	case KeyTypeCreated:
+		if nil != value.Created && nil != other.Created {
+			if value.Created.Content > other.Created.Content {
+				return 1
+			} else if value.Created.Content < other.Created.Content {
+				return -1
+			} else {
+				return int(value.CreatedAt - other.CreatedAt)
+			}
+		}
+	case KeyTypeUpdated:
+		if nil != value.Updated && nil != other.Updated {
+			if value.Updated.Content > other.Updated.Content {
+				return 1
+			} else if value.Updated.Content < other.Updated.Content {
+				return -1
+			} else {
+				return int(value.CreatedAt - other.CreatedAt)
+			}
+		}
+	case KeyTypeSelect, KeyTypeMSelect:
+		if nil != value.MSelect && nil != other.MSelect {
+			var v1 string
+			for _, v := range value.MSelect {
+				v1 += v.Content
+			}
+			var v2 string
+			for _, v := range other.MSelect {
+				v2 += v.Content
+			}
+			ret := strings.Compare(v1, v2)
+			if 0 == ret {
+				ret = int(value.CreatedAt - other.CreatedAt)
+			}
+			return ret
+		}
+	case KeyTypeURL:
+		if nil != value.URL && nil != other.URL {
+			ret := strings.Compare(value.URL.Content, other.URL.Content)
+			if 0 == ret {
+				ret = int(value.CreatedAt - other.CreatedAt)
+			}
+			return ret
+		}
+	case KeyTypeEmail:
+		if nil != value.Email && nil != other.Email {
+			ret := strings.Compare(value.Email.Content, other.Email.Content)
+			if 0 == ret {
+				ret = int(value.CreatedAt - other.CreatedAt)
+			}
+			return ret
+		}
+	case KeyTypePhone:
+		if nil != value.Phone && nil != other.Phone {
+			ret := strings.Compare(value.Phone.Content, other.Phone.Content)
+			if 0 == ret {
+				ret = int(value.CreatedAt - other.CreatedAt)
+			}
+			return ret
+		}
+	case KeyTypeMAsset:
+		if nil != value.MAsset && nil != other.MAsset {
+			var v1 string
+			for _, v := range value.MAsset {
+				v1 += v.Content
+			}
+			var v2 string
+			for _, v := range other.MAsset {
+				v2 += v.Content
+			}
+			ret := strings.Compare(v1, v2)
+			if 0 == ret {
+				ret = int(value.CreatedAt - other.CreatedAt)
+			}
+			return ret
+		}
+	case KeyTypeTemplate:
+		if nil != value.Template && nil != other.Template {
+			vContent := strings.TrimSpace(value.Template.Content)
+			oContent := strings.TrimSpace(other.Template.Content)
+			if util.IsNumeric(vContent) && util.IsNumeric(oContent) {
+				v1, _ := strconv.ParseFloat(vContent, 64)
+				v2, _ := strconv.ParseFloat(oContent, 64)
+				if v1 > v2 {
+					return 1
+				}
+				if v1 < v2 {
+					return -1
+				}
+				return int(value.CreatedAt - other.CreatedAt)
+			}
+			ret := strings.Compare(value.Template.Content, other.Template.Content)
+			if 0 == ret {
+				ret = int(value.CreatedAt - other.CreatedAt)
+			}
+			return ret
+		}
+	case KeyTypeCheckbox:
+		if nil != value.Checkbox && nil != other.Checkbox {
+			if value.Checkbox.Checked && !other.Checkbox.Checked {
+				return 1
+			}
+			if !value.Checkbox.Checked && other.Checkbox.Checked {
+				return -1
+			}
+			return int(value.CreatedAt - other.CreatedAt)
+		}
+	case KeyTypeRelation:
+		if nil != value.Relation && nil != other.Relation {
+			vContentBuf := bytes.Buffer{}
+			for _, c := range value.Relation.Contents {
+				vContentBuf.WriteString(c.String())
+				vContentBuf.WriteByte(' ')
+			}
+			vContent := strings.TrimSpace(vContentBuf.String())
+			oContentBuf := bytes.Buffer{}
+			for _, c := range other.Relation.Contents {
+				oContentBuf.WriteString(c.String())
+				oContentBuf.WriteByte(' ')
+			}
+			oContent := strings.TrimSpace(oContentBuf.String())
+
+			if util.IsNumeric(vContent) && util.IsNumeric(oContent) {
+				v1, _ := strconv.ParseFloat(vContent, 64)
+				v2, _ := strconv.ParseFloat(oContent, 64)
+				if v1 > v2 {
+					return 1
+				}
+
+				if v1 < v2 {
+					return -1
+				}
+				return int(value.CreatedAt - other.CreatedAt)
+			}
+			ret := strings.Compare(vContent, oContent)
+			if 0 == ret {
+				ret = int(value.CreatedAt - other.CreatedAt)
+			}
+			return ret
+		}
+	case KeyTypeRollup:
+		if nil != value.Rollup && nil != other.Rollup {
+			vContentBuf := bytes.Buffer{}
+			for _, c := range value.Rollup.Contents {
+				vContentBuf.WriteString(c.String())
+				vContentBuf.WriteByte(' ')
+			}
+			vContent := strings.TrimSpace(vContentBuf.String())
+			oContentBuf := bytes.Buffer{}
+			for _, c := range other.Rollup.Contents {
+				oContentBuf.WriteString(c.String())
+				oContentBuf.WriteByte(' ')
+			}
+			oContent := strings.TrimSpace(oContentBuf.String())
+
+			if util.IsNumeric(vContent) && util.IsNumeric(oContent) {
+				v1, _ := strconv.ParseFloat(vContent, 64)
+				v2, _ := strconv.ParseFloat(oContent, 64)
+				if v1 > v2 {
+					return 1
+				}
+				if v1 < v2 {
+					return -1
+				}
+				return int(value.CreatedAt - other.CreatedAt)
+			}
+			ret := strings.Compare(vContent, oContent)
+			if 0 == ret {
+				ret = int(value.CreatedAt - other.CreatedAt)
+			}
+			return ret
+		}
+	}
+	return int(value.CreatedAt - other.CreatedAt)
+}

+ 0 - 773
kernel/av/table.go

@@ -17,13 +17,9 @@
 package av
 
 import (
-	"bytes"
-	"github.com/siyuan-note/siyuan/kernel/util"
 	"math"
 	"sort"
 	"strconv"
-	"strings"
-	"time"
 )
 
 // LayoutTable 描述了表格布局的结构。
@@ -82,775 +78,6 @@ const (
 	CalcOperatorPercentUnchecked  CalcOperator = "Percent unchecked"
 )
 
-func (value *Value) Compare(other *Value) int {
-	switch value.Type {
-	case KeyTypeBlock:
-		if nil != value.Block && nil != other.Block {
-			ret := strings.Compare(value.Block.Content, other.Block.Content)
-			if 0 == ret {
-				ret = int(value.CreatedAt - other.CreatedAt)
-			}
-			return ret
-		}
-	case KeyTypeText:
-		if nil != value.Text && nil != other.Text {
-			ret := strings.Compare(value.Text.Content, other.Text.Content)
-			if 0 == ret {
-				ret = int(value.CreatedAt - other.CreatedAt)
-			}
-			return ret
-		}
-	case KeyTypeNumber:
-		if nil != value.Number && nil != other.Number {
-			if value.Number.IsNotEmpty {
-				if !other.Number.IsNotEmpty {
-					return 1
-				}
-
-				if value.Number.Content > other.Number.Content {
-					return 1
-				} else if value.Number.Content < other.Number.Content {
-					return -1
-				} else {
-					return int(value.CreatedAt - other.CreatedAt)
-				}
-			} else {
-				if other.Number.IsNotEmpty {
-					return -1
-				}
-				return int(value.CreatedAt - other.CreatedAt)
-			}
-		}
-	case KeyTypeDate:
-		if nil != value.Date && nil != other.Date {
-			if value.Date.IsNotEmpty {
-				if !other.Date.IsNotEmpty {
-					return 1
-				}
-				if value.Date.Content > other.Date.Content {
-					return 1
-				} else if value.Date.Content < other.Date.Content {
-					return -1
-				} else {
-					return int(value.CreatedAt - other.CreatedAt)
-				}
-			} else {
-				if other.Date.IsNotEmpty {
-					return -1
-				}
-				return int(value.CreatedAt - other.CreatedAt)
-			}
-		}
-	case KeyTypeCreated:
-		if nil != value.Created && nil != other.Created {
-			if value.Created.Content > other.Created.Content {
-				return 1
-			} else if value.Created.Content < other.Created.Content {
-				return -1
-			} else {
-				return int(value.CreatedAt - other.CreatedAt)
-			}
-		}
-	case KeyTypeUpdated:
-		if nil != value.Updated && nil != other.Updated {
-			if value.Updated.Content > other.Updated.Content {
-				return 1
-			} else if value.Updated.Content < other.Updated.Content {
-				return -1
-			} else {
-				return int(value.CreatedAt - other.CreatedAt)
-			}
-		}
-	case KeyTypeSelect, KeyTypeMSelect:
-		if nil != value.MSelect && nil != other.MSelect {
-			var v1 string
-			for _, v := range value.MSelect {
-				v1 += v.Content
-			}
-			var v2 string
-			for _, v := range other.MSelect {
-				v2 += v.Content
-			}
-			ret := strings.Compare(v1, v2)
-			if 0 == ret {
-				ret = int(value.CreatedAt - other.CreatedAt)
-			}
-			return ret
-		}
-	case KeyTypeURL:
-		if nil != value.URL && nil != other.URL {
-			ret := strings.Compare(value.URL.Content, other.URL.Content)
-			if 0 == ret {
-				ret = int(value.CreatedAt - other.CreatedAt)
-			}
-			return ret
-		}
-	case KeyTypeEmail:
-		if nil != value.Email && nil != other.Email {
-			ret := strings.Compare(value.Email.Content, other.Email.Content)
-			if 0 == ret {
-				ret = int(value.CreatedAt - other.CreatedAt)
-			}
-			return ret
-		}
-	case KeyTypePhone:
-		if nil != value.Phone && nil != other.Phone {
-			ret := strings.Compare(value.Phone.Content, other.Phone.Content)
-			if 0 == ret {
-				ret = int(value.CreatedAt - other.CreatedAt)
-			}
-			return ret
-		}
-	case KeyTypeMAsset:
-		if nil != value.MAsset && nil != other.MAsset {
-			var v1 string
-			for _, v := range value.MAsset {
-				v1 += v.Content
-			}
-			var v2 string
-			for _, v := range other.MAsset {
-				v2 += v.Content
-			}
-			ret := strings.Compare(v1, v2)
-			if 0 == ret {
-				ret = int(value.CreatedAt - other.CreatedAt)
-			}
-			return ret
-		}
-	case KeyTypeTemplate:
-		if nil != value.Template && nil != other.Template {
-			vContent := strings.TrimSpace(value.Template.Content)
-			oContent := strings.TrimSpace(other.Template.Content)
-			if util.IsNumeric(vContent) && util.IsNumeric(oContent) {
-				v1, _ := strconv.ParseFloat(vContent, 64)
-				v2, _ := strconv.ParseFloat(oContent, 64)
-				if v1 > v2 {
-					return 1
-				}
-				if v1 < v2 {
-					return -1
-				}
-				return int(value.CreatedAt - other.CreatedAt)
-			}
-			ret := strings.Compare(value.Template.Content, other.Template.Content)
-			if 0 == ret {
-				ret = int(value.CreatedAt - other.CreatedAt)
-			}
-			return ret
-		}
-	case KeyTypeCheckbox:
-		if nil != value.Checkbox && nil != other.Checkbox {
-			if value.Checkbox.Checked && !other.Checkbox.Checked {
-				return 1
-			}
-			if !value.Checkbox.Checked && other.Checkbox.Checked {
-				return -1
-			}
-			return int(value.CreatedAt - other.CreatedAt)
-		}
-	case KeyTypeRelation:
-		if nil != value.Relation && nil != other.Relation {
-			vContentBuf := bytes.Buffer{}
-			for _, c := range value.Relation.Contents {
-				vContentBuf.WriteString(c.String())
-				vContentBuf.WriteByte(' ')
-			}
-			vContent := strings.TrimSpace(vContentBuf.String())
-			oContentBuf := bytes.Buffer{}
-			for _, c := range other.Relation.Contents {
-				oContentBuf.WriteString(c.String())
-				oContentBuf.WriteByte(' ')
-			}
-			oContent := strings.TrimSpace(oContentBuf.String())
-
-			if util.IsNumeric(vContent) && util.IsNumeric(oContent) {
-				v1, _ := strconv.ParseFloat(vContent, 64)
-				v2, _ := strconv.ParseFloat(oContent, 64)
-				if v1 > v2 {
-					return 1
-				}
-
-				if v1 < v2 {
-					return -1
-				}
-				return int(value.CreatedAt - other.CreatedAt)
-			}
-			ret := strings.Compare(vContent, oContent)
-			if 0 == ret {
-				ret = int(value.CreatedAt - other.CreatedAt)
-			}
-			return ret
-		}
-	case KeyTypeRollup:
-		if nil != value.Rollup && nil != other.Rollup {
-			vContentBuf := bytes.Buffer{}
-			for _, c := range value.Rollup.Contents {
-				vContentBuf.WriteString(c.String())
-				vContentBuf.WriteByte(' ')
-			}
-			vContent := strings.TrimSpace(vContentBuf.String())
-			oContentBuf := bytes.Buffer{}
-			for _, c := range other.Rollup.Contents {
-				oContentBuf.WriteString(c.String())
-				oContentBuf.WriteByte(' ')
-			}
-			oContent := strings.TrimSpace(oContentBuf.String())
-
-			if util.IsNumeric(vContent) && util.IsNumeric(oContent) {
-				v1, _ := strconv.ParseFloat(vContent, 64)
-				v2, _ := strconv.ParseFloat(oContent, 64)
-				if v1 > v2 {
-					return 1
-				}
-				if v1 < v2 {
-					return -1
-				}
-				return int(value.CreatedAt - other.CreatedAt)
-			}
-			ret := strings.Compare(vContent, oContent)
-			if 0 == ret {
-				ret = int(value.CreatedAt - other.CreatedAt)
-			}
-			return ret
-		}
-	}
-	return int(value.CreatedAt - other.CreatedAt)
-}
-
-func (value *Value) Filter(filter *ViewFilter, attrView *AttributeView, rowID string) bool {
-	if nil == filter || (nil == filter.Value && nil == filter.RelativeDate) {
-		return true
-	}
-
-	if nil != filter.Value && value.Type != filter.Value.Type {
-		// 由于字段类型被用户编辑过导致和过滤器值类型不匹配,该情况下不过滤
-		return true
-	}
-
-	if nil != value.Rollup && KeyTypeRollup == value.Type && nil != filter && nil != filter.Value && KeyTypeRollup == filter.Value.Type &&
-		nil != filter.Value.Rollup && 0 < len(filter.Value.Rollup.Contents) {
-		// 单独处理汇总类型的比较
-		key, _ := attrView.GetKey(value.KeyID)
-		if nil == key {
-			return false
-		}
-
-		relKey, _ := attrView.GetKey(key.Rollup.RelationKeyID)
-		if nil == relKey {
-			return false
-		}
-
-		relVal := attrView.GetValue(relKey.ID, rowID)
-		if nil == relVal || nil == relVal.Relation {
-			return false
-		}
-
-		destAv, _ := ParseAttributeView(relKey.Relation.AvID)
-		if nil == destAv {
-			return false
-		}
-
-		for _, blockID := range relVal.Relation.BlockIDs {
-			destVal := destAv.GetValue(key.Rollup.KeyID, blockID)
-			if nil == destVal {
-				continue
-			}
-
-			if destVal.filter(filter.Value.Rollup.Contents[0], filter.RelativeDate, filter.RelativeDate2, filter.Operator) {
-				return true
-			}
-		}
-		return false
-	}
-
-	if nil != value.Relation && KeyTypeRelation == value.Type && 0 < len(value.Relation.Contents) && nil != filter && nil != filter.Value && KeyTypeRelation == filter.Value.Type &&
-		nil != filter.Value.Relation && 0 < len(filter.Value.Relation.BlockIDs) {
-		// 单独处理关联类型的比较
-		for _, relationValue := range value.Relation.Contents {
-			filterValue := &Value{Type: KeyTypeBlock, Block: &ValueBlock{Content: filter.Value.Relation.BlockIDs[0]}}
-			if relationValue.filter(filterValue, filter.RelativeDate, filter.RelativeDate2, filter.Operator) {
-				return true
-			}
-		}
-		return false
-	}
-
-	return value.filter(filter.Value, filter.RelativeDate, filter.RelativeDate2, filter.Operator)
-}
-
-func (value *Value) filter(other *Value, relativeDate, relativeDate2 *RelativeDate, operator FilterOperator) bool {
-	switch value.Type {
-	case KeyTypeBlock:
-		if nil != value.Block && nil != other && nil != other.Block {
-			switch operator {
-			case FilterOperatorIsEqual:
-				return value.Block.Content == other.Block.Content
-			case FilterOperatorIsNotEqual:
-				return value.Block.Content != other.Block.Content
-			case FilterOperatorContains:
-				return strings.Contains(value.Block.Content, other.Block.Content)
-			case FilterOperatorDoesNotContain:
-				return !strings.Contains(value.Block.Content, other.Block.Content)
-			case FilterOperatorStartsWith:
-				return strings.HasPrefix(value.Block.Content, other.Block.Content)
-			case FilterOperatorEndsWith:
-				return strings.HasSuffix(value.Block.Content, other.Block.Content)
-			case FilterOperatorIsEmpty:
-				return "" == strings.TrimSpace(value.Block.Content)
-			case FilterOperatorIsNotEmpty:
-				return "" != strings.TrimSpace(value.Block.Content)
-			}
-		}
-	case KeyTypeText:
-		if nil != value.Text && nil != other && nil != other.Text {
-			switch operator {
-			case FilterOperatorIsEqual:
-				if "" == strings.TrimSpace(other.Text.Content) {
-					return true
-				}
-				return value.Text.Content == other.Text.Content
-			case FilterOperatorIsNotEqual:
-				if "" == strings.TrimSpace(other.Text.Content) {
-					return true
-				}
-				return value.Text.Content != other.Text.Content
-			case FilterOperatorContains:
-				if "" == strings.TrimSpace(other.Text.Content) {
-					return true
-				}
-				return strings.Contains(value.Text.Content, other.Text.Content)
-			case FilterOperatorDoesNotContain:
-				if "" == strings.TrimSpace(other.Text.Content) {
-					return true
-				}
-				return !strings.Contains(value.Text.Content, other.Text.Content)
-			case FilterOperatorStartsWith:
-				if "" == strings.TrimSpace(other.Text.Content) {
-					return true
-				}
-				return strings.HasPrefix(value.Text.Content, other.Text.Content)
-			case FilterOperatorEndsWith:
-				if "" == strings.TrimSpace(other.Text.Content) {
-					return true
-				}
-				return strings.HasSuffix(value.Text.Content, other.Text.Content)
-			case FilterOperatorIsEmpty:
-				return "" == strings.TrimSpace(value.Text.Content)
-			case FilterOperatorIsNotEmpty:
-				return "" != strings.TrimSpace(value.Text.Content)
-			}
-		}
-	case KeyTypeNumber:
-		if nil != value.Number && nil != other && nil != other.Number {
-			switch operator {
-			case FilterOperatorIsEqual:
-				if !other.Number.IsNotEmpty {
-					return true
-				}
-				return value.Number.Content == other.Number.Content
-			case FilterOperatorIsNotEqual:
-				if !other.Number.IsNotEmpty {
-					return true
-				}
-				return value.Number.Content != other.Number.Content
-			case FilterOperatorIsGreater:
-				return value.Number.Content > other.Number.Content
-			case FilterOperatorIsGreaterOrEqual:
-				return value.Number.Content >= other.Number.Content
-			case FilterOperatorIsLess:
-				return value.Number.Content < other.Number.Content
-			case FilterOperatorIsLessOrEqual:
-				return value.Number.Content <= other.Number.Content
-			case FilterOperatorIsEmpty:
-				return !value.Number.IsNotEmpty
-			case FilterOperatorIsNotEmpty:
-				return value.Number.IsNotEmpty
-			}
-		}
-	case KeyTypeDate:
-		if nil != value.Date {
-			if nil != relativeDate {
-				// 使用相对时间比较
-
-				count := relativeDate.Count
-				unit := relativeDate.Unit
-				direction := relativeDate.Direction
-				relativeTimeStart, relativeTimeEnd := calcRelativeTimeRegion(count, unit, direction)
-				_, relativeTimeEnd2 := calcRelativeTimeRegion(relativeDate2.Count, relativeDate2.Unit, relativeDate2.Direction)
-				return filterTime(value.Date.Content, value.Date.IsNotEmpty, relativeTimeStart, relativeTimeEnd, relativeTimeEnd2, operator)
-			} else { // 使用具体时间比较
-				if nil == other.Date {
-					return true
-				}
-
-				otherTime := time.UnixMilli(other.Date.Content)
-				otherStart := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 0, 0, 0, 0, otherTime.Location())
-				otherEnd := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 23, 59, 59, 999999999, otherTime.Location())
-				return filterTime(value.Date.Content, value.Date.IsNotEmpty, otherStart, otherEnd, time.Now(), operator)
-			}
-		}
-	case KeyTypeCreated:
-		if nil != value.Created {
-			if nil != relativeDate {
-				// 使用相对时间比较
-
-				count := relativeDate.Count
-				unit := relativeDate.Unit
-				direction := relativeDate.Direction
-				relativeTimeStart, relativeTimeEnd := calcRelativeTimeRegion(count, unit, direction)
-				return filterTime(value.Created.Content, true, relativeTimeStart, relativeTimeEnd, time.Now(), operator)
-			} else { // 使用具体时间比较
-				if nil == other.Created {
-					return true
-				}
-
-				otherTime := time.UnixMilli(other.Created.Content)
-				otherStart := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 0, 0, 0, 0, otherTime.Location())
-				otherEnd := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 23, 59, 59, 999999999, otherTime.Location())
-				return filterTime(value.Created.Content, value.Created.IsNotEmpty, otherStart, otherEnd, time.Now(), operator)
-			}
-		}
-	case KeyTypeUpdated:
-		if nil != value.Updated {
-			if nil != relativeDate {
-				// 使用相对时间比较
-
-				count := relativeDate.Count
-				unit := relativeDate.Unit
-				direction := relativeDate.Direction
-				relativeTimeStart, relativeTimeEnd := calcRelativeTimeRegion(count, unit, direction)
-				return filterTime(value.Updated.Content, true, relativeTimeStart, relativeTimeEnd, time.Now(), operator)
-			} else { // 使用具体时间比较
-				if nil == other.Updated {
-					return true
-				}
-
-				otherTime := time.UnixMilli(other.Updated.Content)
-				otherStart := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 0, 0, 0, 0, otherTime.Location())
-				otherEnd := time.Date(otherTime.Year(), otherTime.Month(), otherTime.Day(), 23, 59, 59, 999999999, otherTime.Location())
-				return filterTime(value.Updated.Content, value.Updated.IsNotEmpty, otherStart, otherEnd, time.Now(), operator)
-			}
-		}
-	case KeyTypeSelect, KeyTypeMSelect:
-		if nil != value.MSelect {
-			if nil != other && nil != other.MSelect {
-				switch operator {
-				case FilterOperatorIsEqual, FilterOperatorContains:
-					contains := false
-					for _, v := range value.MSelect {
-						for _, v2 := range other.MSelect {
-							if v.Content == v2.Content {
-								contains = true
-								break
-							}
-						}
-					}
-					return contains
-				case FilterOperatorIsNotEqual, FilterOperatorDoesNotContain:
-					contains := false
-					for _, v := range value.MSelect {
-						for _, v2 := range other.MSelect {
-							if v.Content == v2.Content {
-								contains = true
-								break
-							}
-						}
-					}
-					return !contains
-				case FilterOperatorIsEmpty:
-					return 0 == len(value.MSelect) || 1 == len(value.MSelect) && "" == value.MSelect[0].Content
-				case FilterOperatorIsNotEmpty:
-					return 0 != len(value.MSelect) && !(1 == len(value.MSelect) && "" == value.MSelect[0].Content)
-				}
-				return false
-			}
-
-			// 没有设置比较值
-
-			switch operator {
-			case FilterOperatorIsEqual, FilterOperatorIsNotEqual, FilterOperatorContains, FilterOperatorDoesNotContain:
-				return true
-			case FilterOperatorIsEmpty:
-				return 0 == len(value.MSelect) || 1 == len(value.MSelect) && "" == value.MSelect[0].Content
-			case FilterOperatorIsNotEmpty:
-				return 0 != len(value.MSelect) && !(1 == len(value.MSelect) && "" == value.MSelect[0].Content)
-			}
-		}
-	case KeyTypeURL:
-		if nil != value.URL && nil != other && nil != other.URL {
-			switch operator {
-			case FilterOperatorIsEqual:
-				return value.URL.Content == other.URL.Content
-			case FilterOperatorIsNotEqual:
-				return value.URL.Content != other.URL.Content
-			case FilterOperatorContains:
-				return strings.Contains(value.URL.Content, other.URL.Content)
-			case FilterOperatorDoesNotContain:
-				return !strings.Contains(value.URL.Content, other.URL.Content)
-			case FilterOperatorStartsWith:
-				return strings.HasPrefix(value.URL.Content, other.URL.Content)
-			case FilterOperatorEndsWith:
-				return strings.HasSuffix(value.URL.Content, other.URL.Content)
-			case FilterOperatorIsEmpty:
-				return "" == strings.TrimSpace(value.URL.Content)
-			case FilterOperatorIsNotEmpty:
-				return "" != strings.TrimSpace(value.URL.Content)
-			}
-		}
-	case KeyTypeEmail:
-		if nil != value.Email && nil != other && nil != other.Email {
-			switch operator {
-			case FilterOperatorIsEqual:
-				return value.Email.Content == other.Email.Content
-			case FilterOperatorIsNotEqual:
-				return value.Email.Content != other.Email.Content
-			case FilterOperatorContains:
-				return strings.Contains(value.Email.Content, other.Email.Content)
-			case FilterOperatorDoesNotContain:
-				return !strings.Contains(value.Email.Content, other.Email.Content)
-			case FilterOperatorStartsWith:
-				return strings.HasPrefix(value.Email.Content, other.Email.Content)
-			case FilterOperatorEndsWith:
-				return strings.HasSuffix(value.Email.Content, other.Email.Content)
-			case FilterOperatorIsEmpty:
-				return "" == strings.TrimSpace(value.Email.Content)
-			case FilterOperatorIsNotEmpty:
-				return "" != strings.TrimSpace(value.Email.Content)
-			}
-		}
-	case KeyTypePhone:
-		if nil != value.Phone && nil != other && nil != other.Phone {
-			switch operator {
-			case FilterOperatorIsEqual:
-				return value.Phone.Content == other.Phone.Content
-			case FilterOperatorIsNotEqual:
-				return value.Phone.Content != other.Phone.Content
-			case FilterOperatorContains:
-				return strings.Contains(value.Phone.Content, other.Phone.Content)
-			case FilterOperatorDoesNotContain:
-				return !strings.Contains(value.Phone.Content, other.Phone.Content)
-			case FilterOperatorStartsWith:
-				return strings.HasPrefix(value.Phone.Content, other.Phone.Content)
-			case FilterOperatorEndsWith:
-				return strings.HasSuffix(value.Phone.Content, other.Phone.Content)
-			case FilterOperatorIsEmpty:
-				return "" == strings.TrimSpace(value.Phone.Content)
-			case FilterOperatorIsNotEmpty:
-				return "" != strings.TrimSpace(value.Phone.Content)
-			}
-		}
-	case KeyTypeMAsset:
-		if nil != value.MAsset && nil != other && nil != other.MAsset && 0 < len(value.MAsset) && 0 < len(other.MAsset) {
-			switch operator {
-			case FilterOperatorIsEqual, FilterOperatorContains:
-				contains := false
-				for _, v := range value.MAsset {
-					for _, v2 := range other.MAsset {
-						if v.Content == v2.Content {
-							contains = true
-							break
-						}
-					}
-				}
-				return contains
-			case FilterOperatorIsNotEqual, FilterOperatorDoesNotContain:
-				contains := false
-				for _, v := range value.MAsset {
-					for _, v2 := range other.MAsset {
-						if v.Content == v2.Content {
-							contains = true
-							break
-						}
-					}
-				}
-				return !contains
-			case FilterOperatorIsEmpty:
-				return 0 == len(value.MAsset) || 1 == len(value.MAsset) && "" == value.MAsset[0].Content
-			case FilterOperatorIsNotEmpty:
-				return 0 != len(value.MAsset) && !(1 == len(value.MAsset) && "" == value.MAsset[0].Content)
-			}
-		}
-	case KeyTypeTemplate:
-		if nil != value.Template && nil != other && nil != other.Template {
-			switch operator {
-			case FilterOperatorIsEqual:
-				if "" == strings.TrimSpace(other.Template.Content) {
-					return true
-				}
-				return value.Template.Content == other.Template.Content
-			case FilterOperatorIsNotEqual:
-				if "" == strings.TrimSpace(other.Template.Content) {
-					return true
-				}
-				return value.Template.Content != other.Template.Content
-			case FilterOperatorIsGreater:
-				if "" == strings.TrimSpace(other.Template.Content) {
-					return true
-				}
-				return value.Template.Content > other.Template.Content
-			case FilterOperatorIsGreaterOrEqual:
-				if "" == strings.TrimSpace(other.Template.Content) {
-					return true
-				}
-				return value.Template.Content >= other.Template.Content
-			case FilterOperatorIsLess:
-				if "" == strings.TrimSpace(other.Template.Content) {
-					return true
-				}
-				return value.Template.Content < other.Template.Content
-			case FilterOperatorIsLessOrEqual:
-				if "" == strings.TrimSpace(other.Template.Content) {
-					return true
-				}
-				return value.Template.Content <= other.Template.Content
-			case FilterOperatorContains:
-				if "" == strings.TrimSpace(other.Template.Content) {
-					return true
-				}
-				return strings.Contains(value.Template.Content, other.Template.Content)
-			case FilterOperatorDoesNotContain:
-				if "" == strings.TrimSpace(other.Template.Content) {
-					return true
-				}
-				return !strings.Contains(value.Template.Content, other.Template.Content)
-			case FilterOperatorStartsWith:
-				if "" == strings.TrimSpace(other.Template.Content) {
-					return true
-				}
-				return strings.HasPrefix(value.Template.Content, other.Template.Content)
-			case FilterOperatorEndsWith:
-				if "" == strings.TrimSpace(other.Template.Content) {
-					return true
-				}
-				return strings.HasSuffix(value.Template.Content, other.Template.Content)
-			case FilterOperatorIsEmpty:
-				return "" == strings.TrimSpace(value.Template.Content)
-			case FilterOperatorIsNotEmpty:
-				return "" != strings.TrimSpace(value.Template.Content)
-			}
-		}
-	case KeyTypeCheckbox:
-		if nil != value.Checkbox {
-			switch operator {
-			case FilterOperatorIsTrue:
-				return value.Checkbox.Checked
-			case FilterOperatorIsFalse:
-				return !value.Checkbox.Checked
-			}
-		}
-	}
-	return false
-}
-
-func filterTime(valueMills int64, valueIsNotEmpty bool, otherValueStart, otherValueEnd, otherValueEnd2 time.Time, operator FilterOperator) bool {
-	valueTime := time.UnixMilli(valueMills)
-	switch operator {
-	case FilterOperatorIsEqual:
-		return (valueTime.After(otherValueStart) || valueTime.Equal(otherValueStart)) && valueTime.Before(otherValueEnd)
-	case FilterOperatorIsNotEqual:
-		return valueTime.Before(otherValueStart) || valueTime.After(otherValueEnd)
-	case FilterOperatorIsGreater:
-		return valueTime.After(otherValueEnd) || valueTime.Equal(otherValueEnd)
-	case FilterOperatorIsGreaterOrEqual:
-		return valueTime.After(otherValueStart) || valueTime.Equal(otherValueStart)
-	case FilterOperatorIsLess:
-		return valueTime.Before(otherValueStart)
-	case FilterOperatorIsLessOrEqual:
-		return valueTime.Before(otherValueEnd) || valueTime.Equal(otherValueEnd)
-	case FilterOperatorIsBetween:
-		return (valueTime.After(otherValueStart) || valueTime.Equal(otherValueStart)) && (valueTime.Before(otherValueEnd2) || valueTime.Equal(otherValueEnd2))
-	case FilterOperatorIsEmpty:
-		return !valueIsNotEmpty
-	case FilterOperatorIsNotEmpty:
-		return valueIsNotEmpty
-	}
-	return false
-}
-
-// 根据 Count、Unit 和 Direction 计算相对当前时间的开始时间和结束时间
-func calcRelativeTimeRegion(count int, unit RelativeDateUnit, direction RelativeDateDirection) (start, end time.Time) {
-	now := time.Now()
-	switch unit {
-	case RelativeDateUnitDay:
-		switch direction {
-		case RelativeDateDirectionBefore:
-			// 结束时间使用今天的开始时间
-			end = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
-			// 开始时间使用结束时间减去 count 天
-			start = end.AddDate(0, 0, -count)
-		case RelativeDateDirectionThis:
-			// 开始时间使用今天的开始时间
-			start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
-			// 结束时间使用开始时间加上 count 天
-			end = start.AddDate(0, 0, count)
-		case RelativeDateDirectionAfter:
-			// 开始时间使用今天的结束时间
-			start = time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 999999999, now.Location())
-			// 结束时间使用开始时间加上 count 天
-			end = start.AddDate(0, 0, count)
-		}
-	case RelativeDateUnitWeek:
-		weekday := int(now.Weekday())
-		if 0 == weekday {
-			weekday = 7
-		}
-		switch direction {
-		case RelativeDateDirectionBefore:
-			// 结束时间使用本周的开始时间
-			end = time.Date(now.Year(), now.Month(), now.Day()-weekday, 0, 0, 0, 0, now.Location())
-			// 开始时间使用结束时间减去 count*7 天
-			start = end.AddDate(0, 0, -count*7)
-		case RelativeDateDirectionThis:
-			// 开始时间使用本周的开始时间
-			start = time.Date(now.Year(), now.Month(), now.Day()-weekday, 0, 0, 0, 0, now.Location())
-			// 结束时间使用开始时间加上 count*7 天
-			end = start.AddDate(0, 0, count*7)
-		case RelativeDateDirectionAfter:
-			//  开始时间使用本周的结束时间
-			start = time.Date(now.Year(), now.Month(), now.Day()-weekday+7, 23, 59, 59, 999999999, now.Location())
-			// 结束时间使用开始时间加上 count*7 天
-			end = start.AddDate(0, 0, count*7)
-		}
-	case RelativeDateUnitMonth:
-		switch direction {
-		case RelativeDateDirectionBefore:
-			// 结束时间使用本月的开始时间
-			end = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
-			// 开始时间使用结束时间减去 count 个月
-			start = end.AddDate(0, -count, 0)
-		case RelativeDateDirectionThis:
-			// 开始时间使用本月的开始时间
-			start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
-			// 结束时间使用开始时间加上 count 个月
-			end = start.AddDate(0, count, 0)
-		case RelativeDateDirectionAfter:
-			// 开始时间使用本月的结束时间
-			start = time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()).Add(-time.Nanosecond)
-			// 结束时间使用开始时间加上 count 个月
-			end = start.AddDate(0, count, 0)
-		}
-	case RelativeDateUnitYear:
-		switch direction {
-		case RelativeDateDirectionBefore:
-			// 结束时间使用今年的开始时间
-			end = time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location())
-			// 开始时间使用结束时间减去 count 年
-			start = end.AddDate(-count, 0, 0)
-		case RelativeDateDirectionThis:
-			// 开始时间使用今年的开始时间
-			start = time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location())
-			// 结束时间使用开始时间加上 count 年
-			end = start.AddDate(count, 0, 0)
-		case RelativeDateDirectionAfter:
-			// 开始时间使用今年的结束时间
-			start = time.Date(now.Year()+1, 1, 1, 0, 0, 0, 0, now.Location()).Add(-time.Nanosecond)
-			// 结束时间使用开始时间加上 count 年
-			end = start.AddDate(count, 0, 0)
-		}
-	}
-	return
-}
-
 // Table 描述了表格实例的结构。
 type Table struct {
 	ID               string         `json:"id"`               // 表格布局 ID

+ 9 - 10
kernel/model/attribute_view.go

@@ -1376,15 +1376,14 @@ func (tx *Transaction) doSortAttrViewView(operation *Operation) (ret *TxErr) {
 		return &TxErr{code: TxErrWriteAttributeView, id: operation.AvID, msg: err.Error()}
 	}
 
-	view, err := getAttrViewViewByBlockID(attrView, operation.BlockID)
+	view := attrView.GetView(operation.ID)
 	if nil == view {
 		logging.LogErrorf("get view failed: %s", operation.BlockID)
 		return &TxErr{code: TxErrWriteAttributeView, id: operation.AvID, msg: err.Error()}
 	}
 	viewID := view.ID
-	previewViewID := operation.PreviousID
-
-	if viewID == previewViewID {
+	previousViewID := operation.PreviousID
+	if viewID == previousViewID {
 		return
 	}
 
@@ -1402,7 +1401,7 @@ func (tx *Transaction) doSortAttrViewView(operation *Operation) (ret *TxErr) {
 
 	attrView.Views = append(attrView.Views[:index], attrView.Views[index+1:]...)
 	for i, v := range attrView.Views {
-		if v.ID == previewViewID {
+		if v.ID == previousViewID {
 			previousIndex = i + 1
 			break
 		}
@@ -1926,14 +1925,14 @@ func setAttributeViewColumnCalc(operation *Operation) (err error) {
 }
 
 func (tx *Transaction) doInsertAttrViewBlock(operation *Operation) (ret *TxErr) {
-	err := AddAttributeViewBlock(tx, operation.SrcIDs, operation.AvID, operation.BlockID, operation.PreviousID, operation.IsDetached)
+	err := AddAttributeViewBlock(tx, operation.SrcIDs, operation.AvID, operation.BlockID, operation.PreviousID, operation.IsDetached, operation.IgnoreFillFilterVal)
 	if nil != err {
 		return &TxErr{code: TxErrWriteAttributeView, id: operation.AvID, msg: err.Error()}
 	}
 	return
 }
 
-func AddAttributeViewBlock(tx *Transaction, srcIDs []string, avID, blockID, previousBlockID string, isDetached bool) (err error) {
+func AddAttributeViewBlock(tx *Transaction, srcIDs []string, avID, blockID, previousBlockID string, isDetached, ignoreFillFilter bool) (err error) {
 	for _, id := range srcIDs {
 		var tree *parse.Tree
 		if !isDetached {
@@ -1949,14 +1948,14 @@ func AddAttributeViewBlock(tx *Transaction, srcIDs []string, avID, blockID, prev
 			}
 		}
 
-		if avErr := addAttributeViewBlock(avID, blockID, previousBlockID, id, isDetached, tree, tx); nil != avErr {
+		if avErr := addAttributeViewBlock(avID, blockID, previousBlockID, id, isDetached, ignoreFillFilter, tree, tx); nil != avErr {
 			return avErr
 		}
 	}
 	return
 }
 
-func addAttributeViewBlock(avID, blockID, previousBlockID, addingBlockID string, isDetached bool, tree *parse.Tree, tx *Transaction) (err error) {
+func addAttributeViewBlock(avID, blockID, previousBlockID, addingBlockID string, isDetached, ignoreFillFilter bool, tree *parse.Tree, tx *Transaction) (err error) {
 	var node *ast.Node
 	if !isDetached {
 		node = treenode.GetNodeInTree(tree, addingBlockID)
@@ -2007,7 +2006,7 @@ func addAttributeViewBlock(avID, blockID, previousBlockID, addingBlockID string,
 
 	// 如果存在过滤条件,则将过滤条件应用到新添加的块上
 	view, _ := getAttrViewViewByBlockID(attrView, blockID)
-	if nil != view && 0 < len(view.Table.Filters) {
+	if nil != view && 0 < len(view.Table.Filters) && !ignoreFillFilter {
 		viewable, _ := renderAttributeViewTable(attrView, view, "")
 		viewable.FilterRows(attrView)
 		viewable.SortRows()

+ 12 - 0
kernel/model/conf.go

@@ -587,6 +587,18 @@ func Close(force bool, execInstallPkg int) (exitCode int) {
 	clearWorkspaceTemp()
 	clearCorruptedNotebooks()
 	clearPortJSON()
+
+	// 将当前工作空间放到工作空间列表的最后一个
+	// Open the last workspace by default https://github.com/siyuan-note/siyuan/issues/10570
+	workspacePaths, err := util.ReadWorkspacePaths()
+	if nil != err {
+		logging.LogErrorf("read workspace paths failed: %s", err)
+	} else {
+		workspacePaths = gulu.Str.RemoveElem(workspacePaths, util.WorkspaceDir)
+		workspacePaths = append(workspacePaths, util.WorkspaceDir)
+		util.WriteWorkspacePaths(workspacePaths)
+	}
+
 	util.UnlockWorkspace()
 
 	time.Sleep(500 * time.Millisecond)

+ 5 - 2
kernel/model/repository.go

@@ -1475,8 +1475,11 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
 		}
 
 		util.WaitForUILoaded()
-		util.BroadcastByType("main", "syncMergeResult", 0, "",
-			map[string]interface{}{"upsertRootIDs": upsertRootIDs, "removeRootIDs": removeRootIDs})
+
+		if 0 < len(upsertRootIDs) || 0 < len(removeRootIDs) {
+			util.BroadcastByType("main", "syncMergeResult", 0, "",
+				map[string]interface{}{"upsertRootIDs": upsertRootIDs, "removeRootIDs": removeRootIDs})
+		}
 
 		time.Sleep(2 * time.Second)
 		util.PushStatusBar(fmt.Sprintf(Conf.Language(149), elapsed.Seconds()))

+ 11 - 10
kernel/model/transaction.go

@@ -1231,16 +1231,17 @@ type Operation struct {
 
 	DeckID string `json:"deckID"` // 用于添加/删除闪卡
 
-	AvID              string   `json:"avID"`              // 属性视图 ID
-	SrcIDs            []string `json:"srcIDs"`            // 用于将块拖拽到属性视图中
-	IsDetached        bool     `json:"isDetached"`        // 用于标识是否是脱离块,仅存在于属性视图中
-	Name              string   `json:"name"`              // 属性视图列名
-	Typ               string   `json:"type"`              // 属性视图列类型
-	Format            string   `json:"format"`            // 属性视图列格式化
-	KeyID             string   `json:"keyID"`             // 属性视列 ID
-	RowID             string   `json:"rowID"`             // 属性视图行 ID
-	IsTwoWay          bool     `json:"isTwoWay"`          // 属性视图关联列是否是双向关系
-	BackRelationKeyID string   `json:"backRelationKeyID"` // 属性视图关联列回链关联列的 ID
+	AvID                string   `json:"avID"`              // 属性视图 ID
+	SrcIDs              []string `json:"srcIDs"`            // 用于将块拖拽到属性视图中
+	IsDetached          bool     `json:"isDetached"`        // 用于标识是否未绑定块,仅存在于属性视图中
+	IgnoreFillFilterVal bool     `json:"ignoreFillFilter"`  // 用于标识是否忽略填充筛选值
+	Name                string   `json:"name"`              // 属性视图列名
+	Typ                 string   `json:"type"`              // 属性视图列类型
+	Format              string   `json:"format"`            // 属性视图列格式化
+	KeyID               string   `json:"keyID"`             // 属性视列 ID
+	RowID               string   `json:"rowID"`             // 属性视图行 ID
+	IsTwoWay            bool     `json:"isTwoWay"`          // 属性视图关联列是否是双向关系
+	BackRelationKeyID   string   `json:"backRelationKeyID"` // 属性视图关联列回链关联列的 ID
 }
 
 type Transaction struct {

+ 0 - 1
kernel/util/working.go

@@ -243,7 +243,6 @@ func initWorkspaceDir(workspaceArg string) {
 	} else {
 		workspacePaths, _ = ReadWorkspacePaths()
 		if 0 < len(workspacePaths) {
-			// 取最后一个(也就是最近打开的)工作空间
 			WorkspaceDir = workspacePaths[len(workspacePaths)-1]
 		} else {
 			WorkspaceDir = defaultWorkspaceDir