mirror of
https://github.com/mattermost/focalboard.git
synced 2025-02-07 19:30:18 +02:00
Server generated ids (#1667)
* Adds server ID generation on the insert blocks endpoint * Fix linter * Fix server linter * Fix integration tests * Update endpoint docs * Update code to use the BlockType2IDType function when generating new IDs * Handle new block ids on boards, templates and views creation Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
parent
581ae7b97a
commit
fa36e092bb
@ -328,7 +328,9 @@ func stampModificationMetadata(r *http.Request, blocks []model.Block, auditRec *
|
|||||||
func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||||
// swagger:operation POST /api/v1/workspaces/{workspaceID}/blocks updateBlocks
|
// swagger:operation POST /api/v1/workspaces/{workspaceID}/blocks updateBlocks
|
||||||
//
|
//
|
||||||
// Insert or update blocks
|
// Insert blocks. The specified IDs will only be used to link
|
||||||
|
// blocks with existing ones, the rest will be replaced by server
|
||||||
|
// generated IDs
|
||||||
//
|
//
|
||||||
// ---
|
// ---
|
||||||
// produces:
|
// produces:
|
||||||
@ -352,6 +354,10 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
// responses:
|
// responses:
|
||||||
// '200':
|
// '200':
|
||||||
// description: success
|
// description: success
|
||||||
|
// schema:
|
||||||
|
// items:
|
||||||
|
// $ref: '#/definitions/Block'
|
||||||
|
// type: array
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -398,6 +404,8 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blocks = model.GenerateBlockIDs(blocks)
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
|
||||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||||
|
|
||||||
@ -406,14 +414,21 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||||
|
|
||||||
err = a.app.InsertBlocks(*container, blocks, session.UserID, true)
|
newBlocks, err := a.app.InsertBlocks(*container, blocks, session.UserID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a.logger.Debug("POST Blocks", mlog.Int("block_count", len(blocks)))
|
a.logger.Debug("POST Blocks", mlog.Int("block_count", len(blocks)))
|
||||||
jsonStringResponse(w, http.StatusOK, "{}")
|
|
||||||
|
json, err := json.Marshal(newBlocks)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytesResponse(w, http.StatusOK, json)
|
||||||
|
|
||||||
auditRec.AddMeta("blockCount", len(blocks))
|
auditRec.AddMeta("blockCount", len(blocks))
|
||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
@ -912,7 +927,7 @@ func (a *API) handleImport(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||||
err = a.app.InsertBlocks(*container, blocks, session.UserID, false)
|
_, err = a.app.InsertBlocks(*container, model.GenerateBlockIDs(blocks), session.UserID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
return
|
return
|
||||||
|
@ -73,12 +73,12 @@ func (a *App) InsertBlock(c store.Container, block model.Block, userID string) e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) InsertBlocks(c store.Container, blocks []model.Block, userID string, allowNotifications bool) error {
|
func (a *App) InsertBlocks(c store.Container, blocks []model.Block, userID string, allowNotifications bool) ([]model.Block, error) {
|
||||||
needsNotify := make([]model.Block, 0, len(blocks))
|
needsNotify := make([]model.Block, 0, len(blocks))
|
||||||
for i := range blocks {
|
for i := range blocks {
|
||||||
err := a.store.InsertBlock(c, &blocks[i], userID)
|
err := a.store.InsertBlock(c, &blocks[i], userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
blocks[i].WorkspaceID = c.WorkspaceID
|
blocks[i].WorkspaceID = c.WorkspaceID
|
||||||
needsNotify = append(needsNotify, blocks[i])
|
needsNotify = append(needsNotify, blocks[i])
|
||||||
@ -97,7 +97,7 @@ func (a *App) InsertBlocks(c store.Container, blocks []model.Block, userID strin
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return blocks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetSubTree(c store.Container, blockID string, levels int) ([]model.Block, error) {
|
func (a *App) GetSubTree(c store.Container, blockID string, levels int) ([]model.Block, error) {
|
||||||
|
@ -184,14 +184,14 @@ func (c *Client) PatchBlock(blockID string, blockPatch *model.BlockPatch) (bool,
|
|||||||
return true, BuildResponse(r)
|
return true, BuildResponse(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) InsertBlocks(blocks []model.Block) (bool, *Response) {
|
func (c *Client) InsertBlocks(blocks []model.Block) ([]model.Block, *Response) {
|
||||||
r, err := c.DoAPIPost(c.GetBlocksRoute(), toJSON(blocks))
|
r, err := c.DoAPIPost(c.GetBlocksRoute(), toJSON(blocks))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, BuildErrorResponse(r, err)
|
return nil, BuildErrorResponse(r, err)
|
||||||
}
|
}
|
||||||
defer closeBody(r)
|
defer closeBody(r)
|
||||||
|
|
||||||
return true, BuildResponse(r)
|
return model.BlocksFromJSON(r.Body), BuildResponse(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DeleteBlock(blockID string) (bool, *Response) {
|
func (c *Client) DeleteBlock(blockID string) (bool, *Response) {
|
||||||
|
@ -2,6 +2,7 @@ package integrationtests
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mattermost/focalboard/server/model"
|
"github.com/mattermost/focalboard/server/model"
|
||||||
"github.com/mattermost/focalboard/server/utils"
|
"github.com/mattermost/focalboard/server/utils"
|
||||||
@ -17,26 +18,29 @@ func TestGetBlocks(t *testing.T) {
|
|||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
initialCount := len(blocks)
|
initialCount := len(blocks)
|
||||||
|
|
||||||
blockID1 := utils.NewID(utils.IDTypeBlock)
|
initialID1 := utils.NewID(utils.IDTypeBlock)
|
||||||
blockID2 := utils.NewID(utils.IDTypeBlock)
|
initialID2 := utils.NewID(utils.IDTypeBlock)
|
||||||
newBlocks := []model.Block{
|
newBlocks := []model.Block{
|
||||||
{
|
{
|
||||||
ID: blockID1,
|
ID: initialID1,
|
||||||
RootID: blockID1,
|
RootID: initialID1,
|
||||||
CreateAt: 1,
|
CreateAt: 1,
|
||||||
UpdateAt: 1,
|
UpdateAt: 1,
|
||||||
Type: model.TypeBoard,
|
Type: model.TypeBoard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: blockID2,
|
ID: initialID2,
|
||||||
RootID: blockID2,
|
RootID: initialID2,
|
||||||
CreateAt: 1,
|
CreateAt: 1,
|
||||||
UpdateAt: 1,
|
UpdateAt: 1,
|
||||||
Type: model.TypeBoard,
|
Type: model.TypeBoard,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, resp = th.Client.InsertBlocks(newBlocks)
|
newBlocks, resp = th.Client.InsertBlocks(newBlocks)
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
|
require.Len(t, newBlocks, 2)
|
||||||
|
blockID1 := newBlocks[0].ID
|
||||||
|
blockID2 := newBlocks[1].ID
|
||||||
|
|
||||||
blocks, resp = th.Client.GetBlocks()
|
blocks, resp = th.Client.GetBlocks()
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
@ -58,22 +62,25 @@ func TestPostBlock(t *testing.T) {
|
|||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
initialCount := len(blocks)
|
initialCount := len(blocks)
|
||||||
|
|
||||||
blockID1 := utils.NewID(utils.IDTypeBlock)
|
var blockID1 string
|
||||||
blockID2 := utils.NewID(utils.IDTypeBlock)
|
var blockID2 string
|
||||||
blockID3 := utils.NewID(utils.IDTypeBlock)
|
var blockID3 string
|
||||||
|
|
||||||
t.Run("Create a single block", func(t *testing.T) {
|
t.Run("Create a single block", func(t *testing.T) {
|
||||||
|
initialID1 := utils.NewID(utils.IDTypeBlock)
|
||||||
block := model.Block{
|
block := model.Block{
|
||||||
ID: blockID1,
|
ID: initialID1,
|
||||||
RootID: blockID1,
|
RootID: initialID1,
|
||||||
CreateAt: 1,
|
CreateAt: 1,
|
||||||
UpdateAt: 1,
|
UpdateAt: 1,
|
||||||
Type: model.TypeBoard,
|
Type: model.TypeBoard,
|
||||||
Title: "New title",
|
Title: "New title",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, resp := th.Client.InsertBlocks([]model.Block{block})
|
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
|
require.Len(t, newBlocks, 1)
|
||||||
|
blockID1 = newBlocks[0].ID
|
||||||
|
|
||||||
blocks, resp := th.Client.GetBlocks()
|
blocks, resp := th.Client.GetBlocks()
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
@ -87,25 +94,32 @@ func TestPostBlock(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Create a couple of blocks in the same call", func(t *testing.T) {
|
t.Run("Create a couple of blocks in the same call", func(t *testing.T) {
|
||||||
|
initialID2 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
initialID3 := utils.NewID(utils.IDTypeBlock)
|
||||||
newBlocks := []model.Block{
|
newBlocks := []model.Block{
|
||||||
{
|
{
|
||||||
ID: blockID2,
|
ID: initialID2,
|
||||||
RootID: blockID2,
|
RootID: initialID2,
|
||||||
CreateAt: 1,
|
CreateAt: 1,
|
||||||
UpdateAt: 1,
|
UpdateAt: 1,
|
||||||
Type: model.TypeBoard,
|
Type: model.TypeBoard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: blockID3,
|
ID: initialID3,
|
||||||
RootID: blockID3,
|
RootID: initialID3,
|
||||||
CreateAt: 1,
|
CreateAt: 1,
|
||||||
UpdateAt: 1,
|
UpdateAt: 1,
|
||||||
Type: model.TypeBoard,
|
Type: model.TypeBoard,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, resp := th.Client.InsertBlocks(newBlocks)
|
newBlocks, resp := th.Client.InsertBlocks(newBlocks)
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
|
require.Len(t, newBlocks, 2)
|
||||||
|
blockID2 = newBlocks[0].ID
|
||||||
|
blockID3 = newBlocks[1].ID
|
||||||
|
require.NotEqual(t, initialID2, blockID2)
|
||||||
|
require.NotEqual(t, initialID3, blockID3)
|
||||||
|
|
||||||
blocks, resp := th.Client.GetBlocks()
|
blocks, resp := th.Client.GetBlocks()
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
@ -120,7 +134,7 @@ func TestPostBlock(t *testing.T) {
|
|||||||
require.Contains(t, blockIDs, blockID3)
|
require.Contains(t, blockIDs, blockID3)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Update a block", func(t *testing.T) {
|
t.Run("Update a block should not be possible through the insert endpoint", func(t *testing.T) {
|
||||||
block := model.Block{
|
block := model.Block{
|
||||||
ID: blockID1,
|
ID: blockID1,
|
||||||
RootID: blockID1,
|
RootID: blockID1,
|
||||||
@ -130,21 +144,24 @@ func TestPostBlock(t *testing.T) {
|
|||||||
Title: "Updated title",
|
Title: "Updated title",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, resp := th.Client.InsertBlocks([]model.Block{block})
|
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
|
require.Len(t, newBlocks, 1)
|
||||||
|
blockID4 := newBlocks[0].ID
|
||||||
|
require.NotEqual(t, blockID1, blockID4)
|
||||||
|
|
||||||
blocks, resp := th.Client.GetBlocks()
|
blocks, resp := th.Client.GetBlocks()
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
require.Len(t, blocks, initialCount+3)
|
require.Len(t, blocks, initialCount+4)
|
||||||
|
|
||||||
var updatedBlock model.Block
|
var block4 model.Block
|
||||||
for _, b := range blocks {
|
for _, b := range blocks {
|
||||||
if b.ID == blockID1 {
|
if b.ID == blockID4 {
|
||||||
updatedBlock = b
|
block4 = b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
require.NotNil(t, updatedBlock)
|
require.NotNil(t, block4)
|
||||||
require.Equal(t, "Updated title", updatedBlock.Title)
|
require.Equal(t, "Updated title", block4.Title)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,11 +169,11 @@ func TestPatchBlock(t *testing.T) {
|
|||||||
th := SetupTestHelper().InitBasic()
|
th := SetupTestHelper().InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
blockID := utils.NewID(utils.IDTypeBlock)
|
initialID := utils.NewID(utils.IDTypeBlock)
|
||||||
|
|
||||||
block := model.Block{
|
block := model.Block{
|
||||||
ID: blockID,
|
ID: initialID,
|
||||||
RootID: blockID,
|
RootID: initialID,
|
||||||
CreateAt: 1,
|
CreateAt: 1,
|
||||||
UpdateAt: 1,
|
UpdateAt: 1,
|
||||||
Type: model.TypeBoard,
|
Type: model.TypeBoard,
|
||||||
@ -164,8 +181,10 @@ func TestPatchBlock(t *testing.T) {
|
|||||||
Fields: map[string]interface{}{"test": "test value", "test2": "test value 2"},
|
Fields: map[string]interface{}{"test": "test value", "test2": "test value 2"},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, resp := th.Client.InsertBlocks([]model.Block{block})
|
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
|
require.Len(t, newBlocks, 1)
|
||||||
|
blockID := newBlocks[0].ID
|
||||||
|
|
||||||
blocks, resp := th.Client.GetBlocks()
|
blocks, resp := th.Client.GetBlocks()
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
@ -253,19 +272,24 @@ func TestDeleteBlock(t *testing.T) {
|
|||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
initialCount := len(blocks)
|
initialCount := len(blocks)
|
||||||
|
|
||||||
blockID := utils.NewID(utils.IDTypeBlock)
|
var blockID string
|
||||||
t.Run("Create a block", func(t *testing.T) {
|
t.Run("Create a block", func(t *testing.T) {
|
||||||
|
initialID := utils.NewID(utils.IDTypeBlock)
|
||||||
block := model.Block{
|
block := model.Block{
|
||||||
ID: blockID,
|
ID: initialID,
|
||||||
RootID: blockID,
|
RootID: initialID,
|
||||||
CreateAt: 1,
|
CreateAt: 1,
|
||||||
UpdateAt: 1,
|
UpdateAt: 1,
|
||||||
Type: model.TypeBoard,
|
Type: model.TypeBoard,
|
||||||
Title: "New title",
|
Title: "New title",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, resp := th.Client.InsertBlocks([]model.Block{block})
|
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
|
require.Len(t, newBlocks, 1)
|
||||||
|
require.NotZero(t, newBlocks[0].ID)
|
||||||
|
require.NotEqual(t, initialID, newBlocks[0].ID)
|
||||||
|
blockID = newBlocks[0].ID
|
||||||
|
|
||||||
blocks, resp := th.Client.GetBlocks()
|
blocks, resp := th.Client.GetBlocks()
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
@ -279,6 +303,10 @@ func TestDeleteBlock(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Delete a block", func(t *testing.T) {
|
t.Run("Delete a block", func(t *testing.T) {
|
||||||
|
// this avoids triggering uniqueness constraint of
|
||||||
|
// id,insert_at on block history
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
_, resp := th.Client.DeleteBlock(blockID)
|
_, resp := th.Client.DeleteBlock(blockID)
|
||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ package model
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/mattermost/focalboard/server/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Block is the basic data unit
|
// Block is the basic data unit
|
||||||
@ -153,3 +155,59 @@ func (p *BlockPatch) Patch(block *Block) *Block {
|
|||||||
|
|
||||||
return block
|
return block
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateBlockIDs generates new IDs for all the blocks of the list,
|
||||||
|
// keeping consistent any references that other blocks would made to
|
||||||
|
// the original IDs, so a tree of blocks can get new IDs and maintain
|
||||||
|
// its shape.
|
||||||
|
func GenerateBlockIDs(blocks []Block) []Block {
|
||||||
|
blockIDs := map[string]BlockType{}
|
||||||
|
referenceIDs := map[string]bool{}
|
||||||
|
for _, block := range blocks {
|
||||||
|
if _, ok := blockIDs[block.ID]; !ok {
|
||||||
|
blockIDs[block.ID] = block.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := referenceIDs[block.RootID]; !ok {
|
||||||
|
referenceIDs[block.RootID] = true
|
||||||
|
}
|
||||||
|
if _, ok := referenceIDs[block.ParentID]; !ok {
|
||||||
|
referenceIDs[block.ParentID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newIDs := map[string]string{}
|
||||||
|
for id, blockType := range blockIDs {
|
||||||
|
for referenceID := range referenceIDs {
|
||||||
|
if id == referenceID {
|
||||||
|
newIDs[id] = utils.NewID(BlockType2IDType(blockType))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getExistingOrOldID := func(id string) string {
|
||||||
|
if existingID, ok := newIDs[id]; ok {
|
||||||
|
return existingID
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
getExistingOrNewID := func(id string) string {
|
||||||
|
if existingID, ok := newIDs[id]; ok {
|
||||||
|
return existingID
|
||||||
|
}
|
||||||
|
return utils.NewID(BlockType2IDType(blockIDs[id]))
|
||||||
|
}
|
||||||
|
|
||||||
|
newBlocks := make([]Block, len(blocks))
|
||||||
|
for i, block := range blocks {
|
||||||
|
block.ID = getExistingOrNewID(block.ID)
|
||||||
|
block.RootID = getExistingOrOldID(block.RootID)
|
||||||
|
block.ParentID = getExistingOrOldID(block.ParentID)
|
||||||
|
|
||||||
|
newBlocks[i] = block
|
||||||
|
}
|
||||||
|
|
||||||
|
return newBlocks
|
||||||
|
}
|
||||||
|
141
server/model/block_test.go
Normal file
141
server/model/block_test.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mattermost/focalboard/server/utils"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateBlockIDs(t *testing.T) {
|
||||||
|
t.Run("Should generate a new ID for a single block with no references", func(t *testing.T) {
|
||||||
|
blockID := utils.NewID(utils.IDTypeBlock)
|
||||||
|
blocks := []Block{{ID: blockID}}
|
||||||
|
|
||||||
|
blocks = GenerateBlockIDs(blocks)
|
||||||
|
|
||||||
|
require.NotEqual(t, blockID, blocks[0].ID)
|
||||||
|
require.Zero(t, blocks[0].RootID)
|
||||||
|
require.Zero(t, blocks[0].ParentID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should generate a new ID for a single block with references", func(t *testing.T) {
|
||||||
|
blockID := utils.NewID(utils.IDTypeBlock)
|
||||||
|
rootID := utils.NewID(utils.IDTypeBlock)
|
||||||
|
parentID := utils.NewID(utils.IDTypeBlock)
|
||||||
|
blocks := []Block{{ID: blockID, RootID: rootID, ParentID: parentID}}
|
||||||
|
|
||||||
|
blocks = GenerateBlockIDs(blocks)
|
||||||
|
|
||||||
|
require.NotEqual(t, blockID, blocks[0].ID)
|
||||||
|
require.Equal(t, rootID, blocks[0].RootID)
|
||||||
|
require.Equal(t, parentID, blocks[0].ParentID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should generate IDs and link multiple blocks with existing references", func(t *testing.T) {
|
||||||
|
blockID1 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
rootID1 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
parentID1 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1}
|
||||||
|
|
||||||
|
blockID2 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
rootID2 := blockID1
|
||||||
|
parentID2 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2}
|
||||||
|
|
||||||
|
blocks := []Block{block1, block2}
|
||||||
|
|
||||||
|
blocks = GenerateBlockIDs(blocks)
|
||||||
|
|
||||||
|
require.NotEqual(t, blockID1, blocks[0].ID)
|
||||||
|
require.Equal(t, rootID1, blocks[0].RootID)
|
||||||
|
require.Equal(t, parentID1, blocks[0].ParentID)
|
||||||
|
|
||||||
|
require.NotEqual(t, blockID2, blocks[1].ID)
|
||||||
|
require.NotEqual(t, rootID2, blocks[1].RootID)
|
||||||
|
require.Equal(t, parentID2, blocks[1].ParentID)
|
||||||
|
|
||||||
|
// blockID1 was referenced, so it should still be after the ID
|
||||||
|
// changes
|
||||||
|
require.Equal(t, blocks[0].ID, blocks[1].RootID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should generate new IDs but not modify nonexisting references", func(t *testing.T) {
|
||||||
|
blockID1 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
rootID1 := ""
|
||||||
|
parentID1 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1}
|
||||||
|
|
||||||
|
blockID2 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
rootID2 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
parentID2 := ""
|
||||||
|
block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2}
|
||||||
|
|
||||||
|
blocks := []Block{block1, block2}
|
||||||
|
|
||||||
|
blocks = GenerateBlockIDs(blocks)
|
||||||
|
|
||||||
|
// only the IDs should have changed
|
||||||
|
require.NotEqual(t, blockID1, blocks[0].ID)
|
||||||
|
require.Zero(t, blocks[0].RootID)
|
||||||
|
require.Equal(t, parentID1, blocks[0].ParentID)
|
||||||
|
|
||||||
|
require.NotEqual(t, blockID2, blocks[1].ID)
|
||||||
|
require.Equal(t, rootID2, blocks[1].RootID)
|
||||||
|
require.Zero(t, blocks[1].ParentID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should modify correctly multiple blocks with existing and nonexisting references", func(t *testing.T) {
|
||||||
|
blockID1 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
rootID1 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
parentID1 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1}
|
||||||
|
|
||||||
|
// linked to 1
|
||||||
|
blockID2 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
rootID2 := blockID1
|
||||||
|
parentID2 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2}
|
||||||
|
|
||||||
|
// linked to 2
|
||||||
|
blockID3 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
rootID3 := blockID2
|
||||||
|
parentID3 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
block3 := Block{ID: blockID3, RootID: rootID3, ParentID: parentID3}
|
||||||
|
|
||||||
|
// linked to 1
|
||||||
|
blockID4 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
rootID4 := blockID1
|
||||||
|
parentID4 := utils.NewID(utils.IDTypeBlock)
|
||||||
|
block4 := Block{ID: blockID4, RootID: rootID4, ParentID: parentID4}
|
||||||
|
|
||||||
|
// blocks are shuffled
|
||||||
|
blocks := []Block{block4, block2, block1, block3}
|
||||||
|
|
||||||
|
blocks = GenerateBlockIDs(blocks)
|
||||||
|
|
||||||
|
// block 1
|
||||||
|
require.NotEqual(t, blockID1, blocks[2].ID)
|
||||||
|
require.Equal(t, rootID1, blocks[2].RootID)
|
||||||
|
require.Equal(t, parentID1, blocks[2].ParentID)
|
||||||
|
|
||||||
|
// block 2
|
||||||
|
require.NotEqual(t, blockID2, blocks[1].ID)
|
||||||
|
require.NotEqual(t, rootID2, blocks[1].RootID)
|
||||||
|
require.Equal(t, blocks[2].ID, blocks[1].RootID) // link to 1
|
||||||
|
require.Equal(t, parentID2, blocks[1].ParentID)
|
||||||
|
|
||||||
|
// block 3
|
||||||
|
require.NotEqual(t, blockID3, blocks[3].ID)
|
||||||
|
require.NotEqual(t, rootID3, blocks[3].RootID)
|
||||||
|
require.Equal(t, blocks[1].ID, blocks[3].RootID) // link to 2
|
||||||
|
require.Equal(t, parentID3, blocks[3].ParentID)
|
||||||
|
|
||||||
|
// block 4
|
||||||
|
require.NotEqual(t, blockID4, blocks[0].ID)
|
||||||
|
require.NotEqual(t, rootID4, blocks[0].RootID)
|
||||||
|
require.Equal(t, blocks[2].ID, blocks[0].RootID) // link to 1
|
||||||
|
require.Equal(t, parentID4, blocks[0].ParentID)
|
||||||
|
})
|
||||||
|
}
|
@ -59,5 +59,52 @@ function createBlock(block?: Block): Block {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createPatchesFromBlock creates two BlockPatch instances, one that
|
||||||
|
// contains the delta to update the block and another one for the undo
|
||||||
|
// action, in case it happens
|
||||||
|
function createPatchesFromBlocks(newBlock: Block, oldBlock: Block): BlockPatch[] {
|
||||||
|
const oldDeletedFields = [] as string[]
|
||||||
|
const newUpdatedFields = Object.keys(newBlock.fields).reduce((acc, val): Record<string, any> => {
|
||||||
|
// the field is in both old and new, so it is part of the new
|
||||||
|
// patch
|
||||||
|
if (val in oldBlock.fields) {
|
||||||
|
acc[val] = newBlock.fields[val]
|
||||||
|
} else {
|
||||||
|
// the field is only in the new block, so we set it to be
|
||||||
|
// removed in the undo patch
|
||||||
|
oldDeletedFields.push(val)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, any>)
|
||||||
|
|
||||||
|
const newDeletedFields = [] as string[]
|
||||||
|
const oldUpdatedFields = Object.keys(oldBlock.fields).reduce((acc, val): Record<string, any> => {
|
||||||
|
// the field is in both, so in this case we set the old one to
|
||||||
|
// be applied for the undo patch
|
||||||
|
if (val in newBlock.fields) {
|
||||||
|
acc[val] = oldBlock.fields[val]
|
||||||
|
} else {
|
||||||
|
// the field is only on the old block, which means the
|
||||||
|
// update patch should remove it
|
||||||
|
newDeletedFields.push(val)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, any>)
|
||||||
|
|
||||||
|
// ToDo: add tests
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...newBlock as BlockPatch,
|
||||||
|
updatedFields: newUpdatedFields,
|
||||||
|
deletedFields: oldDeletedFields,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...oldBlock as BlockPatch,
|
||||||
|
updatedFields: oldUpdatedFields,
|
||||||
|
deletedFields: newDeletedFields,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
export type {ContentBlockTypes, BlockTypes}
|
export type {ContentBlockTypes, BlockTypes}
|
||||||
export {blockTypes, contentBlockTypes, Block, BlockPatch, createBlock}
|
export {blockTypes, contentBlockTypes, Block, BlockPatch, createBlock, createPatchesFromBlocks}
|
||||||
|
@ -6,6 +6,7 @@ import {injectIntl, IntlShape} from 'react-intl'
|
|||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
import Hotkeys from 'react-hot-keys'
|
import Hotkeys from 'react-hot-keys'
|
||||||
|
|
||||||
|
import {Block} from '../blocks/block'
|
||||||
import {BlockIcons} from '../blockIcons'
|
import {BlockIcons} from '../blockIcons'
|
||||||
import {Card, createCard} from '../blocks/card'
|
import {Card, createCard} from '../blocks/card'
|
||||||
import {Board, IPropertyTemplate, IPropertyOption, BoardGroup} from '../blocks/board'
|
import {Board, IPropertyTemplate, IPropertyOption, BoardGroup} from '../blocks/board'
|
||||||
@ -252,14 +253,14 @@ class CenterPanel extends React.Component<Props, State> {
|
|||||||
card.fields.icon = BlockIcons.shared.randomIcon()
|
card.fields.icon = BlockIcons.shared.randomIcon()
|
||||||
}
|
}
|
||||||
mutator.performAsUndoGroup(async () => {
|
mutator.performAsUndoGroup(async () => {
|
||||||
await mutator.insertBlock(
|
const newCard = await mutator.insertBlock(
|
||||||
card,
|
card,
|
||||||
'add card',
|
'add card',
|
||||||
async () => {
|
async (block: Block) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
this.props.addCard(card)
|
this.props.addCard(createCard(block))
|
||||||
this.props.updateView({...activeView, fields: {...activeView.fields, cardOrder: [...activeView.fields.cardOrder, card.id]}})
|
this.props.updateView({...activeView, fields: {...activeView.fields, cardOrder: [...activeView.fields.cardOrder, block.id]}})
|
||||||
this.showCard(card.id)
|
this.showCard(block.id)
|
||||||
} else {
|
} else {
|
||||||
// Focus on this card's title inline on next render
|
// Focus on this card's title inline on next render
|
||||||
this.setState({cardIdToFocusOnRender: card.id})
|
this.setState({cardIdToFocusOnRender: card.id})
|
||||||
@ -270,7 +271,7 @@ class CenterPanel extends React.Component<Props, State> {
|
|||||||
this.showCard(undefined)
|
this.showCard(undefined)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await mutator.changeViewCardOrder(activeView, [...activeView.fields.cardOrder, card.id], 'add-card')
|
await mutator.changeViewCardOrder(activeView, [...activeView.fields.cardOrder, newCard.id], 'add-card')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,10 +286,11 @@ class CenterPanel extends React.Component<Props, State> {
|
|||||||
await mutator.insertBlock(
|
await mutator.insertBlock(
|
||||||
cardTemplate,
|
cardTemplate,
|
||||||
'add card template',
|
'add card template',
|
||||||
async () => {
|
async (newBlock: Block) => {
|
||||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateCardTemplate, {board: board.id, view: activeView.id, card: cardTemplate.id})
|
const newTemplate = createCard(newBlock)
|
||||||
this.props.addTemplate(cardTemplate)
|
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateCardTemplate, {board: board.id, view: activeView.id, card: newTemplate.id})
|
||||||
this.showCard(cardTemplate.id)
|
this.props.addTemplate(newTemplate)
|
||||||
|
this.showCard(newTemplate.id)
|
||||||
}, async () => {
|
}, async () => {
|
||||||
this.showCard(undefined)
|
this.showCard(undefined)
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,7 @@ import React, {useCallback, useEffect} from 'react'
|
|||||||
import {FormattedMessage, IntlShape, useIntl} from 'react-intl'
|
import {FormattedMessage, IntlShape, useIntl} from 'react-intl'
|
||||||
import {generatePath, useHistory, useRouteMatch} from 'react-router-dom'
|
import {generatePath, useHistory, useRouteMatch} from 'react-router-dom'
|
||||||
|
|
||||||
|
import {Block} from '../../blocks/block'
|
||||||
import {Board, createBoard} from '../../blocks/board'
|
import {Board, createBoard} from '../../blocks/board'
|
||||||
import {createBoardView} from '../../blocks/boardView'
|
import {createBoardView} from '../../blocks/boardView'
|
||||||
import mutator from '../../mutator'
|
import mutator from '../../mutator'
|
||||||
@ -39,9 +40,10 @@ export const addBoardClicked = async (showBoard: (id: string) => void, intl: Int
|
|||||||
await mutator.insertBlocks(
|
await mutator.insertBlocks(
|
||||||
[board, view],
|
[board, view],
|
||||||
'add board',
|
'add board',
|
||||||
async () => {
|
async (newBlocks: Block[]) => {
|
||||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoard, {board: board.id})
|
const newBoardId = newBlocks[0].id
|
||||||
showBoard(board.id)
|
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoard, {board: newBoardId})
|
||||||
|
showBoard(newBoardId)
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
if (oldBoardId) {
|
if (oldBoardId) {
|
||||||
@ -66,9 +68,10 @@ export const addBoardTemplateClicked = async (showBoard: (id: string) => void, i
|
|||||||
await mutator.insertBlocks(
|
await mutator.insertBlocks(
|
||||||
[boardTemplate, view],
|
[boardTemplate, view],
|
||||||
'add board template',
|
'add board template',
|
||||||
async () => {
|
async (newBlocks: Block[]) => {
|
||||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoardTemplate, {board: boardTemplate.id})
|
const newBoardId = newBlocks[0].id
|
||||||
showBoard(boardTemplate.id)
|
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoardTemplate, {board: newBoardId})
|
||||||
|
showBoard(newBoardId)
|
||||||
}, async () => {
|
}, async () => {
|
||||||
if (activeBoardId) {
|
if (activeBoardId) {
|
||||||
showBoard(activeBoardId)
|
showBoard(activeBoardId)
|
||||||
|
@ -9,6 +9,7 @@ import {BoardView, createBoardView, IViewType} from '../blocks/boardView'
|
|||||||
import {Constants} from '../constants'
|
import {Constants} from '../constants'
|
||||||
import mutator from '../mutator'
|
import mutator from '../mutator'
|
||||||
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../telemetry/telemetryClient'
|
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../telemetry/telemetryClient'
|
||||||
|
import {Block} from '../blocks/block'
|
||||||
import {IDType, Utils} from '../utils'
|
import {IDType, Utils} from '../utils'
|
||||||
import AddIcon from '../widgets/icons/add'
|
import AddIcon from '../widgets/icons/add'
|
||||||
import BoardIcon from '../widgets/icons/board'
|
import BoardIcon from '../widgets/icons/board'
|
||||||
@ -49,10 +50,10 @@ const ViewMenu = React.memo((props: Props) => {
|
|||||||
mutator.insertBlock(
|
mutator.insertBlock(
|
||||||
newView,
|
newView,
|
||||||
'duplicate view',
|
'duplicate view',
|
||||||
async () => {
|
async (block: Block) => {
|
||||||
// This delay is needed because WSClient has a default 100 ms notification delay before updates
|
// This delay is needed because WSClient has a default 100 ms notification delay before updates
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showView(newView.id)
|
showView(block.id)
|
||||||
}, 120)
|
}, 120)
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
@ -98,10 +99,10 @@ const ViewMenu = React.memo((props: Props) => {
|
|||||||
mutator.insertBlock(
|
mutator.insertBlock(
|
||||||
view,
|
view,
|
||||||
'add view',
|
'add view',
|
||||||
async () => {
|
async (block: Block) => {
|
||||||
// This delay is needed because WSClient has a default 100 ms notification delay before updates
|
// This delay is needed because WSClient has a default 100 ms notification delay before updates
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showView(view.id)
|
showView(block.id)
|
||||||
}, 120)
|
}, 120)
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
@ -127,11 +128,11 @@ const ViewMenu = React.memo((props: Props) => {
|
|||||||
mutator.insertBlock(
|
mutator.insertBlock(
|
||||||
view,
|
view,
|
||||||
'add view',
|
'add view',
|
||||||
async () => {
|
async (block: Block) => {
|
||||||
// This delay is needed because WSClient has a default 100 ms notification delay before updates
|
// This delay is needed because WSClient has a default 100 ms notification delay before updates
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Utils.log(`showView: ${view.id}`)
|
Utils.log(`showView: ${block.id}`)
|
||||||
showView(view.id)
|
showView(block.id)
|
||||||
}, 120)
|
}, 120)
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
@ -155,11 +156,11 @@ const ViewMenu = React.memo((props: Props) => {
|
|||||||
mutator.insertBlock(
|
mutator.insertBlock(
|
||||||
view,
|
view,
|
||||||
'add view',
|
'add view',
|
||||||
async () => {
|
async (block: Block) => {
|
||||||
// This delay is needed because WSClient has a default 100 ms notification delay before updates
|
// This delay is needed because WSClient has a default 100 ms notification delay before updates
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Utils.log(`showView: ${view.id}`)
|
Utils.log(`showView: ${block.id}`)
|
||||||
showView(view.id)
|
showView(block.id)
|
||||||
}, 120)
|
}, 120)
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import {BlockIcons} from './blockIcons'
|
import {BlockIcons} from './blockIcons'
|
||||||
import {Block} from './blocks/block'
|
import {Block, BlockPatch, createPatchesFromBlocks} from './blocks/block'
|
||||||
import {Board, IPropertyOption, IPropertyTemplate, PropertyType, createBoard} from './blocks/board'
|
import {Board, IPropertyOption, IPropertyTemplate, PropertyType, createBoard} from './blocks/board'
|
||||||
import {BoardView, ISortOption, createBoardView, KanbanCalculationFields} from './blocks/boardView'
|
import {BoardView, ISortOption, createBoardView, KanbanCalculationFields} from './blocks/boardView'
|
||||||
import {Card, createCard} from './blocks/card'
|
import {Card, createCard} from './blocks/card'
|
||||||
@ -50,12 +50,13 @@ class Mutator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateBlock(newBlock: Block, oldBlock: Block, description: string): Promise<void> {
|
async updateBlock(newBlock: Block, oldBlock: Block, description: string): Promise<void> {
|
||||||
|
const [updatePatch, undoPatch] = createPatchesFromBlocks(newBlock, oldBlock)
|
||||||
await undoManager.perform(
|
await undoManager.perform(
|
||||||
async () => {
|
async () => {
|
||||||
await octoClient.updateBlock(newBlock)
|
await octoClient.patchBlock(newBlock.id, updatePatch)
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
await octoClient.updateBlock(oldBlock)
|
await octoClient.patchBlock(oldBlock.id, undoPatch)
|
||||||
},
|
},
|
||||||
description,
|
description,
|
||||||
this.undoGroupId,
|
this.undoGroupId,
|
||||||
@ -63,43 +64,67 @@ class Mutator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateBlocks(newBlocks: Block[], oldBlocks: Block[], description: string): Promise<void> {
|
private async updateBlocks(newBlocks: Block[], oldBlocks: Block[], description: string): Promise<void> {
|
||||||
await undoManager.perform(
|
if (newBlocks.length !== oldBlocks.length) {
|
||||||
|
throw new Error('new and old blocks must have the same length when updating blocks')
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePatches = [] as BlockPatch[]
|
||||||
|
const undoPatches = [] as BlockPatch[]
|
||||||
|
|
||||||
|
newBlocks.forEach((newBlock, i) => {
|
||||||
|
const [updatePatch, undoPatch] = createPatchesFromBlocks(newBlock, oldBlocks[i])
|
||||||
|
updatePatches.push(updatePatch)
|
||||||
|
undoPatches.push(undoPatch)
|
||||||
|
})
|
||||||
|
|
||||||
|
return undoManager.perform(
|
||||||
async () => {
|
async () => {
|
||||||
await octoClient.updateBlocks(newBlocks)
|
await Promise.all(
|
||||||
|
updatePatches.map((patch, i) => octoClient.patchBlock(newBlocks[i].id, patch)),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
await octoClient.updateBlocks(oldBlocks)
|
await Promise.all(
|
||||||
|
undoPatches.map((patch, i) => octoClient.patchBlock(newBlocks[i].id, patch)),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
description,
|
description,
|
||||||
this.undoGroupId,
|
this.undoGroupId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertBlock(block: Block, description = 'add', afterRedo?: () => Promise<void>, beforeUndo?: () => Promise<void>) {
|
//eslint-disable-next-line no-shadow
|
||||||
await undoManager.perform(
|
async insertBlock(block: Block, description = 'add', afterRedo?: (block: Block) => Promise<void>, beforeUndo?: (block: Block) => Promise<void>): Promise<Block> {
|
||||||
|
return undoManager.perform(
|
||||||
async () => {
|
async () => {
|
||||||
await octoClient.insertBlock(block)
|
const res = await octoClient.insertBlock(block)
|
||||||
await afterRedo?.()
|
const jsonres = await res.json()
|
||||||
|
const newBlock = jsonres[0] as Block
|
||||||
|
await afterRedo?.(newBlock)
|
||||||
|
return newBlock
|
||||||
},
|
},
|
||||||
async () => {
|
async (newBlock: Block) => {
|
||||||
await beforeUndo?.()
|
await beforeUndo?.(newBlock)
|
||||||
await octoClient.deleteBlock(block.id)
|
await octoClient.deleteBlock(newBlock.id)
|
||||||
},
|
},
|
||||||
description,
|
description,
|
||||||
this.undoGroupId,
|
this.undoGroupId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertBlocks(blocks: Block[], description = 'add', afterRedo?: () => Promise<void>, beforeUndo?: () => Promise<void>) {
|
//eslint-disable-next-line no-shadow
|
||||||
await undoManager.perform(
|
async insertBlocks(blocks: Block[], description = 'add', afterRedo?: (blocks: Block[]) => Promise<void>, beforeUndo?: () => Promise<void>) {
|
||||||
|
return undoManager.perform(
|
||||||
async () => {
|
async () => {
|
||||||
await octoClient.insertBlocks(blocks)
|
const res = await octoClient.insertBlocks(blocks)
|
||||||
await afterRedo?.()
|
const newBlocks = (await res.json()) as Block[]
|
||||||
|
await afterRedo?.(newBlocks)
|
||||||
|
return newBlocks
|
||||||
},
|
},
|
||||||
async () => {
|
async (newBlocks: Block[]) => {
|
||||||
await beforeUndo?.()
|
await beforeUndo?.()
|
||||||
const awaits = []
|
const awaits = []
|
||||||
for (const block of blocks) {
|
for (const block of newBlocks) {
|
||||||
awaits.push(octoClient.deleteBlock(block.id))
|
awaits.push(octoClient.deleteBlock(block.id))
|
||||||
}
|
}
|
||||||
await Promise.all(awaits)
|
await Promise.all(awaits)
|
||||||
@ -639,8 +664,8 @@ class Mutator {
|
|||||||
await this.insertBlocks(
|
await this.insertBlocks(
|
||||||
newBlocks,
|
newBlocks,
|
||||||
description,
|
description,
|
||||||
async () => {
|
async (respBlocks: Block[]) => {
|
||||||
await afterRedo?.(newCard.id)
|
await afterRedo?.(respBlocks[0].id)
|
||||||
},
|
},
|
||||||
beforeUndo,
|
beforeUndo,
|
||||||
)
|
)
|
||||||
@ -668,15 +693,15 @@ class Mutator {
|
|||||||
// Board from template
|
// Board from template
|
||||||
}
|
}
|
||||||
newBoard.fields.isTemplate = asTemplate
|
newBoard.fields.isTemplate = asTemplate
|
||||||
await this.insertBlocks(
|
const createdBlocks = await this.insertBlocks(
|
||||||
newBlocks,
|
newBlocks,
|
||||||
description,
|
description,
|
||||||
async () => {
|
async (respBlocks: Block[]) => {
|
||||||
await afterRedo?.(newBoard.id)
|
await afterRedo?.(respBlocks[0].id)
|
||||||
},
|
},
|
||||||
beforeUndo,
|
beforeUndo,
|
||||||
)
|
)
|
||||||
return [newBlocks, newBoard.id]
|
return [createdBlocks, createdBlocks[0].id]
|
||||||
}
|
}
|
||||||
|
|
||||||
async duplicateFromRootBoard(
|
async duplicateFromRootBoard(
|
||||||
@ -701,15 +726,15 @@ class Mutator {
|
|||||||
// Board from template
|
// Board from template
|
||||||
}
|
}
|
||||||
newBoard.fields.isTemplate = asTemplate
|
newBoard.fields.isTemplate = asTemplate
|
||||||
await this.insertBlocks(
|
const createdBlocks = await this.insertBlocks(
|
||||||
newBlocks,
|
newBlocks,
|
||||||
description,
|
description,
|
||||||
async () => {
|
async (respBlocks: Block[]) => {
|
||||||
await afterRedo?.(newBoard.id)
|
await afterRedo?.(respBlocks[0].id)
|
||||||
},
|
},
|
||||||
beforeUndo,
|
beforeUndo,
|
||||||
)
|
)
|
||||||
return [newBlocks, newBoard.id]
|
return [createdBlocks, createdBlocks[0].id]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other methods
|
// Other methods
|
||||||
|
@ -248,12 +248,8 @@ class OctoClient {
|
|||||||
return fixedBlocks
|
return fixedBlocks
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateBlock(block: Block): Promise<Response> {
|
|
||||||
return this.insertBlocks([block])
|
|
||||||
}
|
|
||||||
|
|
||||||
async patchBlock(blockId: string, blockPatch: BlockPatch): Promise<Response> {
|
async patchBlock(blockId: string, blockPatch: BlockPatch): Promise<Response> {
|
||||||
Utils.log(`patchBlocks: ${blockId} block`)
|
Utils.log(`patchBlock: ${blockId} block`)
|
||||||
const body = JSON.stringify(blockPatch)
|
const body = JSON.stringify(blockPatch)
|
||||||
return fetch(this.getBaseURL() + this.workspacePath() + '/blocks/' + blockId, {
|
return fetch(this.getBaseURL() + this.workspacePath() + '/blocks/' + blockId, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@ -262,10 +258,6 @@ class OctoClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateBlocks(blocks: Block[]): Promise<Response> {
|
|
||||||
return this.insertBlocks(blocks)
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteBlock(blockId: string): Promise<Response> {
|
async deleteBlock(blockId: string): Promise<Response> {
|
||||||
Utils.log(`deleteBlock: ${blockId}`)
|
Utils.log(`deleteBlock: ${blockId}`)
|
||||||
return fetch(this.getBaseURL() + this.workspacePath() + `/blocks/${encodeURIComponent(blockId)}`, {
|
return fetch(this.getBaseURL() + this.workspacePath() + `/blocks/${encodeURIComponent(blockId)}`, {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user