You've already forked focalboard
mirror of
https://github.com/mattermost/focalboard.git
synced 2025-07-15 23:54:29 +02:00
Merge pull request #2778 from wiggin77/GH-2744_export_with_pound
GH-2744 Fix export with hash sign (#) in block title breaks export to CSV
This commit is contained in:
@ -683,3 +683,42 @@ func (c *Client) GetTemplatesForTeam(teamID string) ([]*model.Board, *Response)
|
|||||||
|
|
||||||
return model.BoardsFromJSON(r.Body), BuildResponse(r)
|
return model.BoardsFromJSON(r.Body), BuildResponse(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) ExportBoardArchive(boardID string) ([]byte, *Response) {
|
||||||
|
r, err := c.DoAPIGet(c.GetBoardRoute(boardID)+"/archive/export", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildErrorResponse(r, err)
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
buf, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildErrorResponse(r, err)
|
||||||
|
}
|
||||||
|
return buf, BuildResponse(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ImportArchive(teamID string, data io.Reader) *Response {
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
part, err := writer.CreateFormFile(api.UploadFormFileKey, "file")
|
||||||
|
if err != nil {
|
||||||
|
return &Response{Error: err}
|
||||||
|
}
|
||||||
|
if _, err = io.Copy(part, data); err != nil {
|
||||||
|
return &Response{Error: err}
|
||||||
|
}
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
opt := func(r *http.Request) {
|
||||||
|
r.Header.Add("Content-Type", writer.FormDataContentType())
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetTeamRoute(teamID)+"/archive/import", body, "", opt)
|
||||||
|
if err != nil {
|
||||||
|
return BuildErrorResponse(r, err)
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
return BuildResponse(r)
|
||||||
|
}
|
||||||
|
@ -268,7 +268,7 @@ func (th *TestHelper) InitBasic() *TestHelper {
|
|||||||
th.RegisterAndLogin(th.Client, user1Username, "user1@sample.com", password, "")
|
th.RegisterAndLogin(th.Client, user1Username, "user1@sample.com", password, "")
|
||||||
|
|
||||||
// get token
|
// get token
|
||||||
team, resp := th.Client.GetTeam("0")
|
team, resp := th.Client.GetTeam(model.GlobalTeamID)
|
||||||
th.CheckOK(resp)
|
th.CheckOK(resp)
|
||||||
require.NotNil(th.T, team)
|
require.NotNil(th.T, team)
|
||||||
require.NotNil(th.T, team.SignupToken)
|
require.NotNil(th.T, team.SignupToken)
|
||||||
|
66
server/integrationtests/export_test.go
Normal file
66
server/integrationtests/export_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package integrationtests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mattermost/focalboard/server/model"
|
||||||
|
"github.com/mattermost/focalboard/server/utils"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExportBoard(t *testing.T) {
|
||||||
|
t.Run("export single board", func(t *testing.T) {
|
||||||
|
th := SetupTestHelper(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
board := &model.Board{
|
||||||
|
ID: utils.NewID(utils.IDTypeBoard),
|
||||||
|
TeamID: "test-team",
|
||||||
|
Title: "Export Test Board",
|
||||||
|
CreatedBy: th.GetUser1().ID,
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
CreateAt: utils.GetMillis(),
|
||||||
|
UpdateAt: utils.GetMillis(),
|
||||||
|
}
|
||||||
|
|
||||||
|
block := model.Block{
|
||||||
|
ID: utils.NewID(utils.IDTypeCard),
|
||||||
|
ParentID: board.ID,
|
||||||
|
Type: model.TypeCard,
|
||||||
|
BoardID: board.ID,
|
||||||
|
Title: "Test card # for export",
|
||||||
|
CreatedBy: th.GetUser1().ID,
|
||||||
|
CreateAt: utils.GetMillis(),
|
||||||
|
UpdateAt: utils.GetMillis(),
|
||||||
|
}
|
||||||
|
|
||||||
|
babs := &model.BoardsAndBlocks{
|
||||||
|
Boards: []*model.Board{board},
|
||||||
|
Blocks: []model.Block{block},
|
||||||
|
}
|
||||||
|
|
||||||
|
babs, resp := th.Client.CreateBoardsAndBlocks(babs)
|
||||||
|
th.CheckOK(resp)
|
||||||
|
|
||||||
|
// export the board to an in-memory archive file
|
||||||
|
buf, resp := th.Client.ExportBoardArchive(babs.Boards[0].ID)
|
||||||
|
th.CheckOK(resp)
|
||||||
|
require.NotNil(t, buf)
|
||||||
|
|
||||||
|
// import the archive file to team 0
|
||||||
|
resp = th.Client.ImportArchive(model.GlobalTeamID, bytes.NewReader(buf))
|
||||||
|
th.CheckOK(resp)
|
||||||
|
require.NoError(t, resp.Error)
|
||||||
|
|
||||||
|
// check for test card
|
||||||
|
boardsImported, err := th.Server.App().GetBoardsForUserAndTeam(th.GetUser1().ID, model.GlobalTeamID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, boardsImported, 1)
|
||||||
|
boardImported := boardsImported[0]
|
||||||
|
blocksImported, err := th.Server.App().GetBlocksForBoard(boardImported.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, blocksImported, 1)
|
||||||
|
require.Equal(t, block.Title, blocksImported[0].Title)
|
||||||
|
})
|
||||||
|
}
|
@ -10,6 +10,7 @@ import {Utils} from './utils'
|
|||||||
import {IAppWindow} from './types'
|
import {IAppWindow} from './types'
|
||||||
|
|
||||||
declare let window: IAppWindow
|
declare let window: IAppWindow
|
||||||
|
const hashSignToken = '___hash_sign___'
|
||||||
|
|
||||||
class CsvExporter {
|
class CsvExporter {
|
||||||
static exportTableCsv(board: Board, activeView: BoardView, cards: Card[], intl: IntlShape, view?: BoardView): void {
|
static exportTableCsv(board: Board, activeView: BoardView, cards: Card[], intl: IntlShape, view?: BoardView): void {
|
||||||
@ -28,8 +29,9 @@ class CsvExporter {
|
|||||||
csvContent += encodedRow + '\r\n'
|
csvContent += encodedRow + '\r\n'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const encodedUri = encodeURI(csvContent).replace(hashSignToken, '%23')
|
||||||
|
|
||||||
const filename = `${Utils.sanitizeFilename(viewToExport.title || 'Untitled')}.csv`
|
const filename = `${Utils.sanitizeFilename(viewToExport.title || 'Untitled')}.csv`
|
||||||
const encodedUri = encodeURI(csvContent)
|
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.style.display = 'none'
|
link.style.display = 'none'
|
||||||
link.setAttribute('href', encodedUri)
|
link.setAttribute('href', encodedUri)
|
||||||
@ -47,7 +49,7 @@ class CsvExporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static encodeText(text: string): string {
|
private static encodeText(text: string): string {
|
||||||
return text.replace(/"/g, '""')
|
return text.replace(/"/g, '""').replace(/#/g, hashSignToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static generateTableArray(board: Board, cards: Card[], viewToExport: BoardView, intl: IntlShape): string[][] {
|
private static generateTableArray(board: Board, cards: Card[], viewToExport: BoardView, intl: IntlShape): string[][] {
|
||||||
|
Reference in New Issue
Block a user