diff --git a/API.md b/API.md index 3d0fc0b12..3cb22c74b 100644 --- a/API.md +++ b/API.md @@ -28,6 +28,8 @@ * [Update a block](#Update-a-block) * [Delete a block](#Delete-a-block) * [Move a block](#Move-a-block) + * [Fold a block](#Fold-a-block) + * [Unfold a block](#Unfold-a-block) * [Get a block kramdown](#Get-a-block-kramdown) * [Get child blocks](#get-child-blocks) * [Transfer block ref](#transfer-block-ref) @@ -783,6 +785,50 @@ View API token in Settings - About, request header: `Authorization: T } ``` +### Fold a block + +* `/api/block/foldBlock` +* Parameters + + ```json + { + "id": "20231224160424-2f5680o" + } + ``` + + * `id`: Block ID to fold +* Return value + + ```json + { + "code": 0, + "msg": "", + "data": null + } + ``` + +### Unfold a block + +* `/api/block/unfoldBlock` +* Parameters + + ```json + { + "id": "20231224160424-2f5680o" + } + ``` + + * `id`: Block ID to unfold +* Return value + + ```json + { + "code": 0, + "msg": "", + "data": null + } + ``` + ### Get a block kramdown * `/api/block/getBlockKramdown` diff --git a/API_zh_CN.md b/API_zh_CN.md index a66d9d8dc..d708bec67 100644 --- a/API_zh_CN.md +++ b/API_zh_CN.md @@ -29,6 +29,8 @@ * [更新块](#更新块) * [删除块](#删除块) * [移动块](#移动块) + * [折叠块](#折叠块) + * [展开块](#展开块) * [获取块 kramdown 源码](#获取块-kramdown-源码) * [获取子块](#获取子块) * [转移块引用](#转移块引用) @@ -776,6 +778,50 @@ } ``` +### 折叠块 + +* `/api/block/foldBlock` +* 参数 + + ```json + { + "id": "20231224160424-2f5680o" + } + ``` + + * `id`:待折叠块的 ID +* 返回值 + + ```json + { + "code": 0, + "msg": "", + "data": null + } + ``` + +### 展开块 + +* `/api/block/unfoldBlock` +* 参数 + + ```json + { + "id": "20231224160424-2f5680o" + } + ``` + + * `id`:待展开块的 ID +* 返回值 + + ```json + { + "code": 0, + "msg": "", + "data": null + } + ``` + ### 获取块 kramdown 源码 * `/api/block/getBlockKramdown` diff --git a/kernel/api/block_op.go b/kernel/api/block_op.go index 469cfbf36..446dd0f01 100644 --- a/kernel/api/block_op.go +++ b/kernel/api/block_op.go @@ -29,6 +29,126 @@ import ( "github.com/siyuan-note/siyuan/kernel/util" ) +func unfoldBlock(c *gin.Context) { + ret := gulu.Ret.NewResult() + defer c.JSON(http.StatusOK, ret) + + arg, ok := util.JsonArg(c, ret) + if !ok { + return + } + + id := arg["id"].(string) + if util.InvalidIDPattern(id, ret) { + return + } + + bt := treenode.GetBlockTree(id) + if nil == bt { + ret.Code = -1 + ret.Msg = "block tree not found [id=" + id + "]" + return + } + + if bt.Type == "d" { + ret.Code = -1 + ret.Msg = "document can not be unfolded" + return + } + + var transactions []*model.Transaction + if "h" == bt.Type { + transactions = []*model.Transaction{ + { + DoOperations: []*model.Operation{ + { + Action: "unfoldHeading", + ID: id, + }, + }, + }, + } + } else { + data, _ := gulu.JSON.MarshalJSON(map[string]interface{}{"unfold": "1"}) + transactions = []*model.Transaction{ + { + DoOperations: []*model.Operation{ + { + Action: "setAttrs", + ID: id, + Data: string(data), + }, + }, + }, + } + } + + model.PerformTransactions(&transactions) + model.WaitForWritingFiles() + + broadcastTransactions(transactions) +} + +func foldBlock(c *gin.Context) { + ret := gulu.Ret.NewResult() + defer c.JSON(http.StatusOK, ret) + + arg, ok := util.JsonArg(c, ret) + if !ok { + return + } + + id := arg["id"].(string) + if util.InvalidIDPattern(id, ret) { + return + } + + bt := treenode.GetBlockTree(id) + if nil == bt { + ret.Code = -1 + ret.Msg = "block tree not found [id=" + id + "]" + return + } + + if bt.Type == "d" { + ret.Code = -1 + ret.Msg = "document can not be folded" + return + } + + var transactions []*model.Transaction + if "h" == bt.Type { + transactions = []*model.Transaction{ + { + DoOperations: []*model.Operation{ + { + Action: "foldHeading", + ID: id, + }, + }, + }, + } + } else { + data, _ := gulu.JSON.MarshalJSON(map[string]interface{}{"fold": "1"}) + transactions = []*model.Transaction{ + { + DoOperations: []*model.Operation{ + { + Action: "setAttrs", + ID: id, + Data: string(data), + }, + }, + }, + } + } + + model.PerformTransactions(&transactions) + model.WaitForWritingFiles() + + broadcastTransactions(transactions) +} + func moveBlock(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret) diff --git a/kernel/api/router.go b/kernel/api/router.go index e3b5ff7df..fa2c50019 100644 --- a/kernel/api/router.go +++ b/kernel/api/router.go @@ -181,6 +181,8 @@ func ServeAPI(ginServer *gin.Engine) { ginServer.Handle("POST", "/api/block/updateBlock", model.CheckAuth, model.CheckReadonly, updateBlock) ginServer.Handle("POST", "/api/block/deleteBlock", model.CheckAuth, model.CheckReadonly, deleteBlock) ginServer.Handle("POST", "/api/block/moveBlock", model.CheckAuth, model.CheckReadonly, moveBlock) + ginServer.Handle("POST", "/api/block/foldBlock", model.CheckAuth, model.CheckReadonly, foldBlock) + ginServer.Handle("POST", "/api/block/unfoldBlock", model.CheckAuth, model.CheckReadonly, unfoldBlock) ginServer.Handle("POST", "/api/block/setBlockReminder", model.CheckAuth, model.CheckReadonly, setBlockReminder) ginServer.Handle("POST", "/api/block/getHeadingLevelTransaction", model.CheckAuth, getHeadingLevelTransaction) ginServer.Handle("POST", "/api/block/getHeadingDeleteTransaction", model.CheckAuth, getHeadingDeleteTransaction) diff --git a/kernel/model/attribute_view.go b/kernel/model/attribute_view.go index 795551b61..710eecaea 100644 --- a/kernel/model/attribute_view.go +++ b/kernel/model/attribute_view.go @@ -862,10 +862,15 @@ func updateAttributeViewColRelation(operation *Operation) (err error) { if !destAdded { if operation.IsTwoWay { + name := strings.TrimSpace(operation.Name) + if "" == name { + name = srcAv.Name + } + destAv.KeyValues = append(destAv.KeyValues, &av.KeyValues{ Key: &av.Key{ ID: operation.BackRelationKeyID, - Name: operation.Name, + Name: name, Type: av.KeyTypeRelation, Relation: &av.Relation{AvID: operation.AvID, IsTwoWay: operation.IsTwoWay, BackKeyID: operation.KeyID}, }, @@ -1893,13 +1898,43 @@ func removeAttributeViewColumn(operation *Operation) (err error) { return } + var removedKey *av.Key for i, keyValues := range attrView.KeyValues { if keyValues.Key.ID == operation.ID { attrView.KeyValues = append(attrView.KeyValues[:i], attrView.KeyValues[i+1:]...) + removedKey = keyValues.Key break } } + // 删除双向关联的目标列 + if nil != removedKey && nil != removedKey.Relation && removedKey.Relation.IsTwoWay { + destAv, _ := av.ParseAttributeView(removedKey.Relation.AvID) + if nil != destAv { + for i, keyValues := range destAv.KeyValues { + if keyValues.Key.ID == removedKey.Relation.BackKeyID { + destAv.KeyValues = append(destAv.KeyValues[:i], destAv.KeyValues[i+1:]...) + break + } + } + + for _, view := range destAv.Views { + switch view.LayoutType { + case av.LayoutTypeTable: + for i, column := range view.Table.Columns { + if column.ID == removedKey.Relation.BackKeyID { + view.Table.Columns = append(view.Table.Columns[:i], view.Table.Columns[i+1:]...) + break + } + } + } + } + + av.SaveAttributeView(destAv) + util.BroadcastByType("protyle", "refreshAttributeView", 0, "", map[string]interface{}{"id": destAv.ID}) + } + } + for _, view := range attrView.Views { switch view.LayoutType { case av.LayoutTypeTable: diff --git a/kernel/model/transaction.go b/kernel/model/transaction.go index 35153b835..aaa101fd1 100644 --- a/kernel/model/transaction.go +++ b/kernel/model/transaction.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "path/filepath" + "runtime/debug" "strings" "sync" "sync/atomic" @@ -168,6 +169,19 @@ func performTx(tx *Transaction) (ret *TxErr) { return } + defer func() { + if e := recover(); nil != e { + stack := debug.Stack() + msg := fmt.Sprintf("PANIC RECOVERED: %v\n\t%s\n", e, stack) + logging.LogErrorf(msg) + + if 0 == tx.state.Load() { + tx.rollback() + return + } + } + }() + for _, op := range tx.DoOperations { switch op.Action { case "create":