package model import ( "encoding/json" "fmt" "io" "github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/focalboard/server/utils" ) // Block is the basic data unit // swagger:model type Block struct { // The id for this block // required: true ID string `json:"id"` // The id for this block's parent block. Empty for root blocks // required: false ParentID string `json:"parentId"` // The id for this block's root block // required: true RootID string `json:"rootId"` // The id for user who created this block // required: true CreatedBy string `json:"createdBy"` // The id for user who last modified this block // required: true ModifiedBy string `json:"modifiedBy"` // The schema version of this block // required: true Schema int64 `json:"schema"` // The block type // required: true Type BlockType `json:"type"` // The display title // required: false Title string `json:"title"` // The block fields // required: false Fields map[string]interface{} `json:"fields"` // The creation time // required: true CreateAt int64 `json:"createAt"` // The last modified time // required: true UpdateAt int64 `json:"updateAt"` // The deleted time. Set to indicate this block is deleted // required: false DeleteAt int64 `json:"deleteAt"` // The workspace id that the block belongs to // required: true WorkspaceID string `json:"workspaceId"` } // BlockPatch is a patch for modify blocks // swagger:model type BlockPatch struct { // The id for this block's parent block. Empty for root blocks // required: false ParentID *string `json:"parentId"` // The id for this block's root block // required: false RootID *string `json:"rootId"` // The schema version of this block // required: false Schema *int64 `json:"schema"` // The block type // required: false Type *BlockType `json:"type"` // The display title // required: false Title *string `json:"title"` // The block updated fields // required: false UpdatedFields map[string]interface{} `json:"updatedFields"` // The block removed fields // required: false DeletedFields []string `json:"deletedFields"` } // BlockPatchBatch is a batch of IDs and patches for modify blocks // swagger:model type BlockPatchBatch struct { // The id's for of the blocks to patch BlockIDs []string `json:"block_ids"` // The BlockPatches to be applied BlockPatches []BlockPatch `json:"block_patches"` } // BlockModifier is a callback that can modify each block during an import. // A cache of arbitrary data will be passed for each call and any changes // to the cache will be preserved for the next call. // Return true to import the block or false to skip import. type BlockModifier func(block *Block, cache map[string]interface{}) bool func BlocksFromJSON(data io.Reader) []Block { var blocks []Block _ = json.NewDecoder(data).Decode(&blocks) return blocks } // LogClone implements the `mlog.LogCloner` interface to provide a subset of Block fields for logging. func (b Block) LogClone() interface{} { return struct { ID string ParentID string RootID string Type BlockType }{ ID: b.ID, ParentID: b.ParentID, RootID: b.RootID, Type: b.Type, } } // Patch returns an update version of the block. func (p *BlockPatch) Patch(block *Block) *Block { if p.ParentID != nil { block.ParentID = *p.ParentID } if p.RootID != nil { block.RootID = *p.RootID } if p.Schema != nil { block.Schema = *p.Schema } if p.Type != nil { block.Type = *p.Type } if p.Title != nil { block.Title = *p.Title } for key, field := range p.UpdatedFields { block.Fields[key] = field } for _, key := range p.DeletedFields { delete(block.Fields, key) } return block } // QuerySubtreeOptions are query options that can be passed to GetSubTree methods. type QuerySubtreeOptions struct { BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt Limit uint64 // if non-zero then limit the number of returned records } // QueryBlockHistoryOptions are query options that can be passed to GetBlockHistory. type QueryBlockHistoryOptions struct { BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt Limit uint64 // if non-zero then limit the number of returned records Descending bool // if true then the records are sorted by insert_at in descending order } // 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, logger *mlog.Logger) []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 } if _, ok := block.Fields["contentOrder"]; ok { contentOrder, typeOk := block.Fields["contentOrder"].([]interface{}) if !typeOk { logger.Warn( "type assertion failed for content order when saving reference block IDs", mlog.String("blockID", block.ID), mlog.String("actionType", fmt.Sprintf("%T", block.Fields["contentOrder"])), mlog.String("expectedType", "[]interface{}"), mlog.String("contentOrder", fmt.Sprintf("%v", block.Fields["contentOrder"])), ) continue } for _, blockID := range contentOrder { switch v := blockID.(type) { case []interface{}: for _, columnBlockID := range v { referenceIDs[columnBlockID.(string)] = true } case string: referenceIDs[v] = true default: } } } } 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) if _, ok := block.Fields["contentOrder"]; ok { contentOrder, typeOk := block.Fields["contentOrder"].([]interface{}) if !typeOk { logger.Warn( "type assertion failed for content order when setting new block IDs", mlog.String("blockID", block.ID), mlog.String("actionType", fmt.Sprintf("%T", block.Fields["contentOrder"])), mlog.String("expectedType", "[]interface{}"), mlog.String("contentOrder", fmt.Sprintf("%v", block.Fields["contentOrder"])), ) } else { for j := range contentOrder { switch v := contentOrder[j].(type) { case string: contentOrder[j] = getExistingOrOldID(v) case []interface{}: subOrder := contentOrder[j].([]interface{}) for k := range v { subOrder[k] = getExistingOrOldID(v[k].(string)) } } } } } newBlocks[i] = block } return newBlocks }