Browse Source

feat: table and view reordering, option to disable views for roles(GUI)

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/847/head
Pranav C 3 years ago
parent
commit
298ccd8a5b
  1. 14
      packages/nc-gui/assets/style.css
  2. 263
      packages/nc-gui/components/ProjectTreeView.vue
  3. 5
      packages/nc-gui/components/project/projectMetadata/uiAcl/toggleTableUIAcl.vue
  4. 241
      packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue
  5. 5
      packages/nc-gui/helpers/treeViewDataSerializer.js
  6. 6
      packages/nocodb/src/lib/noco/common/XcMigrationSource.ts
  7. 1
      packages/nocodb/src/lib/noco/meta/NcMetaIO.ts
  8. 6
      packages/nocodb/src/lib/noco/meta/NcMetaIOImpl.ts
  9. 336
      packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts
  10. 70
      packages/nocodb/src/lib/noco/meta/NcMetaMgrEE.ts
  11. 46
      packages/nocodb/src/lib/noco/migrations/nc_009_add_model_order.ts
  12. 14
      packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts

14
packages/nc-gui/assets/style.css

@ -33,6 +33,11 @@
} }
.sortable-drag {
border: 2px solid var(--v-backgroundColor-base) !important;
border-radius: 2px;
}
.v-treeview--dense .v-treeview-node { .v-treeview--dense .v-treeview-node {
/*margin-left: 12px !important;*/ /*margin-left: 12px !important;*/
} }
@ -278,6 +283,7 @@ tbody tr:nth-of-type(odd) {
height: calc(100% - 30px); height: calc(100% - 30px);
overflow: auto; overflow: auto;
} }
.table-tabs.hidden-tab > .v-tabs-items { .table-tabs.hidden-tab > .v-tabs-items {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
@ -383,7 +389,7 @@ td .v-input--selection-controls {
height: 100%; height: 100%;
} }
.scroll-auto{ .scroll-auto {
overflow: auto; overflow: auto;
} }
@ -443,7 +449,7 @@ input, textarea, select {
} }
/* Toast css */ /* Toast css */
.toasted .primary, .toasted.toasted-primary{ .toasted .primary, .toasted.toasted-primary {
font-family: "Roboto", sans-serif !important; font-family: "Roboto", sans-serif !important;
font-weight: 400 !important; font-weight: 400 !important;
} }
@ -482,6 +488,6 @@ body.dark .toasted .primary.info, body.dark .toasted.toasted-primary.info {
} }
.v-date-picker-table{ .v-date-picker-table {
height:auto !important; height: auto !important;
} }

263
packages/nc-gui/components/ProjectTreeView.vue

