Browse Source

feat: add project creation from excel

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/765/head
Pranav C 3 years ago
parent
commit
39c644ad65
  1. 90
      packages/nc-gui/components/import/ExcelTemplateAdapter.js
  2. 13
      packages/nc-gui/components/import/TemplateGenerator.js
  3. 177
      packages/nc-gui/components/import/excelImport.vue
  4. 148
      packages/nc-gui/components/templates/createProjectFromTemplateBtn.vue
  5. 172
      packages/nc-gui/components/templates/editor.vue
  6. 3
      packages/nc-gui/lang/en.json
  7. 29
      packages/nc-gui/pages/projects/index.vue
  8. 4
      packages/nc-gui/plugins/ncApis/index.js
  9. 11
      packages/nc-gui/plugins/ncApis/restApi.js
  10. 2
      packages/nc-gui/store/meta.js
  11. 3
      packages/nc-gui/store/sqlMgr.js
  12. 4
      packages/nocodb/src/lib/noco/meta/NcMetaIOImpl.ts

90
packages/nc-gui/components/import/ExcelTemplateAdapter.js

@ -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
}
}

13
packages/nc-gui/components/import/TemplateGenerator.js

@ -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')
}
}

177
packages/nc-gui/components/import/excelImport.vue

@ -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>

148
packages/nc-gui/components/templates/createProjectFromTemplateBtn.vue

@ -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>

172
packages/nc-gui/components/templates/editor.vue

