diff --git a/server/api/api.go b/server/api/api.go index a33f9bbc8..89b19b1b8 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -440,6 +440,16 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session := ctx.Value(sessionContextKey).(*model.Session) + // this query param exists only when creating + // template from board + sourceBoardID := r.URL.Query().Get("sourceBoardID") + if sourceBoardID != "" { + if updateFileIDsErr := a.app.CopyCardFiles(sourceBoardID, blocks); updateFileIDsErr != nil { + a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", updateFileIDsErr) + return + } + } + newBlocks, err := a.app.InsertBlocks(*container, blocks, session.UserID, true) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) diff --git a/server/app/blocks.go b/server/app/blocks.go index f72ecc1ac..30bc584da 100644 --- a/server/app/blocks.go +++ b/server/app/blocks.go @@ -133,6 +133,36 @@ func (a *App) InsertBlocks(c store.Container, blocks []model.Block, modifiedByID return blocks, nil } +func (a *App) CopyCardFiles(sourceBoardID string, blocks []model.Block) error { + // Images attached in cards have a path comprising the card's board ID. + // When we create a template from this board, we need to copy the files + // with the new board ID in path. + // Not doing so causing images in templates (and boards created from this + // template) to fail to load. + + for i := range blocks { + block := blocks[i] + + fileName, ok := block.Fields["fileId"] + if block.Type == model.TypeImage && ok { + sourceFilePath := filepath.Join(block.WorkspaceID, sourceBoardID, fileName.(string)) + destinationFilePath := filepath.Join(block.WorkspaceID, block.RootID, fileName.(string)) + if err := a.filesBackend.CopyFile(sourceFilePath, destinationFilePath); err != nil { + a.logger.Error( + "CopyCardFiles failed to copy file", + mlog.String("sourceFilePath", sourceFilePath), + mlog.String("destinationFilePath", destinationFilePath), + mlog.Err(err), + ) + + return err + } + } + } + + return nil +} + func (a *App) GetSubTree(c store.Container, blockID string, levels int) ([]model.Block, error) { // Only 2 or 3 levels are supported for now if levels >= 3 { diff --git a/webapp/src/mutator.ts b/webapp/src/mutator.ts index 0a7a21f83..e06798035 100644 --- a/webapp/src/mutator.ts +++ b/webapp/src/mutator.ts @@ -132,10 +132,10 @@ class Mutator { } //eslint-disable-next-line no-shadow - async insertBlocks(blocks: Block[], description = 'add', afterRedo?: (blocks: Block[]) => Promise, beforeUndo?: () => Promise) { + async insertBlocks(blocks: Block[], description = 'add', afterRedo?: (blocks: Block[]) => Promise, beforeUndo?: () => Promise, sourceBoardID?: string) { return undoManager.perform( async () => { - const res = await octoClient.insertBlocks(blocks) + const res = await octoClient.insertBlocks(blocks, sourceBoardID) const newBlocks = (await res.json()) as Block[] updateAllBlocks(newBlocks) await afterRedo?.(newBlocks) @@ -793,6 +793,7 @@ class Mutator { await afterRedo?.(board?.id || '') }, beforeUndo, + boardId, ) const board = createdBlocks.find((b: Block) => b.type === 'board') return [createdBlocks, board.id] diff --git a/webapp/src/octoClient.ts b/webapp/src/octoClient.ts index 85c697d79..1a187ffe4 100644 --- a/webapp/src/octoClient.ts +++ b/webapp/src/octoClient.ts @@ -315,13 +315,14 @@ class OctoClient { return this.insertBlocks([block]) } - async insertBlocks(blocks: Block[]): Promise { + async insertBlocks(blocks: Block[], sourceBoardID?: string): Promise { Utils.log(`insertBlocks: ${blocks.length} blocks(s)`) blocks.forEach((block) => { Utils.log(`\t ${block.type}, ${block.id}, ${block.title?.substr(0, 50) || ''}`) }) const body = JSON.stringify(blocks) - return fetch(this.getBaseURL() + this.workspacePath() + '/blocks', { + const url = this.getBaseURL() + this.workspacePath() + '/blocks' + (sourceBoardID ? `?sourceBoardID=${encodeURIComponent(sourceBoardID)}` : '') + return fetch(url, { method: 'POST', headers: this.headers(), body,