@ -201,106 +201,121 @@
</template> </template>
<v-list-item-group :value="selectedItem"> <v-list-item-group :value="selectedItem">
<v-list-item <draggable
v-for="child in item.children || []" v-model=" item.children"
v-show="!search || child.name.toLowerCase().includes(search.toLowerCase())" draggable="div"
:key="child.key" v-bind="dragOptions"
color="x-active" @start="drag=true"
active-class="font-weight-bold" @end="drag=false"
:selectable="true" @change="onMove($event, item.children)"
dense
:value="`${(child._nodes && child._nodes).type || ''}||${
(child._nodes && child._nodes.dbAlias) || ''
}||${child.name}`"
class="nested ml-3"
@click.stop="addTab({ ...child }, false, true)"
@contextmenu.prevent.stop="showCTXMenu($event, child, false, true)"
> >
<v-list-item-icon> <transition-group type="transition" :name="!drag ? 'flip-list' : null">
<v-icon <v-list-item
v-if="icons[child._nodes.type].openIcon" v-for="child in item.children || []"
x-small v-show="!search || child.name.toLowerCase().includes(search.toLowerCase())"
style="cursor: auto" :key="child.key"
:color="icons[child._nodes.type].openColor" color="x-active"
active-class="font-weight-bold"
:selectable="true"
dense
:value="`${(child._nodes && child._nodes).type || ''}||${
(child._nodes && child._nodes.dbAlias) || ''
}||${child.name}`"
class="nested ml-3 nc-draggable-child"
style="position: relative"
@click.stop="addTab({ ...child }, false, true)"
@contextmenu.prevent.stop="showCTXMenu($event, child, false, true)"
> >
{{ icons[child._nodes.type].openIcon }} <v-icon small class="nc-child-draggable-icon">
</v-icon> mdi-drag-vertical
<v-icon </v-icon>
v-else <v-list-item-icon>
x-small <v-icon
style="cursor: auto" v-if="icons[child._nodes.type].openIcon"
:color="icons[child._nodes.type].color" style="cursor: auto"
> x-small
{{ icons[child._nodes.type].icon }} :color="icons[child._nodes.type].openColor"
</v-icon> >
</v-list-item-icon> {{ icons[child._nodes.type].openIcon }}
<v-list-item-title> </v-icon>
<v-tooltip <v-icon
v-if="_isUIAllowed('creator_tooltip') && child.creator_tooltip" v-else
bottom x-small
> style="cursor: auto"
<template #activator="{ on }"> :color="icons[child._nodes.type].color"
<span class="caption" v-on="on" @dblclick="showSqlClient = true"> >
{{ child.name }} {{ icons[child._nodes.type].icon }}
</span> </v-icon>
</template> </v-list-item-icon>
<span class="caption">{{ child.creator_tooltip }}</span> <v-list-item-title>
</v-tooltip> <v-tooltip
<span v-else class="caption">{{ child.name }}</span> v-if="_isUIAllowed('creator_tooltip') && child.creator_tooltip"
</v-list-item-title> bottom
<template v-if="child.type === 'table'"> >
<v-spacer /> <template #activator="{ on }">
<div class="action d-flex" @click.stop> <span class="caption" v-on="on" @dblclick="showSqlClient = true">
<v-menu> {{ child.name }}
<template #activator="{ on }"> </span>
<v-icon </template>
v-if=" <span class="caption">{{ child.creator_tooltip }}</span>
_isUIAllowed('treeview-rename-button')||_isUIAllowed('ui-acl') </v-tooltip>
" <span v-else class="caption">{{ child.name }}</span>
small </v-list-item-title>
v-on="on" <template v-if="child.type === 'table'">
> <v-spacer />
mdi-dots-vertical <div class="action d-flex" @click.stop>
</v-icon> <v-menu>
</template> <template #activator="{ on }">
<v-icon
<v-list dense> v-if="
<v-list-item _isUIAllowed('treeview-rename-button')||_isUIAllowed('ui-acl')
v-if="_isUIAllowed('treeview-rename-button')" "
dense small
@click=" v-on="on"
menuItem = child; >
dialogRenameTable.cookie = child; mdi-dots-vertical
dialogRenameTable.dialogShow = true;
dialogRenameTable.defaultValue = child.name;
"
>
<v-list-item-icon>
<v-icon x-small>
mdi-pencil-outline
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<span classs="caption">Rename</span>
</v-list-item-title>
</v-list-item>
<v-list-item v-if="_isUIAllowed('ui-acl')" dense @click="openUIACL">
<v-list-item-icon>
<v-icon x-small>
mdi-shield-outline
</v-icon> </v-icon>
</v-list-item-icon> </template>
<v-list-item-title>
<span classs="caption">UI ACL</span> <v-list dense>
</v-list-item-title> <v-list-item
</v-list-item> v-if="_isUIAllowed('treeview-rename-button')"
</v-list> dense
</v-menu> @click="
menuItem = child;
<!-- <v-icon @click.stop="" x-small>mdi-delete-outline</v-icon>--> dialogRenameTable.cookie = child;
</div> dialogRenameTable.dialogShow = true;
</template> dialogRenameTable.defaultValue = child.name;
</v-list-item> "
>
<v-list-item-icon>
<v-icon x-small>
mdi-pencil-outline
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<span classs="caption">Rename</span>
</v-list-item-title>
</v-list-item>
<v-list-item v-if="_isUIAllowed('ui-acl')" dense @click="openUIACL">
<v-list-item-icon>
<v-icon x-small>
mdi-shield-outline
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<span classs="caption">UI ACL</span>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- <v-icon @click.stop="" x-small>mdi-delete-outline</v-icon>-->
</div>
</template>
</v-list-item>
</transition-group>
</draggable>
</v-list-item-group> </v-list-item-group>
</v-list-group> </v-list-group>
<v-list-item <v-list-item
@ -688,10 +703,11 @@ import SponsorMini from '@/components/sponsorMini';
import {validateTableName} from "~/helpers"; import {validateTableName} from "~/helpers";
import ExcelImport from "~/components/import/excelImport"; import ExcelImport from "~/components/import/excelImport";
// const {clipboard} = require('electron'); import draggable from 'vuedraggable'
export default { export default {
components: { components: {
draggable,
ExcelImport, ExcelImport,
SponsorMini, SponsorMini,
DlgViewCreate, DlgViewCreate,
@ -700,6 +716,12 @@ export default {
dlgLabelSubmitCancel, dlgLabelSubmitCancel,
}, },
data: () => ({ data: () => ({
dragOptions:{
animation: 200,
group: "description",
disabled: false,
ghostClass: "ghost"
},
validateTableName, validateTableName,
roleIcon: { roleIcon: {
owner: 'mdi-account-star', owner: 'mdi-account-star',
@ -833,7 +855,23 @@ export default {
}, },
}, },
methods: { methods: {
openUIACL() { async onMove(event, children) {
if (children.length - 1 === event.moved.newIndex) {
this.$set(children[event.moved.newIndex], 'order', children[event.moved.newIndex - 1].order + 1)
} else if (event.moved.newIndex === 0) {
this.$set(children[event.moved.newIndex], 'order', children[1].order / 2)
} else {
this.$set(children[event.moved.newIndex], 'order', (children[event.moved.newIndex - 1].order + children[event.moved.newIndex + 1].order) / 2)
}
await this.$store.dispatch('sqlMgr/ActSqlOp', [{dbAlias: 'db'}, 'xcModelOrderSet', {
tn: children[event.moved.newIndex].tn,
order: children[event.moved.newIndex].order,
}])
}, openUIACL() {
this.disableOrEnableModelTabAdd(); this.disableOrEnableModelTabAdd();
setTimeout(() => { setTimeout(() => {
this.$router.push({ this.$router.push({
@ -1790,12 +1828,12 @@ export default {
/deep/ .nc-table-list-filter .v-input__slot { /deep/ .nc-table-list-filter .v-input__slot {
min-height: 30px !important; min-height: 30px !important;
} }
/deep/ .nc-table-list-filter .v-input__slot label { /deep/ .nc-table-list-filter .v-input__slot label {
top: 6px; top: 6px;
} }
/deep/ .nc-table-list-filter.theme--light.v-text-field > .v-input__control > .v-input__slot:before { /deep/ .nc-table-list-filter.theme--light.v-text-field > .v-input__control > .v-input__slot:before {
border-top-color: rgba(0, 0, 0, 0.12) !important; border-top-color: rgba(0, 0, 0, 0.12) !important;
} }
@ -1804,6 +1842,31 @@ export default {
border-top-color: rgba(255, 255, 255, 0.12) !important; border-top-color: rgba(255, 255, 255, 0.12) !important;
} }
.nc-draggable-child .nc-child-draggable-icon {
opacity: 0;
transition: .3s opacity;
position: absolute;
left: 0;
}
.nc-draggable-child:hover .nc-child-draggable-icon {
opacity: 1;
}
.flip-list-move {
transition: transform 0.5s;
}
.no-move {
transition: transform 0s;
}
.ghost {
opacity: 0.5;
background: grey;
}
</style> </style>
<!-- <!--

5
packages/nc-gui/components/project/projectMetadata/uiAcl/toggleTableUIAcl.vue

@ -128,7 +128,7 @@ export default {
dbAlias: this.db.meta.dbAlias, dbAlias: this.db.meta.dbAlias,
env: this.$store.getters['project/GtrEnv'] env: this.$store.getters['project/GtrEnv']
}, 'xcVisibilityMetaGet', { }, 'xcVisibilityMetaGet', {
type: 'table' type: 'all'
}])) }]))
}, },
async save() { async save() {
@ -136,8 +136,7 @@ export default {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{ await this.$store.dispatch('sqlMgr/ActSqlOp', [{
dbAlias: this.db.meta.dbAlias, dbAlias: this.db.meta.dbAlias,
env: this.$store.getters['project/GtrEnv'] env: this.$store.getters['project/GtrEnv']
}, 'xcVisibilityMetaSet', { }, 'xcVisibilityMetaSetAll', {
type: 'table',
disableList: this.tables.filter(t => t.edited) disableList: this.tables.filter(t => t.edited)
}]) }])
this.$toast.success('Updated UI ACL for tables successfully').goAway(3000) this.$toast.success('Updated UI ACL for tables successfully').goAway(3000)

241
packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue

@ -16,97 +16,111 @@
<span class="body-2 grey--text">{{ $t('nav_drawer.title') }}</span> <span class="body-2 grey--text">{{ $t('nav_drawer.title') }}</span>
</v-list-item> </v-list-item>
<v-list-item-group v-model="selectedViewIdLocal" mandatory color="primary"> <v-list-item-group v-model="selectedViewIdLocal" mandatory color="primary">
<v-list-item <draggable
v-for="(view, i) in viewsList" v-model="viewsList"
:key="view.id" draggable="div"
dense v-bind="dragOptions"
:value="view.id" @start="drag=true"
active-class="x-active--text" @end="drag=false"
class="body-2 view nc-view-item" @change="onMove($event)"
:class="`nc-${view.show_as}-view-item`"
@click="$emit('generateNewViewKey')"
> >
<v-list-item-icon class="mr-n1"> <transition-group type="transition" :name="!drag ? 'flip-list' : null">
<v-icon <v-list-item
v-if="viewIcons[view.show_as]" v-for="(view, i) in viewsList"
x-small :key="view.id"
:color="viewIcons[view.show_as].color" dense
:value="view.id"
active-class="x-active--text"
class="body-2 view nc-view-item nc-draggable-child"
:class="`nc-${view.show_as}-view-item`"
@click="$emit('generateNewViewKey')"
> >
{{ viewIcons[view.show_as].icon }} <v-icon small class="nc-child-draggable-icon" @click.stop>
</v-icon> mdi-drag-vertical
<v-icon v-else color="primary" small> </v-icon>
mdi-table <v-list-item-icon class="mr-n1">
</v-icon> <v-icon
</v-list-item-icon> v-if="viewIcons[view.show_as]"
<v-list-item-title> x-small
<v-tooltip bottom> :color="viewIcons[view.show_as].color"
<template #activator="{ on }">
<div
class="font-weight-regular"
style="overflow: hidden; text-overflow: ellipsis"
> >
<input {{ viewIcons[view.show_as].icon }}
v-if="view.edit" </v-icon>
:ref="`input${i}`" <v-icon v-else color="primary" small>
v-model="view.title_temp" mdi-table
@click.stop </v-icon>
@keydown.enter.stop="updateViewName(view, i)" </v-list-item-icon>
@blur="updateViewName(view, i)" <v-list-item-title>
> <v-tooltip bottom>
<template <template #activator="{ on }">
v-else <div
> class="font-weight-regular"
<span v-on="on">{{ view.alias || view.title }}</span> style="overflow: hidden; text-overflow: ellipsis"
>
<input
v-if="view.edit"
:ref="`input${i}`"
v-model="view.title_temp"
@click.stop
@keydown.enter.stop="updateViewName(view, i)"
@blur="updateViewName(view, i)"
>
<template
v-else
>
<span v-on="on">{{ view.alias || view.title }}</span>
</template>
</div>
</template> </template>
</div> {{ view.alias || view.title }}
</v-tooltip>
</v-list-item-title>
<v-spacer />
<template v-if="_isUIAllowed('virtualViewsCreateOrEdit')">
<!-- Copy view -->
<x-icon
v-if="view.type === 'vtable' && !view.edit"
:tooltip="$t('nav_drawer.virtual_views.action.copy')"
x-small
color="primary"
icon-class="view-icon nc-view-copy-icon"
@click.stop="copyView(view, i)"
>
mdi-content-copy
</x-icon>
<!-- Rename view -->
<x-icon
v-if="view.type === 'vtable' && !view.edit"
:tooltip="$t('nav_drawer.virtual_views.action.rename')"
x-small
color="primary"
icon-class="view-icon nc-view-edit-icon"
@click.stop="showRenameTextBox(view, i)"
>
mdi-pencil
</x-icon>
<!-- Delete view" -->
<x-icon
v-if="view.type === 'vtable'"
:tooltip="$t('nav_drawer.virtual_views.action.delete')"
small
color="error"
icon-class="view-icon nc-view-delete-icon"
@click.stop="deleteView(view)"
>
mdi-delete-outline
</x-icon>
</template> </template>
{{ view.alias || view.title }} <v-icon
</v-tooltip> v-if="view.id === selectedViewId"
</v-list-item-title> small
<v-spacer /> class="check-icon"
<template v-if="_isUIAllowed('virtualViewsCreateOrEdit')"> >
<!-- Copy view --> mdi-check-bold
<x-icon </v-icon>
v-if="view.type === 'vtable' && !view.edit" </v-list-item>
:tooltip="$t('nav_drawer.virtual_views.action.copy')" </transition-group>
x-small </draggable>
color="primary"
icon-class="view-icon nc-view-copy-icon"
@click.stop="copyView(view, i)"
>
mdi-content-copy
</x-icon>
<!-- Rename view -->
<x-icon
v-if="view.type === 'vtable' && !view.edit"
:tooltip="$t('nav_drawer.virtual_views.action.rename')"
x-small
color="primary"
icon-class="view-icon nc-view-edit-icon"
@click.stop="showRenameTextBox(view, i)"
>
mdi-pencil
</x-icon>
<!-- Delete view" -->
<x-icon
v-if="view.type === 'vtable'"
:tooltip="$t('nav_drawer.virtual_views.action.delete')"
small
color="error"
icon-class="view-icon nc-view-delete-icon"
@click.stop="deleteView(view)"
>
mdi-delete-outline
</x-icon>
</template>
<v-icon
v-if="view.id === selectedViewId"
small
class="check-icon"
>
mdi-check-bold
</v-icon>
</v-list-item>
</v-list-item-group> </v-list-item-group>
</v-list> </v-list>
<template v-if="hideViews && _isUIAllowed('virtualViewsCreateOrEdit')"> <template v-if="hideViews && _isUIAllowed('virtualViewsCreateOrEdit')">
@ -499,13 +513,14 @@
</template> </template>
<script> <script>
import draggable from 'vuedraggable'
import CreateViewDialog from '@/components/project/spreadsheet/dialog/createViewDialog' import CreateViewDialog from '@/components/project/spreadsheet/dialog/createViewDialog'
import Extras from '~/components/project/spreadsheet/components/extras' import Extras from '~/components/project/spreadsheet/components/extras'
import viewIcons from '~/helpers/viewIcons' import viewIcons from '~/helpers/viewIcons'
export default { export default {
name: 'SpreadsheetNavDrawer', name: 'SpreadsheetNavDrawer',
components: { Extras, CreateViewDialog }, components: { Extras, CreateViewDialog, draggable },
props: { props: {
extraViewParams: Object, extraViewParams: Object,
showAdvanceOptions: Boolean, showAdvanceOptions: Boolean,
@ -537,6 +552,12 @@ export default {
showSystemFields: Boolean showSystemFields: Boolean
}, },
data: () => ({ data: () => ({
dragOptions: {
animation: 200,
group: 'description',
disabled: false,
ghostClass: 'ghost'
},
time: Date.now(), time: Date.now(),
sponsorMiniVisible: true, sponsorMiniVisible: true,
enableDummyFeat: false, enableDummyFeat: false,
@ -578,8 +599,9 @@ export default {
get() { get() {
let id let id
if (this.viewsList) { if (this.viewsList) {
console.log(this.viewsList)
const view = this.viewsList.find(v => (v.alias ? v.alias : v.title) === this.$route.query.view) const view = this.viewsList.find(v => (v.alias ? v.alias : v.title) === this.$route.query.view)
id = (view && view.id) || (this.viewsList[0] && this.viewsList[0].id) id = (view && view.id) || (this.viewsList && this.viewsList[0] || {}).id
} }
return id return id
} }
@ -603,6 +625,22 @@ export default {
this.onViewIdChange(this.selectedViewIdLocal) this.onViewIdChange(this.selectedViewIdLocal)
}, },
methods: { methods: {
async onMove(event) {
if (this.viewsList.length - 1 === event.moved.newIndex) {
this.$set(this.viewsList[event.moved.newIndex], 'view_order', this.viewsList[event.moved.newIndex - 1].view_order + 1)
} else if (event.moved.newIndex === 0) {
this.$set(this.viewsList[event.moved.newIndex], 'view_order', this.viewsList[1].view_order / 2)
} else {
this.$set(this.viewsList[event.moved.newIndex], 'view_order', (this.viewsList[event.moved.newIndex - 1].view_order + this.viewsList[event.moved.newIndex + 1].view_order) / 2)
}
console.log(this.viewsList)
await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: 'db' }, 'xcModelViewOrderSet', {
id: this.viewsList[event.moved.newIndex].id,
view_order: this.viewsList[event.moved.newIndex].view_order
}])
},
onViewIdChange(id) { onViewIdChange(id) {
const selectedView = this.viewsList && this.viewsList.find(v => v.id === id) const selectedView = this.viewsList && this.viewsList.find(v => v.id === id)
let queryParams = {} let queryParams = {}
@ -923,4 +961,31 @@ export default {
opacity: 1; opacity: 1;
} }
} }
.nc-draggable-child .nc-child-draggable-icon {
opacity: 0;
transition: .3s opacity;
position: absolute;
left: 0;
}
.nc-draggable-child:hover .nc-child-draggable-icon {
opacity: 1;
}
.nc-draggable-child:hover .nc-child-draggable-icon {
opacity: 1;
}
.flip-list-move {
transition: transform 0.5s;
}
.no-move {
transition: transform 0s;
}
.ghost {
opacity: 0.5;
background: grey;
}
</style> </style>

5
packages/nc-gui/helpers/treeViewDataSerializer.js

@ -344,6 +344,9 @@ function tableParser(data = [], dbKey, env, dbAlias, dbConnection) {
const json = { const json = {
type: "table", type: "table",
name: table._tn, name: table._tn,
tn: table.tn,
_tn: table._tn,
order: table.order,
key: tableDirKey + "." + i, key: tableDirKey + "." + i,
children: [], children: [],
_nodes: { _nodes: {
@ -385,6 +388,8 @@ function viewsParser(data = [], dbKey, env, dbAlias, dbConnection) {
const viewKey = `${viewDirKey}.${i}`; const viewKey = `${viewDirKey}.${i}`;
const json = { const json = {
type: "view", type: "view",
tn: view.tn || view.view_name,
_tn: view._tn,
name: view._tn || view.view_name, name: view._tn || view.view_name,
key: viewDirKey + "." + i, key: viewDirKey + "." + i,
children: [], children: [],

6
packages/nocodb/src/lib/noco/common/XcMigrationSource.ts

@ -6,6 +6,7 @@ import * as viewName from '../migrations/nc_005_add_view_name_column';
import * as nc_006_alter_nc_shared_views from '../migrations/nc_006_alter_nc_shared_views'; import * as nc_006_alter_nc_shared_views from '../migrations/nc_006_alter_nc_shared_views';
import * as nc_007_alter_nc_shared_views_1 from '../migrations/nc_007_alter_nc_shared_views_1'; import * as nc_007_alter_nc_shared_views_1 from '../migrations/nc_007_alter_nc_shared_views_1';
import * as nc_008_add_nc_shared_bases from '../migrations/nc_008_add_nc_shared_bases'; import * as nc_008_add_nc_shared_bases from '../migrations/nc_008_add_nc_shared_bases';
import * as nc_009_add_model_order from '../migrations/nc_009_add_model_order';
// Create a custom migration source class // Create a custom migration source class
export default class XcMigrationSource { export default class XcMigrationSource {
@ -22,7 +23,8 @@ export default class XcMigrationSource {
'viewName', 'viewName',
'nc_006_alter_nc_shared_views', 'nc_006_alter_nc_shared_views',
'nc_007_alter_nc_shared_views_1', 'nc_007_alter_nc_shared_views_1',
'nc_008_add_nc_shared_bases' 'nc_008_add_nc_shared_bases',
'nc_009_add_model_order'
]); ]);
} }
@ -48,6 +50,8 @@ export default class XcMigrationSource {
return nc_007_alter_nc_shared_views_1; return nc_007_alter_nc_shared_views_1;
case 'nc_008_add_nc_shared_bases': case 'nc_008_add_nc_shared_bases':
return nc_008_add_nc_shared_bases; return nc_008_add_nc_shared_bases;
case 'nc_009_add_model_order':
return nc_009_add_model_order;
} }
} }
} }

1
packages/nocodb/src/lib/noco/meta/NcMetaIO.ts

@ -121,6 +121,7 @@ export default abstract class NcMetaIO {
offset?: number; offset?: number;
xcCondition?: XcCondition; xcCondition?: XcCondition;
fields?: string[]; fields?: string[];
orderBy?: { [key: string]: 'asc' | 'desc' };
} }
): Promise<any[]>; ): Promise<any[]>;

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

@ -200,6 +200,7 @@ export default class NcMetaIOImpl extends NcMetaIO {
offset?: number; offset?: number;
xcCondition?; xcCondition?;
fields?: string[]; fields?: string[];
orderBy?: { [key: string]: 'asc' | 'desc' };
} }
): Promise<any[]> { ): Promise<any[]> {
const query = this.knexConnection(target); const query = this.knexConnection(target);
@ -224,6 +225,11 @@ export default class NcMetaIOImpl extends NcMetaIO {
(query as any).condition(args.xcCondition); (query as any).condition(args.xcCondition);
} }
if (args?.orderBy) {
for (const [col, dir] of Object.entries(args.orderBy)) {
query.orderBy(col, dir);
}
}
if (args?.fields?.length) { if (args?.fields?.length) {
query.select(...args.fields); query.select(...args.fields);
} }

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

@ -1436,7 +1436,7 @@ export default class NcMetaMgr {
result = await this.xcVirtualTableDelete(args, req); result = await this.xcVirtualTableDelete(args, req);
break; break;
case 'xcVirtualTableList': case 'xcVirtualTableList':
result = await this.xcVirtualTableList(args); result = await this.xcVirtualTableList(args, req);
break; break;
case 'xcVersionLetters': case 'xcVersionLetters':
@ -1477,6 +1477,10 @@ export default class NcMetaMgr {
result = await this.xcVisibilityMetaSet(args); result = await this.xcVisibilityMetaSet(args);
break; break;
case 'xcVisibilityMetaSetAll':
result = await this.xcVisibilityMetaSetAll(args);
break;
case 'tableList': case 'tableList':
result = await this.xcTableList(req, args); result = await this.xcTableList(req, args);
break; break;
@ -1794,6 +1798,14 @@ export default class NcMetaMgr {
result = await this.xcModelSet(args); result = await this.xcModelSet(args);
break; break;
case 'xcModelOrderSet':
result = await this.xcModelOrderSet(args);
break;
case 'xcModelViewOrderSet':
result = await this.xcModelViewOrderSet(args);
break;
case 'xcUpdateVirtualKeyAlias': case 'xcUpdateVirtualKeyAlias':
result = await this.xcUpdateVirtualKeyAlias(args); result = await this.xcUpdateVirtualKeyAlias(args);
break; break;
@ -2319,7 +2331,7 @@ export default class NcMetaMgr {
// NOTE: updated // NOTE: updated
protected async xcModelSet(args): Promise<any> { protected async xcModelSet(args): Promise<any> {
const dbAlias = await this.getDbAlias(args); const dbAlias = this.getDbAlias(args);
this.cacheModelDel(this.getProjectId(args), dbAlias, 'table', args.args.tn); this.cacheModelDel(this.getProjectId(args), dbAlias, 'table', args.args.tn);
return this.xcMeta.metaUpdate( return this.xcMeta.metaUpdate(
args.project_id, args.project_id,
@ -2334,6 +2346,36 @@ export default class NcMetaMgr {
); );
} }
// NOTE: updated
protected async xcModelOrderSet(args): Promise<any> {
const dbAlias = this.getDbAlias(args);
return this.xcMeta.metaUpdate(
this.getProjectId(args),
dbAlias,
'nc_models',
{
order: args.args.order
},
{
title: args.args.tn
}
);
}
// NOTE: updated
protected async xcModelViewOrderSet(args): Promise<any> {
const dbAlias = this.getDbAlias(args);
return this.xcMeta.metaUpdate(
this.getProjectId(args),
dbAlias,
'nc_models',
{
view_order: args.args.view_order
},
args.args.id
);
}
protected async xcUpdateVirtualKeyAlias(args): Promise<any> { protected async xcUpdateVirtualKeyAlias(args): Promise<any> {
const dbAlias = await this.getDbAlias(args); const dbAlias = await this.getDbAlias(args);
const model = await this.xcMeta.metaGet( const model = await this.xcMeta.metaGet(
@ -4571,6 +4613,7 @@ export default class NcMetaMgr {
obj[table.title] = { obj[table.title] = {
tn: table.title, tn: table.title,
_tn: table.alias, _tn: table.alias,
order: table.order,
disabled: { ...defaultDisabled } disabled: { ...defaultDisabled }
}; };
return obj; return obj;
@ -4591,8 +4634,10 @@ export default class NcMetaMgr {
result[d.title].disabled[d.role] = !!d.disabled; result[d.title].disabled[d.role] = !!d.disabled;
} }
return Object.values(result)?.sort((a: any, b: any) => return Object.values(result)?.sort(
(a?._tn || a?.tn)?.localeCompare(b?._tn || b?.tn) (a: any, b: any) =>
(a.order || 0) - (b.order || 0) ||
(a?._tn || a?.tn)?.localeCompare(b?._tn || b?.tn)
); );
} }
break; break;
@ -4760,6 +4805,77 @@ export default class NcMetaMgr {
return Object.values(result); return Object.values(result);
} }
break; break;
case 'all':
{
const models = await this.xcMeta.metaList(
this.getProjectId(args),
this.getDbAlias(args),
'nc_models',
{
condition: {
...(args?.args?.includeM2M ? {} : { mm: null })
},
xcCondition: {
_or: [
{
type: 'table'
},
{
type: 'view'
},
{
type: 'vtable'
}
]
}
}
);
const result = models.reduce((obj, table) => {
obj[table.title] = {
tn: table.title,
_tn: table.alias || table.title,
order: table.order,
disabled: { ...defaultDisabled },
type: table.type,
show_as: table.show_as
};
return obj;
}, {});
const disabledList = await this.xcMeta.metaList(
args.project_id,
this.getDbAlias(args),
'nc_disabled_models_for_role',
{
xcCondition: {
_or: [
{
type: 'table'
},
{
type: 'view'
},
{
type: 'vtable'
}
]
}
}
);
for (const d of disabledList) {
result[d.title].disabled[d.role] = !!d.disabled;
}
return Object.values(result)?.sort((a: any, b: any) =>
(a?.parent_model_title || a?.tn)?.localeCompare(
b?.parent_model_title || b?.tn
)
);
}
break;
} }
} catch (e) { } catch (e) {
throw e; throw e;
@ -4767,84 +4883,13 @@ export default class NcMetaMgr {
} }
// @ts-ignore // @ts-ignore
protected async xcVisibilityMetaSet(args) { protected async xcVisibilityMetaSetAll(args) {
// if (!this.isEe) {
throw new XCEeError('Please upgrade'); throw new XCEeError('Please upgrade');
// } }
// try { // @ts-ignore
// let field = ''; protected async xcVisibilityMetaSet(args) {
// switch (args.args.type) { throw new XCEeError('Please upgrade');
// case 'table':
// field = 'tn';
// break;
// case 'function':
// field = 'function_name';
// break;
// case 'procedure':
// field = 'procedure_name';
// break;
// case 'view':
// field = 'view_name';
// break;
// case 'relation':
// field = 'relationType';
// break;
// }
//
// for (const d of args.args.disableList) {
// const props = {};
// if (field === 'relationType') {
// Object.assign(props, {
// tn: d.tn,
// rtn: d.rtn,
// cn: d.cn,
// rcn: d.rcn,
// relation_type: d.relationType
// })
// }
// for (const role of Object.keys(d.disabled)) {
// const dataInDb = await this.xcMeta.metaGet(this.getProjectId(args), this.getDbAlias(args), 'nc_disabled_models_for_role', {
// type: args.args.type,
// title: d[field],
// role,
// ...props
// });
// if (dataInDb) {
// if (d.disabled[role]) {
// if (!dataInDb.disabled) {
// await this.xcMeta.metaUpdate(this.getProjectId(args), this.getDbAlias(args), 'nc_disabled_models_for_role', {
// disabled: d.disabled[role]
// }, {
// type: args.args.type,
// title: d[field],
// role, ...props
// })
// }
// } else {
//
// await this.xcMeta.metaDelete(this.getProjectId(args), this.getDbAlias(args), 'nc_disabled_models_for_role', {
// type: args.args.type,
// title: d[field],
// role, ...props
// })
// }
// } else if (d.disabled[role]) {
// await this.xcMeta.metaInsert(this.getProjectId(args), this.getDbAlias(args), 'nc_disabled_models_for_role', {
// disabled: d.disabled[role],
// type: args.args.type,
// title: d[field],
// role, ...props
// })
//
// }
// }
// }
//
//
// } catch (e) {
// throw e;
// }
} }
protected async xcPluginList(_args): Promise<any> { protected async xcPluginList(_args): Promise<any> {
@ -4930,6 +4975,22 @@ export default class NcMetaMgr {
} }
); );
const view_order =
((
await this.xcMeta
.knex('nc_models')
.where({
project_id: this.getProjectId(args),
db_alias: this.getDbAlias(args)
})
.andWhere(qb => {
qb.where({ title: args.args.parent_model_title });
qb.orWhere({ parent_model_title: args.args.parent_model_title });
})
.max('view_order as max')
.first()
)?.max || 0) + 1;
if (!parentModel) { if (!parentModel) {
return; return;
} }
@ -4940,7 +5001,8 @@ export default class NcMetaMgr {
// meta: parentModel.meta, // meta: parentModel.meta,
query_params: JSON.stringify(args.args.query_params), query_params: JSON.stringify(args.args.query_params),
parent_model_title: args.args.parent_model_title, parent_model_title: args.args.parent_model_title,
show_as: args.args.show_as show_as: args.args.show_as,
view_order
}; };
const projectId = this.getProjectId(args); const projectId = this.getProjectId(args);
const dbAlias = this.getDbAlias(args); const dbAlias = this.getDbAlias(args);
@ -5168,45 +5230,87 @@ export default class NcMetaMgr {
}; };
} }
protected async xcVirtualTableList(args): Promise<any> { protected async xcVirtualTableList(args, req): Promise<any> {
return ( const roles = (await this.xcMeta.metaList('', '', 'nc_roles'))
await this.xcMeta.metaList( .map(r => r.title)
this.getProjectId(args), .filter(role => !['owner', 'guest', 'creator'].includes(role));
this.getDbAlias(args),
'nc_models', const defaultDisabled = roles.reduce((o, r) => ({ ...o, [r]: false }), {});
{ const list = await this.xcMeta.metaList(
xcCondition: { this.getProjectId(args),
_or: [ this.getDbAlias(args),
{ 'nc_models',
parent_model_title: { {
eq: args.args.tn xcCondition: {
} _or: [
}, {
{ parent_model_title: {
title: { eq: args.args.tn
eq: args.args.tn
}
} }
] },
}, {
fields: [ title: {
'id', eq: args.args.tn
'alias', }
'meta', }
'parent_model_title',
'query_params',
'show_as',
'title',
'type'
] ]
// todo: handle sort },
fields: [
'id',
'alias',
'meta',
'parent_model_title',
'query_params',
'show_as',
'title',
'type',
'view_order'
],
orderBy: {
view_order: 'asc'
} }
) // todo: handle sort
).sort( }
(a, b) => );
+(a.type === 'vtable' ? a.id : -Infinity) -
+(b.type === 'vtable' ? b.id : -Infinity) const result = list.reduce((obj, table) => {
obj[table.title] = {
...table,
disabled: { ...defaultDisabled }
};
return obj;
}, {});
const disabledList = await this.xcMeta.metaList(
args.project_id,
this.getDbAlias(args),
'nc_disabled_models_for_role',
{
xcCondition: {
_or: [
{
type: 'table'
},
{
type: 'vtable'
}
]
}
}
); );
for (const d of disabledList) {
if (result?.[d.title]?.disabled)
result[d.title].disabled[d.role] = !!d.disabled;
}
const models = Object.values(result).filter((table: any) => {
return Object.keys(req.session?.passport?.user?.roles).some(
role =>
req.session?.passport?.user?.roles[role] && !table.disabled[role]
);
});
return models;
} }
protected async xcVirtualTableDelete(args, req): Promise<any> { protected async xcVirtualTableDelete(args, req): Promise<any> {

70
packages/nocodb/src/lib/noco/meta/NcMetaMgrEE.ts

@ -396,6 +396,76 @@ export default class NcMetaMgrEE extends NcMetaMgr {
} }
} }
protected async xcVisibilityMetaSetAll(args) {
try {
for (const d of args.args.disableList) {
const field = 'tn';
const props = {};
for (const role of Object.keys(d.disabled)) {
const dataInDb = await this.xcMeta.metaGet(
this.getProjectId(args),
this.getDbAlias(args),
'nc_disabled_models_for_role',
{
type: d.type,
title: d[field],
role,
...props
}
);
if (dataInDb) {
if (d.disabled[role]) {
if (!dataInDb.disabled) {
await this.xcMeta.metaUpdate(
this.getProjectId(args),
this.getDbAlias(args),
'nc_disabled_models_for_role',
{
disabled: d.disabled[role]
},
{
type: args.args.type,
title: d[field],
role,
...props
}
);
}
} else {
await this.xcMeta.metaDelete(
this.getProjectId(args),
this.getDbAlias(args),
'nc_disabled_models_for_role',
{
type: args.args.type,
title: d[field],
role,
...props
}
);
}
} else if (d.disabled[role]) {
await this.xcMeta.metaInsert(
this.getProjectId(args),
this.getDbAlias(args),
'nc_disabled_models_for_role',
{
disabled: d.disabled[role],
type: args.args.type,
title: d[field],
role,
...props
}
);
}
}
}
} catch (e) {
throw e;
}
}
protected async xcAuditList(args): Promise<any> { protected async xcAuditList(args): Promise<any> {
return this.xcMeta.metaPaginatedList( return this.xcMeta.metaPaginatedList(
this.getProjectId(args), this.getProjectId(args),

46
packages/nocodb/src/lib/noco/migrations/nc_009_add_model_order.ts

@ -0,0 +1,46 @@
import Knex from 'knex';
const up = async (knex: Knex) => {
await knex.schema.alterTable('nc_models', table => {
table
.float('order')
.unsigned()
.index();
table
.float('view_order')
.unsigned()
.index();
});
};
const down = async knex => {
await knex.schema.alterTable('nc_models', table => {
table.dropColumn('order');
table.dropColumn('view_order');
});
};
export { up, down };
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

14
packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts

@ -355,6 +355,18 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
let tables; let tables;
const swaggerRefs: { [table: string]: any[] } = {}; const swaggerRefs: { [table: string]: any[] } = {};
let order =
(
await this.xcMeta
.knex('nc_models')
.where({
project_id: this.projectId,
db_alias: this.dbAlias
})
.max('order as max')
.first()
)?.max || 0;
/* Get all relations */ /* Get all relations */
const relations = await this.relationsSyncAndGet(); const relations = await this.relationsSyncAndGet();
@ -508,6 +520,8 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
this.dbAlias, this.dbAlias,
'nc_models', 'nc_models',
{ {
order: ++order,
view_order: 1,
title: table.tn, title: table.tn,
alias: meta._tn, alias: meta._tn,
meta: JSON.stringify(meta), meta: JSON.stringify(meta),

Loading…
Cancel
Save