mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-12-17 23:20:13 +01:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
cf6895770c
7 changed files with 690 additions and 462 deletions
503
kernel/av/av.go
503
kernel/av/av.go
|
|
@ -20,12 +20,10 @@ package av
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
|
|
@ -33,8 +31,6 @@ import (
|
|||
"github.com/siyuan-note/filelock"
|
||||
"github.com/siyuan-note/logging"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
// AttributeView 描述了属性视图的结构。
|
||||
|
|
@ -47,45 +43,6 @@ type AttributeView struct {
|
|||
Views []*View `json:"views"` // 视图
|
||||
}
|
||||
|
||||
func ShallowCloneAttributeView(av *AttributeView) (ret *AttributeView) {
|
||||
ret = &AttributeView{}
|
||||
data, err := gulu.JSON.MarshalJSON(av)
|
||||
if nil != err {
|
||||
logging.LogErrorf("marshal attribute view [%s] failed: %s", av.ID, err)
|
||||
return nil
|
||||
}
|
||||
if err = gulu.JSON.UnmarshalJSON(data, ret); nil != err {
|
||||
logging.LogErrorf("unmarshal attribute view [%s] failed: %s", av.ID, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
ret.ID = ast.NewNodeID()
|
||||
view, err := ret.GetCurrentView()
|
||||
if nil == err {
|
||||
view.ID = ast.NewNodeID()
|
||||
ret.ViewID = view.ID
|
||||
} else {
|
||||
view, _ = NewTableViewWithBlockKey(ast.NewNodeID())
|
||||
ret.ViewID = view.ID
|
||||
ret.Views = append(ret.Views, view)
|
||||
}
|
||||
|
||||
keyIDMap := map[string]string{}
|
||||
for _, kv := range ret.KeyValues {
|
||||
newID := ast.NewNodeID()
|
||||
keyIDMap[kv.Key.ID] = newID
|
||||
kv.Key.ID = newID
|
||||
kv.Values = []*Value{}
|
||||
}
|
||||
|
||||
view.Table.ID = ast.NewNodeID()
|
||||
for _, column := range view.Table.Columns {
|
||||
column.ID = keyIDMap[column.ID]
|
||||
}
|
||||
view.Table.RowIDs = []string{}
|
||||
return
|
||||
}
|
||||
|
||||
// KeyValues 描述了属性视图属性列值的结构。
|
||||
type KeyValues struct {
|
||||
Key *Key `json:"key"` // 属性视图属性列
|
||||
|
|
@ -109,6 +66,8 @@ const (
|
|||
KeyTypeCreated KeyType = "created"
|
||||
KeyTypeUpdated KeyType = "updated"
|
||||
KeyTypeCheckbox KeyType = "checkbox"
|
||||
KeyTypeRelation KeyType = "relation"
|
||||
KeyTypeRollup KeyType = "rollup"
|
||||
)
|
||||
|
||||
// Key 描述了属性视图属性列的基础结构。
|
||||
|
|
@ -120,9 +79,23 @@ type Key struct {
|
|||
|
||||
// 以下是某些列类型的特有属性
|
||||
|
||||
// 单选/多选列
|
||||
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 {
|
||||
|
|
@ -139,411 +112,6 @@ type KeySelectOption struct {
|
|||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
type Value struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
KeyID string `json:"keyID,omitempty"`
|
||||
BlockID string `json:"blockID,omitempty"`
|
||||
Type KeyType `json:"type,omitempty"`
|
||||
IsDetached bool `json:"isDetached,omitempty"`
|
||||
IsInitialized bool `json:"isInitialized,omitempty"`
|
||||
|
||||
Block *ValueBlock `json:"block,omitempty"`
|
||||
Text *ValueText `json:"text,omitempty"`
|
||||
Number *ValueNumber `json:"number,omitempty"`
|
||||
Date *ValueDate `json:"date,omitempty"`
|
||||
MSelect []*ValueSelect `json:"mSelect,omitempty"`
|
||||
URL *ValueURL `json:"url,omitempty"`
|
||||
Email *ValueEmail `json:"email,omitempty"`
|
||||
Phone *ValuePhone `json:"phone,omitempty"`
|
||||
MAsset []*ValueAsset `json:"mAsset,omitempty"`
|
||||
Template *ValueTemplate `json:"template,omitempty"`
|
||||
Created *ValueCreated `json:"created,omitempty"`
|
||||
Updated *ValueUpdated `json:"updated,omitempty"`
|
||||
Checkbox *ValueCheckbox `json:"checkbox,omitempty"`
|
||||
}
|
||||
|
||||
func (value *Value) String() string {
|
||||
switch value.Type {
|
||||
case KeyTypeBlock:
|
||||
if nil == value.Block {
|
||||
return ""
|
||||
}
|
||||
return value.Block.Content
|
||||
case KeyTypeText:
|
||||
if nil == value.Text {
|
||||
return ""
|
||||
}
|
||||
return value.Text.Content
|
||||
case KeyTypeNumber:
|
||||
if nil == value.Number {
|
||||
return ""
|
||||
}
|
||||
return value.Number.FormattedContent
|
||||
case KeyTypeDate:
|
||||
if nil == value.Date {
|
||||
return ""
|
||||
}
|
||||
return value.Date.FormattedContent
|
||||
case KeyTypeSelect:
|
||||
if 1 > len(value.MSelect) {
|
||||
return ""
|
||||
}
|
||||
return value.MSelect[0].Content
|
||||
case KeyTypeMSelect:
|
||||
if 1 > len(value.MSelect) {
|
||||
return ""
|
||||
}
|
||||
var ret []string
|
||||
for _, v := range value.MSelect {
|
||||
ret = append(ret, v.Content)
|
||||
}
|
||||
return strings.Join(ret, " ")
|
||||
case KeyTypeURL:
|
||||
if nil == value.URL {
|
||||
return ""
|
||||
}
|
||||
return value.URL.Content
|
||||
case KeyTypeEmail:
|
||||
if nil == value.Email {
|
||||
return ""
|
||||
}
|
||||
return value.Email.Content
|
||||
case KeyTypePhone:
|
||||
if nil == value.Phone {
|
||||
return ""
|
||||
}
|
||||
return value.Phone.Content
|
||||
case KeyTypeMAsset:
|
||||
if 1 > len(value.MAsset) {
|
||||
return ""
|
||||
}
|
||||
var ret []string
|
||||
for _, v := range value.MAsset {
|
||||
ret = append(ret, v.Content)
|
||||
}
|
||||
return strings.Join(ret, " ")
|
||||
case KeyTypeTemplate:
|
||||
if nil == value.Template {
|
||||
return ""
|
||||
}
|
||||
return value.Template.Content
|
||||
case KeyTypeCreated:
|
||||
if nil == value.Created {
|
||||
return ""
|
||||
}
|
||||
return value.Created.FormattedContent
|
||||
case KeyTypeUpdated:
|
||||
if nil == value.Updated {
|
||||
return ""
|
||||
}
|
||||
return value.Updated.FormattedContent
|
||||
case KeyTypeCheckbox:
|
||||
if nil == value.Checkbox {
|
||||
return ""
|
||||
}
|
||||
if value.Checkbox.Checked {
|
||||
return "√"
|
||||
}
|
||||
return ""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (value *Value) ToJSONString() string {
|
||||
data, err := gulu.JSON.MarshalJSON(value)
|
||||
if nil != err {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
type ValueBlock struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
}
|
||||
|
||||
type ValueText struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ValueNumber struct {
|
||||
Content float64 `json:"content"`
|
||||
IsNotEmpty bool `json:"isNotEmpty"`
|
||||
Format NumberFormat `json:"format"`
|
||||
FormattedContent string `json:"formattedContent"`
|
||||
}
|
||||
|
||||
type NumberFormat string
|
||||
|
||||
const (
|
||||
NumberFormatNone NumberFormat = ""
|
||||
NumberFormatCommas NumberFormat = "commas"
|
||||
NumberFormatPercent NumberFormat = "percent"
|
||||
NumberFormatUSDollar NumberFormat = "usDollar"
|
||||
NumberFormatYuan NumberFormat = "yuan"
|
||||
NumberFormatEuro NumberFormat = "euro"
|
||||
NumberFormatPound NumberFormat = "pound"
|
||||
NumberFormatYen NumberFormat = "yen"
|
||||
NumberFormatRuble NumberFormat = "ruble"
|
||||
NumberFormatRupee NumberFormat = "rupee"
|
||||
NumberFormatWon NumberFormat = "won"
|
||||
NumberFormatCanadianDollar NumberFormat = "canadianDollar"
|
||||
NumberFormatFranc NumberFormat = "franc"
|
||||
)
|
||||
|
||||
func NewValueNumber(content float64) *ValueNumber {
|
||||
return &ValueNumber{
|
||||
Content: content,
|
||||
IsNotEmpty: true,
|
||||
Format: NumberFormatNone,
|
||||
FormattedContent: fmt.Sprintf("%f", content),
|
||||
}
|
||||
}
|
||||
|
||||
func NewFormattedValueNumber(content float64, format NumberFormat) (ret *ValueNumber) {
|
||||
ret = &ValueNumber{
|
||||
Content: content,
|
||||
IsNotEmpty: true,
|
||||
Format: format,
|
||||
FormattedContent: fmt.Sprintf("%f", content),
|
||||
}
|
||||
|
||||
ret.FormattedContent = formatNumber(content, format)
|
||||
|
||||
switch format {
|
||||
case NumberFormatNone:
|
||||
s := fmt.Sprintf("%.5f", content)
|
||||
ret.FormattedContent = strings.TrimRight(strings.TrimRight(s, "0"), ".")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (number *ValueNumber) FormatNumber() {
|
||||
number.FormattedContent = formatNumber(number.Content, number.Format)
|
||||
}
|
||||
|
||||
func formatNumber(content float64, format NumberFormat) string {
|
||||
switch format {
|
||||
case NumberFormatNone:
|
||||
return strconv.FormatFloat(content, 'f', -1, 64)
|
||||
case NumberFormatCommas:
|
||||
p := message.NewPrinter(language.English)
|
||||
s := p.Sprintf("%f", content)
|
||||
return strings.TrimRight(strings.TrimRight(s, "0"), ".")
|
||||
case NumberFormatPercent:
|
||||
s := fmt.Sprintf("%.2f", content*100)
|
||||
return strings.TrimRight(strings.TrimRight(s, "0"), ".") + "%"
|
||||
case NumberFormatUSDollar:
|
||||
p := message.NewPrinter(language.English)
|
||||
return p.Sprintf("$%.2f", content)
|
||||
case NumberFormatYuan:
|
||||
p := message.NewPrinter(language.Chinese)
|
||||
return p.Sprintf("CN¥%.2f", content)
|
||||
case NumberFormatEuro:
|
||||
p := message.NewPrinter(language.German)
|
||||
return p.Sprintf("€%.2f", content)
|
||||
case NumberFormatPound:
|
||||
p := message.NewPrinter(language.English)
|
||||
return p.Sprintf("£%.2f", content)
|
||||
case NumberFormatYen:
|
||||
p := message.NewPrinter(language.Japanese)
|
||||
return p.Sprintf("¥%.0f", content)
|
||||
case NumberFormatRuble:
|
||||
p := message.NewPrinter(language.Russian)
|
||||
return p.Sprintf("₽%.2f", content)
|
||||
case NumberFormatRupee:
|
||||
p := message.NewPrinter(language.Hindi)
|
||||
return p.Sprintf("₹%.2f", content)
|
||||
case NumberFormatWon:
|
||||
p := message.NewPrinter(language.Korean)
|
||||
return p.Sprintf("₩%.0f", content)
|
||||
case NumberFormatCanadianDollar:
|
||||
p := message.NewPrinter(language.English)
|
||||
return p.Sprintf("CA$%.2f", content)
|
||||
case NumberFormatFranc:
|
||||
p := message.NewPrinter(language.French)
|
||||
return p.Sprintf("CHF%.2f", content)
|
||||
default:
|
||||
return strconv.FormatFloat(content, 'f', -1, 64)
|
||||
}
|
||||
}
|
||||
|
||||
type ValueDate struct {
|
||||
Content int64 `json:"content"`
|
||||
IsNotEmpty bool `json:"isNotEmpty"`
|
||||
HasEndDate bool `json:"hasEndDate"`
|
||||
IsNotTime bool `json:"isNotTime"`
|
||||
Content2 int64 `json:"content2"`
|
||||
IsNotEmpty2 bool `json:"isNotEmpty2"`
|
||||
FormattedContent string `json:"formattedContent"`
|
||||
}
|
||||
|
||||
type DateFormat string
|
||||
|
||||
const (
|
||||
DateFormatNone DateFormat = ""
|
||||
DateFormatDuration DateFormat = "duration"
|
||||
)
|
||||
|
||||
func NewFormattedValueDate(content, content2 int64, format DateFormat, isNotTime bool) (ret *ValueDate) {
|
||||
var formatted string
|
||||
if isNotTime {
|
||||
formatted = time.UnixMilli(content).Format("2006-01-02")
|
||||
} else {
|
||||
formatted = time.UnixMilli(content).Format("2006-01-02 15:04")
|
||||
}
|
||||
if 0 < content2 {
|
||||
var formattedContent2 string
|
||||
if isNotTime {
|
||||
formattedContent2 = time.UnixMilli(content2).Format("2006-01-02")
|
||||
} else {
|
||||
formattedContent2 = time.UnixMilli(content2).Format("2006-01-02 15:04")
|
||||
}
|
||||
formatted += " → " + formattedContent2
|
||||
}
|
||||
switch format {
|
||||
case DateFormatNone:
|
||||
case DateFormatDuration:
|
||||
t1 := time.UnixMilli(content)
|
||||
t2 := time.UnixMilli(content2)
|
||||
formatted = util.HumanizeRelTime(t1, t2, util.Lang)
|
||||
}
|
||||
ret = &ValueDate{
|
||||
Content: content,
|
||||
Content2: content2,
|
||||
HasEndDate: false,
|
||||
IsNotTime: true,
|
||||
FormattedContent: formatted,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RoundUp rounds like 12.3416 -> 12.35
|
||||
func RoundUp(val float64, precision int) float64 {
|
||||
return math.Ceil(val*(math.Pow10(precision))) / math.Pow10(precision)
|
||||
}
|
||||
|
||||
// RoundDown rounds like 12.3496 -> 12.34
|
||||
func RoundDown(val float64, precision int) float64 {
|
||||
return math.Floor(val*(math.Pow10(precision))) / math.Pow10(precision)
|
||||
}
|
||||
|
||||
// Round rounds to nearest like 12.3456 -> 12.35
|
||||
func Round(val float64, precision int) float64 {
|
||||
return math.Round(val*(math.Pow10(precision))) / math.Pow10(precision)
|
||||
}
|
||||
|
||||
type ValueSelect struct {
|
||||
Content string `json:"content"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
type ValueURL struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ValueEmail struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ValuePhone struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type AssetType string
|
||||
|
||||
const (
|
||||
AssetTypeFile = "file"
|
||||
AssetTypeImage = "image"
|
||||
)
|
||||
|
||||
type ValueAsset struct {
|
||||
Type AssetType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ValueTemplate struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ValueCreated struct {
|
||||
Content int64 `json:"content"`
|
||||
IsNotEmpty bool `json:"isNotEmpty"`
|
||||
Content2 int64 `json:"content2"`
|
||||
IsNotEmpty2 bool `json:"isNotEmpty2"`
|
||||
FormattedContent string `json:"formattedContent"`
|
||||
}
|
||||
|
||||
type CreatedFormat string
|
||||
|
||||
const (
|
||||
CreatedFormatNone CreatedFormat = "" // 2006-01-02 15:04
|
||||
CreatedFormatDuration CreatedFormat = "duration"
|
||||
)
|
||||
|
||||
func NewFormattedValueCreated(content, content2 int64, format CreatedFormat) (ret *ValueCreated) {
|
||||
formatted := time.UnixMilli(content).Format("2006-01-02 15:04")
|
||||
if 0 < content2 {
|
||||
formatted += " → " + time.UnixMilli(content2).Format("2006-01-02 15:04")
|
||||
}
|
||||
switch format {
|
||||
case CreatedFormatNone:
|
||||
case CreatedFormatDuration:
|
||||
t1 := time.UnixMilli(content)
|
||||
t2 := time.UnixMilli(content2)
|
||||
formatted = util.HumanizeRelTime(t1, t2, util.Lang)
|
||||
}
|
||||
ret = &ValueCreated{
|
||||
Content: content,
|
||||
Content2: content2,
|
||||
FormattedContent: formatted,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ValueUpdated struct {
|
||||
Content int64 `json:"content"`
|
||||
IsNotEmpty bool `json:"isNotEmpty"`
|
||||
Content2 int64 `json:"content2"`
|
||||
IsNotEmpty2 bool `json:"isNotEmpty2"`
|
||||
FormattedContent string `json:"formattedContent"`
|
||||
}
|
||||
|
||||
type UpdatedFormat string
|
||||
|
||||
const (
|
||||
UpdatedFormatNone UpdatedFormat = "" // 2006-01-02 15:04
|
||||
UpdatedFormatDuration UpdatedFormat = "duration"
|
||||
)
|
||||
|
||||
func NewFormattedValueUpdated(content, content2 int64, format UpdatedFormat) (ret *ValueUpdated) {
|
||||
formatted := time.UnixMilli(content).Format("2006-01-02 15:04")
|
||||
if 0 < content2 {
|
||||
formatted += " → " + time.UnixMilli(content2).Format("2006-01-02 15:04")
|
||||
}
|
||||
switch format {
|
||||
case UpdatedFormatNone:
|
||||
case UpdatedFormatDuration:
|
||||
t1 := time.UnixMilli(content)
|
||||
t2 := time.UnixMilli(content2)
|
||||
formatted = util.HumanizeRelTime(t1, t2, util.Lang)
|
||||
}
|
||||
ret = &ValueUpdated{
|
||||
Content: content,
|
||||
Content2: content2,
|
||||
FormattedContent: formatted,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ValueCheckbox struct {
|
||||
Checked bool `json:"checked"`
|
||||
}
|
||||
|
||||
// View 描述了视图的结构。
|
||||
type View struct {
|
||||
ID string `json:"id"` // 视图 ID
|
||||
|
|
@ -769,6 +337,45 @@ func (av *AttributeView) GetDuplicateViewName(masterViewName string) (ret string
|
|||
return
|
||||
}
|
||||
|
||||
func (av *AttributeView) ShallowClone() (ret *AttributeView) {
|
||||
ret = &AttributeView{}
|
||||
data, err := gulu.JSON.MarshalJSON(av)
|
||||
if nil != err {
|
||||
logging.LogErrorf("marshal attribute view [%s] failed: %s", av.ID, err)
|
||||
return nil
|
||||
}
|
||||
if err = gulu.JSON.UnmarshalJSON(data, ret); nil != err {
|
||||
logging.LogErrorf("unmarshal attribute view [%s] failed: %s", av.ID, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
ret.ID = ast.NewNodeID()
|
||||
view, err := ret.GetCurrentView()
|
||||
if nil == err {
|
||||
view.ID = ast.NewNodeID()
|
||||
ret.ViewID = view.ID
|
||||
} else {
|
||||
view, _ = NewTableViewWithBlockKey(ast.NewNodeID())
|
||||
ret.ViewID = view.ID
|
||||
ret.Views = append(ret.Views, view)
|
||||
}
|
||||
|
||||
keyIDMap := map[string]string{}
|
||||
for _, kv := range ret.KeyValues {
|
||||
newID := ast.NewNodeID()
|
||||
keyIDMap[kv.Key.ID] = newID
|
||||
kv.Key.ID = newID
|
||||
kv.Values = []*Value{}
|
||||
}
|
||||
|
||||
view.Table.ID = ast.NewNodeID()
|
||||
for _, column := range view.Table.Columns {
|
||||
column.ID = keyIDMap[column.ID]
|
||||
}
|
||||
view.Table.RowIDs = []string{}
|
||||
return
|
||||
}
|
||||
|
||||
func GetAttributeViewDataPath(avID string) (ret string) {
|
||||
av := filepath.Join(util.DataDir, "storage", "av")
|
||||
ret = filepath.Join(av, avID+".json")
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
456
kernel/av/value.go
Normal file
456
kernel/av/value.go
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package av
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
type Value struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
KeyID string `json:"keyID,omitempty"`
|
||||
BlockID string `json:"blockID,omitempty"`
|
||||
Type KeyType `json:"type,omitempty"`
|
||||
IsDetached bool `json:"isDetached,omitempty"`
|
||||
IsInitialized bool `json:"isInitialized,omitempty"`
|
||||
|
||||
Block *ValueBlock `json:"block,omitempty"`
|
||||
Text *ValueText `json:"text,omitempty"`
|
||||
Number *ValueNumber `json:"number,omitempty"`
|
||||
Date *ValueDate `json:"date,omitempty"`
|
||||
MSelect []*ValueSelect `json:"mSelect,omitempty"`
|
||||
URL *ValueURL `json:"url,omitempty"`
|
||||
Email *ValueEmail `json:"email,omitempty"`
|
||||
Phone *ValuePhone `json:"phone,omitempty"`
|
||||
MAsset []*ValueAsset `json:"mAsset,omitempty"`
|
||||
Template *ValueTemplate `json:"template,omitempty"`
|
||||
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 {
|
||||
switch value.Type {
|
||||
case KeyTypeBlock:
|
||||
if nil == value.Block {
|
||||
return ""
|
||||
}
|
||||
return value.Block.Content
|
||||
case KeyTypeText:
|
||||
if nil == value.Text {
|
||||
return ""
|
||||
}
|
||||
return value.Text.Content
|
||||
case KeyTypeNumber:
|
||||
if nil == value.Number {
|
||||
return ""
|
||||
}
|
||||
return value.Number.FormattedContent
|
||||
case KeyTypeDate:
|
||||
if nil == value.Date {
|
||||
return ""
|
||||
}
|
||||
return value.Date.FormattedContent
|
||||
case KeyTypeSelect:
|
||||
if 1 > len(value.MSelect) {
|
||||
return ""
|
||||
}
|
||||
return value.MSelect[0].Content
|
||||
case KeyTypeMSelect:
|
||||
if 1 > len(value.MSelect) {
|
||||
return ""
|
||||
}
|
||||
var ret []string
|
||||
for _, v := range value.MSelect {
|
||||
ret = append(ret, v.Content)
|
||||
}
|
||||
return strings.Join(ret, " ")
|
||||
case KeyTypeURL:
|
||||
if nil == value.URL {
|
||||
return ""
|
||||
}
|
||||
return value.URL.Content
|
||||
case KeyTypeEmail:
|
||||
if nil == value.Email {
|
||||
return ""
|
||||
}
|
||||
return value.Email.Content
|
||||
case KeyTypePhone:
|
||||
if nil == value.Phone {
|
||||
return ""
|
||||
}
|
||||
return value.Phone.Content
|
||||
case KeyTypeMAsset:
|
||||
if 1 > len(value.MAsset) {
|
||||
return ""
|
||||
}
|
||||
var ret []string
|
||||
for _, v := range value.MAsset {
|
||||
ret = append(ret, v.Content)
|
||||
}
|
||||
return strings.Join(ret, " ")
|
||||
case KeyTypeTemplate:
|
||||
if nil == value.Template {
|
||||
return ""
|
||||
}
|
||||
return value.Template.Content
|
||||
case KeyTypeCreated:
|
||||
if nil == value.Created {
|
||||
return ""
|
||||
}
|
||||
return value.Created.FormattedContent
|
||||
case KeyTypeUpdated:
|
||||
if nil == value.Updated {
|
||||
return ""
|
||||
}
|
||||
return value.Updated.FormattedContent
|
||||
case KeyTypeCheckbox:
|
||||
if nil == value.Checkbox {
|
||||
return ""
|
||||
}
|
||||
if value.Checkbox.Checked {
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
||||
func (value *Value) ToJSONString() string {
|
||||
data, err := gulu.JSON.MarshalJSON(value)
|
||||
if nil != err {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
type ValueBlock struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
}
|
||||
|
||||
type ValueText struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ValueNumber struct {
|
||||
Content float64 `json:"content"`
|
||||
IsNotEmpty bool `json:"isNotEmpty"`
|
||||
Format NumberFormat `json:"format"`
|
||||
FormattedContent string `json:"formattedContent"`
|
||||
}
|
||||
|
||||
type NumberFormat string
|
||||
|
||||
const (
|
||||
NumberFormatNone NumberFormat = ""
|
||||
NumberFormatCommas NumberFormat = "commas"
|
||||
NumberFormatPercent NumberFormat = "percent"
|
||||
NumberFormatUSDollar NumberFormat = "usDollar"
|
||||
NumberFormatYuan NumberFormat = "yuan"
|
||||
NumberFormatEuro NumberFormat = "euro"
|
||||
NumberFormatPound NumberFormat = "pound"
|
||||
NumberFormatYen NumberFormat = "yen"
|
||||
NumberFormatRuble NumberFormat = "ruble"
|
||||
NumberFormatRupee NumberFormat = "rupee"
|
||||
NumberFormatWon NumberFormat = "won"
|
||||
NumberFormatCanadianDollar NumberFormat = "canadianDollar"
|
||||
NumberFormatFranc NumberFormat = "franc"
|
||||
)
|
||||
|
||||
func NewValueNumber(content float64) *ValueNumber {
|
||||
return &ValueNumber{
|
||||
Content: content,
|
||||
IsNotEmpty: true,
|
||||
Format: NumberFormatNone,
|
||||
FormattedContent: fmt.Sprintf("%f", content),
|
||||
}
|
||||
}
|
||||
|
||||
func NewFormattedValueNumber(content float64, format NumberFormat) (ret *ValueNumber) {
|
||||
ret = &ValueNumber{
|
||||
Content: content,
|
||||
IsNotEmpty: true,
|
||||
Format: format,
|
||||
FormattedContent: fmt.Sprintf("%f", content),
|
||||
}
|
||||
|
||||
ret.FormattedContent = formatNumber(content, format)
|
||||
|
||||
switch format {
|
||||
case NumberFormatNone:
|
||||
s := fmt.Sprintf("%.5f", content)
|
||||
ret.FormattedContent = strings.TrimRight(strings.TrimRight(s, "0"), ".")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (number *ValueNumber) FormatNumber() {
|
||||
number.FormattedContent = formatNumber(number.Content, number.Format)
|
||||
}
|
||||
|
||||
func formatNumber(content float64, format NumberFormat) string {
|
||||
switch format {
|
||||
case NumberFormatNone:
|
||||
return strconv.FormatFloat(content, 'f', -1, 64)
|
||||
case NumberFormatCommas:
|
||||
p := message.NewPrinter(language.English)
|
||||
s := p.Sprintf("%f", content)
|
||||
return strings.TrimRight(strings.TrimRight(s, "0"), ".")
|
||||
case NumberFormatPercent:
|
||||
s := fmt.Sprintf("%.2f", content*100)
|
||||
return strings.TrimRight(strings.TrimRight(s, "0"), ".") + "%"
|
||||
case NumberFormatUSDollar:
|
||||
p := message.NewPrinter(language.English)
|
||||
return p.Sprintf("$%.2f", content)
|
||||
case NumberFormatYuan:
|
||||
p := message.NewPrinter(language.Chinese)
|
||||
return p.Sprintf("CN¥%.2f", content)
|
||||
case NumberFormatEuro:
|
||||
p := message.NewPrinter(language.German)
|
||||
return p.Sprintf("€%.2f", content)
|
||||
case NumberFormatPound:
|
||||
p := message.NewPrinter(language.English)
|
||||
return p.Sprintf("£%.2f", content)
|
||||
case NumberFormatYen:
|
||||
p := message.NewPrinter(language.Japanese)
|
||||
return p.Sprintf("¥%.0f", content)
|
||||
case NumberFormatRuble:
|
||||
p := message.NewPrinter(language.Russian)
|
||||
return p.Sprintf("₽%.2f", content)
|
||||
case NumberFormatRupee:
|
||||
p := message.NewPrinter(language.Hindi)
|
||||
return p.Sprintf("₹%.2f", content)
|
||||
case NumberFormatWon:
|
||||
p := message.NewPrinter(language.Korean)
|
||||
return p.Sprintf("₩%.0f", content)
|
||||
case NumberFormatCanadianDollar:
|
||||
p := message.NewPrinter(language.English)
|
||||
return p.Sprintf("CA$%.2f", content)
|
||||
case NumberFormatFranc:
|
||||
p := message.NewPrinter(language.French)
|
||||
return p.Sprintf("CHF%.2f", content)
|
||||
default:
|
||||
return strconv.FormatFloat(content, 'f', -1, 64)
|
||||
}
|
||||
}
|
||||
|
||||
type ValueDate struct {
|
||||
Content int64 `json:"content"`
|
||||
IsNotEmpty bool `json:"isNotEmpty"`
|
||||
HasEndDate bool `json:"hasEndDate"`
|
||||
IsNotTime bool `json:"isNotTime"`
|
||||
Content2 int64 `json:"content2"`
|
||||
IsNotEmpty2 bool `json:"isNotEmpty2"`
|
||||
FormattedContent string `json:"formattedContent"`
|
||||
}
|
||||
|
||||
type DateFormat string
|
||||
|
||||
const (
|
||||
DateFormatNone DateFormat = ""
|
||||
DateFormatDuration DateFormat = "duration"
|
||||
)
|
||||
|
||||
func NewFormattedValueDate(content, content2 int64, format DateFormat, isNotTime bool) (ret *ValueDate) {
|
||||
var formatted string
|
||||
if isNotTime {
|
||||
formatted = time.UnixMilli(content).Format("2006-01-02")
|
||||
} else {
|
||||
formatted = time.UnixMilli(content).Format("2006-01-02 15:04")
|
||||
}
|
||||
if 0 < content2 {
|
||||
var formattedContent2 string
|
||||
if isNotTime {
|
||||
formattedContent2 = time.UnixMilli(content2).Format("2006-01-02")
|
||||
} else {
|
||||
formattedContent2 = time.UnixMilli(content2).Format("2006-01-02 15:04")
|
||||
}
|
||||
formatted += " → " + formattedContent2
|
||||
}
|
||||
switch format {
|
||||
case DateFormatNone:
|
||||
case DateFormatDuration:
|
||||
t1 := time.UnixMilli(content)
|
||||
t2 := time.UnixMilli(content2)
|
||||
formatted = util.HumanizeRelTime(t1, t2, util.Lang)
|
||||
}
|
||||
ret = &ValueDate{
|
||||
Content: content,
|
||||
Content2: content2,
|
||||
HasEndDate: false,
|
||||
IsNotTime: true,
|
||||
FormattedContent: formatted,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RoundUp rounds like 12.3416 -> 12.35
|
||||
func RoundUp(val float64, precision int) float64 {
|
||||
return math.Ceil(val*(math.Pow10(precision))) / math.Pow10(precision)
|
||||
}
|
||||
|
||||
// RoundDown rounds like 12.3496 -> 12.34
|
||||
func RoundDown(val float64, precision int) float64 {
|
||||
return math.Floor(val*(math.Pow10(precision))) / math.Pow10(precision)
|
||||
}
|
||||
|
||||
// Round rounds to nearest like 12.3456 -> 12.35
|
||||
func Round(val float64, precision int) float64 {
|
||||
return math.Round(val*(math.Pow10(precision))) / math.Pow10(precision)
|
||||
}
|
||||
|
||||
type ValueSelect struct {
|
||||
Content string `json:"content"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
type ValueURL struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ValueEmail struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ValuePhone struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type AssetType string
|
||||
|
||||
const (
|
||||
AssetTypeFile = "file"
|
||||
AssetTypeImage = "image"
|
||||
)
|
||||
|
||||
type ValueAsset struct {
|
||||
Type AssetType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ValueTemplate struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ValueCreated struct {
|
||||
Content int64 `json:"content"`
|
||||
IsNotEmpty bool `json:"isNotEmpty"`
|
||||
Content2 int64 `json:"content2"`
|
||||
IsNotEmpty2 bool `json:"isNotEmpty2"`
|
||||
FormattedContent string `json:"formattedContent"`
|
||||
}
|
||||
|
||||
type CreatedFormat string
|
||||
|
||||
const (
|
||||
CreatedFormatNone CreatedFormat = "" // 2006-01-02 15:04
|
||||
CreatedFormatDuration CreatedFormat = "duration"
|
||||
)
|
||||
|
||||
func NewFormattedValueCreated(content, content2 int64, format CreatedFormat) (ret *ValueCreated) {
|
||||
formatted := time.UnixMilli(content).Format("2006-01-02 15:04")
|
||||
if 0 < content2 {
|
||||
formatted += " → " + time.UnixMilli(content2).Format("2006-01-02 15:04")
|
||||
}
|
||||
switch format {
|
||||
case CreatedFormatNone:
|
||||
case CreatedFormatDuration:
|
||||
t1 := time.UnixMilli(content)
|
||||
t2 := time.UnixMilli(content2)
|
||||
formatted = util.HumanizeRelTime(t1, t2, util.Lang)
|
||||
}
|
||||
ret = &ValueCreated{
|
||||
Content: content,
|
||||
Content2: content2,
|
||||
FormattedContent: formatted,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ValueUpdated struct {
|
||||
Content int64 `json:"content"`
|
||||
IsNotEmpty bool `json:"isNotEmpty"`
|
||||
Content2 int64 `json:"content2"`
|
||||
IsNotEmpty2 bool `json:"isNotEmpty2"`
|
||||
FormattedContent string `json:"formattedContent"`
|
||||
}
|
||||
|
||||
type UpdatedFormat string
|
||||
|
||||
const (
|
||||
UpdatedFormatNone UpdatedFormat = "" // 2006-01-02 15:04
|
||||
UpdatedFormatDuration UpdatedFormat = "duration"
|
||||
)
|
||||
|
||||
func NewFormattedValueUpdated(content, content2 int64, format UpdatedFormat) (ret *ValueUpdated) {
|
||||
formatted := time.UnixMilli(content).Format("2006-01-02 15:04")
|
||||
if 0 < content2 {
|
||||
formatted += " → " + time.UnixMilli(content2).Format("2006-01-02 15:04")
|
||||
}
|
||||
switch format {
|
||||
case UpdatedFormatNone:
|
||||
case UpdatedFormatDuration:
|
||||
t1 := time.UnixMilli(content)
|
||||
t2 := time.UnixMilli(content2)
|
||||
formatted = util.HumanizeRelTime(t1, t2, util.Lang)
|
||||
}
|
||||
ret = &ValueUpdated{
|
||||
Content: content,
|
||||
Content2: content2,
|
||||
FormattedContent: formatted,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -257,14 +257,6 @@ func (box *Box) Ls(p string) (ret []*FileInfo, totals int, err error) {
|
|||
if util.IsReservedFilename(name) {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(name, ".tmp") {
|
||||
// 移除写入失败时产生的临时文件
|
||||
removePath := filepath.Join(util.DataDir, box.ID, p, name)
|
||||
if removeErr := os.Remove(removePath); nil != removeErr {
|
||||
logging.LogWarnf("remove tmp file [%s] failed: %s", removePath, removeErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
totals += 1
|
||||
fi := &FileInfo{}
|
||||
|
|
|
|||
|
|
@ -317,7 +317,7 @@ func renderTemplate(p, id string, preview bool) (string, error) {
|
|||
if nil != parseErr {
|
||||
logging.LogErrorf("parse attribute view [%s] failed: %s", n.AttributeViewID, parseErr)
|
||||
} else {
|
||||
cloned := av.ShallowCloneAttributeView(attrView)
|
||||
cloned := attrView.ShallowClone()
|
||||
if nil != cloned {
|
||||
n.AttributeViewID = cloned.ID
|
||||
if !preview {
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue