Add clone functionality to Reporter web app

This commit is contained in:
Emmy Leke
2024-06-13 11:03:10 +01:00
committed by EmmyMay
parent 944c2bcfcd
commit 53e0a53888
12 changed files with 173 additions and 46 deletions
@@ -28,16 +28,27 @@
size="lg"
size-confirm="lg"
variant="danger"
:disabled="deleteDisabled || processingDelete"
:disabled="deleteDisabled || processingDelete || processing"
:processing="processingDelete"
:text="$t('general:label.delete')"
:borderless="false"
@confirmed="$emit('delete')"
/>
<c-button-submit
data-test-id="button-clone"
:disabled="cloneDisabled || processingClone || processing"
:processing="processingClone"
variant="light"
:text="$t('general:label.clone')"
class="text-nowrap"
size="lg"
@submit="$emit('clone')"
/>
<c-button-submit
data-test-id="button-save"
:disabled="saveDisabled || processing"
:disabled="saveDisabled || processingSave || processing"
:processing="processingSave"
:text="$t('general:label.save')"
size="lg"
@@ -79,6 +90,10 @@ export default {
type: Boolean,
},
cloneDisabled: {
type: Boolean,
},
processing: {
type: Boolean,
},
@@ -91,6 +106,10 @@ export default {
type: Boolean,
required: false,
},
processingClone: {
type: Boolean,
},
},
}
</script>
@@ -72,7 +72,7 @@ export default {
return reports.map(({ reportID, handle, meta: { name = '' } }) => {
return {
page: { pageID: reportID, name: 'report.view', title: name || handle },
page: { pageID: reportID, name: this.$route.name, title: name || handle },
params: { reportID },
}
})
@@ -90,6 +90,14 @@ export default {
},
},
created () {
this.$root.$on('refetch:reports', this.fetchReports)
},
beforeDestroy () {
this.$root.$off('refetch:reports', this.fetchReports)
},
methods: {
fetchReports () {
this.$SystemAPI.reportList()
@@ -48,6 +48,7 @@ import {
faQuestion,
faEllipsisV,
faTools,
faClone,
} from '@fortawesome/free-solid-svg-icons'
import {
@@ -112,4 +113,5 @@ library.add(
faQuestion,
faEllipsisV,
faTools,
faClone,
)
@@ -53,3 +53,4 @@ Vue.component('c-permissions-button', components.CPermissionsButton)
Vue.component('c-input-confirm', components.CInputConfirm)
Vue.component('c-button-submit', components.CButtonSubmit)
Vue.component('c-input-select', components.CInputSelect)
Vue.component('c-form-table-wrapper', components.CFormTableWrapper)
+28 -1
View File
@@ -6,6 +6,7 @@ export default {
processing: false,
processingSave: false,
processingDelete: false,
processingClone: false,
report: undefined,
}
},
@@ -21,7 +22,9 @@ export default {
})
.catch(this.toastErrorHandler(this.$t('notification:report.fetchFailed')))
.finally(() => {
this.processing = false
setTimeout(() => {
this.processing = false
}, 400)
})
},
@@ -90,5 +93,29 @@ export default {
this.processingDelete = false
})
},
handleClone (report) {
this.processing = true
this.processingClone = true
const { meta, sources, blocks, scenarios, labels } = report
const cloneSuffix = `${meta.name} (${this.$t('general:cloneSuffix')})`
let name = meta.name
if (!this.initialReportState || this.initialReportState.meta.name === name) {
name = cloneSuffix
}
return this.$SystemAPI.reportCreate({ handle: '', meta: { ...meta, name }, sources, blocks, scenarios, labels })
.then(report => {
report = new system.Report(report)
this.toastSuccess(this.$t('notification:report.created'))
return report
})
.catch(this.toastErrorHandler(this.$t('notification:report.createFailed')))
.finally(() => {
this.processing = false
this.processingClone = false
})
},
},
}
@@ -2,7 +2,10 @@
<div
class="d-flex overflow-auto p-2 w-100"
>
<portal to="topbar-title">
<portal
v-if="!fetchingReport"
to="topbar-title"
>
{{ pageTitle }}
</portal>
@@ -13,10 +16,10 @@
:options="scenarioOptions"
:get-option-key="getOptionKey"
:placeholder="$t('builder:pick-scenario')"
:disabled="processing || fetchingReport"
size="sm"
@input="refreshReport()"
/>
<b-input-group-append>
<b-button
v-b-tooltip.noninteractive.hover="{ title: $t('builder:tooltip.configure-scenarios'), container: '#body' }"
@@ -41,7 +44,6 @@
>
{{ $t('builder:datasources.label') }}
</b-button>
<b-button-group
size="sm"
>
@@ -72,8 +74,15 @@
</b-button-group>
</portal>
<div
v-if="fetchingReport"
class="d-flex align-items-center justify-content-center w-100 h-100"
>
<b-spinner />
</div>
<grid
v-if="report && canRead && showReport"
v-if="report && canRead && showReport && !fetchingReport"
:blocks.sync="reportBlocks"
editable
@item-updated="onBlockUpdated"
@@ -97,7 +106,6 @@
class="text-warning"
/>
</div>
<b-button-group>
<b-button
v-b-tooltip.noninteractive.hover="{ title: $t('builder:tooltip.add.displayElement'), container: '#body' }"
@@ -109,7 +117,6 @@
:icon="['fas', 'plus']"
/>
</b-button>
<b-button
v-b-tooltip.noninteractive.hover="{ title: $t('builder:tooltip.edit.block'), container: '#body' }"
variant="outline-light"
@@ -121,7 +128,6 @@
/>
</b-button>
</b-button-group>
<c-input-confirm
:tooltip="$t('builder:tooltip.delete.block')"
show-icon
@@ -130,7 +136,6 @@
@confirmed="deleteBlock(index)"
/>
</div>
<block
v-if="block"
:index="index"
@@ -142,7 +147,6 @@
</div>
</template>
</grid>
<b-modal
:title="$t('builder:block.configuration')"
:ok-title="$t('builder:save-button')"
@@ -176,7 +180,6 @@
:placeholder="$t('builder:block.title')"
/>
</b-form-group>
<b-form-group
:label="$t('builder:description')"
label-class="text-primary"
@@ -186,7 +189,6 @@
:placeholder="$t('builder:block.description')"
/>
</b-form-group>
<b-form-group
:label="$t('builder:layout')"
label-class="text-primary"
@@ -199,7 +201,6 @@
/>
</b-form-group>
</b-tab>
<b-tab
:active="!!currentBlock.elements.length"
:title="$t('builder:elements')"
@@ -218,9 +219,7 @@
:icon="['fas', 'bars']"
class="text-secondary grab"
/>
</template>
<template #configurator>
</template> <template #configurator>
<display-element-configurator
v-if="currentDisplayElement"
:display-element.sync="currentDisplayElement"
@@ -233,7 +232,6 @@
</b-tab>
</b-tabs>
</b-modal>
<b-modal
v-model="datasources.showConfigurator"
:title="$t('builder:datasources.label')"
@@ -261,7 +259,6 @@
{{ datasourceLabel(step, datasources.currentIndex) }}
</span>
</template>
<template #configurator>
<component
:is="getDatasourceComponent(datasources.tempItems[datasources.currentIndex])"
@@ -273,7 +270,6 @@
/>
</template>
</configurator>
<template #modal-footer>
<c-button-submit
data-test-id="button-save"
@@ -284,7 +280,6 @@
/>
</template>
</b-modal>
<b-modal
v-model="displayElements.showSelector"
size="lg"
@@ -299,7 +294,6 @@
@select="addDisplayElement"
/>
</b-modal>
<b-modal
v-model="datasources.showSelector"
size="lg"
@@ -315,7 +309,6 @@
@select="addDatasource"
/>
</b-modal>
<b-modal
v-model="scenarios.showConfigurator"
size="xl"
@@ -342,7 +335,6 @@
{{ label }}
</span>
</template>
<template #configurator>
<scenario-configurator
v-if="currentScenario"
@@ -353,7 +345,6 @@
</template>
</configurator>
</b-modal>
<portal to="report-toolbar">
<editor-toolbar
:back-link="{ name: 'report.list' }"
@@ -362,12 +353,15 @@
:processing="processing"
:processing-save="processingSave"
:processing-delete="processingDelete"
:processing-clone="processingClone"
@clone="handleReportCloning"
@delete="handleDelete"
@save="handleReportSave"
>
<b-button
variant="light"
size="lg"
:disabled="processing"
@click="createBlock"
>
<font-awesome-icon
@@ -427,6 +421,8 @@ export default {
processing: false,
processingSave: false,
processingDelete: false,
processingClone: false,
fetchingReport: false,
showReport: true,
@@ -665,15 +661,23 @@ export default {
handler (reportID) {
this.unsavedBlocks.clear()
this.scenarios.selected = undefined
this.reportBlocks = []
this.report = undefined
if (reportID) {
this.processing = true
this.fetchingReport = true
this.fetchReport(this.reportID)
.then(() => {
this.mapBlocks()
}).finally(() => {
this.processing = false
}).catch(() => {
this.toastErrorHandler(this.$t('notification:report.loadFailed'))
})
.finally(() => {
setTimeout(() => {
this.fetchingReport = false
this.processing = false
}, 400)
})
}
},
@@ -853,6 +857,13 @@ export default {
})
},
handleReportCloning () {
this.handleClone(this.report).then(({ reportID }) => {
this.$root.$emit('refetch:reports')
this.$router.push({ name: 'report.builder', params: { reportID } })
})
},
mapBlocks () {
this.reportBlocks = this.report.blocks.map(({ xywh, ...p }, i) => {
const [x, y, w, h] = xywh
@@ -26,6 +26,7 @@
:icon="['fas', 'tools']"
/>
</b-button>
<b-button
v-b-tooltip.noninteractive.hover="{ title: $t('tooltip.view-report'), container: '#body' }"
variant="primary"
@@ -159,15 +160,25 @@
</b-row>
</b-container>
<div
v-else
class="d-flex align-items-center justify-content-center w-100 h-100"
>
<b-spinner />
</div>
<portal to="report-toolbar">
<editor-toolbar
:back-link="{ name: 'report.list' }"
:hide-delete="isNew"
:delete-disabled="!canDelete"
:save-disabled="!canSave"
:clone-disabled="!canSave"
:processing="processing"
:processing-save="processingSave"
:processing-delete="processingDelete"
:processing-clone="processingClone"
@clone="handleReportCloning"
@delete="handleDelete"
@save="handleSave"
/>
@@ -319,6 +330,12 @@ export default {
next(!isEqual(reportState, initialReportState) ? window.confirm(this.$t('unsavedChanges')) : true)
},
handleReportCloning () {
this.handleClone(this.report).then(({ reportID }) => {
this.$router.push({ name: 'report.builder', params: { reportID } })
})
},
},
}
</script>
+19 -8
View File
@@ -122,6 +122,13 @@
/>
</b-dropdown-item>
<b-dropdown-item-button @click="handleReportCloning(r)">
<font-awesome-icon
:icon="['fa','clone']"
/>
{{ $t('general:resourceList.clone') }}
</b-dropdown-item-button>
<c-input-confirm
v-if="r.canDeleteReport"
:processing="processingDelete"
@@ -145,6 +152,7 @@
<script>
import { mapGetters } from 'vuex'
import listHelpers from 'corteza-webapp-reporter/src/mixins/listHelpers'
import report from 'corteza-webapp-reporter/src/mixins/report'
import { components } from '@cortezaproject/corteza-vue'
const { CResourceList } = components
@@ -161,6 +169,7 @@ export default {
mixins: [
listHelpers,
report,
],
data () {
@@ -224,14 +233,10 @@ export default {
methods: {
viewReport ({ reportID, canReadReport = false }) {
if (reportID) {
if (canReadReport) {
this.$router.push({
name: 'report.view',
params: { reportID },
})
} else {
this.toastDanger(this.$t('notification:report.notAllowed.read'))
}
this.$router.push({
name: 'report.view',
params: { reportID },
})
}
},
@@ -252,6 +257,12 @@ export default {
this.processingDelete = false
})
},
handleReportCloning (report) {
this.handleClone(report).then(({ reportID }) => {
this.viewReport({ reportID })
})
},
},
}
</script>
+27 -7
View File
@@ -2,7 +2,10 @@
<div
class="d-flex overflow-auto p-2 w-100"
>
<portal to="topbar-title">
<portal
v-if="!fetchingReport"
to="topbar-title"
>
{{ pageTitle }}
</portal>
@@ -13,6 +16,7 @@
:options="scenarioOptions"
:get-option-key="getOptionKey"
:placeholder="$t('pick-scenario')"
:disabled="processing || fetchingReport"
size="sm"
style="max-width: 300px; min-width: 150px;"
@input="refreshReport()"
@@ -47,8 +51,15 @@
</b-button-group>
</portal>
<div
v-if="fetchingReport"
class="d-flex align-items-center justify-content-center w-100 h-100"
>
<b-spinner />
</div>
<grid
v-if="report && canRead && showReport"
v-if="report && canRead && showReport && !fetchingReport"
:blocks="report.blocks"
>
<template
@@ -86,6 +97,7 @@ export default {
return {
processing: false,
showReport: true,
fetchingReport: false,
report: undefined,
dataframes: [],
@@ -93,6 +105,7 @@ export default {
scenarios: {
selected: undefined,
},
}
},
@@ -144,9 +157,21 @@ export default {
immediate: true,
handler (reportID) {
this.scenarios.selected = undefined
this.report = undefined
if (reportID) {
this.fetchingReport = true
this.processing = true
this.fetchReport(reportID)
.catch(() => {
this.toastErrorHandler(this.$t('notification:report.loadFailed'))
})
.finally(() => {
setTimeout(() => {
this.fetchingReport = false
this.processing = false
}, 400)
})
}
},
},
@@ -161,8 +186,6 @@ export default {
},
async fetchReport (reportID) {
this.processing = true
this.report = undefined
return this.$SystemAPI.reportRead({ reportID })
@@ -175,9 +198,6 @@ export default {
})
})
.catch(this.toastErrorHandler(this.$t('notification:report.fetchFailed')))
.finally(() => {
this.processing = false
})
},
getOptionKey (scenario) {
@@ -6,8 +6,9 @@
:clearable="clearable"
:options="options"
:searchable="searchable"
:disabled="disabled"
:calculate-position="calculateDropdownPosition"
:append-to-body="appendToBody"
:append-to-body="appendToBody"
class="bg-white rounded"
:class="sizeClass"
@search="onSearch"
@@ -73,6 +74,11 @@ export default {
type: String,
default: 'md',
},
disabled: {
type: Boolean,
default: false,
},
},
computed: {
@@ -17,6 +17,7 @@ label:
openLinkInNewTab: Open link in a new tab
save: Save
saveAndClose: Save and close
clone: Save as copy
today: Today
urlPlaceholder: https://example.tld
default: Default
@@ -34,8 +35,11 @@ resourceList:
single: 'One {{data}}'
single_plural: '{{count}} {{data}}'
recordsPerPage: Records per page
clone: Clone
themes:
labels:
light: Light
dark: Dark
cloneSuffix: copy
@@ -13,6 +13,7 @@ namespace:
fetch-failed: Failed to fetch namespaces
report:
createFailed: Failed to create report
loadFailed: Loading the report failed
created: Report created
delete: Report deleted
deleteFailed: Failed to delete report