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

This commit is contained in:
Vanessa 2025-06-13 11:45:37 +08:00
commit b2febe82b1
9 changed files with 250 additions and 349 deletions

View file

@ -7,17 +7,17 @@
## NPM dependencies
Install pnpm: `npm install -g pnpm@10.11.0`
Install pnpm: `npm install -g pnpm@10.12.1`
<details>
<summary>For China mainland</summary>
Set the Electron mirror environment variable and install Electron:
* macOS/Linux: `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ pnpm install electron@v35.5.0 -D`
* macOS/Linux: `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ pnpm install electron@v36.4.0 -D`
* Windows:
* `SET ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/`
* `pnpm install electron@v35.5.0 -D`
* `pnpm install electron@v36.4.0 -D`
NPM mirror:
@ -27,7 +27,7 @@ NPM mirror:
Enter the app folder and execute:
* `pnpm install electron@v35.5.0 -D`
* `pnpm install electron@v36.4.0 -D`
* `pnpm run dev`
* `pnpm run start`

View file

@ -7,17 +7,17 @@
## NPM 依赖
安装 pnpm`npm install -g pnpm@10.11.0`
安装 pnpm`npm install -g pnpm@10.12.1`
<details>
<summary>适用于中国大陆</summary>
设置 Electron 镜像环境变量并安装 Electron
* macOS/Linux`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ pnpm install electron@v35.5.0 -D`
* macOS/Linux`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ pnpm install electron@v36.4.0 -D`
* Windows
* `SET ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/`
* `pnpm install electron@v35.5.0 -D`
* `pnpm install electron@v36.4.0 -D`
NPM 镜像:
@ -27,7 +27,7 @@ NPM 镜像:
进入 app 文件夹执行:
* `pnpm install electron@v35.5.0 -D`
* `pnpm install electron@v36.4.0 -D`
* `pnpm run dev`
* `pnpm run start`

View file

