Add Export/import board (#403)
* Resolve GH-261 * fix eslint * Address comments * update mocks * add test
This commit is contained in:
parent
8a3b4cacb2
commit
e2dd9a978a
11 changed files with 135 additions and 15 deletions
|
@ -597,16 +597,19 @@ func (a *API) handleExport(w http.ResponseWriter, r *http.Request) {
|
|||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
query := r.URL.Query()
|
||||
rootID := query.Get("root_id")
|
||||
container, err := a.getContainer(r)
|
||||
if err != nil {
|
||||
noContainerErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
blocks, err := a.app().GetAllBlocks(*container)
|
||||
if err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
blocks := []model.Block{}
|
||||
if rootID == "" {
|
||||
blocks, err = a.app().GetAllBlocks(*container)
|
||||
} else {
|
||||
blocks, err = a.app().GetBlocksWithRootID(*container, rootID)
|
||||
}
|
||||
|
||||
log.Printf("%d raw block(s)", len(blocks))
|
||||
|
|
|
@ -17,6 +17,10 @@ func (a *App) GetBlocks(c store.Container, parentID string, blockType string) ([
|
|||
return a.store.GetBlocksWithParent(c, parentID)
|
||||
}
|
||||
|
||||
func (a *App) GetBlocksWithRootID(c store.Container, rootID string) ([]model.Block, error) {
|
||||
return a.store.GetBlocksWithRootID(c, rootID)
|
||||
}
|
||||
|
||||
func (a *App) GetRootID(c store.Container, blockID string) (string, error) {
|
||||
return a.store.GetRootID(c, blockID)
|
||||
}
|
||||
|
|
|
@ -165,6 +165,21 @@ func (mr *MockStoreMockRecorder) GetBlocksWithParentAndType(arg0, arg1, arg2 int
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithParentAndType", reflect.TypeOf((*MockStore)(nil).GetBlocksWithParentAndType), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// GetBlocksWithRootID mocks base method.
|
||||
func (m *MockStore) GetBlocksWithRootID(arg0 store.Container, arg1 string) ([]model.Block, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetBlocksWithRootID", arg0, arg1)
|
||||
ret0, _ := ret[0].([]model.Block)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetBlocksWithRootID indicates an expected call of GetBlocksWithRootID.
|
||||
func (mr *MockStoreMockRecorder) GetBlocksWithRootID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithRootID", reflect.TypeOf((*MockStore)(nil).GetBlocksWithRootID), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetBlocksWithType mocks base method.
|
||||
func (m *MockStore) GetBlocksWithType(arg0 store.Container, arg1 string) ([]model.Block, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -74,6 +74,35 @@ func (s *SQLStore) GetBlocksWithParent(c store.Container, parentID string) ([]mo
|
|||
return blocksFromRows(rows)
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBlocksWithRootID(c store.Container, rootID string) ([]model.Block, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select(
|
||||
"id",
|
||||
"parent_id",
|
||||
"root_id",
|
||||
"modified_by",
|
||||
s.escapeField("schema"),
|
||||
"type",
|
||||
"title",
|
||||
"COALESCE(fields, '{}')",
|
||||
"create_at",
|
||||
"update_at",
|
||||
"delete_at",
|
||||
).
|
||||
From(s.tablePrefix + "blocks").
|
||||
Where(sq.Eq{"root_id": rootID}).
|
||||
Where(sq.Eq{"coalesce(workspace_id, '0')": c.WorkspaceID})
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
log.Printf(`GetBlocksWithRootID ERROR: %v`, err)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return blocksFromRows(rows)
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBlocksWithType(c store.Container, blockType string) ([]model.Block, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select(
|
||||
|
|
|
@ -13,6 +13,7 @@ type Container struct {
|
|||
type Store interface {
|
||||
GetBlocksWithParentAndType(c Container, parentID string, blockType string) ([]model.Block, error)
|
||||
GetBlocksWithParent(c Container, parentID string) ([]model.Block, error)
|
||||
GetBlocksWithRootID(c Container, rootID string) ([]model.Block, error)
|
||||
GetBlocksWithType(c Container, blockType string) ([]model.Block, error)
|
||||
GetSubTree2(c Container, blockID string) ([]model.Block, error)
|
||||
GetSubTree3(c Container, blockID string) ([]model.Block, error)
|
||||
|
|
|
@ -59,6 +59,11 @@ func StoreTestBlocksStore(t *testing.T, setup func(t *testing.T) (store.Store, f
|
|||
defer tearDown()
|
||||
testGetBlocksWithType(t, store, container)
|
||||
})
|
||||
t.Run("GetBlocksWithRootID", func(t *testing.T) {
|
||||
store, tearDown := setup(t)
|
||||
defer tearDown()
|
||||
testGetBlocksWithRootID(t, store, container)
|
||||
})
|
||||
}
|
||||
|
||||
func testInsertBlock(t *testing.T, store store.Store, container store.Container) {
|
||||
|
@ -656,3 +661,64 @@ func testGetBlocksWithType(t *testing.T, store store.Store, container store.Cont
|
|||
require.Len(t, blocks, 4)
|
||||
})
|
||||
}
|
||||
|
||||
func testGetBlocksWithRootID(t *testing.T, store store.Store, container store.Container) {
|
||||
userID := "user-id"
|
||||
|
||||
blocks, err := store.GetAllBlocks(container)
|
||||
require.NoError(t, err)
|
||||
|
||||
blocksToInsert := []model.Block{
|
||||
{
|
||||
ID: "block1",
|
||||
ParentID: "",
|
||||
RootID: "block1",
|
||||
ModifiedBy: userID,
|
||||
Type: "test",
|
||||
},
|
||||
{
|
||||
ID: "block2",
|
||||
ParentID: "block1",
|
||||
RootID: "block1",
|
||||
ModifiedBy: userID,
|
||||
Type: "test",
|
||||
},
|
||||
{
|
||||
ID: "block3",
|
||||
ParentID: "block1",
|
||||
RootID: "block1",
|
||||
ModifiedBy: userID,
|
||||
Type: "test",
|
||||
},
|
||||
{
|
||||
ID: "block4",
|
||||
ParentID: "block1",
|
||||
RootID: "block1",
|
||||
ModifiedBy: userID,
|
||||
Type: "test2",
|
||||
},
|
||||
{
|
||||
ID: "block5",
|
||||
ParentID: "block2",
|
||||
RootID: "block2",
|
||||
ModifiedBy: userID,
|
||||
Type: "test",
|
||||
},
|
||||
}
|
||||
InsertBlocks(t, store, container, blocksToInsert)
|
||||
defer DeleteBlocks(t, store, container, blocksToInsert, "test")
|
||||
|
||||
t.Run("not existing parent", func(t *testing.T) {
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
blocks, err = store.GetBlocksWithRootID(container, "not-exists")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, 0)
|
||||
})
|
||||
|
||||
t.Run("valid parent", func(t *testing.T) {
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
blocks, err = store.GetBlocksWithRootID(container, "block1")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, 4)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,13 +8,13 @@ import {Utils} from './utils'
|
|||
import {BoardTree} from './viewModel/boardTree'
|
||||
|
||||
class Archiver {
|
||||
static async exportBoardTree(boardTree: BoardTree): Promise<void> {
|
||||
const blocks = boardTree.allBlocks
|
||||
static async exportBoardArchive(boardTree: BoardTree): Promise<void> {
|
||||
const blocks = await mutator.exportArchive(boardTree.board.id)
|
||||
this.exportArchive(blocks)
|
||||
}
|
||||
|
||||
static async exportFullArchive(): Promise<void> {
|
||||
const blocks = await mutator.exportFullArchive()
|
||||
const blocks = await mutator.exportArchive()
|
||||
this.exportArchive(blocks)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import React, {useState} from 'react'
|
|||
import {useIntl, IntlShape} from 'react-intl'
|
||||
|
||||
import {CsvExporter} from '../../csvExporter'
|
||||
import {Archiver} from '../../archiver'
|
||||
import {UserContext} from '../../user'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import IconButton from '../../widgets/buttons/iconButton'
|
||||
|
@ -104,11 +105,11 @@ const ViewHeaderActionsMenu = React.memo((props: Props) => {
|
|||
name={intl.formatMessage({id: 'ViewHeader.export-csv', defaultMessage: 'Export to CSV'})}
|
||||
onClick={() => onExportCsvTrigger(boardTree, intl)}
|
||||
/>
|
||||
{/* <Menu.Text
|
||||
<Menu.Text
|
||||
id='exportBoardArchive'
|
||||
name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export board archive'})}
|
||||
onClick={() => Archiver.exportBoardTree(boardTree)}
|
||||
/> */}
|
||||
onClick={() => Archiver.exportBoardArchive(boardTree)}
|
||||
/>
|
||||
<UserContext.Consumer>
|
||||
{(user) => (user && user.id !== 'single-user' &&
|
||||
<Menu.Text
|
||||
|
|
|
@ -615,8 +615,8 @@ class Mutator {
|
|||
// Other methods
|
||||
|
||||
// Not a mutator, but convenient to put here since Mutator wraps OctoClient
|
||||
async exportFullArchive(): Promise<IBlock[]> {
|
||||
return octoClient.exportFullArchive()
|
||||
async exportArchive(boardID?: string): Promise<IBlock[]> {
|
||||
return octoClient.exportArchive(boardID)
|
||||
}
|
||||
|
||||
// Not a mutator, but convenient to put here since Mutator wraps OctoClient
|
||||
|
|
|
@ -28,7 +28,7 @@ test('OctoClient: get blocks', async () => {
|
|||
expect(boards.length).toBe(blocks.length)
|
||||
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify(blocks)))
|
||||
boards = await octoClient.exportFullArchive()
|
||||
boards = await octoClient.exportArchive()
|
||||
expect(boards.length).toBe(blocks.length)
|
||||
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify(blocks)))
|
||||
|
|
|
@ -135,8 +135,9 @@ class OctoClient {
|
|||
return blocks
|
||||
}
|
||||
|
||||
async exportFullArchive(): Promise<IBlock[]> {
|
||||
const path = this.workspacePath() + '/blocks/export'
|
||||
// If no boardID is provided, it will export the entire archive
|
||||
async exportArchive(boardID = ''): Promise<IBlock[]> {
|
||||
const path = `${this.workspacePath()}/blocks/export?root_id=${boardID}`
|
||||
const response = await fetch(this.serverUrl + path, {headers: this.headers()})
|
||||
if (response.status !== 200) {
|
||||
return []
|
||||
|
|
Loading…
Reference in a new issue