@ -1,88 +1,90 @@
<template>
<div class="h-100">
<v-toolbar v-if="!viewMode" class="elevation-0">
<!-- <v-text-field
v-model="url"
clearable
placeholder="Enter template url"
outlined
hide-details
dense
@keydown.enter="loadUrl"
/>-->
<!-- <v-btn outlined class='ml-1' @click='loadUrl'> Load URL</v-btn>-->
<v-tooltip bottom>
<template #activator="{on}">
<v-btn
small
outlined
v-on="on"
@click="$toast.info('Happy hacking!').goAway(3000)"
>
<v-icon small class="mr-1">
mdi-file-excel-outline
<slot name="toolbar">
<!-- <v-text-field
v-model="url"
clearable
placeholder="Enter template url"
outlined
hide-details
dense
@keydown.enter="loadUrl"
/>-->
<!-- <v-btn outlined class='ml-1' @click='loadUrl'> Load URL</v-btn>-->
<v-tooltip bottom>
<template #activator="{on}">
<v-btn
small
outlined
v-on="on"
@click="$toast.info('Happy hacking!').goAway(3000)"
>
<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-spacer />
<v-icon class="mr-3" @click="helpModal=true">
mdi-information-outline
</v-icon>
<!-- <v-icon class="mr-3" @click="openUrl">
mdi-web
</v-icon>-->
<!-- <v-tooltip bottom>
<template #activator="{on}">
<v-icon
class="mr-3"
v-on="on"
@click="url = '',project.tables= []"
>
mdi-close-circle-outline
</v-icon>
Import
</v-btn>
</template>
<span class="caption">Create template from Excel</span>
</v-tooltip>
<v-spacer />
<v-icon class="mr-3" @click="helpModal=true">
mdi-information-outline
</v-icon>
<!-- <v-icon class="mr-3" @click="openUrl">
mdi-web
</v-icon>-->
<!-- <v-tooltip bottom>
<template #activator="{on}">
<v-icon
class="mr-3"
v-on="on"
@click="url = '',project.tables= []"
>
mdi-close-circle-outline
</template>
<span class="caption">Reset template</span>
</v-tooltip>-->
<v-btn small outlined class="mr-1" @click="project = {tables : []}">
<v-icon small>
mdi-close
</v-icon>
</template>
<span class="caption">Reset template</span>
</v-tooltip>-->
Reset
</v-btn>
<!-- <v-icon
:color="$store.getters['github/isAuthorized'] ? '' : 'error'"
class="mr-3"
@click="githubConfigForm = !githubConfigForm"
>
mdi-github
</v-icon>-->
<v-btn small outlined class="mr-1" @click="createTablesDialog = true">
<v-icon small>
mdi-plus
</v-icon>
New table
</v-btn>
<v-btn small outlined class="mr-1" @click="project = {tables : []}">
<v-icon small>
mdi-close
</v-icon>
Reset
</v-btn>
<!-- <v-icon
:color="$store.getters['github/isAuthorized'] ? '' : 'error'"
class="mr-3"
@click="githubConfigForm = !githubConfigForm"
>
mdi-github
</v-icon>-->
<v-btn small outlined class="mr-1" @click="createTablesDialog = true">
<v-icon small>
mdi-plus
</v-icon>
New table
</v-btn>
<!-- <v-btn outlined small class='mr-1' @click='submitTemplate'> Submit Template</v-btn>-->
<v-btn
color="primary"
outlined
small
class="mr-1"
:loading="loading"
:disabled="loading"
@click="saveTemplate"
>
{{ id || localId ? 'Update in' : 'Submit to' }} NocoDB
</v-btn>
<!-- <v-btn outlined small class='mr-1' @click='submitTemplate'> Submit Template</v-btn>-->
<v-btn
color="primary"
outlined
small
class="mr-1"
:loading="loading"
:disabled="loading"
@click="saveTemplate"
>
{{ id || localId ? 'Update in' : 'Submit to' }} NocoDB
</v-btn>
</slot>
</v-toolbar>
<v-container class="text-center" style="height:calc(100% - 64px);overflow-y: auto">
<v-form ref="form">
@ -620,7 +622,7 @@ export default {
return this.url && this.url.split('/').pop()
},
projectTemplate() {
return {
const template = {
...this.project,
tables: (this.project.tables || []).map((t) => {
const table = { tn: t.tn, columns: [], hasMany: [], manyToMany: [], belongsTo: [], v: [] }
@ -673,6 +675,10 @@ export default {
return table
})
}
this.$emit('update:templateData', template)
return template
}
},
@ -851,7 +857,7 @@ export default {
isRelation(col) {
return col.uidt === 'LinkToAnotherRecord' ||
col.uidt === 'ForeignKey'
col.uidt === 'ForeignKey'
},
isLookup(col) {
return col.uidt === 'Lookup'
@ -864,11 +870,11 @@ export default {
},
isLookupOrRollup(col) {
return this.isLookup(col) ||
this.isRollup(col)
this.isRollup(col)
},
isSelect(col) {
return col.uidt === 'MultiSelect' ||
col.uidt === 'SingleSelect'
col.uidt === 'SingleSelect'
},
addNewColumnRow(table, uidt) {
table.columns.push({

3
packages/nc-gui/lang/en.json

@ -8,7 +8,8 @@
"subtext_1_tooltip": "Create a new project",
"subtext_2": "Create By Connecting <br>To An External Database",
"subtext_2_tooltip": "Supports MySQL, PostgreSQL, SQL Server & SQLite",
"from_template": "Create Project from template"
"from_template": "Create Project from template",
"from_excel": "Create Project from excel"
},
"search_project": "Search Project",
"import_metadata": "Import Metadata",

29
packages/nc-gui/pages/projects/index.vue

@ -211,6 +211,27 @@
/>
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item
title
class="pt-2 nc-create-project-from-excel"
@click="onCreateProjectFromExcel()"
>
<v-list-item-icon class="mr-2">
<v-icon small class="">
mdi-file-excel-outline
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<!-- Create By Connecting <br>To An External Database -->
<span
class="caption font-weight-regular"
v-html="
$t('projects.create_new_project_button.from_excel')
"
/>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
@ -686,6 +707,7 @@
:dialog-show="dialogShow"
:heading="confirmMessage"
/>
<excel-import ref="excelImport" v-model="excelImportModal" hide-label />
<templates-modal v-model="templatesModal" hide-label create-project />
</v-container>
</template>
@ -696,9 +718,11 @@ import ShareIcons from '../../components/share-icons'
import SponsorMini from '@/components/sponsorMini'
import colors from '~/mixins/colors'
import TemplatesModal from '~/components/templates/templatesModal'
import ExcelImport from '~/components/import/excelImport'
export default {
components: {
ExcelImport,
TemplatesModal,
ShareIcons,
SponsorMini,
@ -711,6 +735,7 @@ export default {
},
data() {
return {
excelImportModal: false,
templatesModal: false,
overlayVisible: true,
showCommunity: false,
@ -979,6 +1004,10 @@ export default {
onCreateProjectFromTemplate() {
this.templatesModal = true
},
onCreateProjectFromExcel() {
// this.$refs.excelImport.selectFile()
this.excelImportModal = true
},
async importProjectFromJSON() {
},
onTourCompletion() {

4
packages/nc-gui/plugins/ncApis/index.js

@ -5,7 +5,7 @@ export default function({ store: $store, $axios, ...rest }, inject) {
let projectId = null
inject('ncApis', {
get: ({ table, dbAlias = 'db', env = '_noco' }) => {
get: ({ table, dbAlias = 'db', env = '_noco', type }) => {
if (!$store.state.meta.metas[table]) {
return
}
@ -19,7 +19,7 @@ export default function({ store: $store, $axios, ...rest }, inject) {
instanceRefs[env][dbAlias][table] = ApiFactory.create(
table,
$store.getters['project/GtrProjectType'],
type || $store.getters['project/GtrProjectType'],
{ $store, $axios, projectId, dbAlias, env, table }
)

11
packages/nc-gui/plugins/ncApis/restApi.js

@ -93,6 +93,17 @@ export default class RestApi {
})).data
}
async insertBulk(data, {
params = {}
} = {}) {
return (await this.$axios({
method: 'post',
url: `/nc/${this.$ctx.projectId}/api/v1/${this.table}/bulk`,
data,
params
})).data
}
async delete(id) {
return this.$axios({
method: 'delete',

2
packages/nc-gui/store/meta.js

@ -16,7 +16,7 @@ export const mutations = {
}
export const actions = {
async ActLoadMeta({ state, commit, dispatch }, { tn, env, dbAlias, force }) {
async ActLoadMeta({ state, commit, dispatch }, { tn, env = '_noco', dbAlias = 'db', force }) {
if (!force && state.loading[tn]) {
return await new Promise((resolve) => {
const unsubscribe = this.app.store.subscribe((s) => {

3
packages/nc-gui/store/sqlMgr.js

@ -361,6 +361,8 @@ export const actions = {
dispatch
}, [args, op, opArgs, cusHeaders, cusAxiosOptions, queryParams, returnResponse]) {
const params = {}
params.project_id = rootState.project.projectId
if (this.$router.currentRoute && this.$router.currentRoute.params) {
if (this.$router.currentRoute.params.project_id) {
params.project_id = this.$router.currentRoute.params.project_id
@ -419,6 +421,7 @@ export const actions = {
return data
} catch (e) {
console.log(e)
const err = new Error(e.response.data.msg)
err.response = e.response
throw err

4
packages/nocodb/src/lib/noco/meta/NcMetaIOImpl.ts

@ -353,7 +353,7 @@ export default class NcMetaIOImpl extends NcMetaIO {
// }
}
config.id = id;
const project = {
const project: any = {
id,
title: projectName,
description,
@ -368,6 +368,8 @@ export default class NcMetaIOImpl extends NcMetaIO {
created_at: this.knexConnection?.fn?.now(),
updated_at: this.knexConnection?.fn?.now()
});
project.prefix = config.prefix;
return project;
} catch (e) {
console.log(e);

Loading…
Cancel
Save