mirror of https://github.com/nocodb/nocodb
12 changed files with 537 additions and 119 deletions
@ -0,0 +1,90 @@
|
||||
import XLSX from 'xlsx' |
||||
import TemplateGenerator from '~/components/import/TemplateGenerator' |
||||
import { UITypes } from '~/components/project/spreadsheet/helpers/uiTypes' |
||||
|
||||
const excelTypeToUidt = { |
||||
d: UITypes.DateTime, |
||||
b: UITypes.Checkbox, |
||||
n: UITypes.Number, |
||||
s: UITypes.SingleLineText |
||||
} |
||||
|
||||
export default class ExcelTemplateAdapter extends TemplateGenerator { |
||||
constructor(name, ab) { |
||||
super() |
||||
this.name = name |
||||
this.wb = XLSX.read(new Uint8Array(ab), { type: 'array' }) |
||||
this.project = { |
||||
title: this.name, |
||||
tables: [] |
||||
} |
||||
this.data = {} |
||||
} |
||||
|
||||
parse() { |
||||
for (let i = 0; i < this.wb.SheetNames.length; i++) { |
||||
const sheet = this.wb.SheetNames[i] |
||||
const table = { tn: sheet, columns: [] } |
||||
this.data[sheet] = [] |
||||
const ws = this.wb.Sheets[sheet] |
||||
const rows = XLSX.utils.sheet_to_json(ws, { header: 1 }) |
||||
|
||||
for (let col = 0; col < rows[0].length; col++) { |
||||
const column = { |
||||
cn: rows[0][col] |
||||
} |
||||
|
||||
const cellProps = ws[`${col.toString(26).split('').map(s => (parseInt(s, 26) + 10).toString(36).toUpperCase())}2`] |
||||
|
||||
column.uidt = excelTypeToUidt[cellProps.t] || UITypes.SingleLineText |
||||
|
||||
// todo: optimize
|
||||
if (column.uidt === UITypes.SingleLineText) { |
||||
// check for long text
|
||||
if (rows.some(r => (r[col] || '').toString().length > 255)) { |
||||
column.uidt = UITypes.LongText |
||||
} else { |
||||
const vals = rows.slice(1).map(r => r[col]).filter(v => v !== null && v !== undefined) |
||||
|
||||
// check column is multi or single select by comparing unique values
|
||||
if (vals.some(v => v && v.includes(','))) { |
||||
const flattenedVals = vals.flatMap(v => v ? v.split(',') : []) |
||||
const uniqueVals = new Set(flattenedVals) |
||||
if (flattenedVals.length > uniqueVals.size && uniqueVals.size <= flattenedVals.length / 10) { |
||||
column.uidt = UITypes.MultiSelect |
||||
column.dtxp = [...uniqueVals].join(',') |
||||
} |
||||
} else { |
||||
const uniqueVals = new Set(vals) |
||||
if (vals.length > uniqueVals.size && uniqueVals.size <= vals.length / 10) { |
||||
column.uidt = UITypes.SingleSelect |
||||
column.dtxp = [...uniqueVals].join(',') |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
table.columns.push(column) |
||||
} |
||||
|
||||
for (const row of rows.slice(1)) { |
||||
const rowData = {} |
||||
for (let i = 0; i < table.columns.length; i++) { |
||||
// toto: do parsing if necessary based on type
|
||||
rowData[table.columns[i].cn] = row[i] |
||||
} |
||||
this.data[sheet].push(rowData) |
||||
} |
||||
|
||||
this.project.tables.push(table) |
||||
} |
||||
} |
||||
|
||||
getTemplate() { |
||||
return this.project |
||||
} |
||||
|
||||
getData() { |
||||
return this.data |
||||
} |
||||
} |
@ -0,0 +1,13 @@
|
||||
export default class TemplateGenerator { |
||||
parse() { |
||||
throw new Error('\'parse\' method is not implemented') |
||||
} |
||||
|
||||
getTemplate() { |
||||
throw new Error('\'getTemplate\' method is not implemented') |
||||
} |
||||
|
||||
getData() { |
||||
throw new Error('\'getData\' method is not implemented') |
||||
} |
||||
} |
@ -1,55 +1,170 @@
|
||||
<template> |
||||
<v-tooltip bottom> |
||||
<template #activator="{on}"> |
||||
<input ref="file" type="file" style="display: none" accept=".xlsx, .xls" @change="_change($event)"> |
||||
<v-btn |
||||
small |
||||
outlined |
||||
v-on="on" |
||||
@click="$refs.file.click()" |
||||
> |
||||
<v-icon small class="mr-1"> |
||||
mdi-file-excel-outline |
||||
</v-icon> |
||||
Import |
||||
</v-btn> |
||||
</template> |
||||
<span class="caption">Create template from Excel</span> |
||||
</v-tooltip> |
||||
<div> |
||||
<v-dialog max-width="600" :value="dropOrUpload"> |
||||
<v-card max-width="600"> |
||||
<div class="pa-4"> |
||||
<div |
||||
class="nc-droppable d-flex align-center justify-center" |
||||
:style="{ |
||||
background : dragOver ? '#7774' : '' |
||||
}" |
||||
@click="$refs.file.click()" |
||||
@drop.prevent="dropHandler" |
||||
@dragover.prevent="dragOver = true" |
||||
@dragenter.prevent="dragOver = true" |
||||
@dragexit="dragOver = false" |
||||
@dragleave="dragOver = false" |
||||
@dragend="dragOver = false" |
||||
> |
||||
<v-icon size="50" color="grey"> |
||||
mdi-upload |
||||
</v-icon> |
||||
</div> |
||||
</div> |
||||
</v-card> |
||||
</v-dialog> |
||||
|
||||
<v-tooltip bottom> |
||||
<template #activator="{on}"> |
||||
<input ref="file" type="file" style="display: none" accept=".xlsx, .xls" @change="_change($event)"> |
||||
<v-btn |
||||
|
||||
v-if="!hideLabel" |
||||
small |
||||
outlined |
||||
v-on="on" |
||||
@click="$refs.file.click()" |
||||
> |
||||
<v-icon small class="mr-1"> |
||||
mdi-file-excel-outline |
||||
</v-icon> |
||||
Import |
||||
</v-btn> |
||||
</template> |
||||
<span class="caption">Create template from Excel</span> |
||||
</v-tooltip> |
||||
|
||||
<v-dialog v-if="templateData" :value="true"> |
||||
<v-card> |
||||
<template-editor :template-data.sync="templateData"> |
||||
<template #toolbar> |
||||
<v-spacer /> |
||||
<create-project-from-template-btn |
||||
:loader-message.sync="loaderMessage" |
||||
:template-data="templateData" |
||||
:import-data="importData" |
||||
/> |
||||
</template> |
||||
</template-editor> |
||||
</v-card> |
||||
</v-dialog> |
||||
|
||||
<v-overlay :value="loaderMessage" z-index="99999" opacity=".9"> |
||||
<div class="d-flex flex-column align-center"> |
||||
<v-progress-circular indeterminate size="100" width="15" class="mb-10" /> |
||||
<span class="title">{{ loaderMessage }}</span> |
||||
</div> |
||||
</v-overlay> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
|
||||
import XLSX from 'xlsx' |
||||
// import XLSX from 'xlsx' |
||||
import ExcelTemplateAdapter from '~/components/import/ExcelTemplateAdapter' |
||||
import TemplateEditor from '~/components/templates/editor' |
||||
import CreateProjectFromTemplateBtn from '~/components/templates/createProjectFromTemplateBtn' |
||||
|
||||
export default { |
||||
name: 'ExcelImport', |
||||
components: { CreateProjectFromTemplateBtn, TemplateEditor }, |
||||
props: { |
||||
hideLabel: Boolean, |
||||
value: Boolean |
||||
}, |
||||
data() { |
||||
return { |
||||
templateData: null, |
||||
importData: null, |
||||
dragOver: false, |
||||
loaderMessage: null |
||||
} |
||||
}, |
||||
computed: { |
||||
dropOrUpload: { |
||||
set(v) { |
||||
this.$emit('input', v) |
||||
}, |
||||
get() { |
||||
return this.value |
||||
} |
||||
} |
||||
}, |
||||
mounted() { |
||||
}, |
||||
methods: { |
||||
|
||||
selectFile() { |
||||
this.$refs.file.files = null |
||||
this.$refs.file.click() |
||||
}, |
||||
|
||||
_change(file) { |
||||
const files = file.target.files |
||||
if (files && files[0]) { this._file(files[0]) } |
||||
if (files && files[0]) { |
||||
this._file(files[0]) |
||||
} |
||||
}, |
||||
_file(file) { |
||||
this.loaderMessage = 'Loading excel file...' |
||||
this.dropOrUpload = false |
||||
console.time('excelImport') |
||||
const reader = new FileReader() |
||||
reader.onload = (e) => { |
||||
const ab = e.target.result |
||||
const wb = XLSX.read(new Uint8Array(ab), { type: 'array' }) |
||||
|
||||
// const res = {} |
||||
// iterate each sheet |
||||
// each sheet repensents each table |
||||
for (let i = 0; i < wb.SheetNames.length; i++) { |
||||
const sheet = wb.SheetNames[i] |
||||
const ws = wb.Sheets[sheet] |
||||
const rows = XLSX.utils.sheet_to_json(ws, { header: 1 }) |
||||
console.log(rows) |
||||
} |
||||
const templateGenerator = new ExcelTemplateAdapter(file.name, ab) |
||||
templateGenerator.parse() |
||||
this.templateData = templateGenerator.getTemplate() |
||||
this.importData = templateGenerator.getData() |
||||
console.timeEnd('excelImport') |
||||
this.loaderMessage = null |
||||
} |
||||
reader.onerror = () => { |
||||
this.loaderMessage = null |
||||
} |
||||
reader.readAsArrayBuffer(file) |
||||
}, |
||||
dropHandler(ev) { |
||||
console.log('File(s) dropped') |
||||
let file |
||||
if (ev.dataTransfer.items) { |
||||
// Use DataTransferItemList interface to access the file(s) |
||||
if (ev.dataTransfer.items.length && ev.dataTransfer.items[0].kind === 'file') { |
||||
file = ev.dataTransfer.items[0].getAsFile() |
||||
} |
||||
} else if (ev.dataTransfer.files.length) { |
||||
file = ev.dataTransfer.files[0] |
||||
} |
||||
if (file) { |
||||
this._file(file) |
||||
} |
||||
}, |
||||
dragOverHandler(ev) { |
||||
console.log('File(s) in drop zone') |
||||
|
||||
// Prevent default behavior (Prevent file from being opened) |
||||
ev.preventDefault() |
||||
} |
||||
|
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
|
||||
.nc-droppable { |
||||
width: 100%; |
||||
min-height: 200px; |
||||
border-radius: 4px; |
||||
border: 2px dashed var(--v-textColor-lighten5); |
||||
} |
||||
</style> |
||||
|
@ -0,0 +1,148 @@
|
||||
<template> |
||||
<div> |
||||
<v-menu bottom offset-y> |
||||
<template #activator="{on}"> |
||||
<v-btn |
||||
:loading="projectCreation" |
||||
:disabled="projectCreation" |
||||
class="primary" |
||||
x-large |
||||
v-on="on" |
||||
> |
||||
Use template |
||||
<v-icon>mdi-menu-down</v-icon> |
||||
</v-btn> |
||||
</template> |
||||
<v-list> |
||||
<v-list-item dense class="py-2" @click="useTemplate('rest')"> |
||||
<v-list-item-title> |
||||
<v-icon class="mr-1" :color="textColors[7]"> |
||||
mdi-code-json |
||||
</v-icon> |
||||
Create REST Project |
||||
</v-list-item-title> |
||||
</v-list-item> |
||||
<v-list-item dense class="py-2" @click="useTemplate('graphql')"> |
||||
<v-list-item-title> |
||||
<v-icon class="mr-1" :color="textColors[3]"> |
||||
mdi-graphql |
||||
</v-icon> |
||||
Create GQL Project |
||||
</v-list-item-title> |
||||
</v-list-item> |
||||
</v-list> |
||||
</v-menu> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import colors from '~/mixins/colors' |
||||
|
||||
export default { |
||||
name: 'CreateProjectFromTemplateBtn', |
||||
mixins: [colors], |
||||
props: { |
||||
loading: Boolean, |
||||
templateData: Object, |
||||
importData: Object, |
||||
loaderMessage: String |
||||
}, |
||||
data() { |
||||
return { |
||||
projectCreation: false, |
||||
loaderMessagesIndex: 0, |
||||
loaderMessages: [ |
||||
'Setting up new database configs', |
||||
'Inferring database schema', |
||||
'Generating APIs.', |
||||
'Generating APIs..', |
||||
'Generating APIs...', |
||||
'Generating APIs....', |
||||
'Please wait', |
||||
'Please wait.', |
||||
'Please wait..', |
||||
'Please wait...', |
||||
'Please wait..', |
||||
'Please wait.', |
||||
'Please wait', |
||||
'Please wait.', |
||||
'Please wait..', |
||||
'Please wait...', |
||||
'Please wait..', |
||||
'Please wait.', |
||||
'Please wait..', |
||||
'Please wait...' |
||||
] |
||||
} |
||||
}, |
||||
methods: { |
||||
async useTemplate(projectType) { |
||||
// this.$emit('useTemplate', type) |
||||
|
||||
this.projectCreation = true |
||||
try { |
||||
const interv = setInterval(() => { |
||||
debugger |
||||
this.loaderMessagesIndex = this.loaderMessagesIndex < this.loaderMessages.length - 1 ? this.loaderMessagesIndex + 1 : 6 |
||||
this.$emit('update:loaderMessage', this.loaderMessages[this.loaderMessagesIndex]) |
||||
}, 1000) |
||||
|
||||
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'projectCreateByWebWithXCDB', { |
||||
title: this.templateData.title, |
||||
projectType, |
||||
template: this.templateData |
||||
}]) |
||||
|
||||
await this.$store.dispatch('project/ActLoadProjectInfo') |
||||
|
||||
clearInterval(interv) |
||||
if (this.importData) { |
||||
this.$emit('update:loaderMessage', 'Importing excel data to project') |
||||
await this.importDataToProject({ projectId: result.id, projectType, prefix: result.prefix }) |
||||
} |
||||
|
||||
this.$emit('update:loaderMessage', null) |
||||
|
||||
this.projectReloading = false |
||||
|
||||
this.$router.push({ |
||||
path: `/nc/${result.id}`, |
||||
query: { |
||||
new: 1 |
||||
} |
||||
}) |
||||
} catch (e) { |
||||
console.log(e) |
||||
this.$toast.error(e.message).goAway(3000) |
||||
} |
||||
this.projectCreation = false |
||||
}, |
||||
async importDataToProject({ projectId, projectType, prefix = '' }) { |
||||
this.$store.commit('project/MutProjectId', projectId) |
||||
this.$ncApis.setProjectId(projectId) |
||||
|
||||
await Promise.all(Object.entries(this.importData).map(async([table, data]) => { |
||||
await this.$store.dispatch('meta/ActLoadMeta', { |
||||
tn: `${prefix}${table}` |
||||
}) |
||||
|
||||
// todo: get table name properly |
||||
const api = this.$ncApis.get({ |
||||
table: `${prefix}${table}`, |
||||
type: projectType |
||||
}) |
||||
|
||||
for (let i = 0; i < data.length; i += 500) { |
||||
console.log(data[i]) |
||||
await api.insertBulk(data.slice(i, i + 500)) |
||||
} |
||||
})) |
||||
} |
||||
} |
||||
|
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
|
||||
</style> |
Loading…
Reference in new issue