|
|
@ -1,251 +1,23 @@ |
|
|
|
<script> |
|
|
|
<script lang="ts" setup> |
|
|
|
import FileSaver from 'file-saver' |
|
|
|
import MdiFlashIcon from '~icons/mdi/flash-outline' |
|
|
|
import { ExportTypes } from 'nocodb-sdk' |
|
|
|
import MdiMenuDownIcon from '~icons/mdi/menu-down' |
|
|
|
import { NOCO } from '~/lib/constants' |
|
|
|
|
|
|
|
import DropOrSelectFileModal from '~/components/import/DropOrSelectFileModal' |
|
|
|
|
|
|
|
import ColumnMappingModal from '~/components/project/spreadsheet/components/importExport/ColumnMappingModal' |
|
|
|
|
|
|
|
import CSVTemplateAdapter from '~/components/import/templateParsers/CSVTemplateAdapter' |
|
|
|
|
|
|
|
import { UITypes } from '~/components/project/spreadsheet/helpers/uiTypes' |
|
|
|
|
|
|
|
import WebhookModal from '~/components/project/tableTabs/webhook/WebhookModal' |
|
|
|
|
|
|
|
import WebhookSlider from '~/components/project/tableTabs/webhook/WebhookSlider' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default { |
|
|
|
|
|
|
|
name: 'ExportImport', |
|
|
|
|
|
|
|
components: { |
|
|
|
|
|
|
|
WebhookSlider, |
|
|
|
|
|
|
|
ColumnMappingModal, |
|
|
|
|
|
|
|
DropOrSelectFileModal, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
props: { |
|
|
|
|
|
|
|
meta: Object, |
|
|
|
|
|
|
|
nodes: Object, |
|
|
|
|
|
|
|
selectedView: Object, |
|
|
|
|
|
|
|
publicViewId: String, |
|
|
|
|
|
|
|
queryParams: Object, |
|
|
|
|
|
|
|
isView: Boolean, |
|
|
|
|
|
|
|
reqPayload: Object, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
emits: ['reload', 'showAdditionalFeatOverlay'], |
|
|
|
|
|
|
|
data() { |
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
importModal: false, |
|
|
|
|
|
|
|
columnMappingModal: false, |
|
|
|
|
|
|
|
parsedCsv: {}, |
|
|
|
|
|
|
|
webhookModal: false, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
methods: { |
|
|
|
|
|
|
|
async onCsvFileSelection(file) { |
|
|
|
|
|
|
|
const reader = new FileReader() |
|
|
|
|
|
|
|
reader.onload = async (e) => { |
|
|
|
|
|
|
|
const templateGenerator = new CSVTemplateAdapter(file.name, e.target.result) |
|
|
|
|
|
|
|
await templateGenerator.init() |
|
|
|
|
|
|
|
templateGenerator.parseData() |
|
|
|
|
|
|
|
this.parsedCsv.columns = templateGenerator.getColumns() |
|
|
|
|
|
|
|
this.parsedCsv.data = templateGenerator.getData() |
|
|
|
|
|
|
|
this.columnMappingModal = true |
|
|
|
|
|
|
|
this.importModal = false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
reader.readAsText(file) |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async extractCsvData() { |
|
|
|
|
|
|
|
return Promise.all( |
|
|
|
|
|
|
|
this.data.map(async (r) => { |
|
|
|
|
|
|
|
const row = {} |
|
|
|
|
|
|
|
for (const col of this.availableColumns) { |
|
|
|
|
|
|
|
if (col.virtual) { |
|
|
|
|
|
|
|
let prop, cn |
|
|
|
|
|
|
|
if (col.mm || (col.lk && col.lk.type === 'mm')) { |
|
|
|
|
|
|
|
const tn = col.mm ? col.mm.rtn : col.lk.ltn |
|
|
|
|
|
|
|
const title = col.mm ? col.mm._rtn : col.lk._ltn |
|
|
|
|
|
|
|
await this.$store.dispatch('meta/ActLoadMeta', { |
|
|
|
|
|
|
|
env: this.nodes.env, |
|
|
|
|
|
|
|
dbAlias: this.nodes.dbAlias, |
|
|
|
|
|
|
|
tn, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prop = `${title}MMList` |
|
|
|
|
|
|
|
cn = col.lk |
|
|
|
|
|
|
|
? col.lk._lcn |
|
|
|
|
|
|
|
: ( |
|
|
|
|
|
|
|
this.$store.state.meta.metas[tn].columns.find((c) => c.pv) || |
|
|
|
|
|
|
|
this.$store.state.meta.metas[tn].columns.find((c) => c.pk) || |
|
|
|
|
|
|
|
{} |
|
|
|
|
|
|
|
).title |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
row[col.title] = r.row[prop] && r.row[prop].map((r) => cn && r[cn]) |
|
|
|
|
|
|
|
} else if (col.hm || (col.lk && col.lk.type === 'hm')) { |
|
|
|
|
|
|
|
const tn = col.hm ? col.hm.table_name : col.lk.ltn |
|
|
|
|
|
|
|
const title = col.hm ? col.hm.title : col.lk._ltn |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await this.$store.dispatch('meta/ActLoadMeta', { |
|
|
|
|
|
|
|
env: this.nodes.env, |
|
|
|
|
|
|
|
dbAlias: this.nodes.dbAlias, |
|
|
|
|
|
|
|
tn, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prop = `${title}List` |
|
|
|
|
|
|
|
cn = col.lk |
|
|
|
|
|
|
|
? col.lk._lcn |
|
|
|
|
|
|
|
: ( |
|
|
|
|
|
|
|
this.$store.state.meta.metas[tn].columns.find((c) => c.pv) || |
|
|
|
|
|
|
|
this.$store.state.meta.metas[tn].columns.find((c) => c.pk) |
|
|
|
|
|
|
|
).title |
|
|
|
|
|
|
|
row[col.title] = r.row[prop] && r.row[prop].map((r) => cn && r[cn]) |
|
|
|
|
|
|
|
} else if (col.bt || (col.lk && col.lk.type === 'bt')) { |
|
|
|
|
|
|
|
const tn = col.bt ? col.bt.rtn : col.lk.ltn |
|
|
|
|
|
|
|
const title = col.bt ? col.bt._rtn : col.lk._ltn |
|
|
|
|
|
|
|
await this.$store.dispatch('meta/ActLoadMeta', { |
|
|
|
|
|
|
|
env: this.nodes.env, |
|
|
|
|
|
|
|
dbAlias: this.nodes.dbAlias, |
|
|
|
|
|
|
|
tn, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prop = `${title}Read` |
|
|
|
|
|
|
|
cn = col.lk |
|
|
|
|
|
|
|
? col.lk._lcn |
|
|
|
|
|
|
|
: ( |
|
|
|
|
|
|
|
this.$store.state.meta.metas[tn].columns.find((c) => c.pv) || |
|
|
|
|
|
|
|
this.$store.state.meta.metas[tn].columns.find((c) => c.pk) || |
|
|
|
|
|
|
|
{} |
|
|
|
|
|
|
|
).title |
|
|
|
|
|
|
|
row[col.title] = r.row[prop] && r.row[prop] && cn && r.row[prop][cn] |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
row[col.title] = r.row[col.title] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (col.uidt === 'Attachment') { |
|
|
|
|
|
|
|
let data = [] |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
if (typeof r.row[col.title] === 'string') { |
|
|
|
|
|
|
|
data = JSON.parse(r.row[col.title]) |
|
|
|
|
|
|
|
} else if (r.row[col.title]) { |
|
|
|
|
|
|
|
data = r.row[col.title] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} catch {} |
|
|
|
|
|
|
|
row[col.title] = (data || []).map((a) => `${a.title}(${a.url})`) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
row[col.title] = r.row[col.title] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return row |
|
|
|
|
|
|
|
}), |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
async exportCsv() { |
|
|
|
|
|
|
|
let offset = 0 |
|
|
|
|
|
|
|
let c = 1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
while (!isNaN(offset) && offset > -1) { |
|
|
|
|
|
|
|
let res |
|
|
|
|
|
|
|
if (this.publicViewId) { |
|
|
|
|
|
|
|
res = await this.$api.public.csvExport(this.publicViewId, ExportTypes.CSV, { |
|
|
|
|
|
|
|
responseType: 'blob', |
|
|
|
|
|
|
|
query: { |
|
|
|
|
|
|
|
fields: |
|
|
|
|
|
|
|
this.queryParams && |
|
|
|
|
|
|
|
this.queryParams.fieldsOrder && |
|
|
|
|
|
|
|
this.queryParams.fieldsOrder.filter((c) => this.queryParams.showFields[c]), |
|
|
|
|
|
|
|
offset, |
|
|
|
|
|
|
|
sortArrJson: JSON.stringify( |
|
|
|
|
|
|
|
this.reqPayload && |
|
|
|
|
|
|
|
this.reqPayload.sorts && |
|
|
|
|
|
|
|
this.reqPayload.sorts.map(({ fk_column_id, direction }) => ({ |
|
|
|
|
|
|
|
direction, |
|
|
|
|
|
|
|
fk_column_id, |
|
|
|
|
|
|
|
})), |
|
|
|
|
|
|
|
), |
|
|
|
|
|
|
|
filterArrJson: JSON.stringify(this.reqPayload && this.reqPayload.filters), |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
headers: { |
|
|
|
|
|
|
|
'xc-password': this.reqPayload && this.reqPayload.password, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
res = await this.$api.dbViewRow.export( |
|
|
|
|
|
|
|
NOCO, |
|
|
|
|
|
|
|
this.projectName, |
|
|
|
|
|
|
|
this.meta.title, |
|
|
|
|
|
|
|
this.selectedView.title, |
|
|
|
|
|
|
|
ExportTypes.CSV, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
responseType: 'blob', |
|
|
|
|
|
|
|
query: { |
|
|
|
|
|
|
|
offset, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
const { data } = res |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
offset = +res.headers['nc-export-offset'] |
|
|
|
|
|
|
|
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' }) |
|
|
|
|
|
|
|
FileSaver.saveAs(blob, `${this.meta.title}_exported_${c++}.csv`) |
|
|
|
|
|
|
|
if (offset > -1) { |
|
|
|
|
|
|
|
this.$toast.info('Downloading more files').goAway(3000) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
this.$toast.success('Successfully exported all table data').goAway(3000) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
|
|
console.log(e) |
|
|
|
|
|
|
|
this.$toast.error(e.message).goAway(3000) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
async importData(columnMappings) { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const data = this.parsedCsv.data |
|
|
|
|
|
|
|
for (let i = 0, progress = 0; i < data.length; i += 500) { |
|
|
|
|
|
|
|
const batchData = data.slice(i, i + 500).map((row) => |
|
|
|
|
|
|
|
columnMappings.reduce((res, col) => { |
|
|
|
|
|
|
|
// todo: parse data |
|
|
|
|
|
|
|
if (col.enabled && col.destCn) { |
|
|
|
|
|
|
|
const v = this.meta && this.meta.columns.find((c) => c.title === col.destCn) |
|
|
|
|
|
|
|
let input = row[col.sourceCn] |
|
|
|
|
|
|
|
// parse potential boolean values |
|
|
|
|
|
|
|
if (v.uidt === UITypes.Checkbox) { |
|
|
|
|
|
|
|
input = input.replace(/["']/g, '').toLowerCase().trim() |
|
|
|
|
|
|
|
if (input === 'false' || input === 'no' || input === 'n') { |
|
|
|
|
|
|
|
input = '0' |
|
|
|
|
|
|
|
} else if (input === 'true' || input === 'yes' || input === 'y') { |
|
|
|
|
|
|
|
input = '1' |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (v.uidt === UITypes.Number) { |
|
|
|
|
|
|
|
if (input === '') { |
|
|
|
|
|
|
|
input = null |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (v.uidt === UITypes.SingleSelect || v.uidt === UITypes.MultiSelect) { |
|
|
|
|
|
|
|
if (input === '') { |
|
|
|
|
|
|
|
input = null |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
res[col.destCn] = input |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return res |
|
|
|
|
|
|
|
}, {}), |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
await this.$api.dbTableRow.bulkCreate(NOCO, this.projectName, this.meta.title, batchData) |
|
|
|
|
|
|
|
progress += batchData.length |
|
|
|
|
|
|
|
this.$store.commit('loader/MutMessage', `Importing data : ${progress}/${data.length}`) |
|
|
|
|
|
|
|
this.$store.commit('loader/MutProgress', Math.round((100 * progress) / data.length)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
this.columnMappingModal = false |
|
|
|
|
|
|
|
this.$store.commit('loader/MutClear') |
|
|
|
|
|
|
|
this.$emit('reload') |
|
|
|
|
|
|
|
this.$toast.success('Successfully imported table data').goAway(3000) |
|
|
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
|
|
this.$toast.error(e.message).goAway(3000) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
</script> |
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
|
|
<template> |
|
|
|
<template> |
|
|
|
<div> |
|
|
|
<v-dropdown> |
|
|
|
|
|
|
|
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn"> |
|
|
|
|
|
|
|
<div class="flex gap-1 align-center"> |
|
|
|
|
|
|
|
<MdiFlashIcon class="text-grey" /> |
|
|
|
|
|
|
|
<!-- More --> |
|
|
|
|
|
|
|
{{ $t('general.more') }} |
|
|
|
|
|
|
|
<MdiMenuDownIcon class="text-grey" /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</a-button> |
|
|
|
|
|
|
|
<template #overlay> |
|
|
|
|
|
|
|
<div></div> |
|
|
|
|
|
|
|
</template> |
|
|
|
|
|
|
|
</v-dropdown> |
|
|
|
|
|
|
|
<!-- <div> |
|
|
|
<v-menu open-on-hover bottom offset-y transition="slide-y-transition"> |
|
|
|
<v-menu open-on-hover bottom offset-y transition="slide-y-transition"> |
|
|
|
<template #activator="{ on }"> |
|
|
|
<template #activator="{ on }"> |
|
|
|
<v-btn |
|
|
|
<v-btn |
|
|
@ -257,7 +29,7 @@ export default { |
|
|
|
v-on="on" |
|
|
|
v-on="on" |
|
|
|
> |
|
|
|
> |
|
|
|
<v-icon small color="#777"> mdi-flash-outline </v-icon> |
|
|
|
<v-icon small color="#777"> mdi-flash-outline </v-icon> |
|
|
|
<!-- More --> |
|
|
|
<!– More –> |
|
|
|
{{ $t('general.more') }} |
|
|
|
{{ $t('general.more') }} |
|
|
|
|
|
|
|
|
|
|
|
<v-icon small color="#777"> mdi-menu-down </v-icon> |
|
|
|
<v-icon small color="#777"> mdi-menu-down </v-icon> |
|
|
@ -269,7 +41,7 @@ export default { |
|
|
|
<v-list-item-title> |
|
|
|
<v-list-item-title> |
|
|
|
<v-icon small class="mr-1"> mdi-download-outline </v-icon> |
|
|
|
<v-icon small class="mr-1"> mdi-download-outline </v-icon> |
|
|
|
<span class="caption"> |
|
|
|
<span class="caption"> |
|
|
|
<!-- Download as CSV --> |
|
|
|
<!– Download as CSV –> |
|
|
|
{{ $t('activity.downloadCSV') }} |
|
|
|
{{ $t('activity.downloadCSV') }} |
|
|
|
</span> |
|
|
|
</span> |
|
|
|
</v-list-item-title> |
|
|
|
</v-list-item-title> |
|
|
@ -278,11 +50,11 @@ export default { |
|
|
|
<v-list-item-title> |
|
|
|
<v-list-item-title> |
|
|
|
<v-icon small class="mr-1" color=""> mdi-upload-outline </v-icon> |
|
|
|
<v-icon small class="mr-1" color=""> mdi-upload-outline </v-icon> |
|
|
|
<span class="caption"> |
|
|
|
<span class="caption"> |
|
|
|
<!-- Upload CSV --> |
|
|
|
<!– Upload CSV –> |
|
|
|
{{ $t('activity.uploadCSV') }} |
|
|
|
{{ $t('activity.uploadCSV') }} |
|
|
|
</span> |
|
|
|
</span> |
|
|
|
|
|
|
|
|
|
|
|
<span class="caption grey--text">(<x-icon small color="grey lighten-2"> mdi-alpha </x-icon> version)</span> |
|
|
|
<span class="caption grey--text">(<x-icon small color="grey lighten-2"> mdi-alpha </x-icon> version)</span> |
|
|
|
</v-list-item-title> |
|
|
|
</v-list-item-title> |
|
|
|
</v-list-item> |
|
|
|
</v-list-item> |
|
|
|
<v-list-item |
|
|
|
<v-list-item |
|
|
@ -294,7 +66,7 @@ export default { |
|
|
|
<v-list-item-title> |
|
|
|
<v-list-item-title> |
|
|
|
<v-icon small class="mr-1" color=""> mdi-view-list-outline </v-icon> |
|
|
|
<v-icon small class="mr-1" color=""> mdi-view-list-outline </v-icon> |
|
|
|
<span class="caption"> |
|
|
|
<span class="caption"> |
|
|
|
<!-- Shared View List --> |
|
|
|
<!– Shared View List –> |
|
|
|
{{ $t('activity.listSharedView') }} |
|
|
|
{{ $t('activity.listSharedView') }} |
|
|
|
</span> |
|
|
|
</span> |
|
|
|
</v-list-item-title> |
|
|
|
</v-list-item-title> |
|
|
@ -316,9 +88,9 @@ export default { |
|
|
|
:parsed-csv="parsedCsv" |
|
|
|
:parsed-csv="parsedCsv" |
|
|
|
@import="importData" |
|
|
|
@import="importData" |
|
|
|
/> |
|
|
|
/> |
|
|
|
<!-- <webhook-modal v-if="webhookModal" v-model="webhookModal" :meta="meta" /> --> |
|
|
|
<!– <webhook-modal v-if="webhookModal" v-model="webhookModal" :meta="meta" /> –> |
|
|
|
<WebhookSlider v-model="webhookModal" :meta="meta" /> |
|
|
|
<WebhookSlider v-model="webhookModal" :meta="meta" /> |
|
|
|
</div> |
|
|
|
</div> --> |
|
|
|
</template> |
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
|
|
<style scoped></style> |
|
|
|
<style scoped></style> |
|
|
|