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 {
/*margin-left: 12px !important;*/
}
@ -278,6 +283,7 @@ tbody tr:nth-of-type(odd) {
height: calc(100% - 30px);
overflow: auto;
}
.table-tabs.hidden-tab > .v-tabs-items {
height: 100%;
overflow: auto;
@ -383,7 +389,7 @@ td .v-input--selection-controls {
height: 100%;
}
.scroll-auto{
.scroll-auto {
overflow: auto;
}
@ -443,7 +449,7 @@ input, textarea, select {
}
/* Toast css */
.toasted .primary, .toasted.toasted-primary{
.toasted .primary, .toasted.toasted-primary {
font-family: "Roboto", sans-serif !important;
font-weight: 400 !important;
}
@ -482,6 +488,6 @@ body.dark .toasted .primary.info, body.dark .toasted.toasted-primary.info {
}
.v-date-picker-table{
height:auto !important;
.v-date-picker-table {
height: auto !important;
}

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

@ -201,106 +201,121 @@
</template>
<v-list-item-group :value="selectedItem">
<v-list-item
v-for="child in item.children || []"
v-show="!search || child.name.toLowerCase().includes(search.toLowerCase())"
:key="child.key"
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"
@click.stop="addTab({ ...child }, false, true)"
@contextmenu.prevent.stop="showCTXMenu($event, child, false, true)"
<draggable
v-model=" item.children"
draggable="div"
v-bind="dragOptions"
@start="drag=true"
@end="drag=false"
@change="onMove($event, item.children)"
>
<v-list-item-icon>
<v-icon
v-if="icons[child._nodes.type].openIcon"
x-small
style="cursor: auto"
:color="icons[child._nodes.type].openColor"
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
<v-list-item
v-for="child in item.children || []"
v-show="!search || child.name.toLowerCase().includes(search.toLowerCase())"
:key="child.key"
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>
<v-icon
v-else
x-small
style="cursor: auto"
:color="icons[child._nodes.type].color"
>
{{ icons[child._nodes.type].icon }}
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<v-tooltip
v-if="_isUIAllowed('creator_tooltip') && child.creator_tooltip"
bottom
>
<template #activator="{ on }">
<span class="caption" v-on="on" @dblclick="showSqlClient = true">
{{ child.name }}
</span>
</template>
<span class="caption">{{ child.creator_tooltip }}</span>
</v-tooltip>
<span v-else class="caption">{{ child.name }}</span>
</v-list-item-title>
<template v-if="child.type === 'table'">
<v-spacer />
<div class="action d-flex" @click.stop>
<v-menu>
<template #activator="{ on }">
<v-icon
v-if="
_isUIAllowed('treeview-rename-button')||_isUIAllowed('ui-acl')
"
small
v-on="on"
>
mdi-dots-vertical
</v-icon>
</template>
<v-list dense>
<v-list-item
v-if="_isUIAllowed('treeview-rename-button')"
dense
@click="
menuItem = child;
dialogRenameTable.cookie = child;
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 small class="nc-child-draggable-icon">
mdi-drag-vertical
</v-icon>
<v-list-item-icon>
<v-icon
v-if="icons[child._nodes.type].openIcon"
style="cursor: auto"
x-small
:color="icons[child._nodes.type].openColor"
>
{{ icons[child._nodes.type].openIcon }}
</v-icon>
<v-icon
v-else
x-small
style="cursor: auto"
:color="icons[child._nodes.type].color"
>
{{ icons[child._nodes.type].icon }}
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<v-tooltip
v-if="_isUIAllowed('creator_tooltip') && child.creator_tooltip"
bottom
>
<template #activator="{ on }">
<span class="caption" v-on="on" @dblclick="showSqlClient = true">
{{ child.name }}
</span>
</template>
<span class="caption">{{ child.creator_tooltip }}</span>
</v-tooltip>
<span v-else class="caption">{{ child.name }}</span>
</v-list-item-title>
<template v-if="child.type === 'table'">
<v-spacer />
<div class="action d-flex" @click.stop>
<v-menu>
<template #activator="{ on }">
<v-icon
v-if="
_isUIAllowed('treeview-rename-button')||_isUIAllowed('ui-acl')
"
small
v-on="on"
>
mdi-dots-vertical
</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>
</template>
<v-list dense>
<v-list-item
v-if="_isUIAllowed('treeview-rename-button')"
dense
@click="
menuItem = child;
dialogRenameTable.cookie = child;
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-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-group>
<v-list-item
@ -688,10 +703,11 @@ import SponsorMini from '@/components/sponsorMini';
import {validateTableName} from "~/helpers";
import ExcelImport from "~/components/import/excelImport";
// const {clipboard} = require('electron');
import draggable from 'vuedraggable'
export default {
components: {
draggable,
ExcelImport,
SponsorMini,
DlgViewCreate,
@ -700,6 +716,12 @@ export default {
dlgLabelSubmitCancel,
},
data: () => ({
dragOptions:{
animation: 200,
group: "description",
disabled: false,
ghostClass: "ghost"
},
validateTableName,
roleIcon: {
owner: 'mdi-account-star',
@ -833,7 +855,23 @@ export default {
},
},
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();
setTimeout(() => {
this.$router.push({
@ -1790,12 +1828,12 @@ export default {
/deep/ .nc-table-list-filter .v-input__slot {
min-height: 30px !important;
}
/deep/ .nc-table-list-filter .v-input__slot label {
top: 6px;
}
/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;
}
@ -1804,6 +1842,31 @@ export default {
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>
<!--

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

@ -128,7 +128,7 @@ export default {
dbAlias: this.db.meta.dbAlias,
env: this.$store.getters['project/GtrEnv']
}, 'xcVisibilityMetaGet', {
type: 'table'
type: 'all'
}]))
},
async save() {
@ -136,8 +136,7 @@ export default {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
dbAlias: this.db.meta.dbAlias,
env: this.$store.getters['project/GtrEnv']
}, 'xcVisibilityMetaSet', {
type: 'table',
}, 'xcVisibilityMetaSetAll', {
disableList: this.tables.filter(t => t.edited)
}])
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>
</v-list-item>
<v-list-item-group v-model="selectedViewIdLocal" mandatory color="primary">
<v-list-item
v-for="(view, i) in viewsList"
:key="view.id"
dense
:value="view.id"
active-class="x-active--text"
class="body-2 view nc-view-item"
:class="`nc-${view.show_as}-view-item`"
@click="$emit('generateNewViewKey')"
<draggable
v-model="viewsList"
draggable="div"
v-bind="dragOptions"
@start="drag=true"
@end="drag=false"
@change="onMove($event)"
>
<v-list-item-icon class="mr-n1">
<v-icon
v-if="viewIcons[view.show_as]"
x-small
:color="viewIcons[view.show_as].color"
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
<v-list-item
v-for="(view, i) in viewsList"
:key="view.id"
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>
<v-icon v-else color="primary" small>
mdi-table
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<v-tooltip bottom>
<template #activator="{ on }">
<div
class="font-weight-regular"
style="overflow: hidden; text-overflow: ellipsis"
<v-icon small class="nc-child-draggable-icon" @click.stop>
mdi-drag-vertical
</v-icon>
<v-list-item-icon class="mr-n1">
<v-icon
v-if="viewIcons[view.show_as]"
x-small
:color="viewIcons[view.show_as].color"
>
<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>
{{ viewIcons[view.show_as].icon }}
</v-icon>
<v-icon v-else color="primary" small>
mdi-table
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<v-tooltip bottom>
<template #activator="{ on }">
<div
class="font-weight-regular"
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>
</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>
{{ 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>
<v-icon
v-if="view.id === selectedViewId"
small
class="check-icon"
>
mdi-check-bold
</v-icon>
</v-list-item>
<v-icon
v-if="view.id === selectedViewId"
small
class="check-icon"
>
mdi-check-bold
</v-icon>
</v-list-item>
</transition-group>
</draggable>
</v-list-item-group>
</v-list>
<template v-if="hideViews && _isUIAllowed('virtualViewsCreateOrEdit')">
@ -499,13 +513,14 @@
</template>
<script>
import draggable from 'vuedraggable'
import CreateViewDialog from '@/components/project/spreadsheet/dialog/createViewDialog'
import Extras from '~/components/project/spreadsheet/components/extras'
import viewIcons from '~/helpers/viewIcons'
export default {
name: 'SpreadsheetNavDrawer',
components: { Extras, CreateViewDialog },
components: { Extras, CreateViewDialog, draggable },
props: {
extraViewParams: Object,
showAdvanceOptions: Boolean,
@ -537,6 +552,12 @@ export default {
showSystemFields: Boolean
},
data: () => ({
dragOptions: {
animation: 200,
group: 'description',
disabled: false,
ghostClass: 'ghost'
},
time: Date.now(),
sponsorMiniVisible: true,
enableDummyFeat: false,
@ -578,8 +599,9 @@ export default {
get() {
let id
if (this.viewsList) {
console.log(this.viewsList)
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
}
@ -603,6 +625,22 @@ export default {
this.onViewIdChange(this.selectedViewIdLocal)
},
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) {
const selectedView = this.viewsList && this.viewsList.find(v => v.id === id)
let queryParams = {}
@ -923,4 +961,31 @@ export default {
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>

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

@ -344,6 +344,9 @@ function tableParser(data = [], dbKey, env, dbAlias, dbConnection) {
const json = {
type: "table",
name: table._tn,
tn: table.tn,
_tn: table._tn,
order: table.order,
key: tableDirKey + "." + i,
children: [],
_nodes: {
@ -385,6 +388,8 @@ function viewsParser(data = [], dbKey, env, dbAlias, dbConnection) {
const viewKey = `${viewDirKey}.${i}`;
const json = {
type: "view",
tn: view.tn || view.view_name,
_tn: view._tn,
name: view._tn || view.view_name,
key: viewDirKey + "." + i,
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_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_009_add_model_order from '../migrations/nc_009_add_model_order';
// Create a custom migration source class
export default class XcMigrationSource {
@ -22,7 +23,8 @@ export default class XcMigrationSource {
'viewName',
'nc_006_alter_nc_shared_views',
'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;
case '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;
xcCondition?: XcCondition;
fields?: string[];
orderBy?: { [key: string]: 'asc' | 'desc' };
}
): Promise<any[]>;

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

@ -200,6 +200,7 @@ export default class NcMetaIOImpl extends NcMetaIO {
offset?: number;
xcCondition?;
fields?: string[];
orderBy?: { [key: string]: 'asc' | 'desc' };
}
): Promise<any[]> {
const query = this.knexConnection(target);
@ -224,6 +225,11 @@ export default class NcMetaIOImpl extends NcMetaIO {
(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) {
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);
break;
case 'xcVirtualTableList':
result = await this.xcVirtualTableList(args);
result = await this.xcVirtualTableList(args, req);
break;
case 'xcVersionLetters':
@ -1477,6 +1477,10 @@ export default class NcMetaMgr {
result = await this.xcVisibilityMetaSet(args);
break;
case 'xcVisibilityMetaSetAll':
result = await this.xcVisibilityMetaSetAll(args);
break;
case 'tableList':
result = await this.xcTableList(req, args);
break;
@ -1794,6 +1798,14 @@ export default class NcMetaMgr {
result = await this.xcModelSet(args);
break;
case 'xcModelOrderSet':
result = await this.xcModelOrderSet(args);
break;
case 'xcModelViewOrderSet':
result = await this.xcModelViewOrderSet(args);
break;
case 'xcUpdateVirtualKeyAlias':
result = await this.xcUpdateVirtualKeyAlias(args);
break;
@ -2319,7 +2331,7 @@ export default class NcMetaMgr {
// NOTE: updated
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);
return this.xcMeta.metaUpdate(
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> {
const dbAlias = await this.getDbAlias(args);
const model = await this.xcMeta.metaGet(
@ -4571,6 +4613,7 @@ export default class NcMetaMgr {
obj[table.title] = {
tn: table.title,
_tn: table.alias,
order: table.order,
disabled: { ...defaultDisabled }
};
return obj;
@ -4591,8 +4634,10 @@ export default class NcMetaMgr {
result[d.title].disabled[d.role] = !!d.disabled;
}
return Object.values(result)?.sort((a: any, b: any) =>
(a?._tn || a?.tn)?.localeCompare(b?._tn || b?.tn)
return Object.values(result)?.sort(
(a: any, b: any) =>
(a.order || 0) - (b.order || 0) ||
(a?._tn || a?.tn)?.localeCompare(b?._tn || b?.tn)
);
}
break;
@ -4760,6 +4805,77 @@ export default class NcMetaMgr {
return Object.values(result);
}
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) {
throw e;
@ -4767,84 +4883,13 @@ export default class NcMetaMgr {
}
// @ts-ignore
protected async xcVisibilityMetaSet(args) {
// if (!this.isEe) {
protected async xcVisibilityMetaSetAll(args) {
throw new XCEeError('Please upgrade');
// }
}
// try {
// let field = '';
// switch (args.args.type) {
// 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;
// }
// @ts-ignore
protected async xcVisibilityMetaSet(args) {
throw new XCEeError('Please upgrade');
}
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) {
return;
}
@ -4940,7 +5001,8 @@ export default class NcMetaMgr {
// meta: parentModel.meta,
query_params: JSON.stringify(args.args.query_params),
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 dbAlias = this.getDbAlias(args);
@ -5168,45 +5230,87 @@ export default class NcMetaMgr {
};
}
protected async xcVirtualTableList(args): Promise<any> {
return (
await this.xcMeta.metaList(
this.getProjectId(args),
this.getDbAlias(args),
'nc_models',
{
xcCondition: {
_or: [
{
parent_model_title: {
eq: args.args.tn
}
},
{
title: {
eq: args.args.tn
}
protected async xcVirtualTableList(args, req): Promise<any> {
const roles = (await this.xcMeta.metaList('', '', 'nc_roles'))
.map(r => r.title)
.filter(role => !['owner', 'guest', 'creator'].includes(role));
const defaultDisabled = roles.reduce((o, r) => ({ ...o, [r]: false }), {});
const list = await this.xcMeta.metaList(
this.getProjectId(args),
this.getDbAlias(args),
'nc_models',
{
xcCondition: {
_or: [
{
parent_model_title: {
eq: args.args.tn
}
]
},
fields: [
'id',
'alias',
'meta',
'parent_model_title',
'query_params',
'show_as',
'title',
'type'
},
{
title: {
eq: args.args.tn
}
}
]
// todo: handle sort
},
fields: [
'id',
'alias',
'meta',
'parent_model_title',
'query_params',
'show_as',
'title',
'type',
'view_order'
],
orderBy: {
view_order: 'asc'
}
)
).sort(
(a, b) =>
+(a.type === 'vtable' ? a.id : -Infinity) -
+(b.type === 'vtable' ? b.id : -Infinity)
// todo: handle sort
}
);
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> {

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> {
return this.xcMeta.metaPaginatedList(
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;
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 */
const relations = await this.relationsSyncAndGet();
@ -508,6 +520,8 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
this.dbAlias,
'nc_models',
{
order: ++order,
view_order: 1,
title: table.tn,
alias: meta._tn,
meta: JSON.stringify(meta),

Loading…
Cancel
Save