@ -4,7 +4,7 @@
"description": "Refactor your thinking",
"homepage": "https://b3log.org/siyuan",
"main": "./electron/main.js",
"packageManager": "pnpm@10.11.0",
"packageManager": "pnpm@10.12.1",
"scripts": {
"lint": "eslint . --fix --cache",
"dev": "webpack --mode development",
@ -58,7 +58,7 @@
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.7.1",
"dayjs": "^1.11.5",
"electron": "35.5.0",
"electron": "v36.4.0",
"electron-builder": "26.0.12",
"encoding": "^0.1.13",
"esbuild-loader": "^3.0.1",

24
app/pnpm-lock.yaml generated
View file

@ -10,7 +10,7 @@ importers:
dependencies:
'@electron/remote':
specifier: ^2.1.2
version: 2.1.2(electron@35.5.0)
version: 2.1.2(electron@36.4.0)
devDependencies:
'@eslint/eslintrc':
specifier: ^3.3.1
@ -40,8 +40,8 @@ importers:
specifier: ^1.11.5
version: 1.11.13
electron:
specifier: 35.5.0
version: 35.5.0
specifier: v36.4.0
version: 36.4.0
electron-builder:
specifier: 26.0.12
version: 26.0.12(electron-builder-squirrel-windows@26.0.11)
@ -131,8 +131,8 @@ packages:
resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==}
engines: {node: '>=12'}
'@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
resolution: {tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2}
'@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2':
resolution: {commit: 06b29aafb7708acef8b3669835c8a7857ebc92d2, repo: https://github.com/electron/node-gyp.git, type: git}
version: 10.2.0-electron.1
engines: {node: '>=12.13.0'}
hasBin: true
@ -1191,8 +1191,8 @@ packages:
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
engines: {node: '>=8.0.0'}
electron@35.5.0:
resolution: {integrity: sha512-16ScwDuKgnuL7tSrEgBvQe1Hm4CSK0vbOusPFrDs4oIs3QOdEFtrP9i8+4yKQGXpszj4f4F0MQjKv1tu9E4Gvg==}
electron@36.4.0:
resolution: {integrity: sha512-LLOOZEuW5oqvnjC7HBQhIqjIIJAZCIFjQxltQGLfEC7XFsBoZgQ3u3iFj+Kzw68Xj97u1n57Jdt7P98qLvUibQ==}
engines: {node: '>= 12.20.55'}
hasBin: true
@ -2803,7 +2803,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
'@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2':
dependencies:
env-paths: 2.2.1
exponential-backoff: 3.1.2
@ -2840,7 +2840,7 @@ snapshots:
'@electron/rebuild@3.7.0':
dependencies:
'@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2
'@electron/node-gyp': git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2
'@malept/cross-spawn-promise': 2.0.0
chalk: 4.1.2
debug: 4.4.0
@ -2858,9 +2858,9 @@ snapshots:
- bluebird
- supports-color
'@electron/remote@2.1.2(electron@35.5.0)':
'@electron/remote@2.1.2(electron@36.4.0)':
dependencies:
electron: 35.5.0
electron: 36.4.0
'@electron/universal@2.0.1':
dependencies:
@ -4025,7 +4025,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
electron@35.5.0:
electron@36.4.0:
dependencies:
'@electron/get': 2.0.3
'@types/node': 22.15.3

View file

@ -16,6 +16,8 @@
package av
import "sort"
// BaseLayout 描述了布局的基础结构。
type BaseLayout struct {
Spec int `json:"spec"` // 布局格式版本
@ -44,6 +46,14 @@ type BaseInstance struct {
PageSize int `json:"pageSize"` // 每页项目
}
func (baseInstance *BaseInstance) GetSorts() []*ViewSort {
return baseInstance.Sorts
}
func (baseInstance *BaseInstance) GetFilters() []*ViewFilter {
return baseInstance.Filters
}
// BaseInstanceField 描述了实例字段的基础结构。
type BaseInstanceField struct {
ID string `json:"id"` // ID
@ -63,6 +73,10 @@ type BaseInstanceField struct {
Date *Date `json:"date,omitempty"` // 日期设置
}
func (baseInstanceField *BaseInstanceField) GetID() string {
return baseInstanceField.ID
}
// CollectionLayout 描述了集合布局的接口。
type CollectionLayout interface {
@ -79,6 +93,22 @@ type Collection interface {
// SetItems 设置集合中的项目。
SetItems(items []Item)
// GetFields 返回集合的所有字段。
GetFields() []Field
// GetSorts 返回集合的排序规则。
GetSorts() []*ViewSort
// GetFilters 返回集合的过滤规则。
GetFilters() []*ViewFilter
}
// Field 描述了一个字段的接口。
type Field interface {
// GetID 返回字段的 ID。
GetID() string
}
// Item 描述了一个项目的接口。
@ -97,3 +127,169 @@ type Item interface {
// GetID 返回项目的 ID。
GetID() string
}
func sort0(collection Collection, attrView *AttributeView) {
sorts := collection.GetSorts()
if 1 > len(sorts) {
return
}
type FieldIndexSort struct {
Index int
Order SortOrder
}
var fieldIndexSorts []*FieldIndexSort
for _, s := range sorts {
for i, c := range collection.GetFields() {
if c.GetID() == s.Column {
fieldIndexSorts = append(fieldIndexSorts, &FieldIndexSort{Index: i, Order: s.Order})
break
}
}
}
items := collection.GetItems()
editedValItems := map[string]bool{}
for i, item := range items {
for _, fieldIndexSort := range fieldIndexSorts {
val := items[i].GetValues()[fieldIndexSort.Index]
if KeyTypeCheckbox == val.Type {
if block := item.GetBlockValue(); nil != block && block.IsEdited() {
// 如果主键编辑过,则勾选框也算作编辑过,参与排序 https://github.com/siyuan-note/siyuan/issues/11016
editedValItems[item.GetID()] = true
break
}
}
if val.IsEdited() {
// 如果该卡片某字段的值已经编辑过,则该卡片可参与排序
editedValItems[item.GetID()] = true
break
}
}
}
// 将未编辑的卡片和已编辑的卡片分开排序
var uneditedItems, editedItems []Item
for _, item := range items {
if _, ok := editedValItems[item.GetID()]; ok {
editedItems = append(editedItems, item)
} else {
uneditedItems = append(uneditedItems, item)
}
}
sort.Slice(uneditedItems, func(i, j int) bool {
val1 := uneditedItems[i].GetBlockValue()
if nil == val1 {
return true
}
val2 := uneditedItems[j].GetBlockValue()
if nil == val2 {
return false
}
return val1.CreatedAt < val2.CreatedAt
})
sort.Slice(editedItems, func(i, j int) bool {
sorted := true
for _, fieldIndexSort := range fieldIndexSorts {
val1 := editedItems[i].GetValues()[fieldIndexSort.Index]
val2 := editedItems[j].GetValues()[fieldIndexSort.Index]
if nil == val1 || val1.IsEmpty() {
if nil != val2 && !val2.IsEmpty() {
return false
}
sorted = false
continue
} else {
if nil == val2 || val2.IsEmpty() {
return true
}
}
result := val1.Compare(val2, attrView)
if 0 == result {
sorted = false
continue
}
sorted = true
if fieldIndexSort.Order == SortOrderAsc {
return 0 > result
}
return 0 < result
}
if !sorted {
key1 := editedItems[i].GetBlockValue()
if nil == key1 {
return false
}
key2 := editedItems[j].GetBlockValue()
if nil == key2 {
return false
}
return key1.CreatedAt < key2.CreatedAt
}
return false
})
// 将包含未编辑的卡片放在最后
collection.SetItems(append(editedItems, uneditedItems...))
if 1 > len(collection.GetItems()) {
collection.SetItems([]Item{})
}
}
func filter0(collection Collection, attrView *AttributeView) {
filters := collection.GetFilters()
if 1 > len(filters) {
return
}
var colIndexes []int
for _, f := range filters {
for i, c := range collection.GetFields() {
if c.GetID() == f.Column {
colIndexes = append(colIndexes, i)
break
}
}
}
var items []Item
attrViewCache := map[string]*AttributeView{}
attrViewCache[attrView.ID] = attrView
for _, item := range collection.GetItems() {
pass := true
values := item.GetValues()
for j, index := range colIndexes {
operator := filters[j].Operator
if nil == values[index] {
if FilterOperatorIsNotEmpty == operator {
pass = false
} else if FilterOperatorIsEmpty == operator {
pass = true
break
}
if KeyTypeText != values[index].Type {
pass = false
}
break
}
if !values[index].Filter(filters[j], attrView, item.GetID(), &attrViewCache) {
pass = false
break
}
}
if pass {
items = append(items, item)
}
}
collection.SetItems(items)
}

View file

@ -17,8 +17,6 @@
package av
import (
"sort"
"github.com/88250/lute/ast"
)
@ -163,6 +161,14 @@ func (gallery *Gallery) SetItems(items []Item) {
}
}
func (gallery *Gallery) GetFields() (ret []Field) {
ret = []Field{}
for _, field := range gallery.Fields {
ret = append(ret, field)
}
return ret
}
func (gallery *Gallery) GetType() LayoutType {
return LayoutTypeGallery
}
@ -172,163 +178,9 @@ func (gallery *Gallery) GetID() string {
}
func (gallery *Gallery) Sort(attrView *AttributeView) {
if 1 > len(gallery.Sorts) {
return
}
type FieldIndexSort struct {
Index int
Order SortOrder
}
var fieldIndexSorts []*FieldIndexSort
for _, s := range gallery.Sorts {
for i, c := range gallery.Fields {
if c.ID == s.Column {
fieldIndexSorts = append(fieldIndexSorts, &FieldIndexSort{Index: i, Order: s.Order})
break
}
}
}
editedValCards := map[string]bool{}
for i, card := range gallery.Cards {
for _, fieldIndexSort := range fieldIndexSorts {
val := gallery.Cards[i].Values[fieldIndexSort.Index].Value
if KeyTypeCheckbox == val.Type {
if block := card.GetBlockValue(); nil != block && block.IsEdited() {
// 如果主键编辑过,则勾选框也算作编辑过,参与排序 https://github.com/siyuan-note/siyuan/issues/11016
editedValCards[card.ID] = true
break
}
}
if val.IsEdited() {
// 如果该卡片某字段的值已经编辑过,则该卡片可参与排序
editedValCards[card.ID] = true
break
}
}
}
// 将未编辑的卡片和已编辑的卡片分开排序
var uneditedCards, editedCards []*GalleryCard
for _, card := range gallery.Cards {
if _, ok := editedValCards[card.ID]; ok {
editedCards = append(editedCards, card)
} else {
uneditedCards = append(uneditedCards, card)
}
}
sort.Slice(uneditedCards, func(i, j int) bool {
val1 := uneditedCards[i].GetBlockValue()
if nil == val1 {
return true
}
val2 := uneditedCards[j].GetBlockValue()
if nil == val2 {
return false
}
return val1.CreatedAt < val2.CreatedAt
})
sort.Slice(editedCards, func(i, j int) bool {
sorted := true
for _, fieldIndexSort := range fieldIndexSorts {
val1 := editedCards[i].Values[fieldIndexSort.Index].Value
val2 := editedCards[j].Values[fieldIndexSort.Index].Value
if nil == val1 || val1.IsEmpty() {
if nil != val2 && !val2.IsEmpty() {
return false
}
sorted = false
continue
} else {
if nil == val2 || val2.IsEmpty() {
return true
}
}
result := val1.Compare(val2, attrView)
if 0 == result {
sorted = false
continue
}
sorted = true
if fieldIndexSort.Order == SortOrderAsc {
return 0 > result
}
return 0 < result
}
if !sorted {
key1 := editedCards[i].GetBlockValue()
if nil == key1 {
return false
}
key2 := editedCards[j].GetBlockValue()
if nil == key2 {
return false
}
return key1.CreatedAt < key2.CreatedAt
}
return false
})
// 将包含未编辑的卡片放在最后
gallery.Cards = append(editedCards, uneditedCards...)
if 1 > len(gallery.Cards) {
gallery.Cards = []*GalleryCard{}
}
sort0(gallery, attrView)
}
func (gallery *Gallery) Filter(attrView *AttributeView) {
if 1 > len(gallery.Filters) {
return
}
var fieldIndexes []int
for _, f := range gallery.Filters {
for i, c := range gallery.Fields {
if c.ID == f.Column {
fieldIndexes = append(fieldIndexes, i)
break
}
}
}
cards := []*GalleryCard{}
attrViewCache := map[string]*AttributeView{}
attrViewCache[attrView.ID] = attrView
for _, card := range gallery.Cards {
pass := true
for j, index := range fieldIndexes {
operator := gallery.Filters[j].Operator
if nil == card.Values[index].Value {
if FilterOperatorIsNotEmpty == operator {
pass = false
} else if FilterOperatorIsEmpty == operator {
pass = true
break
}
if KeyTypeText != card.Values[index].ValueType {
pass = false
}
break
}
if !card.Values[index].Value.Filter(gallery.Filters[j], attrView, card.ID, &attrViewCache) {
pass = false
break
}
}
if pass {
cards = append(cards, card)
}
}
gallery.Cards = cards
filter0(gallery, attrView)
}

View file

@ -17,8 +17,6 @@
package av
import (
"sort"
"github.com/88250/lute/ast"
)
@ -151,6 +149,14 @@ func (table *Table) SetItems(items []Item) {
}
}
func (table *Table) GetFields() (ret []Field) {
ret = []Field{}
for _, column := range table.Columns {
ret = append(ret, column)
}
return ret
}
func (*Table) GetType() LayoutType {
return LayoutTypeTable
}
@ -160,163 +166,9 @@ func (table *Table) GetID() string {
}
func (table *Table) Sort(attrView *AttributeView) {
if 1 > len(table.Sorts) {
return
}
type ColIndexSort struct {
Index int
Order SortOrder
}
var colIndexSorts []*ColIndexSort
for _, s := range table.Sorts {
for i, c := range table.Columns {
if c.ID == s.Column {
colIndexSorts = append(colIndexSorts, &ColIndexSort{Index: i, Order: s.Order})
break
}
}
}
editedValRows := map[string]bool{}
for i, row := range table.Rows {
for _, colIndexSort := range colIndexSorts {
val := table.Rows[i].Cells[colIndexSort.Index].Value
if KeyTypeCheckbox == val.Type {
if block := row.GetBlockValue(); nil != block && block.IsEdited() {
// 如果主键编辑过,则勾选框也算作编辑过,参与排序 https://github.com/siyuan-note/siyuan/issues/11016
editedValRows[row.ID] = true
break
}
}
if val.IsEdited() {
// 如果该行某列的值已经编辑过,则该行可参与排序
editedValRows[row.ID] = true
break
}
}
}
// 将未编辑的行和已编辑的行分开排序
var uneditedRows, editedRows []*TableRow
for _, row := range table.Rows {
if _, ok := editedValRows[row.ID]; ok {
editedRows = append(editedRows, row)
} else {
uneditedRows = append(uneditedRows, row)
}
}
sort.Slice(uneditedRows, func(i, j int) bool {
val1 := uneditedRows[i].GetBlockValue()
if nil == val1 {
return true
}
val2 := uneditedRows[j].GetBlockValue()
if nil == val2 {
return false
}
return val1.CreatedAt < val2.CreatedAt
})
sort.Slice(editedRows, func(i, j int) bool {
sorted := true
for _, colIndexSort := range colIndexSorts {
val1 := editedRows[i].Cells[colIndexSort.Index].Value
val2 := editedRows[j].Cells[colIndexSort.Index].Value
if nil == val1 || val1.IsEmpty() {
if nil != val2 && !val2.IsEmpty() {
return false
}
sorted = false
continue
} else {
if nil == val2 || val2.IsEmpty() {
return true
}
}
result := val1.Compare(val2, attrView)
if 0 == result {
sorted = false
continue
}
sorted = true
if colIndexSort.Order == SortOrderAsc {
return 0 > result
}
return 0 < result
}
if !sorted {
key1 := editedRows[i].GetBlockValue()
if nil == key1 {
return false
}
key2 := editedRows[j].GetBlockValue()
if nil == key2 {
return false
}
return key1.CreatedAt < key2.CreatedAt
}
return false
})
// 将包含未编辑的行放在最后
table.Rows = append(editedRows, uneditedRows...)
if 1 > len(table.Rows) {
table.Rows = []*TableRow{}
}
sort0(table, attrView)
}
func (table *Table) Filter(attrView *AttributeView) {
if 1 > len(table.Filters) {
return
}
var colIndexes []int
for _, f := range table.Filters {
for i, c := range table.Columns {
if c.ID == f.Column {
colIndexes = append(colIndexes, i)
break
}
}
}
rows := []*TableRow{}
attrViewCache := map[string]*AttributeView{}
attrViewCache[attrView.ID] = attrView
for _, row := range table.Rows {
pass := true
for j, index := range colIndexes {
operator := table.Filters[j].Operator
if nil == row.Cells[index].Value {
if FilterOperatorIsNotEmpty == operator {
pass = false
} else if FilterOperatorIsEmpty == operator {
pass = true
break
}
if KeyTypeText != row.Cells[index].ValueType {
pass = false
}
break
}
if !row.Cells[index].Value.Filter(table.Filters[j], attrView, row.ID, &attrViewCache) {
pass = false
break
}
}
if pass {
rows = append(rows, row)
}
}
table.Rows = rows
filter0(table, attrView)
}

View file

@ -1180,8 +1180,6 @@ func renderAttributeView(attrView *av.AttributeView, viewID, query string, page,
}
}
view.Table.Sorts = tmpSorts
viewable = sql.RenderAttributeViewTable(attrView, view, query)
case av.LayoutTypeGallery:
// 字段删除以后需要删除设置的过滤和排序
tmpFilters := []*av.ViewFilter{}
@ -1199,10 +1197,9 @@ func renderAttributeView(attrView *av.AttributeView, viewID, query string, page,
}
}
view.Gallery.Sorts = tmpSorts
viewable = sql.RenderAttributeViewGallery(attrView, view, query)
}
viewable = sql.RenderView(view, attrView, query)
if nil == viewable {
err = av.ErrViewNotFound
logging.LogErrorf("render attribute view [%s] failed", attrView.ID)
@ -2424,13 +2421,7 @@ func addAttributeViewBlock(now int64, avID, blockID, previousBlockID, addingBloc
}
if nil != view && 0 < len(filters) && !ignoreFillFilter {
var viewable av.Viewable
switch view.LayoutType {
case av.LayoutTypeTable:
viewable = sql.RenderAttributeViewTable(attrView, view, "")
case av.LayoutTypeGallery:
viewable = sql.RenderAttributeViewGallery(attrView, view, "")
}
viewable := sql.RenderView(view, attrView, "")
viewable.Filter(attrView)
viewable.Sort(attrView)

View file

@ -32,6 +32,16 @@ import (
"github.com/siyuan-note/siyuan/kernel/util"
)
func RenderView(view *av.View, attrView *av.AttributeView, query string) (ret av.Viewable) {
switch view.LayoutType {
case av.LayoutTypeTable:
ret = RenderAttributeViewTable(attrView, view, query)
case av.LayoutTypeGallery:
ret = RenderAttributeViewGallery(attrView, view, query)
}
return
}
func RenderTemplateField(ial map[string]string, keyValues []*av.KeyValues, tplContent string) (ret string, err error) {
if "" == ial["id"] {
block := getBlockValue(keyValues)