多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1356 lines
50 KiB

<template>
<div class="h-100" style="min-height: 500px">
<v-toolbar v-if="!viewMode" class="elevation-0">
<slot name="toolbar" :valid="valid">
<!-- <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>
</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>
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>
</slot>
</v-toolbar>
<v-divider class="mt-6" />
<v-container class="text-center" style="height:calc(100% - 64px);overflow-y: auto">
<v-form ref="form" v-model="valid">
<v-row fluid class="justify-center">
<v-col cols="12">
<v-card class="elevation-0">
<v-card-text>
<div v-if="!viewMode" class="mx-auto" style="max-width:400px">
<div class="mt-1">
<v-text-field
ref="project"
v-model="project.title"
class="title"
outlined
hide-details
denses
:rules="[v => !!v || 'Project name required'] "
>
<template #label>
<span class="caption">Project Name</span>
</template>
</v-text-field>
</div>
</div>
<p v-if="project.tables" class="caption grey--text mt-4">
{{ project.tables.length }} sheet{{ project.tables.length > 1 ? 's' :'' }} available for import
</p>
<v-expansion-panels
v-if="project.tables && project.tables.length"
v-model="expansionPanel"
:multiple="viewMode"
accordion
>
<v-expansion-panel
v-for="(table, i) in project.tables"
:key="i"
>
<v-expansion-panel-header
:id="`tn_${table.tn}`"
>
<v-text-field
v-if="editableTn[i]"
:value="table.tn"
class="font-weight-bold"
style="max-width: 300px"
outlinedk
autofocus
dense
hide-details
@input="e => onTableNameUpdate(table, e)"
@click="e => viewMode || e.stopPropagation()"
@blur="$set(editableTn,i, false)"
@keydown.enter=" $set(editableTn,i, false)"
/>
<span
v-else
class="font-weight-bold"
@click="e => viewMode || (e.stopPropagation() , $set(editableTn,i, true))"
>
<v-icon color="primary lighten-1">mdi-table</v-icon>
{{ table.tn }}
</span>
<v-spacer />
<v-icon v-if="!viewMode" class="flex-grow-0 mr-2" small color="grey" @click.stop="deleteTable(i)">
mdi-delete-outline
</v-icon>
</v-expansion-panel-header>
<v-expansion-panel-content>
<!-- <v-toolbar>
<v-spacer></v-spacer>
<v-btn outlined small @click='showColCreateDialog(table,i)'>New Column
</v-btn>
</v-toolbar>-->
<template>
<v-simple-table v-if="table.columns.length" dense class="my-4">
<thead>
<tr>
<th class="caption text-left pa-1">
<!--Column Name-->
{{ $t('labels.columnName') }}
</th>
<th class="caption text-left pa-1" colspan="4">
<!--Column Type-->
{{ $t('labels.columnType') }}
</th>
<th />
<!-- <th class='text-center'>Related Table</th>-->
<!-- <th class='text-center'>Related Column</th>-->
</tr>
</thead>
<tbody>
<tr v-for="(col,j) in table.columns" :key="j" :data-exp="i">
<td class="pa-1 text-left" :style="{width:viewMode ? '33%' : '15%'}">
<span v-if="viewMode" class="body-1 ">
{{ col.cn }}
</span>
<v-text-field
v-else
:ref="`cn_${table.tn}_${j}`"
:value="col.cn"
outlined
dense
class="caption"
:placeholder="$t('labels.columnName')"
hide-details="auto"
:rules="[
v => !!v || 'Column name required',
v =>!table.columns.some(c=>c !== col && c.cn === v) || 'Duplicate column not allowed'
]"
@input="e => onColumnNameUpdate(col,e,table.tn)"
/>
</td>
<template v-if="viewMode">
<td
:style="{width:viewMode && isRelation(col) || isLookupOrRollup(col) ? '33%' : ''}"
:colspan="isRelation(col) || isLookupOrRollup(col) ? 3 : 5"
class="text-left"
>
<v-icon small>
{{ getIcon(col.uidt) }}
</v-icon>
<span class="caption">{{ col.uidt }}</span>
</td>
<td v-if="isRelation(col) || isLookupOrRollup(col)" class="text-left">
<span
v-if="isRelation(col)"
class="caption pointer primary--text"
@click="navigateToTable(col.rtn)"
>
{{ col.rtn }}
</span>
<template
v-else-if="isLookup(col)"
>
<span
class="caption pointer primary--text"
@click="navigateToTable(col.rtn && col.rtn.tn)"
>
{{ col.rtn && col.rtn.tn }}
</span> <span class="caption">({{ col.rcn }})</span>
</template>
<template
v-else-if="isRollup(col)"
>
<span
class="caption pointer primary--text"
@click="navigateToTable(col.rtn && col.rtn.tn)"
>
{{ col.rtn && col.rtn.tn }}
</span> <span class="caption">({{ col.fn }})</span>
</template>
</td>
</template>
<template v-else>
<td
class="pa-1 text-left"
style="width:200px;max-width:200px"
>
<v-autocomplete
:ref="`uidt_${table.tn}_${j}`"
style="max-width: 200px"
:value="col.uidt"
placeholder="Column Datatype"
outlined
dense
class="caption"
hide-details="auto"
:rules="[v => !!v || 'Column data type required']"
:items="col.uidt ==='ForeignKey' ? [...uiTypes, {
name: 'ForeignKey',
icon: 'mdi-link-variant',
virtual: 1
}] : uiTypes"
item-text="name"
item-value="name"
@input="v => onUidtChange(col.uidt,v,col,table)"
>
<template #item="{item:{name}}">
<v-chip v-if="colors[name]" :color="colors[name]" small>
{{ name }}
</v-chip>
<span v-else class="caption">{{ name }}</span>
</template>
<template #selection="{item:{name}} ">
<v-chip v-if="colors[name]" :color="colors[name]" small style="max-width: 100px">
{{ name }}
</v-chip>
<span v-else class="caption">{{ name }}</span>
</template>
</v-autocomplete>
</td>
<template
v-if="isRelation(col) || isLookupOrRollup(col)"
>
<td class="pa-1 text-left">
<v-autocomplete
:value="col.rtn"
placeholder="Related table"
outlined
class="caption"
dense
hide-details="auto"
:rules="[v => !!v || 'Related table name required', ...getRules(col, table)]"
:items="isLookupOrRollup(col) ? getRelatedTables(table.tn, isRollup(col)) : project.tables"
:item-text="t => isLookupOrRollup(col) ? `${t.tn} (${t.type})` : t.tn"
:item-value="t => isLookupOrRollup(col) ? t : t.tn"
:value-comparator="compareRel"
@input="v => onRtnChange(col.rtn,v, col, table)"
/>
</td>
<td v-if="isRelation(col)" class="pa-1">
<template v-if="col.uidt !== 'ForeignKey'">
<span
v-if="viewMode"
class="caption"
>
<!-- {{ col.type }}-->
</span>
<v-autocomplete
v-else
:value="col.type"
placeholder="Relation Type"
outlined
class="caption"
dense
hide-details="auto"
:rules="[v => !!v || 'Relation type required']"
:items="[{text:'Many To Many', value:'mm'},{text:'Has Many', value:'hm'}]"
@input="v => onRTypeChange(col.type, v, col,table)"
/>
</template>
</td>
<td v-if="isLookupOrRollup(col)" class="pa-1">
<span
v-if="viewMode"
class="caption"
>
{{ col.rcn }}
</span>
<v-autocomplete
v-else
v-model="col.rcn"
placeholder="Related table column"
outlined
dense
class="caption"
hide-details="auto"
:rules="[v => !!v || 'Related column name required']"
:items="(project.tables.find(t => t.tn === (col.rtn && col.rtn.tn || col.rtn)) || {columns:[]}).columns.filter(v=> !isVirtual(v))"
item-text="cn"
item-value="cn"
/>
</td>
<td v-if="isRollup(col)" class="pa-1">
<span
v-if="viewMode"
class="caption"
>
{{ col.fn }}
</span>
<v-autocomplete
v-else
v-model="col.fn"
placeholder="Rollup function"
outlined
dense
class="caption"
hide-details="auto"
:rules="[v => !!v || 'Rollup aggregate function name required']"
:items="rollupFnList"
/>
</td>
</template>
<template
v-if="isSelect(col)"
>
<td class="pa-1 text-left" colspan="2">
<span
v-if="viewMode"
class="caption"
>
{{ col.dtxp }}
</span>
<v-text-field
v-model="col.dtxp"
placeholder="Select options"
outlined
class="caption"
dense
hide-details
/>
</td>
</template>
<td
v-if="!isRollup(col) "
:colspan="isLookupOrRollup(col) || isRelation(col) || isSelect(col) ? (isRollup(col)?
0 :1) : 3"
/>
<td style="max-width: 50px;width: 50px">
<v-icon
v-if="!viewMode"
class="flex-grow-0"
small
color="grey"
@click.stop="deleteTableColumn(i,j, col, table)"
>
mdi-delete-outline
</v-icon>
</td>
</template>
</tr>
<!-- <tr>-->
<!-- <td colspan='4' class='text-center pa-2'>-->
<!-- </td>-->
<!-- </tr>-->
</tbody>
</v-simple-table>
<div v-if="!viewMode" class="text-center">
<v-icon class="mx-2" small @click="addNewColumnRow(table, 'Number')">
{{ getIcon('Number') }}
</v-icon>
<v-icon class="mx-2" small @click="addNewColumnRow(table, 'SingleLineText')">
{{ getIcon('SingleLineText') }}
</v-icon>
<v-icon class="mx-2" small @click="addNewColumnRow(table, 'LongText')">
{{
getIcon('LongText')
}}
</v-icon>
<v-icon class="mx-2" small @click="addNewColumnRow(table, 'LinkToAnotherRecord')">
{{ getIcon('LinkToAnotherRecord') }}
</v-icon>
<v-icon class="mx-2" small @click="addNewColumnRow(table, 'Lookup')">
{{ getIcon('Lookup') }}
</v-icon>
<v-icon class="mx-2" small @click="addNewColumnRow(table, 'Rollup')">
{{ getIcon('Rollup') }}
</v-icon>
<v-btn class="mx-2" small @click="addNewColumnRow(table)">
+ column
</v-btn>
</div>
</template>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<!-- <v-btn small color='primary' class='mt-10' @click='createTablesDialog = true'>New Table</v-btn>-->
<div v-if="!viewMode" class="mx-auto" style="max-width:600px">
<!-- <div class="mt-10">
<v-text-field
ref="project"
v-model="project.title"
class="caption"
outlined
dense
label="Project Name"
:rules="[v => !!v || 'Project name required'] "
/>
</div>-->
<!--
<div
class="rounded pa-5 mb-5 d-100 text-center caption"
:style="{background:project.image_url}"
@click="generateGradient"
>
Click to change gradient
</div>-->
<template v-if="!excelImport">
<gradient-generator v-model="project.image_url" class=" d-100 mt-4" />
<v-row>
<v-col>
<v-text-field
v-model="project.category"
:rules="[v => !!v || 'Category name required']"
class="caption"
outlined
dense
label="Project Category"
/>
</v-col>
<v-col>
<v-text-field
v-model="project.tags"
class="caption"
outlined
dense
label="Project Tags"
/>
</v-col>
</v-row>
<div>
<v-textarea
v-model="project.description"
class="caption"
outlined
dense
label="Project Description"
@click="counter++"
/>
</div>
</template>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-form>
</v-container>
<v-dialog v-model="createTablesDialog" max-width="500">
<v-card>
<v-card-title>
<!--Enter table name-->
{{ $t('msg.info.enterTableName') }}
</v-card-title>
<v-card-text>
<v-text-field
v-model="tableNamesInput"
autofocus
hide-details
dense
outlined
label="Enter comma separated table names"
@keydown.enter="addTables"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn outlined small @click="createTablesDialog=false">
<!-- Cancel -->
{{ $t('general.cancel') }}
</v-btn>
<v-btn outlined color="primary" small @click="addTables">
<!-- Save -->
{{ $t('general.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="createTableColumnsDialog" max-width="500">
<v-card>
<v-card-title>Enter column name</v-card-title>
<v-card-text>
<v-text-field
v-model="columnNamesInput"
autofocus
dense
outlined
hide-details
label="Enter comma separated column names"
@keydown.enter="addColumns"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn outlined small @click="createTableColumnsDialog=false">
<!-- Cancel -->
{{ $t('general.cancel') }}
</v-btn>
<v-btn outlined color="primary" small @click="addColumns">
<!-- Save -->
{{ $t('general.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<help v-model="helpModal" />
<v-tooltip v-if="!viewMode" left>
<template #activator="{on}">
<v-btn
fixed
fab
large
color="primary"
right
style="top:45%"
@click="createTablesDialog = true"
v-on="on"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</template>
<span class="caption">
<!--Add new table-->
{{ $t('tooltip.addTable') }}
</span>
</v-tooltip>
</div>
</template>
<script>
import { uiTypes, getUIDTIcon, UITypes } from '~/components/project/spreadsheet/helpers/uiTypes'
import GradientGenerator from '~/components/templates/gradientGenerator'
import Help from '~/components/templates/help'
const LinkToAnotherRecord = 'LinkToAnotherRecord'
const Lookup = 'Lookup'
const Rollup = 'Rollup'
const defaultColProp = {}
export default {
name: 'TemplateEditor',
components: { Help, GradientGenerator },
props: {
id: [Number, String],
viewMode: Boolean,
projectTemplate: Object,
excelImport: Boolean
},
data: () => ({
loading: false,
localId: null,
valid: false,
url: '',
githubConfigForm: false,
helpModal: false,
editableTn: {},
expansionPanel: 0,
project: {
name: 'Project name',
tables: []
},
tableNamesInput: '',
columnNamesInput: '',
createTablesDialog: false,
createTableColumnsDialog: false,
selectedTable: null,
uiTypes: uiTypes.filter(t => ![UITypes.Formula, UITypes.SpecificDBType].includes(t.name)),
rollupFnList: [
{ text: 'count', value: 'count' },
{ text: 'min', value: 'min' },
{ text: 'max', value: 'max' },
{ text: 'avg', value: 'avg' },
{ text: 'min', value: 'min' },
{ text: 'sum', value: 'sum' },
{ text: 'countDistinct', value: 'countDistinct' },
{ text: 'sumDistinct', value: 'sumDistinct' },
{ text: 'avgDistinct', value: 'avgDistinct' }
],
colors: {
LinkToAnotherRecord: 'blue lighten-5',
Rollup: 'pink lighten-5',
Lookup: 'green lighten-5'
}
}),
computed: {
counter: {
get() {
return this.$store.state.templateC
},
set(c) {
this.$store.commit('mutTemplateC', c)
}
},
updateFilename() {
return this.url && this.url.split('/').pop()
}
},
watch: {
project: {
deep: true,
handler() {
const template = {
...this.project,
tables: (this.project.tables || []).map((t) => {
const table = { ...t, columns: [], hasMany: [], manyToMany: [], belongsTo: [], v: [] }
for (const column of (t.columns || [])) {
if (this.isRelation(column)) {
if (column.type === 'hm') {
table.hasMany.push({
tn: column.rtn,
_cn: column.cn
})
} else if (column.type === 'mm') {
table.manyToMany.push({
rtn: column.rtn,
_cn: column.cn
})
} else if (column.uidt === UITypes.ForeignKey) {
table.belongsTo.push({
tn: column.rtn,
_cn: column.cn
})
}
} else if (this.isLookup(column)) {
if (column.rtn) {
table.v.push({
_cn: column.cn,
lk: {
ltn: column.rtn.tn,
type: column.rtn.type,
lcn: column.rcn
}
})
}
} else if (this.isRollup(column)) {
if (column.rtn) {
table.v.push({
_cn: column.cn,
rl: {
rltn: column.rtn.tn,
rlcn: column.rcn,
type: column.rtn.type,
fn: column.fn
}
})
}
} else {
table.columns.push(column)
}
}
return table
})
}
this.$emit('update:projectTemplate', template)
}
}
},
created() {
document.addEventListener('keydown', this.handleKeyDown)
},
destroyed() {
document.removeEventListener('keydown', this.handleKeyDown)
},
mounted() {
this.parseAndLoadTemplate()
const input = this.$refs.projec && this.$refs.project.$el.querySelector('input')
if (input) {
input.focus()
input.select()
}
},
methods: {
parseAndLoadTemplate() {
if (this.projectTemplate) {
this.parseTemplate(this.projectTemplate)
this.expansionPanel = Array.from({ length: this.project.tables.length }, (_, i) => i)
}
},
getIcon(type) {
return getUIDTIcon(type)
},
getRelatedTables(tableName, rollup = false) {
const tables = []
for (const t of this.projectTemplate.tables) {
if (tableName === t.tn) {
for (const hm of t.hasMany) {
const rTable = this.project.tables.find(t1 => t1.tn === hm.tn)
tables.push({
...rTable,
type: 'hm'
})
}
for (const mm of t.manyToMany) {
const rTable = this.project.tables.find(t1 => t1.tn === mm.rtn)
tables.push({
...rTable,
type: 'mm'
})
}
} else {
for (const hm of t.hasMany) {
if (hm.tn === tableName && !rollup) {
tables.push({
...t,
type: 'bt'
})
}
}
for (const mm of t.manyToMany) {
if (mm.rtn === tableName) {
tables.push({
...t,
type: 'mm'
})
}
}
}
}
return tables
},
validateAndFocus() {
if (!this.$refs.form.validate()) {
const input = this.$el.querySelector('.v-input.error--text')
this.expansionPanel = input && input.parentElement && input.parentElement.parentElement && +input.parentElement.parentElement.dataset.exp
setTimeout(() => {
input.querySelector('input,select').focus()
}, 500)
return false
}
return true
},
deleteTable(i) {
const deleteTable = this.project.tables[i]
for (const table of this.project.tables) {
if (table === deleteTable) {
continue
}
table.columns = table.columns.filter(c => c.rtn !== deleteTable.tn)
}
this.project.tables.splice(i, 1)
},
deleteTableColumn(i, j, col, table) {
const deleteTable = this.project.tables[i]
const deleteColumn = deleteTable.columns[j]
let rTable, index
// if relation column, delete the corresponding relation from other table
if (col.uidt === UITypes.LinkToAnotherRecord) {
if (col.type === 'hm') {
rTable = this.project.tables.find(t => t.tn === col.rtn)
index = rTable && rTable.columns.findIndex(c => c.uidt === UITypes.ForeignKey && c.rtn === table.tn)
} else if (col.type === 'mm') {
rTable = this.project.tables.find(t => t.tn === col.rtn)
index = rTable && rTable.columns.findIndex(c => c.uidt === UITypes.LinkToAnotherRecord && c.rtn === table.tn && c.type === 'mm')
}
} else if (col.uidt === UITypes.ForeignKey) {
rTable = this.project.tables.find(t => t.tn === col.rtn)
index = rTable && rTable.columns.findIndex(c => c.uidt === UITypes.LinkToAnotherRecord && c.rtn === table.tn && c.type === 'hm')
}
if (rTable && index > -1) {
rTable.columns.splice(index, 1)
}
for (const table of this.project.tables) {
if (table === deleteTable) {
continue
}
table.columns = table.columns.filter(c => c.rtn !== deleteTable.tn || c.rcn !== deleteColumn.cn)
}
deleteTable.columns.splice(j, 1)
},
addTables() {
if (!this.tableNamesInput) {
return
}
// todo: fix
const re = /(?:^|,\s*)(\w+)(?:\(((\w+)(?:\s*,\s*\w+)?)?\)){0,1}(?=\s*,|\s*$)/g
let m
// eslint-disable-next-line no-cond-assign
while (m = re.exec(this.tableNamesInput)) {
if (this.project.tables.some(t => t.tn === m[1])) {
this.$toast.info(`Table '${m[1]}' is already exist`).goAway(1000)
continue
}
this.project.tables.push({
tn: m[1],
columns: (m[2] ? m[2].split(/\s*,\s*/) : []).map(col => ({
cn: col,
...defaultColProp
})).filter((v, i, arr) => i === arr.findIndex(c => c.cn === v.cn))
})
}
this.createTablesDialog = false
this.tableNamesInput = ''
},
compareRel(a, b) {
return ((a && a.tn) || a) === ((b && b.tn) || b) && (a && a.type) === (b && b.type)
},
addColumns() {
if (!this.columnNamesInput) {
return
}
const table = this.project.tables[this.expansionPanel]
for (const col of this.columnNamesInput.split(/\s*,\s*/)) {
if (table.columns.some(c => c.cn === col)) {
this.$toast.info(`Column '${col}' is already exist`).goAway(1000)
continue
}
table.columns.push({
cn: col,
...defaultColProp
})
}
this.columnNamesInput = ''
this.createTableColumnsDialog = false
this.$nextTick(() => {
const input = this.$refs[`uidt_${table.tn}_${table.columns.length - 1}`][0].$el.querySelector('input')
input.focus()
this.$nextTick(() => {
input.select()
})
})
},
showColCreateDialog(table) {
this.createTableColumnsDialog = true
this.selectedTable = table
},
isRelation(col) {
return col.uidt === 'LinkToAnotherRecord' ||
col.uidt === 'ForeignKey'
},
isLookup(col) {
return col.uidt === 'Lookup'
},
isRollup(col) {
return col.uidt === 'Rollup'
},
isVirtual(col) {
return col && uiTypes.some(ut => ut.name === col.uidt && ut.virtual)
},
isLookupOrRollup(col) {
return this.isLookup(col) ||
this.isRollup(col)
},
isSelect(col) {
return col.uidt === 'MultiSelect' ||
col.uidt === 'SingleSelect'
},
addNewColumnRow(table, uidt) {
table.columns.push({
cn: `title${table.columns.length + 1}`,
...defaultColProp,
uidt,
...(uidt === LinkToAnotherRecord
? {
type: 'mm'
}
: {})
})
this.$nextTick(() => {
const input = this.$refs[`cn_${table.tn}_${table.columns.length - 1}`][0].$el.querySelector('input')
input.focus()
input.select()
})
},
async handleKeyDown({ metaKey, key, altKey, shiftKey, ctrlKey }) {
if (!(metaKey && ctrlKey) && !(altKey && shiftKey)) {
return
}
switch (key && key.toLowerCase()) {
case 't':
this.createTablesDialog = true
break
case 'c':
this.createTableColumnsDialog = true
break
case 'a':
this.addNewColumnRow(this.project.tables[this.expansionPanel])
break
case 'j':
this.copyJSON()
break
case 's':
await this.saveTemplate()
break
case 'arrowup':
this.expansionPanel = this.expansionPanel ? --this.expansionPanel : this.project.tables.length - 1
break
case 'arrowdown':
this.expansionPanel = ++this.expansionPanel % this.project.tables.length
break
case '1':
this.addNewColumnRow(this.project.tables[this.expansionPanel], 'Number')
break
case '2':
this.addNewColumnRow(this.project.tables[this.expansionPanel], 'SingleLineText')
break
case '3':
this.addNewColumnRow(this.project.tables[this.expansionPanel], 'LongText')
break
case '4':
this.addNewColumnRow(this.project.tables[this.expansionPanel], 'LinkToAnotherRecord')
break
case '5':
this.addNewColumnRow(this.project.tables[this.expansionPanel], 'Lookup')
break
case '6':
this.addNewColumnRow(this.project.tables[this.expansionPanel], 'Rollup')
break
}
},
copyJSON() {
if (!this.validateAndFocus()) {
this.$toast.info('Please fill all the required column!').goAway(5000)
return
}
const el = document.createElement('textarea')
el.addEventListener('focusin', e => e.stopPropagation())
el.value = JSON.stringify(this.projectTemplate, null, 2)
el.style = { position: 'absolute', left: '-9999px' }
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
this.$toast.success('Successfully copied JSON data to clipboard!').goAway(3000)
return true
},
openUrl() {
window.open(this.url, '_blank')
},
async loadUrl() {
try {
let template = (await this.$axios.get(this.url)).data
if (typeof template === 'string') {
template = JSON.parse(template)
}
console.log(template)
this.parseTemplate(template)
} catch (e) {
this.$toast.error(e.message).goAway(5000)
}
},
parseTemplate({ tables = [], ...rest }) {
const parsedTemplate = {
...rest,
tables: tables.map(({ manyToMany = [], hasMany = [], belongsTo = [], v = [], columns = [], ...rest }) => ({
...rest,
columns: [
...columns,
...manyToMany.map(mm => ({
cn: mm._cn || `${rest.tn} <=> ${mm.rtn}`,
uidt: LinkToAnotherRecord,
type: 'mm',
...mm
})),
...hasMany.map(hm => ({
cn: hm._cn || `${rest.tn} => ${hm.tn}`,
uidt: LinkToAnotherRecord,
type: 'hm',
rtn: hm.tn,
...hm
})),
...belongsTo.map(bt => ({
cn: bt._cn || `${rest.tn} => ${bt.rtn}`,
uidt: UITypes.ForeignKey,
rtn: bt.tn,
...bt
})),
...v.map((v) => {
const res = {
cn: v._cn,
rtn: {
...v
}
}
if (v.lk) {
res.uidt = Lookup
res.rtn.tn = v.lk.ltn
res.rcn = v.lk.lcn
res.rtn.type = v.lk.type
} else if (v.rl) {
res.uidt = Rollup
res.rtn.tn = v.rl.rltn
res.rcn = v.rl.rlcn
res.rtn.type = v.rl.type
res.fn = v.rl.fn
}
return res
})
]
}))
}
this.project = parsedTemplate
},
async projectTemplateCreate() {
if (!this.validateAndFocus()) {
this.$toast.info('Please fill all the required column!').goAway(5000)
return
}
try {
const githubConfig = this.$store.state.github
// const token = await models.store.where({ key: 'GITHUB_TOKEN' }).first()
// const branch = await models.store.where({ key: 'GITHUB_BRANCH' }).first()
// const filePath = await models.store.where({ key: 'GITHUB_FILE_PATH' }).first()
// const templateRepo = await models.store.where({ key: 'PROJECT_TEMPLATES_REPO' }).first()
if (!githubConfig.token || !githubConfig.repo) {
throw new Error('Missing token or template path')
}
const data = JSON.stringify(this.projectTemplate, 0, 2)
const filename = this.updateFilename || `${this.projectTemplate.name}_${Date.now()}.json`
const filePath = `${githubConfig.filePath ? githubConfig.filePath + '/' : ''}${filename}`
const apiPath = `https://api.github.com/repos/${githubConfig.repo}/contents/${filePath}`
let sha
if (this.updateFilename) {
const { data: { sha: _sha } } = await this.$axios({
url: `https://api.github.com/repos/${githubConfig.repo}/contents/${filePath}`,
method: 'get',
headers: {
Authorization: 'token ' + githubConfig.token
}
})
sha = _sha
}
await this.$axios({
url: apiPath,
method: 'put',
headers: {
'Content-Type': 'application/json',
Authorization: 'token ' + githubConfig.token
},
data: {
message: `templates : init template ${filename}`,
content: Base64.encode(data),
sha,
branch: githubConfig.branch
}
})
this.url = `https://raw.githubusercontent.com/${githubConfig.repo}/${githubConfig.branch}/${filePath}`
this.$toast.success('Template generated and saved successfully!').goAway(4000)
} catch (e) {
this.$toast.error(e.message).goAway(5000)
}
},
navigateToTable(tn) {
const index = this.projectTemplate.tables.findIndex(t => t.tn === tn)
if (Array.isArray(this.expansionPanel)) {
this.expansionPanel.push(index)
} else {
this.expansionPanel = index
}
this.$nextTick(() => {
const accord = this.$el.querySelector(`#tn_${tn}`)
accord.focus()
accord.scrollIntoView()
})
},
async saveTemplate() {
this.loading = true
try {
if (this.id || this.localId) {
await this.$axios.put(`${process.env.NC_API_URL}/api/v1/nc/templates/${this.id || this.localId}`, this.projectTemplate, {
params: {
token: this.$store.state.template
}
})
this.$toast.success('Template updated successfully').goAway(3000)
} else if (!this.$store.state.template) {
if (!this.copyJSON()) {
return
}
this.$toast.info('Initiating Github for template').goAway(3000)
const res = await this.$axios.post(`${process.env.NC_API_URL}/api/v1/projectTemplateCreate`, this.projectTemplate)
console.log(res)
this.$toast.success('Initiated Github successfully').goAway(3000)
window.open(res.data.path, '_blank')
} else {
const res = await this.$axios.post(`${process.env.NC_API_URL}/api/v1/nc/templates`, this.projectTemplate, {
params: {
token: this.$store.state.template
}
})
this.localId = res.data.id
this.$toast.success('Template updated successfully').goAway(3000)
}
this.$emit('saved')
} catch (e) {
this.$toast.error(e.message).goAway(3000)
} finally {
this.loading = false
}
},
getRules(col, table) {
return v => col.uidt !== UITypes.LinkToAnotherRecord || !table.columns.some(c => c !== col && c.uidt === UITypes.LinkToAnotherRecord && c.type === col.type && c.rtn === col.rtn) || 'Duplicate relation is not allowed'
},
onTableNameUpdate(oldTable, newVal) {
const oldVal = oldTable.tn
this.$set(oldTable, 'tn', newVal)
for (const table of this.project.tables) {
for (const col of table.columns) {
if (col.uidt === UITypes.LinkToAnotherRecord) {
if (col.rtn === oldVal) {
this.$set(col, 'rtn', newVal)
}
} else if (col.uidt === UITypes.Rollup || col.uidt === UITypes.Lookup) {
if (col.rtn && col.rtn.tn === oldVal) {
this.$set(col.rtn, 'tn', newVal)
}
}
}
}
},
onColumnNameUpdate(oldCol, newVal, tn) {
const oldVal = oldCol.cn
this.$set(oldCol, 'cn', newVal)
for (const table of this.project.tables) {
for (const col of table.columns) {
if (col.uidt === UITypes.Rollup || col.uidt === UITypes.Lookup) {
if (col.rtn && col.rcn === oldVal && col.rtn.tn === tn) {
this.$set(col, 'rcn', newVal)
}
}
}
}
},
async onRtnChange(oldVal, newVal, col, table) {
this.$set(col, 'rtn', newVal)
await this.$nextTick()
if (col.uidt !== UITypes.LinkToAnotherRecord && col.uidt !== UITypes.ForeignKey) {
return
}
if (oldVal) {
const rTable = this.project.tables.find(t => t.tn === oldVal)
// delete relation from other table if exist
let index = -1
if (col.uidt === UITypes.LinkToAnotherRecord && col.type === 'mm') {
index = rTable.columns.findIndex(c => c.uidt === UITypes.LinkToAnotherRecord && c.rtn === table.tn && c.type === 'mm')
} else if (col.uidt === UITypes.LinkToAnotherRecord && col.type === 'hm') {
index = rTable.columns.findIndex(c => c.uidt === UITypes.ForeignKey && c.rtn === table.tn)
} else if (col.uidt === UITypes.ForeignKey) {
index = rTable.columns.findIndex(c => c.uidt === UITypes.LinkToAnotherRecord && c.rtn === table.tn && c.type === 'hm')
}
if (index > -1) {
rTable.columns.splice(index, 1)
}
}
if (newVal) {
const rTable = this.project.tables.find(t => t.tn === newVal)
// check relation relation exist in other table
// if not create a relation
if (col.uidt === UITypes.LinkToAnotherRecord && col.type === 'mm') {
if (!rTable.columns.find(c => c.uidt === UITypes.LinkToAnotherRecord && c.rtn === table.tn && c.type === 'mm')) {
rTable.columns.push({
cn: `title${rTable.columns.length + 1}`,
uidt: UITypes.LinkToAnotherRecord,
type: 'mm',
rtn: table.tn
})
}
} else if (col.uidt === UITypes.LinkToAnotherRecord && col.type === 'hm') {
if (!rTable.columns.find(c => c.uidt === UITypes.ForeignKey && c.rtn === table.tn)) {
rTable.columns.push({
cn: `title${rTable.columns.length + 1}`,
uidt: UITypes.ForeignKey,
rtn: table.tn
})
}
} else if (col.uidt === UITypes.ForeignKey) {
if (!rTable.columns.find(c => c.uidt === UITypes.LinkToAnotherRecord && c.rtn === table.tn && c.type === 'hm')) {
rTable.columns.push({
cn: `title${rTable.columns.length + 1}`,
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
rtn: table.tn
})
}
}
}
},
onRTypeChange(oldType, newType, col, table) {
this.$set(col, 'type', newType)
const rTable = this.project.tables.find(t => t.tn === col.rtn)
let index = -1
// find column and update relation
// or create a new column
if (oldType === 'hm') {
index = rTable.columns.findIndex(c => c.uidt === UITypes.ForeignKey && c.rtn === table.tn)
} else if (oldType === 'mm') {
index = rTable.columns.findIndex(c => c.uidt === UITypes.LinkToAnotherRecord && c.rtn === table.tn && c.type === 'mm')
}
const rCol = index === -1 ? { cn: `title${rTable.columns.length + 1}` } : { ...rTable.columns[index] }
index = index === -1 ? rTable.columns.length : index
if (newType === 'mm') {
rCol.type = 'mm'
rCol.uidt = UITypes.LinkToAnotherRecord
} else if (newType === 'hm') {
rCol.type = 'bt'
rCol.uidt = UITypes.ForeignKey
}
rCol.rtn = table.tn
this.$set(rTable.columns, index, rCol)
},
onUidtChange(oldVal, newVal, col, table) {
this.$set(col, 'uidt', newVal)
this.$set(col, 'dtxp', undefined)
// delete relation column from other table
// if previous type is relation
let index = -1
let rTable
if (oldVal === UITypes.LinkToAnotherRecord) {
rTable = this.project.tables.find(t => t.tn === col.rtn)
if (rTable) {
if (col.type === 'hm') {
index = rTable.columns.findIndex(c => c.uidt === UITypes.ForeignKey && c.rtn === table.tn)
} else if (col.type === 'mm') {
index = rTable.columns.findIndex(c => c.uidt === UITypes.LinkToAnotherRecord && c.rtn === table.tn && c.type === 'mm')
}
}
} else if (oldVal === UITypes.ForeignKey) {
rTable = this.project.tables.find(t => t.tn === col.rtn)
if (rTable) { index = rTable.columns.findIndex(c => c.uidt === UITypes.LinkToAnotherRecord && c.rtn === table.tn && c.type === 'hm') }
}
if (rTable && index > -1) {
rTable.columns.splice(index, 1)
}
col.rtn = undefined
col.type = undefined
col.rcn = undefined
if (col.uidt === LinkToAnotherRecord) {
col.type = col.type || 'mm'
}
}
}
}
</script>
<style scoped>
/deep/ .v-select__selections{
flex-wrap: nowrap;
}
</style>