mirror of https://github.com/nocodb/nocodb
Pranav C
3 years ago
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> |
<template> |
||||||
<v-tooltip bottom> |
<div> |
||||||
<template #activator="{on}"> |
<v-dialog max-width="600" :value="dropOrUpload"> |
||||||
<input ref="file" type="file" style="display: none" accept=".xlsx, .xls" @change="_change($event)"> |
<v-card max-width="600"> |
||||||
<v-btn |
<div class="pa-4"> |
||||||
small |
<div |
||||||
outlined |
class="nc-droppable d-flex align-center justify-center" |
||||||
v-on="on" |
:style="{ |
||||||
@click="$refs.file.click()" |
background : dragOver ? '#7774' : '' |
||||||
> |
}" |
||||||
<v-icon small class="mr-1"> |
@click="$refs.file.click()" |
||||||
mdi-file-excel-outline |
@drop.prevent="dropHandler" |
||||||
</v-icon> |
@dragover.prevent="dragOver = true" |
||||||
Import |
@dragenter.prevent="dragOver = true" |
||||||
</v-btn> |
@dragexit="dragOver = false" |
||||||
</template> |
@dragleave="dragOver = false" |
||||||
<span class="caption">Create template from Excel</span> |
@dragend="dragOver = false" |
||||||
</v-tooltip> |
> |
||||||
|
<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> |
</template> |
||||||
|
|
||||||
<script> |
<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 { |
export default { |
||||||
name: 'ExcelImport', |
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: { |
methods: { |
||||||
|
|
||||||
|
selectFile() { |
||||||
|
this.$refs.file.files = null |
||||||
|
this.$refs.file.click() |
||||||
|
}, |
||||||
|
|
||||||
_change(file) { |
_change(file) { |
||||||
const files = file.target.files |
const files = file.target.files |
||||||
if (files && files[0]) { this._file(files[0]) } |
if (files && files[0]) { |
||||||
|
this._file(files[0]) |
||||||
|
} |
||||||
}, |
}, |
||||||
_file(file) { |
_file(file) { |
||||||
|
this.loaderMessage = 'Loading excel file...' |
||||||
|
this.dropOrUpload = false |
||||||
|
console.time('excelImport') |
||||||
const reader = new FileReader() |
const reader = new FileReader() |
||||||
reader.onload = (e) => { |
reader.onload = (e) => { |
||||||
const ab = e.target.result |
const ab = e.target.result |
||||||
const wb = XLSX.read(new Uint8Array(ab), { type: 'array' }) |
const templateGenerator = new ExcelTemplateAdapter(file.name, ab) |
||||||
|
templateGenerator.parse() |
||||||
// const res = {} |
this.templateData = templateGenerator.getTemplate() |
||||||
// iterate each sheet |
this.importData = templateGenerator.getData() |
||||||
// each sheet repensents each table |
console.timeEnd('excelImport') |
||||||
for (let i = 0; i < wb.SheetNames.length; i++) { |
this.loaderMessage = null |
||||||
const sheet = wb.SheetNames[i] |
} |
||||||
const ws = wb.Sheets[sheet] |
reader.onerror = () => { |
||||||
const rows = XLSX.utils.sheet_to_json(ws, { header: 1 }) |
this.loaderMessage = null |
||||||
console.log(rows) |
|
||||||
} |
|
||||||
} |
} |
||||||
reader.readAsArrayBuffer(file) |
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> |
</script> |
||||||
|
|
||||||
<style scoped> |
<style scoped> |
||||||
|
.nc-droppable { |
||||||
|
width: 100%; |
||||||
|
min-height: 200px; |
||||||
|
border-radius: 4px; |
||||||
|
border: 2px dashed var(--v-textColor-lighten5); |
||||||
|
} |
||||||
</style> |
</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