Browse Source

feat: template creation ui updates

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/760/head
Pranav C 3 years ago
parent
commit
b0c91a2e11
  1. 34
      packages/nc-gui/components/templates/categories.vue
  2. 11
      packages/nc-gui/components/templates/detailed.vue
  3. 111
      packages/nc-gui/components/templates/editor.vue
  4. 83
      packages/nc-gui/components/templates/help.vue
  5. 14
      packages/nc-gui/components/templates/list.vue
  6. 14
      packages/nc-gui/components/templates/templatesModal.vue
  7. 3
      packages/nc-gui/nuxt.config.js
  8. 10
      packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts

34
packages/nc-gui/components/templates/categories.vue

@ -3,7 +3,7 @@
<v-list dense> <v-list dense>
<v-list-item dense> <v-list-item dense>
<v-list-item-subtitle> <v-list-item-subtitle>
<span class="caption" @click="counterLoc++">Categories</span> <span class="caption" @dblclick="counterLoc++">Categories</span>
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item> </v-list-item>
<v-list-item-group v-model="category"> <v-list-item-group v-model="category">
@ -11,6 +11,7 @@
<v-list-item-title> <v-list-item-title>
<span <span
:class="{'font-weight-black' : category === c.category } " :class="{'font-weight-black' : category === c.category } "
class="body-1"
> >
{{ {{
c.category c.category
@ -24,20 +25,43 @@
<v-btn <v-btn
class="ml-4" class="ml-4"
color="grey" color="grey"
x-small small
outlined outlined
@click="showTemplateEditor" @click="showTemplateEditor"
> >
New template <v-icon class="mr-1" small>
mdi-plus
</v-icon> New template
</v-btn> </v-btn>
<v-tooltip bottom>
<template #activator="{on}">
<v-btn
class="ml-4 mt-4"
color="grey"
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 templates from multiple Excel files</span>
</v-tooltip>
<v-text-field <v-text-field
v-if="$store.state.templateC >4" v-if="$store.state.templateE > 3"
v-model="t" v-model="t"
outlined outlined
dense dense
:full-width="false"
style="width: 135px"
type="password" type="password"
class="caption mt-4 ml-1 mr-3" class="caption mt-4 ml-1 mr-3 ml-4 "
hide-details hide-details
/> />
</div> </div>

11
packages/nc-gui/components/templates/detailed.vue

@ -1,7 +1,7 @@
<template> <template>
<v-container class="py-0"> <v-container class="py-0">
<div class="d-flex"> <div class="d-flex h-100">
<v-navigation-drawer permanent height="calc(100vh - 40px)"> <v-navigation-drawer permanent height="100%">
<categories <categories
ref="cat" ref="cat"
:counter.sync="counter" :counter.sync="counter"
@ -9,7 +9,7 @@
@input="v => $emit('load-category', v)" @input="v => $emit('load-category', v)"
/> />
</v-navigation-drawer> </v-navigation-drawer>
<v-container v-if="templateData" fluid style="height: calc(100vh - 40px ); overflow: auto"> <v-container v-if="templateData" fluid style="height: 100%; overflow: auto">
<v-img <v-img
:src="templateData.image_url" :src="templateData.image_url"
height="200px" height="200px"
@ -19,7 +19,7 @@
<h2 class="display-2 font-weight-bold my-0 flex-grow-1"> <h2 class="display-2 font-weight-bold my-0 flex-grow-1">
{{ templateData.title }} {{ templateData.title }}
</h2> </h2>
<v-btn class="primary" x-large @click="useTemplate"> <v-btn :loading="loading" :disabled="loading" class="primary" x-large @click="useTemplate">
Use template Use template
</v-btn> </v-btn>
</div> </div>
@ -29,7 +29,7 @@
<templat-editor <templat-editor
:id="templateId" :id="templateId"
:view-mode="$store.state.templateE < 5 && viewMode" :view-mode="$store.state.templateE < 4 && viewMode"
:template-data.sync="templateData" :template-data.sync="templateData"
@saved="onSaved" @saved="onSaved"
/> />
@ -46,6 +46,7 @@ export default {
name: 'ProjectTemplateDetailed', name: 'ProjectTemplateDetailed',
components: { Categories, TemplatEditor }, components: { Categories, TemplatEditor },
props: { props: {
loading: Boolean,
modal: Boolean, modal: Boolean,
viewMode: Boolean, viewMode: Boolean,
id: [String, Number] id: [String, Number]

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

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="h-100">
<v-toolbar v-if="!viewMode" class="elevation-0"> <v-toolbar v-if="!viewMode" class="elevation-0">
<!-- <v-text-field <!-- <v-text-field
v-model="url" v-model="url"
@ -11,6 +11,24 @@
@keydown.enter="loadUrl" @keydown.enter="loadUrl"
/>--> />-->
<!-- <v-btn outlined class='ml-1' @click='loadUrl'> Load URL</v-btn>--> <!-- <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-spacer />
<v-icon class="mr-3" @click="helpModal=true"> <v-icon class="mr-3" @click="helpModal=true">
@ -36,7 +54,8 @@
<v-btn small outlined class="mr-1" @click="project = {tables : []}"> <v-btn small outlined class="mr-1" @click="project = {tables : []}">
<v-icon small> <v-icon small>
mdi-close mdi-close
</v-icon> Reset </v-icon>
Reset
</v-btn> </v-btn>
<!-- <v-icon <!-- <v-icon
:color="$store.getters['github/isAuthorized'] ? '' : 'error'" :color="$store.getters['github/isAuthorized'] ? '' : 'error'"
@ -45,18 +64,27 @@
> >
mdi-github mdi-github
</v-icon>--> </v-icon>-->
<v-btn small outlined class="mr-1"> <v-btn small outlined class="mr-1" @click="createTablesDialog = true">
<v-icon small @click="createTablesDialog = true"> <v-icon small>
mdi-plus mdi-plus
</v-icon> </v-icon>
New table New table
</v-btn> </v-btn>
<!-- <v-btn outlined small class='mr-1' @click='submitTemplate'> Submit Template</v-btn>--> <!-- <v-btn outlined small class='mr-1' @click='submitTemplate'> Submit Template</v-btn>-->
<v-btn color="primary" outlined small class="mr-1" @click="saveTemplate"> <v-btn
{{ id || localId ? 'Update in' :'Submit to' }} NocoDB color="primary"
outlined
small
class="mr-1"
:loading="loading"
:disabled="loading"
@click="saveTemplate"
>
{{ id || localId ? 'Update in' : 'Submit to' }} NocoDB
</v-btn> </v-btn>
</v-toolbar> </v-toolbar>
<v-container class="text-center"> <v-container class="text-center" style="height:calc(100% - 64px);overflow-y: auto">
<v-form ref="form"> <v-form ref="form">
<v-row fluid class="justify-center"> <v-row fluid class="justify-center">
<v-col cols="12"> <v-col cols="12">
@ -77,13 +105,14 @@
> >
<v-text-field <v-text-field
v-if="editableTn[i]" v-if="editableTn[i]"
v-model="table.tn" :value="table.tn"
class="title" class="title"
style="max-width: 300px" style="max-width: 300px"
outlinedk outlinedk
autofocus autofocus
dense dense
hide-details hide-details
@input="e => onTableNameUpdate(table, e)"
@click="e => viewMode || e.stopPropagation()" @click="e => viewMode || e.stopPropagation()"
@blur="$set(editableTn,i, false)" @blur="$set(editableTn,i, false)"
@keydown.enter=" $set(editableTn,i, false)" @keydown.enter=" $set(editableTn,i, false)"
@ -133,14 +162,19 @@
<v-text-field <v-text-field
v-else v-else
:ref="`cn_${table.tn}_${j}`" :ref="`cn_${table.tn}_${j}`"
v-model="col.cn" :value="col.cn"
outlined outlined
dense dense
class="caption" class="caption"
placeholder="Column name" placeholder="Column name"
hide-details="auto" hide-details="auto"
:rules="[v => !!v || 'Column name required']" :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> </td>
@ -233,7 +267,7 @@
class="caption" class="caption"
dense dense
hide-details="auto" hide-details="auto"
:rules="[v => !!v || 'Related table name required']" :rules="[v => !!v || 'Related table name required', ...getRules(col, table)]"
:items="isLookupOrRollup(col) ? getRelatedTables(table.tn, isRollup(col)) : project.tables" :items="isLookupOrRollup(col) ? getRelatedTables(table.tn, isRollup(col)) : project.tables"
:item-text="t => isLookupOrRollup(col) ? `${t.tn} (${t.type})` : t.tn" :item-text="t => isLookupOrRollup(col) ? `${t.tn} (${t.type})` : t.tn"
:item-value="t => isLookupOrRollup(col) ? t : t.tn" :item-value="t => isLookupOrRollup(col) ? t : t.tn"
@ -384,7 +418,7 @@
outlined outlined
dense dense
label="Project Name" label="Project Name"
:rules="[v => !!v || 'Required'] " :rules="[v => !!v || 'Project name required'] "
/> />
</div> </div>
<!-- <!--
@ -402,6 +436,7 @@
<v-col> <v-col>
<v-text-field <v-text-field
v-model="project.category" v-model="project.category"
:rules="[v => !!v || 'Category name required']"
class="caption" class="caption"
outlined outlined
dense dense
@ -424,7 +459,7 @@
class="caption" class="caption"
outlined outlined
dense dense
label="Project Tags" label="Project Description"
@click="counter++" @click="counter++"
/> />
</div> </div>
@ -496,7 +531,7 @@
fab fab
large large
color="primary" color="primary"
right="100" right
style="top:45%" style="top:45%"
@click="createTablesDialog = true" @click="createTablesDialog = true"
v-on="on" v-on="on"
@ -511,8 +546,10 @@
<script> <script>
import UITypes from '../../../nocodb/build/main/lib/sqlUi/UITypes'
import { uiTypes, getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes' import { uiTypes, getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
import GradientGenerator from '~/components/templates/gradientGenerator' import GradientGenerator from '~/components/templates/gradientGenerator'
import Help from '~/components/templates/help'
const LinkToAnotherRecord = 'LinkToAnotherRecord' const LinkToAnotherRecord = 'LinkToAnotherRecord'
const Lookup = 'Lookup' const Lookup = 'Lookup'
@ -521,13 +558,14 @@ const defaultColProp = {}
export default { export default {
name: 'TemplateEditor', name: 'TemplateEditor',
components: { GradientGenerator }, components: { Help, GradientGenerator },
props: { props: {
id: [Number, String], id: [Number, String],
viewMode: Boolean, viewMode: Boolean,
templateData: Object templateData: Object
}, },
data: () => ({ data: () => ({
loading: false,
localId: null, localId: null,
valid: false, valid: false,
url: '', url: '',
@ -544,7 +582,7 @@ export default {
createTablesDialog: false, createTablesDialog: false,
createTableColumnsDialog: false, createTableColumnsDialog: false,
selectedTable: null, selectedTable: null,
uiTypes, uiTypes: uiTypes.filter(t => ![UITypes.Formula, UITypes.SpecificDBType].includes(t.name)),
rollupFnList: [ rollupFnList: [
{ text: 'count', value: 'count' }, { text: 'count', value: 'count' },
{ text: 'min', value: 'min' }, { text: 'min', value: 'min' },
@ -819,8 +857,6 @@ export default {
}, },
async handleKeyDown({ metaKey, key, altKey, shiftKey, ctrlKey }) { async handleKeyDown({ metaKey, key, altKey, shiftKey, ctrlKey }) {
// eslint-disable-next-line no-console
console.log({ metaKey, key, altKey, shiftKey, ctrlKey })
if (!(metaKey && ctrlKey) && !(altKey && shiftKey)) { if (!(metaKey && ctrlKey) && !(altKey && shiftKey)) {
return return
} }
@ -1027,6 +1063,7 @@ export default {
}, },
async saveTemplate() { async saveTemplate() {
this.loading = true
try { try {
if (this.id || this.localId) { if (this.id || this.localId) {
await this.$axios.put(`${process.env.NC_API_URL}/api/v1/nc/templates/${this.id || this.localId}`, this.projectTemplate, { await this.$axios.put(`${process.env.NC_API_URL}/api/v1/nc/templates/${this.id || this.localId}`, this.projectTemplate, {
@ -1058,9 +1095,45 @@ export default {
this.$emit('saved') this.$emit('saved')
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) 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)
}
}
}
}
}
} }
} }
</script> </script>

83
packages/nc-gui/components/templates/help.vue

@ -0,0 +1,83 @@
<template>
<v-dialog v-model='localState' max-width='900'>
<v-card>
<v-card-title>
Shortcuts
</v-card-title>
<v-card-text>
<v-simple-table v-slot dense>
<thead>
<tr>
<th></th>
<th class=' pa-2'>Mac OS</th>
<th class=' pa-2'>Windows / Linux</th>
</tr>
</thead>
<tbody>
<tr v-for='(short,i) in shortcuts' :key='i'>
<td class='caption'>{{ short.description }}</td>
<td class=' caption pa-2' v-html='short.mac'></td>
<td class=' caption pa-2' v-html='short.windows'></td>
</tr>
</tbody>
</v-simple-table>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: 'Help',
props: {
value: Boolean
},
data() {
return {
shortcuts: [{
description: 'Add tables',
mac: '<kbd>Command</kbd> + <kbd>Control</kbd> + <kbd>T</kbd>',
windows: '<kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>T</kbd>'
},{
description: 'Add columns',
mac: '<kbd>Command</kbd> + <kbd>Control</kbd> + <kbd>C</kbd>',
windows: '<kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>C</kbd>'
},{
description: 'Add new column row',
mac: '<kbd>Command</kbd> + <kbd>Control</kbd> + <kbd>A</kbd>',
windows: '<kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>A</kbd>'
},{
description: 'Table navigation',
mac: '<kbd>Command</kbd> + <kbd>Control</kbd> + <kbd>Arrow Down</kbd> <br>AND<br> <kbd>Command</kbd> + <kbd>Control</kbd> + <kbd>Arrow Up</kbd>',
windows: '<kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>Arrow Down</kbd> <br>AND<br> <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>Arrow Up</kbd>'
},{
description: 'Column navigation',
mac: '<kbd>Command</kbd> + <kbd>Control</kbd> + <kbd>Arrow Right</kbd> <br>AND<br> <kbd>Command</kbd> + <kbd>Control</kbd> + <kbd>Arrow Left</kbd>',
windows: '<kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>Arrow Right</kbd> <br>AND<br> <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>Arrow Right</kbd>'
},{
description: 'Copy json to clipboard',
mac: '<kbd>Command</kbd> + <kbd>Control</kbd> + <kbd>J</kbd>',
windows: '<kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>J</kbd>'
},{
description: 'Submit template',
mac: '<kbd>Command</kbd> + <kbd>Control</kbd> + <kbd>S</kbd>',
windows: '<kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd>'
},]
}
},
computed: {
localState: {
get() {
return this.value
}, set(val) {
this.$emit('input', val)
}
}
}
}
</script>
<style scoped>
</style>

14
packages/nc-gui/components/templates/list.vue

@ -1,7 +1,7 @@
<template> <template>
<v-container v-if="newEditor || !modal || selectedId === null " class="py-0"> <v-container v-if="newEditor || !modal || selectedId === null " class="py-0">
<div class="d-flex"> <div class="d-flex h-100">
<v-navigation-drawer permanent height="calc(100vh - 40px)"> <v-navigation-drawer permanent height="100% ">
<categories <categories
ref="cat" ref="cat"
v-model="category" v-model="category"
@ -10,8 +10,8 @@
@showTemplateEditor="newEditor = true" @showTemplateEditor="newEditor = true"
/> />
</v-navigation-drawer> </v-navigation-drawer>
<template-editor v-if="newEditor" style="width:100%" @saved="onSaved" /> <template-editor v-if="newEditor" style="width:100%; height: 100%; " @saved="onSaved" />
<v-container v-else fluid style="height: calc(100vh - 40px); overflow: auto"> <v-container v-else fluid style="height: 100%; overflow: auto">
<v-row <v-row
v-if="templateList && templateList.length" v-if="templateList && templateList.length"
class="align-stretch" class="align-stretch"
@ -33,7 +33,7 @@
> >
<v-img <v-img
:src="template.image_url" :src="template.image_url"
height="200px" height="50px"
:style="{ background: template.image_url }" :style="{ background: template.image_url }"
/> />
@ -60,6 +60,7 @@
<project-template-detailed <project-template-detailed
v-else v-else
:id="selectedId" :id="selectedId"
:loading="loading"
:counter="counter" :counter="counter"
:modal="modal" :modal="modal"
:view-mode="counter < 5" :view-mode="counter < 5"
@ -81,7 +82,8 @@ export default {
name: 'ProjectTemplates', name: 'ProjectTemplates',
components: { TemplateEditor, Categories, ProjectTemplateDetailed }, components: { TemplateEditor, Categories, ProjectTemplateDetailed },
props: { props: {
modal: Boolean modal: Boolean,
loading: Boolean
}, },
data: () => ({ data: () => ({
category: null, category: null,

14
packages/nc-gui/components/templates/templatesModal.vue

@ -2,8 +2,8 @@
<div> <div>
<span v-ripple class="caption font-weight-bold pointer" @click="templatesModal = true">Templates</span> <span v-ripple class="caption font-weight-bold pointer" @click="templatesModal = true">Templates</span>
<v-dialog v-if="templatesModal" v-model="templatesModal"> <v-dialog v-if="templatesModal" v-model="templatesModal">
<v-card> <v-card height="90vh">
<project-templates modal @import="importTemplate" /> <project-templates style="height:90vh" modal :loading="loading" @import="importTemplate" />
</v-card> </v-card>
</v-dialog> </v-dialog>
</div> </div>
@ -16,7 +16,8 @@ export default {
name: 'TemplatesModal', name: 'TemplatesModal',
components: { ProjectTemplates }, components: { ProjectTemplates },
data: () => ({ data: () => ({
templatesModal: false templatesModal: false,
loading: false
}), }),
methods: { methods: {
async importTemplate(template) { async importTemplate(template) {
@ -31,13 +32,6 @@ export default {
if (res && res.tables && res.tables.length) { if (res && res.tables && res.tables.length) {
this.$toast.success(`Imported ${res.tables.length} tables successfully`).goAway(3000) this.$toast.success(`Imported ${res.tables.length} tables successfully`).goAway(3000)
// await this.$router.push({
// query: {
// ...(this.$route.query || {}),
// type: 'table',
// name: res.tables[0]._tn
// }
// })
} else { } else {
this.$toast.success('Template imported successfully').goAway(3000) this.$toast.success('Template imported successfully').goAway(3000)
} }

3
packages/nc-gui/nuxt.config.js

@ -210,8 +210,7 @@ export default {
], ],
env: { env: {
EE: !!process.env.EE, EE: !!process.env.EE,
NC_API_URL: 'http://localhost:3001' NC_API_URL: 'https://nocodb.com'
// NC_API_URL: 'https://nocodb.com'
}, },
pwa: { pwa: {
workbox: { workbox: {

10
packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts

@ -4089,6 +4089,16 @@ export default class NcMetaMgr {
}); });
parser.parse(); parser.parse();
const existingTables = parser.tables.filter(t => apiBuilder.getMeta(t.tn));
if (existingTables?.length) {
throw new Error(
`Import unsuccessful : following tables '${existingTables
.map(t => t._tn)
.join(', ')}' already exists`
);
}
for (const table of parser.tables) { for (const table of parser.tables) {
console.log(table); console.log(table);
// create table and trigger listener // create table and trigger listener

Loading…
Cancel
Save