mirror of https://github.com/nocodb/nocodb
Wing-Kam Wong
2 years ago
1 changed files with 79 additions and 312 deletions
@ -1,324 +1,91 @@
|
||||
<script> |
||||
import FileSaver from 'file-saver' |
||||
import { ExportTypes } from 'nocodb-sdk' |
||||
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, |
||||
}) |
||||
<script setup lang="ts"> |
||||
import MdiFlashOutlineIcon from '~icons/mdi/flash-outline' |
||||
import MdiDownloadOutlineIcon from '~icons/mdi/download-outline' |
||||
import MdiUploadOutlineIcon from '~icons/mdi/upload-outline' |
||||
import MdiViewListOutlineIcon from '~icons/mdi/view-list-outline' |
||||
import MdiHookIcon from '~icons/mdi/hook' |
||||
|
||||
const { isUIAllowed } = useUIPermission() |
||||
// TODO:: identify based on meta |
||||
const isView = ref(false) |
||||
|
||||
function exportCsv() { |
||||
// TODO |
||||
} |
||||
|
||||
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 |
||||
function importCsv() { |
||||
// TODO |
||||
} |
||||
|
||||
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 |
||||
function openSharedViewModal() { |
||||
// TODO: |
||||
} |
||||
|
||||
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) |
||||
} |
||||
}, |
||||
}, |
||||
function openWebhookModal() { |
||||
// TODO: |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<v-menu open-on-hover bottom offset-y transition="slide-y-transition"> |
||||
<template #activator="{ on }"> |
||||
<v-btn |
||||
v-t="['c:actions']" |
||||
outlined |
||||
class="nc-actions-menu-btn caption px-2 nc-remove-border font-weight-medium" |
||||
small |
||||
text |
||||
v-on="on" |
||||
> |
||||
<v-icon small color="#777"> mdi-flash-outline </v-icon> |
||||
<!-- More --> |
||||
<a-menu offset-y transition="slide-y-transition" mode="horizontal"> |
||||
<a-sub-menu key="sub1"> |
||||
<template #icon> |
||||
<setting-outlined /> |
||||
</template> |
||||
<template #title> |
||||
<span class="flex items-center gap-2"> |
||||
<MdiFlashOutlineIcon /> |
||||
{{ $t('general.more') }} |
||||
|
||||
<v-icon small color="#777"> mdi-menu-down </v-icon> |
||||
</v-btn> |
||||
</span> |
||||
</template> |
||||
|
||||
<v-list dense> |
||||
<v-list-item v-t="['a:actions:download-csv']" dense @click="exportCsv"> |
||||
<v-list-item-title> |
||||
<v-icon small class="mr-1"> mdi-download-outline </v-icon> |
||||
<span class="caption"> |
||||
<a-menu-item key="action:downloadCSV" v-t="['c:actions']" @click="exportCsv"> |
||||
<span class="flex items-center gap-2"> |
||||
<MdiDownloadOutlineIcon class="text-primary" /> |
||||
<!-- Download as CSV --> |
||||
{{ $t('activity.downloadCSV') }} |
||||
</span> |
||||
</v-list-item-title> |
||||
</v-list-item> |
||||
<v-list-item v-if="_isUIAllowed('csvImport') && !isView" v-t="['a:actions:upload-csv']" dense @click="importModal = true"> |
||||
<v-list-item-title> |
||||
<v-icon small class="mr-1" color=""> mdi-upload-outline </v-icon> |
||||
<span class="caption"> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-item |
||||
v-if="isUIAllowed('csvImport') && !isView" |
||||
key="action:uploadCSV" |
||||
v-t="['a:actions:upload-csv']" |
||||
@click="importCsv" |
||||
> |
||||
<span class="flex items-center gap-2"> |
||||
<MdiUploadOutlineIcon class="text-primary" /> |
||||
<!-- Upload CSV --> |
||||
{{ $t('activity.uploadCSV') }} |
||||
</span> |
||||
</a-menu-item> |
||||
|
||||
<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> |
||||
<v-list-item |
||||
v-if="_isUIAllowed('SharedViewList') && !isView" |
||||
<a-menu-item |
||||
v-if="isUIAllowed('SharedViewList') && !isView" |
||||
key="action:listSharedView" |
||||
v-t="['a:actions:shared-view-list']" |
||||
dense |
||||
@click="$emit('showAdditionalFeatOverlay', 'shared-views')" |
||||
@click="openSharedViewModal" |
||||
> |
||||
<v-list-item-title> |
||||
<v-icon small class="mr-1" color=""> mdi-view-list-outline </v-icon> |
||||
<span class="caption"> |
||||
<span class="flex items-center gap-2"> |
||||
<MdiViewListOutlineIcon class="text-primary" /> |
||||
<!-- Shared View List --> |
||||
{{ $t('activity.listSharedView') }} |
||||
</span> |
||||
</v-list-item-title> |
||||
</v-list-item> |
||||
<v-list-item v-if="_isUIAllowed('webhook') && !isView" v-t="['c:actions:webhook']" dense @click="webhookModal = true"> |
||||
<v-list-item-title> |
||||
<v-icon small class="mr-1" color=""> mdi-hook </v-icon> |
||||
<span class="caption"> Webhooks </span> |
||||
</v-list-item-title> |
||||
</v-list-item> |
||||
</v-list> |
||||
</v-menu> |
||||
<DropOrSelectFileModal v-model="importModal" accept=".csv" text="CSV" @file="onCsvFileSelection" /> |
||||
<ColumnMappingModal |
||||
v-if="columnMappingModal && meta" |
||||
v-model="columnMappingModal" |
||||
:meta="meta" |
||||
:import-data-columns="parsedCsv.columns" |
||||
:parsed-csv="parsedCsv" |
||||
@import="importData" |
||||
/> |
||||
<!-- <webhook-modal v-if="webhookModal" v-model="webhookModal" :meta="meta" /> --> |
||||
<WebhookSlider v-model="webhookModal" :meta="meta" /> |
||||
</div> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-item |
||||
v-if="isUIAllowed('webhook') && !isView" |
||||
key="action:webhooks" |
||||
v-t="['c:actions:webhook']" |
||||
@click="openWebhookModal" |
||||
> |
||||
<span class="flex items-center gap-2"> |
||||
<MdiHookIcon class="text-primary" /> |
||||
<!-- TODO: i18n --> |
||||
Webhooks |
||||
</span> |
||||
</a-menu-item> |
||||
</a-sub-menu> |
||||
</a-menu> |
||||
</template> |
||||
|
||||
<style scoped></style> |
||||
|
Loading…
Reference in new issue