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:
Miguel de la Cruz 2021-11-05 11:54:27 +01:00 committed by GitHub
parent 581ae7b97a
commit fa36e092bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 421 additions and 109 deletions

View file

@ -328,7 +328,9 @@ func stampModificationMetadata(r *http.Request, blocks []model.Block, auditRec *
func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
// 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:
@ -352,6 +354,10 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
// responses:
// '200':
// description: success
// schema:
// items:
// $ref: '#/definitions/Block'
// type: array
// default:
// description: internal error
// 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)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
@ -406,14 +414,21 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
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 {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
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.Success()
@ -912,7 +927,7 @@ func (a *API) handleImport(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
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 {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return

View file

@ -73,12 +73,12 @@ func (a *App) InsertBlock(c store.Container, block model.Block, userID string) e
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))
for i := range blocks {
err := a.store.InsertBlock(c, &blocks[i], userID)
if err != nil {
return err
return nil, err
}
blocks[i].WorkspaceID = c.WorkspaceID
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) {

View file

@ -184,14 +184,14 @@ func (c *Client) PatchBlock(blockID string, blockPatch *model.BlockPatch) (bool,
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))
if err != nil {
return false, BuildErrorResponse(r, err)
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return true, BuildResponse(r)
return model.BlocksFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) DeleteBlock(blockID string) (bool, *Response) {

View file

@ -2,6 +2,7 @@ package integrationtests
import (
"testing"
"time"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/utils"
@ -17,26 +18,29 @@ func TestGetBlocks(t *testing.T) {
require.NoError(t, resp.Error)
initialCount := len(blocks)
blockID1 := utils.NewID(utils.IDTypeBlock)
blockID2 := utils.NewID(utils.IDTypeBlock)
initialID1 := utils.NewID(utils.IDTypeBlock)
initialID2 := utils.NewID(utils.IDTypeBlock)
newBlocks := []model.Block{
{
ID: blockID1,
RootID: blockID1,
ID: initialID1,
RootID: initialID1,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
},
{
ID: blockID2,
RootID: blockID2,
ID: initialID2,
RootID: initialID2,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
},
}
_, resp = th.Client.InsertBlocks(newBlocks)
newBlocks, resp = th.Client.InsertBlocks(newBlocks)
require.NoError(t, resp.Error)
require.Len(t, newBlocks, 2)
blockID1 := newBlocks[0].ID
blockID2 := newBlocks[1].ID
blocks, resp = th.Client.GetBlocks()
require.NoError(t, resp.Error)
@ -58,22 +62,25 @@ func TestPostBlock(t *testing.T) {
require.NoError(t, resp.Error)
initialCount := len(blocks)
blockID1 := utils.NewID(utils.IDTypeBlock)
blockID2 := utils.NewID(utils.IDTypeBlock)
blockID3 := utils.NewID(utils.IDTypeBlock)
var blockID1 string
var blockID2 string
var blockID3 string
t.Run("Create a single block", func(t *testing.T) {
initialID1 := utils.NewID(utils.IDTypeBlock)
block := model.Block{
ID: blockID1,
RootID: blockID1,
ID: initialID1,
RootID: initialID1,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
Title: "New title",
}
_, resp := th.Client.InsertBlocks([]model.Block{block})
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
require.NoError(t, resp.Error)
require.Len(t, newBlocks, 1)
blockID1 = newBlocks[0].ID
blocks, resp := th.Client.GetBlocks()
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) {
initialID2 := utils.NewID(utils.IDTypeBlock)
initialID3 := utils.NewID(utils.IDTypeBlock)
newBlocks := []model.Block{
{
ID: blockID2,
RootID: blockID2,
ID: initialID2,
RootID: initialID2,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
},
{
ID: blockID3,
RootID: blockID3,
ID: initialID3,
RootID: initialID3,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
},
}
_, resp := th.Client.InsertBlocks(newBlocks)
newBlocks, resp := th.Client.InsertBlocks(newBlocks)
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()
require.NoError(t, resp.Error)
@ -120,7 +134,7 @@ func TestPostBlock(t *testing.T) {
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{
ID: blockID1,
RootID: blockID1,
@ -130,21 +144,24 @@ func TestPostBlock(t *testing.T) {
Title: "Updated title",
}
_, resp := th.Client.InsertBlocks([]model.Block{block})
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
require.NoError(t, resp.Error)
require.Len(t, newBlocks, 1)
blockID4 := newBlocks[0].ID
require.NotEqual(t, blockID1, blockID4)
blocks, resp := th.Client.GetBlocks()
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 {
if b.ID == blockID1 {
updatedBlock = b
if b.ID == blockID4 {
block4 = b
}
}
require.NotNil(t, updatedBlock)
require.Equal(t, "Updated title", updatedBlock.Title)
require.NotNil(t, block4)
require.Equal(t, "Updated title", block4.Title)
})
}
@ -152,11 +169,11 @@ func TestPatchBlock(t *testing.T) {
th := SetupTestHelper().InitBasic()
defer th.TearDown()
blockID := utils.NewID(utils.IDTypeBlock)
initialID := utils.NewID(utils.IDTypeBlock)
block := model.Block{
ID: blockID,
RootID: blockID,
ID: initialID,
RootID: initialID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
@ -164,8 +181,10 @@ func TestPatchBlock(t *testing.T) {
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.Len(t, newBlocks, 1)
blockID := newBlocks[0].ID
blocks, resp := th.Client.GetBlocks()
require.NoError(t, resp.Error)
@ -253,19 +272,24 @@ func TestDeleteBlock(t *testing.T) {
require.NoError(t, resp.Error)
initialCount := len(blocks)
blockID := utils.NewID(utils.IDTypeBlock)
var blockID string
t.Run("Create a block", func(t *testing.T) {
initialID := utils.NewID(utils.IDTypeBlock)
block := model.Block{
ID: blockID,
RootID: blockID,
ID: initialID,
RootID: initialID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
Title: "New title",
}
_, resp := th.Client.InsertBlocks([]model.Block{block})
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
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()
require.NoError(t, resp.Error)
@ -279,6 +303,10 @@ func TestDeleteBlock(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)
require.NoError(t, resp.Error)

View file

@ -3,6 +3,8 @@ package model
import (
"encoding/json"
"io"
"github.com/mattermost/focalboard/server/utils"
)
// Block is the basic data unit
@ -153,3 +155,59 @@ func (p *BlockPatch) Patch(block *Block) *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
View 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)
})
}

View file

@ -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 {blockTypes, contentBlockTypes, Block, BlockPatch, createBlock}
export {blockTypes, contentBlockTypes, Block, BlockPatch, createBlock, createPatchesFromBlocks}

View file

@ -6,6 +6,7 @@ import {injectIntl, IntlShape} from 'react-intl'
import {connect} from 'react-redux'
import Hotkeys from 'react-hot-keys'
import {Block} from '../blocks/block'
import {BlockIcons} from '../blockIcons'
import {Card, createCard} from '../blocks/card'
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()
}
mutator.performAsUndoGroup(async () => {
await mutator.insertBlock(
const newCard = await mutator.insertBlock(
card,
'add card',
async () => {
async (block: Block) => {
if (show) {
this.props.addCard(card)
this.props.updateView({...activeView, fields: {...activeView.fields, cardOrder: [...activeView.fields.cardOrder, card.id]}})
this.showCard(card.id)
this.props.addCard(createCard(block))
this.props.updateView({...activeView, fields: {...activeView.fields, cardOrder: [...activeView.fields.cardOrder, block.id]}})
this.showCard(block.id)
} else {
// Focus on this card's title inline on next render
this.setState({cardIdToFocusOnRender: card.id})
@ -270,7 +271,7 @@ class CenterPanel extends React.Component<Props, State> {
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(
cardTemplate,
'add card template',
async () => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateCardTemplate, {board: board.id, view: activeView.id, card: cardTemplate.id})
this.props.addTemplate(cardTemplate)
this.showCard(cardTemplate.id)
async (newBlock: Block) => {
const newTemplate = createCard(newBlock)
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateCardTemplate, {board: board.id, view: activeView.id, card: newTemplate.id})
this.props.addTemplate(newTemplate)
this.showCard(newTemplate.id)
}, async () => {
this.showCard(undefined)
},

View file

@ -4,6 +4,7 @@ import React, {useCallback, useEffect} from 'react'
import {FormattedMessage, IntlShape, useIntl} from 'react-intl'
import {generatePath, useHistory, useRouteMatch} from 'react-router-dom'
import {Block} from '../../blocks/block'
import {Board, createBoard} from '../../blocks/board'
import {createBoardView} from '../../blocks/boardView'
import mutator from '../../mutator'
@ -39,9 +40,10 @@ export const addBoardClicked = async (showBoard: (id: string) => void, intl: Int
await mutator.insertBlocks(
[board, view],
'add board',
async () => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoard, {board: board.id})
showBoard(board.id)
async (newBlocks: Block[]) => {
const newBoardId = newBlocks[0].id
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoard, {board: newBoardId})
showBoard(newBoardId)
},
async () => {
if (oldBoardId) {
@ -66,9 +68,10 @@ export const addBoardTemplateClicked = async (showBoard: (id: string) => void, i
await mutator.insertBlocks(
[boardTemplate, view],
'add board template',
async () => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoardTemplate, {board: boardTemplate.id})
showBoard(boardTemplate.id)
async (newBlocks: Block[]) => {
const newBoardId = newBlocks[0].id
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoardTemplate, {board: newBoardId})
showBoard(newBoardId)
}, async () => {
if (activeBoardId) {
showBoard(activeBoardId)

View file

@ -9,6 +9,7 @@ import {BoardView, createBoardView, IViewType} from '../blocks/boardView'
import {Constants} from '../constants'
import mutator from '../mutator'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../telemetry/telemetryClient'
import {Block} from '../blocks/block'
import {IDType, Utils} from '../utils'
import AddIcon from '../widgets/icons/add'
import BoardIcon from '../widgets/icons/board'
@ -49,10 +50,10 @@ const ViewMenu = React.memo((props: Props) => {
mutator.insertBlock(
newView,
'duplicate view',
async () => {
async (block: Block) => {
// This delay is needed because WSClient has a default 100 ms notification delay before updates
setTimeout(() => {
showView(newView.id)
showView(block.id)
}, 120)
},
async () => {
@ -98,10 +99,10 @@ const ViewMenu = React.memo((props: Props) => {
mutator.insertBlock(
view,
'add view',
async () => {
async (block: Block) => {
// This delay is needed because WSClient has a default 100 ms notification delay before updates
setTimeout(() => {
showView(view.id)
showView(block.id)
}, 120)
},
async () => {
@ -127,11 +128,11 @@ const ViewMenu = React.memo((props: Props) => {
mutator.insertBlock(
view,
'add view',
async () => {
async (block: Block) => {
// This delay is needed because WSClient has a default 100 ms notification delay before updates
setTimeout(() => {
Utils.log(`showView: ${view.id}`)
showView(view.id)
Utils.log(`showView: ${block.id}`)
showView(block.id)
}, 120)
},
async () => {
@ -155,11 +156,11 @@ const ViewMenu = React.memo((props: Props) => {
mutator.insertBlock(
view,
'add view',
async () => {
async (block: Block) => {
// This delay is needed because WSClient has a default 100 ms notification delay before updates
setTimeout(() => {
Utils.log(`showView: ${view.id}`)
showView(view.id)
Utils.log(`showView: ${block.id}`)
showView(block.id)
}, 120)
},
async () => {

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
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 {BoardView, ISortOption, createBoardView, KanbanCalculationFields} from './blocks/boardView'
import {Card, createCard} from './blocks/card'
@ -50,12 +50,13 @@ class Mutator {
}
async updateBlock(newBlock: Block, oldBlock: Block, description: string): Promise<void> {
const [updatePatch, undoPatch] = createPatchesFromBlocks(newBlock, oldBlock)
await undoManager.perform(
async () => {
await octoClient.updateBlock(newBlock)
await octoClient.patchBlock(newBlock.id, updatePatch)
},
async () => {
await octoClient.updateBlock(oldBlock)
await octoClient.patchBlock(oldBlock.id, undoPatch)
},
description,
this.undoGroupId,
@ -63,43 +64,67 @@ class Mutator {
}
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 () => {
await octoClient.updateBlocks(newBlocks)
await Promise.all(
updatePatches.map((patch, i) => octoClient.patchBlock(newBlocks[i].id, patch)),
)
},
async () => {
await octoClient.updateBlocks(oldBlocks)
await Promise.all(
undoPatches.map((patch, i) => octoClient.patchBlock(newBlocks[i].id, patch)),
)
},
description,
this.undoGroupId,
)
}
async insertBlock(block: Block, description = 'add', afterRedo?: () => Promise<void>, beforeUndo?: () => Promise<void>) {
await undoManager.perform(
//eslint-disable-next-line no-shadow
async insertBlock(block: Block, description = 'add', afterRedo?: (block: Block) => Promise<void>, beforeUndo?: (block: Block) => Promise<void>): Promise<Block> {
return undoManager.perform(
async () => {
await octoClient.insertBlock(block)
await afterRedo?.()
const res = await octoClient.insertBlock(block)
const jsonres = await res.json()
const newBlock = jsonres[0] as Block
await afterRedo?.(newBlock)
return newBlock
},
async () => {
await beforeUndo?.()
await octoClient.deleteBlock(block.id)
async (newBlock: Block) => {
await beforeUndo?.(newBlock)
await octoClient.deleteBlock(newBlock.id)
},
description,
this.undoGroupId,
)
}
async insertBlocks(blocks: Block[], description = 'add', afterRedo?: () => Promise<void>, beforeUndo?: () => Promise<void>) {
await undoManager.perform(
//eslint-disable-next-line no-shadow
async insertBlocks(blocks: Block[], description = 'add', afterRedo?: (blocks: Block[]) => Promise<void>, beforeUndo?: () => Promise<void>) {
return undoManager.perform(
async () => {
await octoClient.insertBlocks(blocks)
await afterRedo?.()
const res = await octoClient.insertBlocks(blocks)
const newBlocks = (await res.json()) as Block[]
await afterRedo?.(newBlocks)
return newBlocks
},
async () => {
async (newBlocks: Block[]) => {
await beforeUndo?.()
const awaits = []
for (const block of blocks) {
for (const block of newBlocks) {
awaits.push(octoClient.deleteBlock(block.id))
}
await Promise.all(awaits)
@ -639,8 +664,8 @@ class Mutator {
await this.insertBlocks(
newBlocks,
description,
async () => {
await afterRedo?.(newCard.id)
async (respBlocks: Block[]) => {
await afterRedo?.(respBlocks[0].id)
},
beforeUndo,
)
@ -668,15 +693,15 @@ class Mutator {
// Board from template
}
newBoard.fields.isTemplate = asTemplate
await this.insertBlocks(
const createdBlocks = await this.insertBlocks(
newBlocks,
description,
async () => {
await afterRedo?.(newBoard.id)
async (respBlocks: Block[]) => {
await afterRedo?.(respBlocks[0].id)
},
beforeUndo,
)
return [newBlocks, newBoard.id]
return [createdBlocks, createdBlocks[0].id]
}
async duplicateFromRootBoard(
@ -701,15 +726,15 @@ class Mutator {
// Board from template
}
newBoard.fields.isTemplate = asTemplate
await this.insertBlocks(
const createdBlocks = await this.insertBlocks(
newBlocks,
description,
async () => {
await afterRedo?.(newBoard.id)
async (respBlocks: Block[]) => {
await afterRedo?.(respBlocks[0].id)
},
beforeUndo,
)
return [newBlocks, newBoard.id]
return [createdBlocks, createdBlocks[0].id]
}
// Other methods

View file

@ -248,12 +248,8 @@ class OctoClient {
return fixedBlocks
}
async updateBlock(block: Block): Promise<Response> {
return this.insertBlocks([block])
}
async patchBlock(blockId: string, blockPatch: BlockPatch): Promise<Response> {
Utils.log(`patchBlocks: ${blockId} block`)
Utils.log(`patchBlock: ${blockId} block`)
const body = JSON.stringify(blockPatch)
return fetch(this.getBaseURL() + this.workspacePath() + '/blocks/' + blockId, {
method: 'PATCH',
@ -262,10 +258,6 @@ class OctoClient {
})
}
async updateBlocks(blocks: Block[]): Promise<Response> {
return this.insertBlocks(blocks)
}
async deleteBlock(blockId: string): Promise<Response> {
Utils.log(`deleteBlock: ${blockId}`)
return fetch(this.getBaseURL() + this.workspacePath() + `/blocks/${encodeURIComponent(blockId)}`, {