Add clone functionality to Reporter web app
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user