Apply all filters from current record list when exporting, and adjust Export UI/UX
This commit is contained in:
@@ -30,7 +30,7 @@
|
||||
>
|
||||
<div class="text-nowrap flex-grow-1">
|
||||
<div
|
||||
class="wrap-with-vertical-gutters"
|
||||
class="d-flex align-items-center flex-wrap wrap-with-vertical-gutters"
|
||||
>
|
||||
<template v-if="recordListModule.canCreateRecord">
|
||||
<template v-if="inlineEditing">
|
||||
@@ -50,7 +50,7 @@
|
||||
<router-link
|
||||
v-if="!options.hideAddButton"
|
||||
data-test-id="button-add-record"
|
||||
class="btn btn-lg btn-primary float-left mr-1"
|
||||
class="btn btn-lg btn-primary mr-1"
|
||||
:to="newRecordRoute"
|
||||
>
|
||||
+ {{ $t('recordList.addRecord') }}
|
||||
@@ -59,7 +59,7 @@
|
||||
v-if="!options.hideImportButton"
|
||||
:module="recordListModule"
|
||||
:namespace="namespace"
|
||||
class="mr-1 float-left"
|
||||
class="mr-1"
|
||||
@importSuccessful="onImportSuccessful"
|
||||
/>
|
||||
</template>
|
||||
@@ -68,14 +68,12 @@
|
||||
<exporter-modal
|
||||
v-if="options.allowExport && !inlineEditing"
|
||||
:module="recordListModule"
|
||||
:record-count="pagination.count"
|
||||
:query="query"
|
||||
:prefilter="prefilter"
|
||||
:filter="filter.query"
|
||||
:selection="selected"
|
||||
:selected-all-records-count="selectedAllRecords ? pagination.count : 0"
|
||||
:selected-all-records="selectedAllRecords"
|
||||
:processing="processing"
|
||||
:preselected-fields="fields.map(({ moduleField }) => moduleField)"
|
||||
class="mr-1 float-left"
|
||||
class="mr-1"
|
||||
@export="onExport"
|
||||
/>
|
||||
|
||||
@@ -84,7 +82,7 @@
|
||||
:id="`${uniqueID}-filter-preset`"
|
||||
variant="light"
|
||||
size="lg"
|
||||
class="dropdown-toggle"
|
||||
class="mr-1"
|
||||
>
|
||||
{{ $t('recordList.filter.filters.label') }}
|
||||
</b-button>
|
||||
@@ -115,14 +113,14 @@
|
||||
v-if="!options.hideConfigureFieldsButton"
|
||||
:module="recordListModule"
|
||||
:fields="fields"
|
||||
class="mr-1 float-left"
|
||||
class="mr-1"
|
||||
@updateFields="onUpdateFields"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!options.hideSearch"
|
||||
class="flex-grow-1 w-25"
|
||||
class="d-flex align-items-center flex-grow-1 w-25"
|
||||
>
|
||||
<c-input-search
|
||||
v-model.trim="query"
|
||||
@@ -1395,7 +1393,7 @@ export default {
|
||||
query: {
|
||||
fields: e.fields,
|
||||
// url.Make already URL encodes the the values, so the filter shouldn't be encoded
|
||||
filter: this.selectedAllRecords ? '' : filter,
|
||||
filter: this.selectedAllRecords ? this.bulkQuery : filter,
|
||||
jwt: this.$auth.accessToken,
|
||||
timezone: timezone ? timezone.tzCode : undefined,
|
||||
},
|
||||
|
||||
@@ -1,664 +0,0 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-bg-variant="white"
|
||||
footer-bg-variant="white"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('recordList.export.selectFields')"
|
||||
:description="$t('recordList.export.limitations')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<field-picker
|
||||
v-if="module"
|
||||
:module="module"
|
||||
:system-fields="systemFields"
|
||||
:disabled-types="disabledTypes"
|
||||
:fields.sync="selectedFields"
|
||||
style="height: 45vh;"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="forTimezone"
|
||||
class="mb-1"
|
||||
>
|
||||
{{ $t('recordList.export.specifyTimezone') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<vue-select
|
||||
v-if="forTimezone"
|
||||
v-model="exportTimezone"
|
||||
:options="timezones"
|
||||
:get-option-key="getOptionKey"
|
||||
:calculate-position="calculateDropdownPosition"
|
||||
:placeholder="$t('recordList.export.timezonePlaceholder')"
|
||||
class="bg-white"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group>
|
||||
<b-form-radio-group
|
||||
v-model="rangeType"
|
||||
:options="rangeTypeOptions"
|
||||
stacked
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<template v-if="rangeType === 'range'">
|
||||
<b-row
|
||||
v-if="rangeType === 'range'"
|
||||
>
|
||||
<b-col
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('recordList.export.rangeBy')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="rangeBy"
|
||||
:options="rangeByOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('recordList.export.dateRange')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="range"
|
||||
:options="dateRangeOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
|
||||
<b-row
|
||||
v-if="rangeType === 'range'"
|
||||
class="mb-3"
|
||||
>
|
||||
<b-col
|
||||
cols="6"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="start"
|
||||
:state="dateRangeValid ? null : false"
|
||||
type="date"
|
||||
:max="end"
|
||||
@keydown.prevent
|
||||
/>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="6"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="end"
|
||||
:state="dateRangeValid ? null : false"
|
||||
type="date"
|
||||
:min="start"
|
||||
@keydown.prevent
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<template
|
||||
v-if="rangeType !== 'selection'"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('recordList.export.query.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="exportQuery"
|
||||
:placeholder="$t('recordList.export.query.placeholder')"
|
||||
debounce="500"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('recordList.export.filter.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="exportFilter"
|
||||
:placeholder="$t('recordList.export.filter.placeholder')"
|
||||
debounce="500"
|
||||
/>
|
||||
</b-form-group>
|
||||
</template>
|
||||
|
||||
<div
|
||||
slot="footer"
|
||||
class="d-flex"
|
||||
>
|
||||
<span
|
||||
class="my-auto"
|
||||
>
|
||||
<b-spinner
|
||||
v-if="processingCount"
|
||||
small
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
>
|
||||
{{ $t('recordList.export.recordCount', { count: getExportableCount || 0 }) }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="ml-auto">
|
||||
<c-input-processing
|
||||
v-if="allowJSON"
|
||||
:processing="processing"
|
||||
:disabled="exportDisabled"
|
||||
variant="light"
|
||||
size="lg"
|
||||
@click="doExport('json')"
|
||||
>
|
||||
{{ $t('recordList.export.json') }}
|
||||
</c-input-processing>
|
||||
<c-input-processing
|
||||
v-if="allowCSV"
|
||||
:processing="processing"
|
||||
:disabled="exportDisabled"
|
||||
variant="light"
|
||||
size="lg"
|
||||
class="ml-2"
|
||||
@click="doExport('csv')"
|
||||
>
|
||||
{{ $t('recordList.export.csv') }}
|
||||
</c-input-processing>
|
||||
</span>
|
||||
</div>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { compose, NoID } from '@cortezaproject/corteza-js'
|
||||
import FieldPicker from 'corteza-webapp-compose/src/components/Common/FieldPicker'
|
||||
import moment from 'moment'
|
||||
import tz from 'compact-timezone-list'
|
||||
import { evaluatePrefilter, queryToFilter } from 'corteza-webapp-compose/src/lib/record-filter'
|
||||
import { VueSelect } from 'vue-select'
|
||||
const fmtDate = (d) => d.format('YYYY-MM-DD')
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'block',
|
||||
},
|
||||
|
||||
components: {
|
||||
FieldPicker,
|
||||
VueSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
allowJSON: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
allowCSV: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
module: {
|
||||
type: compose.Module,
|
||||
required: true,
|
||||
},
|
||||
preselectedFields: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
recordCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
prefilter: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
selection: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
selectedAllRecordsCount: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
filterRangeType: {
|
||||
type: String,
|
||||
default: 'all',
|
||||
},
|
||||
filterRangeBy: {
|
||||
type: String,
|
||||
default: 'createdAt',
|
||||
},
|
||||
dateRange: {
|
||||
type: String,
|
||||
default: 'lastMonth',
|
||||
},
|
||||
startDate: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
endDate: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
systemFields: {
|
||||
type: Array,
|
||||
default: () => ['ownedBy', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy'],
|
||||
},
|
||||
disabledTypes: {
|
||||
type: Array,
|
||||
default: () => ['User', 'Record', 'File'],
|
||||
},
|
||||
processing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
fields: [],
|
||||
forTimezone: false,
|
||||
exportTimezone: undefined,
|
||||
filter: {
|
||||
rangeType: null,
|
||||
query: this.query,
|
||||
filter: this.prefilter,
|
||||
rangeBy: null,
|
||||
date: {
|
||||
range: null,
|
||||
start: null,
|
||||
end: null,
|
||||
},
|
||||
},
|
||||
rangeRecordCount: 0,
|
||||
processingCount: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
timezones () {
|
||||
return tz.map(({ label, tzCode, offset }) => ({ label, tzCode, offset }))
|
||||
},
|
||||
|
||||
// These should be computed, because of i18n
|
||||
rangeTypeOptions () {
|
||||
const options = [
|
||||
{
|
||||
value: 'all',
|
||||
text: this.$t('recordList.export.all'),
|
||||
},
|
||||
{
|
||||
value: 'range',
|
||||
text: this.$t('recordList.export.inRange'),
|
||||
},
|
||||
]
|
||||
|
||||
if (this.hasSelection) {
|
||||
options.push({
|
||||
value: 'selection',
|
||||
text: this.$t('recordList.export.selection'),
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
},
|
||||
|
||||
/**
|
||||
* checks if the given date-range is valid
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
dateRangeValid () {
|
||||
if (this.end < this.start) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
rangeByOptions () {
|
||||
return [
|
||||
{
|
||||
value: 'createdAt',
|
||||
text: this.$t('recordList.export.filter.createdAt'),
|
||||
},
|
||||
{
|
||||
value: 'updatedAt',
|
||||
text: this.$t('recordList.export.filter.updatedAt'),
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
dateRangeOptions () {
|
||||
return [
|
||||
{
|
||||
value: 'lastMonth',
|
||||
text: this.$t('recordList.export.filter.lastMonth'),
|
||||
},
|
||||
{
|
||||
value: 'thisMonth',
|
||||
text: this.$t('recordList.export.filter.thisMonth'),
|
||||
},
|
||||
{
|
||||
value: 'lastWeek',
|
||||
text: this.$t('recordList.export.filter.lastWeek'),
|
||||
},
|
||||
{
|
||||
value: 'thisWeek',
|
||||
text: this.$t('recordList.export.filter.thisWeek'),
|
||||
},
|
||||
{
|
||||
value: 'today',
|
||||
text: this.$t('recordList.export.filter.today'),
|
||||
},
|
||||
{
|
||||
value: 'custom',
|
||||
text: this.$t('recordList.export.filter.custom'),
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
hasSelection () {
|
||||
return !!this.selection.length
|
||||
},
|
||||
|
||||
getExportableCount () {
|
||||
// when exporting selection, only selected records are applicable
|
||||
if (this.rangeType === 'selection') {
|
||||
if (this.selectedAllRecordsCount !== 0) {
|
||||
return this.selectedAllRecordsCount
|
||||
}
|
||||
|
||||
return this.selection.length
|
||||
}
|
||||
|
||||
return this.rangeRecordCount
|
||||
},
|
||||
|
||||
exportDisabled () {
|
||||
return !this.dateRangeValid || this.fields.length === 0 || !this.getExportableCount
|
||||
},
|
||||
|
||||
selectedFields: {
|
||||
get () {
|
||||
return this.fields
|
||||
},
|
||||
|
||||
set (selectedFields) {
|
||||
this.fields = selectedFields
|
||||
},
|
||||
},
|
||||
|
||||
rangeBy: {
|
||||
get () {
|
||||
return this.filter.rangeBy
|
||||
},
|
||||
|
||||
set (rangeBy) {
|
||||
this.filter.rangeBy = rangeBy
|
||||
},
|
||||
},
|
||||
|
||||
exportQuery: {
|
||||
get () {
|
||||
return this.filter.query
|
||||
},
|
||||
set (v) {
|
||||
this.filter.query = v
|
||||
},
|
||||
},
|
||||
|
||||
exportFilter: {
|
||||
get () {
|
||||
return this.filter.filter
|
||||
},
|
||||
set (v) {
|
||||
this.filter.filter = v
|
||||
},
|
||||
},
|
||||
|
||||
rangeType: {
|
||||
get () {
|
||||
return this.filter.rangeType
|
||||
},
|
||||
|
||||
set (rangeType) {
|
||||
this.filter.rangeType = rangeType
|
||||
},
|
||||
},
|
||||
|
||||
range: {
|
||||
get () {
|
||||
return this.filter.date.range
|
||||
},
|
||||
|
||||
set (range) {
|
||||
this.filter.date.range = range
|
||||
if (range !== 'custom') {
|
||||
this.filter.date.start = this.calcStart(moment(), range)
|
||||
this.filter.date.end = this.calcEnd(moment(), range)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
start: {
|
||||
get () {
|
||||
return this.filter.date.start
|
||||
},
|
||||
|
||||
set (start) {
|
||||
this.filter.date.start = start
|
||||
this.filter.date.range = 'custom'
|
||||
},
|
||||
},
|
||||
|
||||
end: {
|
||||
get () {
|
||||
return this.filter.date.end
|
||||
},
|
||||
|
||||
set (end) {
|
||||
this.filter.date.end = end
|
||||
this.filter.date.range = 'custom'
|
||||
},
|
||||
},
|
||||
|
||||
currentRange () {
|
||||
if (this.rangeType === 'range') {
|
||||
return { start: this.start, end: this.end, rangeBy: this.rangeBy }
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
|
||||
// Watchers needed for storybook
|
||||
watch: {
|
||||
filter: {
|
||||
handler () {
|
||||
this.$emit('change', this.filter)
|
||||
this.getTotalCount()
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
|
||||
preselectedFields: {
|
||||
handler (value) {
|
||||
if (!this.fields.length) {
|
||||
this.fields = value.filter(f => this.disabledTypes.indexOf(f.kind) < 0)
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
|
||||
filterRangeType: {
|
||||
immediate: true,
|
||||
handler (value) {
|
||||
this.filter.rangeType = value
|
||||
},
|
||||
},
|
||||
|
||||
filterRangeBy: {
|
||||
handler (value) {
|
||||
this.filter.rangeBy = value
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
|
||||
startDate: {
|
||||
handler (value) {
|
||||
this.start = value
|
||||
},
|
||||
},
|
||||
|
||||
endDate: {
|
||||
handler (value) {
|
||||
this.end = value
|
||||
},
|
||||
},
|
||||
|
||||
dateRange: {
|
||||
handler (value) {
|
||||
this.range = value
|
||||
},
|
||||
},
|
||||
|
||||
hasSelection: {
|
||||
handler (h) {
|
||||
if (h) {
|
||||
this.rangeType = 'selection'
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.startDate || this.endDate) {
|
||||
this.start = this.startDate
|
||||
this.end = this.endDate
|
||||
} else {
|
||||
this.range = this.dateRange
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
calcStart (m, range) {
|
||||
if (range === 'lastMonth') {
|
||||
return fmtDate(m.subtract('1', 'months').startOf('month'))
|
||||
} else if (range === 'thisMonth') {
|
||||
return fmtDate(m.startOf('month'))
|
||||
} else if (range === 'lastWeek') {
|
||||
return fmtDate(m.subtract('1', 'week').startOf('week'))
|
||||
} else if (range === 'thisWeek') {
|
||||
return fmtDate(m.startOf('week'))
|
||||
} else if (range === 'today') {
|
||||
return fmtDate(m.startOf('day'))
|
||||
} else {
|
||||
throw new Error(this.$t('recordList.export.datePresetUndefined'))
|
||||
}
|
||||
},
|
||||
|
||||
calcEnd (m, range) {
|
||||
if (range === 'lastMonth') {
|
||||
return fmtDate(m.subtract('1', 'months').endOf('month'))
|
||||
} else if (range === 'thisMonth') {
|
||||
return fmtDate(m.endOf('month'))
|
||||
} else if (range === 'lastWeek') {
|
||||
return fmtDate(m.subtract('1', 'week').endOf('week'))
|
||||
} else if (range === 'thisWeek') {
|
||||
return fmtDate(m.endOf('week'))
|
||||
} else if (range === 'today') {
|
||||
return fmtDate(m.endOf('day'))
|
||||
} else {
|
||||
throw new Error(this.$t('recordList.export.datePresetUndefined'))
|
||||
}
|
||||
},
|
||||
|
||||
makeFilter ({ query, filter, rangeType, rangeBy, date }) {
|
||||
if (filter) {
|
||||
filter = evaluatePrefilter(filter, {
|
||||
userID: (this.$auth.user || {}).userID || NoID,
|
||||
})
|
||||
}
|
||||
|
||||
query = queryToFilter(query, filter, this.module.fields)
|
||||
|
||||
if (rangeType === 'all') {
|
||||
return query
|
||||
}
|
||||
|
||||
if (rangeType === 'selection') {
|
||||
// @todo improve with IN operator when supported
|
||||
return this.selection.map(r => `recordID='${r}'`).join(' OR ')
|
||||
}
|
||||
|
||||
let dateQuery, start, end
|
||||
if (date.start) {
|
||||
start = `DATE(${rangeBy})>='${date.start}'`
|
||||
}
|
||||
|
||||
if (date.end) {
|
||||
end = `DATE(${rangeBy})<='${date.end}'`
|
||||
}
|
||||
|
||||
if (start && end) {
|
||||
dateQuery = `(${start}) AND (${end})`
|
||||
} else {
|
||||
dateQuery = start || end
|
||||
}
|
||||
|
||||
return query ? `(${query}) AND ${dateQuery}` : dateQuery
|
||||
},
|
||||
|
||||
doExport (kind) {
|
||||
this.$emit('export', {
|
||||
ext: kind,
|
||||
fields: encodeURIComponent(this.fields.map(({ name }) => name)),
|
||||
filter: encodeURIComponent(this.makeFilter(this.filter)),
|
||||
filterRaw: encodeURIComponent(this.filter),
|
||||
timezone: encodeURIComponent(this.forTimezone ? this.exportTimezone : undefined),
|
||||
})
|
||||
},
|
||||
|
||||
getTotalCount () {
|
||||
const { moduleID, namespaceID } = this.module || {}
|
||||
|
||||
if (moduleID && namespaceID) {
|
||||
this.processingCount = true
|
||||
|
||||
const query = this.makeFilter(this.filter)
|
||||
|
||||
this.$ComposeAPI.recordList({ namespaceID, moduleID, query, limit: 1, incTotal: true })
|
||||
.then(({ filter = {} }) => {
|
||||
this.rangeRecordCount = filter.total || 0
|
||||
})
|
||||
.catch(() => {
|
||||
this.rangeRecordCount = 0
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingCount = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
getOptionKey ({ tzCode }) {
|
||||
return tzCode
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,45 +1,283 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-inline-block">
|
||||
<b-button
|
||||
size="lg"
|
||||
variant="light"
|
||||
@click="toggleModal"
|
||||
>
|
||||
{{ $t('label.export') }}
|
||||
{{ $t('general:label.export') }}
|
||||
</b-button>
|
||||
|
||||
<b-modal
|
||||
:visible="showExportModal"
|
||||
size="lg"
|
||||
:title="$t('label.export')"
|
||||
hide-footer
|
||||
body-class="p-0"
|
||||
:title="$t('general:label.export')"
|
||||
no-fade
|
||||
scrollable
|
||||
footer-class="d-flex align-items-center justify-content-between"
|
||||
@hide="toggleModal"
|
||||
>
|
||||
<configurator
|
||||
:processing="processing"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
<b-form-group
|
||||
:label="$t('recordList.export.selectFields')"
|
||||
:description="$t('recordList.export.limitations')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<field-picker
|
||||
v-if="module"
|
||||
:module="module"
|
||||
:system-fields="systemFields"
|
||||
:disabled-types="disabledTypes"
|
||||
:fields.sync="selectedFields"
|
||||
style="height: 45vh;"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group>
|
||||
<b-form-radio-group
|
||||
v-model="rangeType"
|
||||
:options="rangeTypeOptions"
|
||||
stacked
|
||||
@change="getTotalCount()"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<template v-if="rangeType === 'range'">
|
||||
<b-row
|
||||
v-if="rangeType === 'range'"
|
||||
>
|
||||
<b-col
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('recordList.export.rangeBy')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="rangeBy"
|
||||
:options="rangeByOptions"
|
||||
@change="getTotalCount()"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('recordList.export.dateRange')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="range"
|
||||
:options="dateRangeOptions"
|
||||
@change="getTotalCount()"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
|
||||
<b-row
|
||||
v-if="rangeType === 'range'"
|
||||
>
|
||||
<b-col
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('recordList.export.filter.from')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-date-time
|
||||
v-model="start"
|
||||
no-time
|
||||
only-past
|
||||
:labels="{
|
||||
clear: $t('general:label.clear'),
|
||||
none: $t('general:label.none'),
|
||||
now: $t('general:label.now'),
|
||||
today: $t('general:label.today'),
|
||||
}"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('recordList.export.filter.to')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<c-input-date-time
|
||||
v-model="end"
|
||||
no-time
|
||||
only-past
|
||||
:labels="{
|
||||
clear: $t('general:label.clear'),
|
||||
none: $t('general:label.none'),
|
||||
now: $t('general:label.now'),
|
||||
today: $t('general:label.today'),
|
||||
}"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<template
|
||||
v-if="rangeType !== 'selection'"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('recordList.export.filter.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="exportFilter"
|
||||
:placeholder="$t('recordList.export.filter.placeholder')"
|
||||
debounce="500"
|
||||
/>
|
||||
</b-form-group>
|
||||
</template>
|
||||
|
||||
<b-form-group>
|
||||
<b-form-checkbox
|
||||
v-model="forTimezone"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $t('recordList.export.specifyTimezone') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<vue-select
|
||||
v-if="forTimezone"
|
||||
v-model="exportTimezone"
|
||||
:options="timezones"
|
||||
:get-option-key="getOptionKey"
|
||||
:calculate-position="calculateDropdownPosition"
|
||||
:placeholder="$t('recordList.export.timezonePlaceholder')"
|
||||
class="bg-white"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<template #modal-footer>
|
||||
<div>
|
||||
<b-spinner
|
||||
v-if="processingCount"
|
||||
small
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
>
|
||||
{{ $t('recordList.export.recordCount', { count: getExportableCount || 0 }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<c-input-processing
|
||||
v-if="allowJSON"
|
||||
:processing="processing"
|
||||
:disabled="exportDisabled"
|
||||
variant="light"
|
||||
size="lg"
|
||||
@click="doExport('json')"
|
||||
>
|
||||
{{ $t('recordList.export.json') }}
|
||||
</c-input-processing>
|
||||
<c-input-processing
|
||||
v-if="allowCSV"
|
||||
:processing="processing"
|
||||
:disabled="exportDisabled"
|
||||
variant="light"
|
||||
size="lg"
|
||||
class="ml-2"
|
||||
@click="doExport('csv')"
|
||||
>
|
||||
{{ $t('recordList.export.csv') }}
|
||||
</c-input-processing>
|
||||
</div>
|
||||
</template>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Configurator from './Configurator'
|
||||
import { compose } from '@cortezaproject/corteza-js'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import moment from 'moment'
|
||||
import { throttle } from 'lodash'
|
||||
import tz from 'compact-timezone-list'
|
||||
import { VueSelect } from 'vue-select'
|
||||
import FieldPicker from 'corteza-webapp-compose/src/components/Common/FieldPicker'
|
||||
import { getFieldFilter } from 'corteza-webapp-compose/src/lib/record-filter'
|
||||
|
||||
const { CInputDateTime } = components
|
||||
const fmtDate = (d) => d.format('YYYY-MM-DD')
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'general',
|
||||
namespaces: 'block',
|
||||
},
|
||||
|
||||
components: {
|
||||
Configurator,
|
||||
FieldPicker,
|
||||
VueSelect,
|
||||
CInputDateTime,
|
||||
},
|
||||
|
||||
inheritAttrs: true,
|
||||
|
||||
props: {
|
||||
allowJSON: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
allowCSV: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
module: {
|
||||
type: compose.Module,
|
||||
required: true,
|
||||
},
|
||||
preselectedFields: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
filter: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
selection: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
selectedAllRecords: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
filterRangeType: {
|
||||
type: String,
|
||||
default: 'all',
|
||||
},
|
||||
filterRangeBy: {
|
||||
type: String,
|
||||
default: 'createdAt',
|
||||
},
|
||||
startDate: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
endDate: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
systemFields: {
|
||||
type: Array,
|
||||
default: () => ['ownedBy', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy'],
|
||||
},
|
||||
disabledTypes: {
|
||||
type: Array,
|
||||
default: () => ['User', 'Record', 'File'],
|
||||
},
|
||||
processing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -49,13 +287,366 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
showExportModal: false,
|
||||
|
||||
fields: [],
|
||||
forTimezone: false,
|
||||
exportTimezone: undefined,
|
||||
exportConfig: {
|
||||
rangeType: 'all',
|
||||
query: this.query,
|
||||
filter: this.filter,
|
||||
rangeBy: null,
|
||||
date: {
|
||||
range: 'lastMonth',
|
||||
start: this.calcStart(moment(), 'lastMonth'),
|
||||
end: this.calcEnd(moment(), 'lastMonth'),
|
||||
},
|
||||
},
|
||||
rangeRecordCount: 0,
|
||||
processingCount: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
timezones () {
|
||||
return tz.map(({ label, tzCode, offset }) => ({ label, tzCode, offset }))
|
||||
},
|
||||
|
||||
// These should be computed, because of i18n
|
||||
rangeTypeOptions () {
|
||||
const options = [
|
||||
{
|
||||
value: 'all',
|
||||
text: this.$t('recordList.export.all'),
|
||||
},
|
||||
{
|
||||
value: 'range',
|
||||
text: this.$t('recordList.export.inRange'),
|
||||
},
|
||||
]
|
||||
|
||||
if (this.hasSelection) {
|
||||
options.push({
|
||||
value: 'selection',
|
||||
text: this.$t('recordList.export.selection'),
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
},
|
||||
|
||||
/**
|
||||
* checks if the given date-range is valid
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
dateRangeValid () {
|
||||
if (this.end < this.start) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
rangeByOptions () {
|
||||
return [
|
||||
{
|
||||
value: 'createdAt',
|
||||
text: this.$t('recordList.export.filter.createdAt'),
|
||||
},
|
||||
{
|
||||
value: 'updatedAt',
|
||||
text: this.$t('recordList.export.filter.updatedAt'),
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
dateRangeOptions () {
|
||||
return [
|
||||
{
|
||||
value: 'lastMonth',
|
||||
text: this.$t('recordList.export.filter.lastMonth'),
|
||||
},
|
||||
{
|
||||
value: 'thisMonth',
|
||||
text: this.$t('recordList.export.filter.thisMonth'),
|
||||
},
|
||||
{
|
||||
value: 'lastWeek',
|
||||
text: this.$t('recordList.export.filter.lastWeek'),
|
||||
},
|
||||
{
|
||||
value: 'thisWeek',
|
||||
text: this.$t('recordList.export.filter.thisWeek'),
|
||||
},
|
||||
{
|
||||
value: 'today',
|
||||
text: this.$t('recordList.export.filter.today'),
|
||||
},
|
||||
{
|
||||
value: 'custom',
|
||||
text: this.$t('recordList.export.filter.custom'),
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
hasSelection () {
|
||||
return !!this.selection.length
|
||||
},
|
||||
|
||||
getExportableCount () {
|
||||
// when exporting selection, only selected records are applicable
|
||||
if (this.rangeType === 'selection') {
|
||||
return this.selectedAllRecords ? this.rangeRecordCount : this.selection.length
|
||||
}
|
||||
|
||||
return this.rangeRecordCount
|
||||
},
|
||||
|
||||
exportDisabled () {
|
||||
return !this.dateRangeValid || this.fields.length === 0 || !this.getExportableCount
|
||||
},
|
||||
|
||||
selectedFields: {
|
||||
get () {
|
||||
return this.fields
|
||||
},
|
||||
|
||||
set (selectedFields) {
|
||||
this.fields = selectedFields
|
||||
},
|
||||
},
|
||||
|
||||
rangeBy: {
|
||||
get () {
|
||||
return this.exportConfig.rangeBy
|
||||
},
|
||||
|
||||
set (rangeBy) {
|
||||
this.exportConfig.rangeBy = rangeBy
|
||||
},
|
||||
},
|
||||
|
||||
exportFilter: {
|
||||
get () {
|
||||
return this.exportConfig.filter
|
||||
},
|
||||
set (v) {
|
||||
this.exportConfig.filter = v
|
||||
this.getTotalCount()
|
||||
},
|
||||
},
|
||||
|
||||
rangeType: {
|
||||
get () {
|
||||
return this.exportConfig.rangeType
|
||||
},
|
||||
|
||||
set (rangeType) {
|
||||
this.exportConfig.rangeType = rangeType
|
||||
},
|
||||
},
|
||||
|
||||
range: {
|
||||
get () {
|
||||
return this.exportConfig.date.range
|
||||
},
|
||||
|
||||
set (range) {
|
||||
this.exportConfig.date.range = range
|
||||
if (range !== 'custom') {
|
||||
this.exportConfig.date.start = this.calcStart(moment(), range)
|
||||
this.exportConfig.date.end = this.calcEnd(moment(), range)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
start: {
|
||||
get () {
|
||||
return this.exportConfig.date.start
|
||||
},
|
||||
|
||||
set (start) {
|
||||
this.exportConfig.date.start = start
|
||||
this.exportConfig.date.range = 'custom'
|
||||
this.getTotalCount()
|
||||
},
|
||||
},
|
||||
|
||||
end: {
|
||||
get () {
|
||||
return this.exportConfig.date.end
|
||||
},
|
||||
|
||||
set (end) {
|
||||
this.exportConfig.date.end = end
|
||||
this.exportConfig.date.range = 'custom'
|
||||
this.getTotalCount()
|
||||
},
|
||||
},
|
||||
|
||||
currentRange () {
|
||||
if (this.rangeType === 'range') {
|
||||
return { start: this.start, end: this.end, rangeBy: this.rangeBy }
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
filter: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (filter) {
|
||||
this.exportConfig.filter = filter
|
||||
this.getTotalCount()
|
||||
},
|
||||
},
|
||||
|
||||
preselectedFields: {
|
||||
handler (value) {
|
||||
if (!this.fields.length) {
|
||||
this.fields = value.filter(f => this.disabledTypes.indexOf(f.kind) < 0)
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
|
||||
filterRangeType: {
|
||||
immediate: true,
|
||||
handler (value) {
|
||||
this.exportConfig.rangeType = value
|
||||
},
|
||||
},
|
||||
|
||||
filterRangeBy: {
|
||||
immediate: true,
|
||||
handler (value) {
|
||||
this.exportConfig.rangeBy = value
|
||||
},
|
||||
},
|
||||
|
||||
startDate: {
|
||||
handler (value) {
|
||||
this.start = value
|
||||
},
|
||||
},
|
||||
|
||||
endDate: {
|
||||
handler (value) {
|
||||
this.end = value
|
||||
},
|
||||
},
|
||||
|
||||
hasSelection: {
|
||||
immediate: true,
|
||||
handler (h) {
|
||||
if (h) {
|
||||
this.rangeType = 'selection'
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleModal () {
|
||||
this.showExportModal = !this.showExportModal
|
||||
},
|
||||
|
||||
calcStart (m, range) {
|
||||
if (range === 'lastMonth') {
|
||||
return fmtDate(m.subtract('1', 'months').startOf('month'))
|
||||
} else if (range === 'thisMonth') {
|
||||
return fmtDate(m.startOf('month'))
|
||||
} else if (range === 'lastWeek') {
|
||||
return fmtDate(m.subtract('1', 'week').startOf('week'))
|
||||
} else if (range === 'thisWeek') {
|
||||
return fmtDate(m.startOf('week'))
|
||||
} else if (range === 'today') {
|
||||
return fmtDate(m.startOf('day'))
|
||||
} else {
|
||||
throw new Error(this.$t('recordList.export.datePresetUndefined'))
|
||||
}
|
||||
},
|
||||
|
||||
calcEnd (m, range) {
|
||||
if (range === 'lastMonth') {
|
||||
return fmtDate(m.subtract('1', 'months').endOf('month'))
|
||||
} else if (range === 'thisMonth') {
|
||||
return fmtDate(m.endOf('month'))
|
||||
} else if (range === 'lastWeek') {
|
||||
return fmtDate(m.subtract('1', 'week').endOf('week'))
|
||||
} else if (range === 'thisWeek') {
|
||||
return fmtDate(m.endOf('week'))
|
||||
} else if (range === 'today') {
|
||||
return fmtDate(m.endOf('day'))
|
||||
} else {
|
||||
throw new Error(this.$t('recordList.export.datePresetUndefined'))
|
||||
}
|
||||
},
|
||||
|
||||
makeFilter ({ filter, rangeType, rangeBy, date }) {
|
||||
if (rangeType === 'all') {
|
||||
return filter
|
||||
}
|
||||
|
||||
if (rangeType === 'selection') {
|
||||
return this.selectedAllRecords ? filter : this.selection.map(r => `recordID='${r}'`).join(' OR ')
|
||||
}
|
||||
|
||||
let dateRangeQuery = ''
|
||||
|
||||
if (date.start && date.end) {
|
||||
// If dates are the same, set range to that date
|
||||
date = { ...date }
|
||||
if (date.start === date.end) {
|
||||
date.start = moment(date.start, 'YYYY-MM-DD HH:mm').utc().format()
|
||||
date.end = moment(date.end, 'YYYY-MM-DD HH:mm').add(1, 'days').utc().format()
|
||||
}
|
||||
|
||||
dateRangeQuery = getFieldFilter(rangeBy, 'DateTime', date, 'BETWEEN')
|
||||
} else if (date.start) {
|
||||
dateRangeQuery = getFieldFilter(rangeBy, 'DateTime', date.start, '>=')
|
||||
} else if (date.end) {
|
||||
dateRangeQuery = getFieldFilter(rangeBy, 'DateTime', date.end, '<=')
|
||||
}
|
||||
|
||||
return filter && dateRangeQuery ? `(${filter}) AND ${dateRangeQuery}` : dateRangeQuery
|
||||
},
|
||||
|
||||
doExport (kind) {
|
||||
this.$emit('export', {
|
||||
ext: kind,
|
||||
fields: encodeURIComponent(this.fields.map(({ name }) => name)),
|
||||
filter: encodeURIComponent(this.makeFilter(this.exportConfig)),
|
||||
filterRaw: encodeURIComponent(this.exportConfig),
|
||||
timezone: encodeURIComponent(this.forTimezone ? this.exportTimezone : undefined),
|
||||
})
|
||||
},
|
||||
|
||||
getTotalCount: throttle(function () {
|
||||
const { moduleID, namespaceID } = this.module || {}
|
||||
|
||||
if (moduleID && namespaceID) {
|
||||
const query = this.makeFilter(this.exportConfig)
|
||||
|
||||
this.processingCount = true
|
||||
|
||||
this.$ComposeAPI.recordList({ namespaceID, moduleID, query, limit: 1, incTotal: true })
|
||||
.then(({ filter = {} }) => {
|
||||
this.rangeRecordCount = filter.total || 0
|
||||
})
|
||||
.catch(e => {
|
||||
this.rangeRecordCount = 0
|
||||
this.toastErrorHandler(this.$t('notification:record.countFailed'))(e)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingCount = false
|
||||
})
|
||||
}
|
||||
}, 500),
|
||||
|
||||
getOptionKey ({ tzCode }) {
|
||||
return tzCode
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-inline-block">
|
||||
<b-button
|
||||
size="lg"
|
||||
variant="light"
|
||||
@@ -15,6 +15,7 @@
|
||||
hide-footer
|
||||
body-class="p-0"
|
||||
no-fade
|
||||
scrollable
|
||||
@hide="onModalHide"
|
||||
>
|
||||
<keep-alive>
|
||||
|
||||
@@ -295,12 +295,12 @@ recordList:
|
||||
all: Export all records
|
||||
allow: Users will be able to export records
|
||||
csv: CSV Export
|
||||
dateRange: 'Select date range:'
|
||||
dateRange: Select date range
|
||||
query:
|
||||
label: Search query
|
||||
placeholder: Search...
|
||||
filter:
|
||||
label: Filter records
|
||||
label: Record filter
|
||||
placeholder: field1 = 1 AND field2 = 232
|
||||
footnote: Simplified SQL condition (WHERE ...) syntax is supported. Variables like {{0}}, {{1}} and {{2}} are evaluated (when available)
|
||||
createdAt: Record created
|
||||
@@ -310,12 +310,14 @@ recordList:
|
||||
thisMonth: This month
|
||||
thisWeek: This week
|
||||
today: Today
|
||||
from: From
|
||||
to: To
|
||||
updatedAt: Record updated
|
||||
inRange: Set date range
|
||||
inRange: Date range
|
||||
includeQuery: Filter by search query
|
||||
json: JSON Export
|
||||
limitations: 'CSV export limitation: only the first value in the multi value fields will be exported'
|
||||
rangeBy: 'Set range by:'
|
||||
rangeBy: Set range by
|
||||
recordCount: '{{count}} records ready for export'
|
||||
selectFields: 'Select fields you want to export:'
|
||||
selection: Selected records
|
||||
|
||||
@@ -125,6 +125,7 @@ record:
|
||||
invalidRecordVar: Can not use ${record...} variable in non-record pages
|
||||
listLoadFailed: Could not load record list
|
||||
loadFailed: Could not load record
|
||||
countFailed: Failed to fetch number of avaiable records
|
||||
moduleMismatch: Module incompatible, module mismatch
|
||||
moduleNotDefined: Record module not defined
|
||||
moduleOrPageNotSet: 'RecordList block error: module or page option not set'
|
||||
|
||||
Reference in New Issue
Block a user