feat: add board import and export

Signed-off-by: Luka Trovic <luka@nextcloud.com>
This commit is contained in:
Luka Trovic
2025-04-01 18:54:12 +02:00
committed by Luka Trovic
parent 9f06a43d4b
commit 03cdc47540
12 changed files with 463 additions and 3 deletions

View File

@@ -42,6 +42,7 @@
</template>
</AppNavigationBoardCategory>
<AppNavigationAddBoard v-if="canCreate" />
<AppNavigationImportBoard v-if="canCreate" />
</template>
<template #footer>
<NcAppNavigationSettings :name="t('deck', 'Deck settings')">
@@ -116,6 +117,7 @@ import DeckIcon from './../icons/DeckIcon.vue'
import ShareVariantIcon from 'vue-material-design-icons/Share.vue'
import HelpModal from './../modals/HelpModal.vue'
import { subscribe } from '@nextcloud/event-bus'
import AppNavigationImportBoard from './AppNavigationImportBoard.vue'
const canCreateState = loadState('deck', 'canCreate')
@@ -127,6 +129,7 @@ export default {
NcButton,
AppNavigationAddBoard,
AppNavigationBoardCategory,
AppNavigationImportBoard,
NcSelect,
NcAppNavigationItem,
ArchiveIcon,

View File

@@ -15,6 +15,10 @@
<template #icon>
<NcAppNavigationIconBullet :color="board.color" />
<BoardCloneModal v-if="cloneModalOpen" :board-title="board.title" @close="onCloseCloneModal" />
<BoardExportModal v-if="exportModalOpen"
:board-title="board.title"
@export="onExportBoard"
@close="onCloseExportBoard" />
</template>
<template #counter>
@@ -161,6 +165,8 @@ import { emit } from '@nextcloud/event-bus'
import isTouchDevice from '../../mixins/isTouchDevice.js'
import BoardCloneModal from './BoardCloneModal.vue'
import BoardExportModal from './BoardExportModal.vue'
import { showLoading } from '@nextcloud/dialogs'
const canCreateState = loadState('deck', 'canCreate')
@@ -179,6 +185,7 @@ export default {
CloseIcon,
CheckIcon,
BoardCloneModal,
BoardExportModal,
},
directives: {
ClickOutside,
@@ -207,6 +214,7 @@ export default {
updateDueSetting: null,
canCreate: canCreateState,
cloneModalOpen: false,
exportModalOpen: false,
}
},
computed: {
@@ -346,7 +354,16 @@ export default {
this.updateDueSetting = null
},
actionExport() {
this.boardApi.exportBoard(this.board)
this.exportModalOpen = true
},
async onExportBoard(format) {
this.exportModalOpen = false
const loadingToast = showLoading(t('deck', 'Exporting board...'))
await this.boardApi.exportBoard(this.board, format)
loadingToast.hideToast()
},
onCloseExportBoard() {
this.exportModalOpen = false
},
onNavigate() {
if (this.isTouchDevice) {

View File

@@ -0,0 +1,55 @@
<!--
- SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div>
<NcAppNavigationItem :name="t('deck', 'Import board')" icon="icon-upload" @click.prevent.stop="startImportBoard" />
<input ref="fileInput"
type="file"
accept="application/json"
style="display: none;"
@change="doImportBoard">
</div>
</template>
<script>
import { NcAppNavigationItem } from '@nextcloud/vue'
import { showError } from '../../helpers/errors.js'
import { showSuccess, showLoading } from '@nextcloud/dialogs'
export default {
name: 'AppNavigationImportBoard',
components: { NcAppNavigationItem },
props: {
loading: {
type: Boolean,
default: false,
},
},
data() {
return {
value: '',
}
},
methods: {
startImportBoard() {
this.$refs.fileInput.value = ''
this.$refs.fileInput.click()
},
async doImportBoard(event) {
const file = event.target.files[0]
if (file) {
const loadingToast = showLoading(t('deck', 'Importing board...'))
const result = await this.$store.dispatch('importBoard', file)
loadingToast.hideToast()
if (result?.message) {
showError(result)
} else {
showSuccess(t('deck', 'Board imported successfully'))
}
}
},
},
}
</script>

View File

@@ -0,0 +1,78 @@
<!--
- SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcDialog :name="t('deck', 'Export {boardTitle}', {boardTitle: boardTitle})" @update:open="close">
<div class="modal__content">
<NcCheckboxRadioSwitch :checked.sync="exportFormat"
value="json"
type="radio"
name="board_export_format">
{{ t('deck', 'Export as JSON') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="exportFormat"
value="csv"
type="radio"
name="board_export_format">
{{ t('deck', 'Export as CSV') }}
</NcCheckboxRadioSwitch>
<p class="note">
{{ t('deck', 'Note: Only the JSON format is supported for importing back into the Deck app.') }}
</p>
</div>
<template #actions>
<NcButton @click="close">
{{ t('deck', 'Cancel') }}
</NcButton>
<NcButton type="primary" @click="exportBoard">
{{ t('deck', 'Export') }}
</NcButton>
</template>
</NcDialog>
</template>
<script>
import { NcButton, NcCheckboxRadioSwitch, NcDialog } from '@nextcloud/vue'
export default {
name: 'BoardExportModal',
components: {
NcDialog,
NcCheckboxRadioSwitch,
NcButton,
},
props: {
boardTitle: {
type: String,
default: 'Board',
},
},
data() {
return {
exportFormat: 'json',
}
},
methods: {
exportBoard() {
this.$emit('export', this.exportFormat)
this.close()
},
close() {
this.$emit('close')
},
},
}
</script>
<style scoped>
.modal__content {
margin: 20px;
}
p.note {
margin-top: 10px;
}
</style>

View File

@@ -139,10 +139,29 @@ export class BoardApi {
}
}
exportBoard(board) {
exportBoard(board, format) {
return axios.get(this.url(`/boards/${board.id}/export`))
.then(
(response) => {
if (format === 'json') {
const exportData = {
boards: [response.data],
}
const stacks = {}
for (const stack of response.data.stacks) {
stacks[stack.id] = stack
}
exportData.boards[0].stacks = stacks
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
const blobUrl = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = blobUrl
a.download = response.data.title + '.json'
a.click()
a.remove()
return Promise.resolve()
}
const fields = { title: t('deck', 'Card title'), description: t('deck', 'Description'), stackId: t('deck', 'List name'), labels: t('deck', 'Tags'), assignedUsers: t('deck', 'Assigned users'), duedate: t('deck', 'Due date'), createdAt: t('deck', 'Created'), lastModified: t('deck', 'Modified') }
let row = ''
Object.keys(fields).forEach(field => {
@@ -215,6 +234,27 @@ export class BoardApi {
})
}
importBoard(file) {
const formData = new FormData()
formData.append('file', file)
return axios.post(this.url('/boards/import'), formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
},
)
.catch((err) => {
return Promise.reject(err)
})
}
// Label API Calls
deleteLabel(id) {
return axios.delete(this.url(`/labels/${id}`))

View File

@@ -398,6 +398,14 @@ export default new Vuex.Store({
return err
}
},
async importBoard({ commit }, file) {
try {
const board = await apiClient.importBoard(file)
commit('addBoard', board)
} catch (err) {
return err
}
},
async cloneBoard({ commit }, { boardData, settings }) {
const { withCards, withAssignments, withLabels, withDueDate, moveCardsToLeftStack, restoreArchivedCards } = settings