1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-05 14:50:29 +02:00

Merge pull request #2700 from wiggin77/GH-2692_archive_import_broken

GH-2692 - Fix archive import
This commit is contained in:
Scott Bishel 2022-03-31 14:24:49 -06:00 committed by GitHub
commit d972cac4a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 102 additions and 6 deletions

View File

@ -155,7 +155,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
// archives
apiv1.HandleFunc("/boards/{boardID}/archive/export", a.sessionRequired(a.handleArchiveExportBoard)).Methods("GET")
apiv1.HandleFunc("/boards/{boardID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST")
apiv1.HandleFunc("/teams/{teamID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST")
}
func (a *API) RegisterAdminRoutes(r *mux.Router) {

View File

@ -8,6 +8,8 @@ import (
"github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
const (
@ -143,7 +145,7 @@ func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
}
func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /api/v1/boards/{boardID}/archive/import archiveImport
// swagger:operation POST /api/v1/teams/{teamID}/archive/import archiveImport
//
// Import an archive of boards.
//
@ -153,9 +155,9 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
// consumes:
// - multipart/form-data
// parameters:
// - name: boardID
// - name: teamID
// in: path
// description: Workspace ID
// description: Team ID
// required: true
// type: string
// - name: file
@ -198,6 +200,10 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
}
if err := a.app.ImportArchive(file, opt); err != nil {
a.logger.Debug("Error importing archive",
mlog.String("team_id", teamID),
mlog.Err(err),
)
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}

View File

@ -120,11 +120,12 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
var boardID string
lineNum := 1
firstLine := true
for {
line, errRead := readLine(lineReader)
if len(line) != 0 {
var skip bool
if lineNum == 1 {
if firstLine {
// first line might be a header tag (old archive format)
if strings.HasPrefix(string(line), legacyFileBegin) {
skip = true
@ -138,7 +139,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
}
// first line must be a board
if lineNum == 1 && archiveLine.Type == "block" {
if firstLine && archiveLine.Type == "block" {
archiveLine.Type = "board_block"
}
@ -179,6 +180,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
default:
return "", model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type)
}
firstLine = false
}
}
@ -204,6 +206,18 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
return "", fmt.Errorf("error inserting archive blocks: %w", err)
}
// add user to all the new boards.
for _, board := range boardsAndBlocks.Boards {
boardMember := &model.BoardMember{
BoardID: board.ID,
UserID: opt.ModifiedBy,
SchemeAdmin: true,
}
if _, err := a.AddMemberToBoard(boardMember); err != nil {
return "", fmt.Errorf("cannot add member to board: %w", err)
}
}
// find new board id
for _, board := range boardsAndBlocks.Boards {
return board.ID, nil

69
server/app/import_test.go Normal file
View File

@ -0,0 +1,69 @@
package app
import (
"bytes"
"testing"
"github.com/golang/mock/gomock"
"github.com/mattermost/focalboard/server/model"
"github.com/stretchr/testify/require"
)
func TestApp_ImportArchive(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
board := &model.Board{
ID: "d14b9df9-1f31-4732-8a64-92bc7162cd28",
TeamID: "test-team",
Title: "Cross-Functional Project Plan",
}
block := model.Block{
ID: "2c1873e0-1484-407d-8b2c-3c3b5a2a9f9e",
ParentID: board.ID,
Type: model.TypeView,
BoardID: board.ID,
}
babs := &model.BoardsAndBlocks{
Boards: []*model.Board{board},
Blocks: []model.Block{block},
}
boardMember := &model.BoardMember{
BoardID: board.ID,
UserID: "user",
}
t.Run("import asana archive", func(t *testing.T) {
r := bytes.NewReader([]byte(asana))
opts := model.ImportArchiveOptions{
TeamID: "test-team",
ModifiedBy: "user",
}
th.Store.EXPECT().CreateBoardsAndBlocks(gomock.AssignableToTypeOf(&model.BoardsAndBlocks{}), "user").Return(babs, nil)
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{boardMember}, nil)
th.Store.EXPECT().GetBoard(board.ID).Return(board, nil)
th.Store.EXPECT().GetMemberForBoard(board.ID, "user").Return(boardMember, nil)
err := th.App.ImportArchive(r, opts)
require.NoError(t, err, "import archive should not fail")
})
}
//nolint:lll
const asana = `{"version":1,"date":1614714686842}
{"type":"block","data":{"id":"d14b9df9-1f31-4732-8a64-92bc7162cd28","fields":{"icon":"","description":"","cardProperties":[{"id":"3bdcbaeb-bc78-4884-8531-a0323b74676a","name":"Section","type":"select","options":[{"id":"d8d94ef1-5e74-40bb-8be5-fc0eb3f47732","value":"Planning","color":"propColorGray"},{"id":"454559bb-b788-4ff6-873e-04def8491d2c","value":"Milestones","color":"propColorBrown"},{"id":"deaab476-c690-48df-828f-725b064dc476","value":"Next steps","color":"propColorOrange"},{"id":"2138305a-3157-461c-8bbe-f19ebb55846d","value":"Comms Plan","color":"propColorYellow"}]}]},"createAt":1614714686836,"updateAt":1614714686836,"deleteAt":0,"schema":1,"parentId":"","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"board","title":"Cross-Functional Project Plan"}}
{"type":"block","data":{"id":"2c1873e0-1484-407d-8b2c-3c3b5a2a9f9e","fields":{"sortOptions":[],"visiblePropertyIds":[],"visibleOptionIds":[],"hiddenOptionIds":[],"filter":{"operation":"and","filters":[]},"cardOrder":[],"columnWidths":{},"viewType":"board"},"createAt":1614714686840,"updateAt":1614714686840,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"view","title":"Board View"}}
{"type":"block","data":{"id":"520c332b-adf5-4a32-88ab-43655c8b6aa2","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"d8d94ef1-5e74-40bb-8be5-fc0eb3f47732"},"contentOrder":["deb3966c-6d56-43b1-8e95-36806877ce81"]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[READ ME] - Instructions for using this template"}}
{"type":"block","data":{"id":"deb3966c-6d56-43b1-8e95-36806877ce81","fields":{},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"520c332b-adf5-4a32-88ab-43655c8b6aa2","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"text","title":"This project template is set up in List View with sections and Asana-created Custom Fields to help you track your team's work. We've provided some example content in this template to get you started, but you should add tasks, change task names, add more Custom Fields, and change any other info to make this project your own.\n\nSend feedback about this template: https://asa.na/templatesfeedback"}}
{"type":"block","data":{"id":"be791f66-a5e5-4408-82f6-cb1280f5bc45","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"d8d94ef1-5e74-40bb-8be5-fc0eb3f47732"},"contentOrder":["2688b31f-e7ff-4de1-87ae-d4b5570f8712"]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"Redesign the landing page of our website"}}
{"type":"block","data":{"id":"2688b31f-e7ff-4de1-87ae-d4b5570f8712","fields":{},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"be791f66-a5e5-4408-82f6-cb1280f5bc45","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"text","title":"Redesign the landing page to focus on the main persona."}}
{"type":"block","data":{"id":"98f74948-1700-4a3c-8cc2-8bb632499def","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"454559bb-b788-4ff6-873e-04def8491d2c"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Consider trying a new email marketing service"}}
{"type":"block","data":{"id":"142fba5d-05e6-4865-83d9-b3f54d9de96e","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"454559bb-b788-4ff6-873e-04def8491d2c"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Budget finalization"}}
{"type":"block","data":{"id":"ca6670b1-b034-4e42-8971-c659b478b9e0","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"deaab476-c690-48df-828f-725b064dc476"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Find a venue for the holiday party"}}
{"type":"block","data":{"id":"db1dd596-0999-4741-8b05-72ca8e438e31","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"deaab476-c690-48df-828f-725b064dc476"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Approve campaign copy"}}
{"type":"block","data":{"id":"16861c05-f31f-46af-8429-80a87b5aa93a","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"2138305a-3157-461c-8bbe-f19ebb55846d"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Send out updated attendee list"}}
`

View File

@ -34,6 +34,11 @@ func TestApp_initializeTemplates(t *testing.T) {
Blocks: []model.Block{block},
}
boardMember := &model.BoardMember{
BoardID: board.ID,
UserID: "test-user",
}
t.Run("Needs template init", func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
@ -42,6 +47,8 @@ func TestApp_initializeTemplates(t *testing.T) {
th.Store.EXPECT().RemoveDefaultTemplates([]*model.Board{}).Return(nil)
th.Store.EXPECT().CreateBoardsAndBlocks(gomock.Any(), gomock.Any()).AnyTimes().Return(boardsAndBlocks, nil)
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{}, nil)
th.Store.EXPECT().GetBoard(board.ID).AnyTimes().Return(board, nil)
th.Store.EXPECT().GetMemberForBoard(gomock.Any(), gomock.Any()).AnyTimes().Return(boardMember, nil)
th.FilesBackend.On("WriteFile", mock.Anything, mock.Anything).Return(int64(1), nil)