Skip to content

Commit 423bd7e

Browse files
committed
fix(backport): add missing import/export changes
- Wire up BoardExportModal in AppNavigationBoard - Add JSON export to BoardApi.exportBoard - Add importBoard store action - Add board import/export Cypress tests - Use showMessage instead of showLoading (not in @nextcloud/dialogs 5.x) Signed-off-by: silver <s.szmajduch@posteo.de> Assisted-by: ClaudeCode:claude-opus-5.5
1 parent ba313d0 commit 423bd7e

5 files changed

Lines changed: 120 additions & 4 deletions

File tree

‎cypress/e2e/boardFeatures.js‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55
import { randUser } from '../utils/index.js'
6+
import { sampleBoard } from '../utils/sampleBoard.js'
67
const user = randUser()
78
const recipient = randUser()
89

@@ -58,3 +59,81 @@ describe('Board', function() {
5859
.should('be.visible')
5960
})
6061
})
62+
63+
describe('Board export', function() {
64+
before(function() {
65+
cy.createUser(user)
66+
})
67+
68+
it('Exports a board as JSON', function() {
69+
const boardName = 'Export JSON board'
70+
const board = sampleBoard(boardName)
71+
cy.createExampleBoard({ user, board }).then((board) => {
72+
const boardId = board.id
73+
cy.visit(`/apps/deck/board/${boardId}`)
74+
cy.get('.app-navigation__list .app-navigation-entry:contains("' + boardName + '")')
75+
.parent()
76+
.find('button[aria-label="Actions"]')
77+
.click()
78+
cy.get('button:contains("Export board")')
79+
.click()
80+
cy.get('.modal-container .checkbox-radio-switch__text:contains("Export as JSON")')
81+
.click()
82+
cy.get('.modal-container button:contains("Export")')
83+
.click()
84+
85+
const downloadsFolder = Cypress.config('downloadsFolder')
86+
cy.readFile(`${downloadsFolder}/${boardName}.json`)
87+
})
88+
})
89+
90+
it('Exports a board as CSV', function() {
91+
const boardName = 'Export CSV board'
92+
const board = sampleBoard(boardName)
93+
cy.createExampleBoard({ user, board }).then((board) => {
94+
const boardId = board.id
95+
cy.visit(`/apps/deck/board/${boardId}`)
96+
cy.get('.app-navigation__list .app-navigation-entry:contains("' + boardName + '")')
97+
.parent()
98+
.find('button[aria-label="Actions"]')
99+
.click()
100+
cy.get('button:contains("Export board")')
101+
.click()
102+
cy.get('.modal-container .checkbox-radio-switch__text:contains("Export as CSV")')
103+
.click()
104+
cy.get('.modal-container button:contains("Export")')
105+
.click()
106+
107+
const downloadsFolder = Cypress.config('downloadsFolder')
108+
cy.readFile(`${downloadsFolder}/${boardName}.csv`)
109+
})
110+
})
111+
})
112+
113+
describe('Board import', function() {
114+
before(function() {
115+
cy.createUser(user)
116+
})
117+
beforeEach(function() {
118+
cy.login(user)
119+
cy.visit('/apps/deck')
120+
})
121+
122+
it('Imports a board from JSON', function() {
123+
cy.get('#app-navigation-vue .app-navigation__list .app-navigation-entry:contains("Import board")')
124+
.should('be.visible')
125+
.click()
126+
127+
// Upload a JSON file
128+
cy.get('input[type="file"]')
129+
.selectFile([
130+
{
131+
contents: 'cypress/fixtures/import-board.json',
132+
fileName: 'import-board.json',
133+
},
134+
], { force: true })
135+
136+
cy.get('.app-navigation__list .app-navigation-entry:contains("Imported board")')
137+
.should('be.visible')
138+
})
139+
})

‎src/components/navigation/AppNavigationBoard.vue‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212
:force-display-actions="isTouchDevice"
1313
@click="onNavigate"
1414
@undo="unDelete">
15-
<NcAppNavigationIconBullet slot="icon" :color="board.color" />
15+
<template #icon>
16+
<NcAppNavigationIconBullet :color="board.color" />
17+
<BoardExportModal v-if="exportModalOpen"
18+
:board-title="board.title"
19+
@export="onExportBoard"
20+
@close="onCloseExportBoard" />
21+
</template>
1622

