From d862e66af452c5b72d0837c31ae54024266a32ad Mon Sep 17 00:00:00 2001 From: wiggin77 Date: Thu, 31 Mar 2022 14:04:20 -0400 Subject: [PATCH] fix archive import --- server/api/api.go | 2 +- server/api/archive.go | 12 +++++-- server/app/import.go | 18 ++++++++-- server/app/import_test.go | 69 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 server/app/import_test.go diff --git a/server/api/api.go b/server/api/api.go index c35ed69cd..650a41f70 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -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) { diff --git a/server/api/archive.go b/server/api/archive.go index 190c9c750..372da3d09 100644 --- a/server/api/archive.go +++ b/server/api/archive.go @@ -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 } diff --git a/server/app/import.go b/server/app/import.go index 18fce2ad7..945f5b077 100644 --- a/server/app/import.go +++ b/server/app/import.go @@ -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 diff --git a/server/app/import_test.go b/server/app/import_test.go new file mode 100644 index 000000000..f05fcafba --- /dev/null +++ b/server/app/import_test.go @@ -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", + } + + // CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) + th.Store.EXPECT().CreateBoardsAndBlocks(gomock.AssignableToTypeOf(&model.BoardsAndBlocks{}), "user").Return(babs, nil) + // GetMembersForBoard(boardID string) ([]*model.BoardMember, error) + th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{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"}} +`