1723
<template #counter>
1824
<AccountIcon v-if="board.acl.length > 0" />
@@ -157,6 +163,8 @@ import { loadState } from '@nextcloud/initial-state'
157163
import { emit } from '@nextcloud/event-bus'
158164
159165
import isTouchDevice from '../../mixins/isTouchDevice.js'
166+
import BoardExportModal from './BoardExportModal.vue'
167+
import { showMessage, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs'
160168
161169
const canCreateState = loadState('deck', 'canCreate')
162170
@@ -174,6 +182,7 @@ export default {
174182
CloneIcon,
175183
CloseIcon,
176184
CheckIcon,
185+
BoardExportModal,
177186
},
178187
directives: {
179188
ClickOutside,
@@ -201,6 +210,7 @@ export default {
201210
isDueSubmenuActive: false,
202211
updateDueSetting: null,
203212
canCreate: canCreateState,
213+
exportModalOpen: false,
204214
}
205215
},
206216
computed: {
@@ -344,7 +354,7 @@ export default {
344354
},
345355
async onExportBoard(format) {
346356
this.exportModalOpen = false
347-
const loadingToast = showLoading(t('deck', 'Exporting board...'))
357+
const loadingToast = showMessage(t('deck', 'Exporting board...'), { timeout: TOAST_PERMANENT_TIMEOUT })
348358
await this.boardApi.exportBoard(this.board, format)
349359
loadingToast.hideToast()
350360
},

‎src/components/navigation/AppNavigationImportBoard.vue‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<script>
1717
import { NcAppNavigationItem } from '@nextcloud/vue'
1818
import { showError } from '../../helpers/errors.js'
19-
import { showSuccess, showLoading } from '@nextcloud/dialogs'
19+
import { showSuccess, showMessage, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs'
2020
2121
export default {
2222
name: 'AppNavigationImportBoard',
@@ -40,7 +40,7 @@ export default {
4040
async doImportBoard(event) {
4141
const file = event.target.files[0]
4242
if (file) {
43-
const loadingToast = showLoading(t('deck', 'Importing board...'))
43+
const loadingToast = showMessage(t('deck', 'Importing board...'), { timeout: TOAST_PERMANENT_TIMEOUT })
4444
const result = await this.$store.dispatch('importBoard', file)
4545
loadingToast.hideToast()
4646
if (result?.message) {

‎src/services/BoardApi.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,25 @@ export class BoardApi {
136136
return axios.get(this.url(`/boards/${board.id}/export`))
137137
.then(
138138
(response) => {
139+
if (format === 'json') {
140+
const exportData = {
141+
boards: [response.data],
142+
}
143+
const stacks = {}
144+
for (const stack of response.data.stacks) {
145+
stacks[stack.id] = stack
146+
}
147+
exportData.boards[0].stacks = stacks
148+
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
149+
const blobUrl = URL.createObjectURL(blob)
150+
const a = document.createElement('a')
151+
a.href = blobUrl
152+
a.download = response.data.title + '.json'
153+
a.click()
154+
a.remove()
155+
return Promise.resolve()
156+
}
157+
139158
const fields = { title: t('deck', 'Card title'), description: t('deck', 'Description'), stackId: t('deck', 'List name'), labels: t('deck', 'Tags'), duedate: t('deck', 'Due date'), createdAt: t('deck', 'Created'), lastModified: t('deck', 'Modified') }
140159
let row = ''
141160
Object.keys(fields).forEach(field => {

‎src/store/main.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,14 @@ export default function storeFactory() {
409409
return err
410410
}
411411
},
412+
async importBoard({ commit }, file) {
413+
try {
414+
const board = await apiClient.importBoard(file)
415+
commit('addBoard', board)
416+
} catch (err) {
417+
return err
418+
}
419+
},
412420
async cloneBoard({ commit }, boardData) {
413421
try {
414422
const newBoard = await apiClient.cloneBoard(boardData)

0 commit comments

Comments
 (0)