Browse Source

feat: order and ui control for bases

Signed-off-by: mertmit <mertmit99@gmail.com>
pull/3573/head
mertmit 2 years ago
parent
commit
600d18de22
  1. 2
      packages/nc-gui/components.d.ts
  2. 4
      packages/nc-gui/components/dashboard/TreeView.vue
  3. 65
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  4. 4
      packages/nc-gui/composables/useProject.ts
  5. 2
      packages/nocodb-sdk/src/lib/Api.ts
  6. 8
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  7. 10
      packages/nocodb/src/lib/migrations/v2/nc_023_multiple_source.ts
  8. 137
      packages/nocodb/src/lib/models/Base.ts
  9. 6
      scripts/sdk/swagger.json

2
packages/nc-gui/components.d.ts vendored

@ -118,10 +118,12 @@ declare module '@vue/runtime-core' {
MdiApi: typeof import('~icons/mdi/api')['default'] MdiApi: typeof import('~icons/mdi/api')['default']
MdiAppleKeyboardShift: typeof import('~icons/mdi/apple-keyboard-shift')['default'] MdiAppleKeyboardShift: typeof import('~icons/mdi/apple-keyboard-shift')['default']
MdiArrowCollapse: typeof import('~icons/mdi/arrow-collapse')['default'] MdiArrowCollapse: typeof import('~icons/mdi/arrow-collapse')['default']
MdiArrowDownBox: typeof import('~icons/mdi/arrow-down-box')['default']
MdiArrowDownDropCircle: typeof import('~icons/mdi/arrow-down-drop-circle')['default'] MdiArrowDownDropCircle: typeof import('~icons/mdi/arrow-down-drop-circle')['default']
MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default'] MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default'] MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default'] MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default']
MdiArrowUpBox: typeof import('~icons/mdi/arrow-up-box')['default']
MdiAt: typeof import('~icons/mdi/at')['default'] MdiAt: typeof import('~icons/mdi/at')['default']
MdiBackburger: typeof import('~icons/mdi/backburger')['default'] MdiBackburger: typeof import('~icons/mdi/backburger')['default']
MdiBookOpenOutline: typeof import('~icons/mdi/book-open-outline')['default'] MdiBookOpenOutline: typeof import('~icons/mdi/book-open-outline')['default']

4
packages/nc-gui/components/dashboard/TreeView.vue

@ -67,6 +67,8 @@ const filteredTables = $computed(() =>
tables.value?.filter((table) => !filterQuery || table.title.toLowerCase().includes(filterQuery.toLowerCase())), tables.value?.filter((table) => !filterQuery || table.title.toLowerCase().includes(filterQuery.toLowerCase())),
) )
const filteredBases = $computed(() => bases.value.filter((base) => base.enabled))
let sortable: Sortable let sortable: Sortable
// todo: replace with vuedraggable // todo: replace with vuedraggable
@ -399,7 +401,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<div v-if="tables.length" class="transition-height duration-200 overflow-hidden"> <div v-if="tables.length" class="transition-height duration-200 overflow-hidden">
<div :key="key" ref="menuRef" class="border-none sortable-list"> <div :key="key" ref="menuRef" class="border-none sortable-list">
<div v-for="[index, base] of Object.entries(bases)" :key="`${base.id}-index`"> <div v-for="[index, base] of Object.entries(filteredBases)" :key="`${base.id}-index`">
<div v-if="index === '0'"> <div v-if="index === '0'">
<div <div
v-for="table of tables.filter((table) => table.base_id === base.id)" v-for="table of tables.filter((table) => table.base_id === base.id)"

65
packages/nc-gui/components/dashboard/settings/DataSources.vue

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Empty } from 'ant-design-vue' import { Empty } from 'ant-design-vue'
import type { BaseType } from 'nocodb-sdk' import type { BaseType } from 'nocodb-sdk'
import type { CheckboxChangeEvent } from 'ant-design-vue/lib/checkbox/interface'
import CreateBase from './data-sources/CreateBase.vue' import CreateBase from './data-sources/CreateBase.vue'
import EditBase from './data-sources/EditBase.vue' import EditBase from './data-sources/EditBase.vue'
import Metadata from './Metadata.vue' import Metadata from './Metadata.vue'
@ -92,6 +93,44 @@ const deleteBase = (base: BaseType) => {
}) })
} }
const toggleBase = async (base: BaseType, e: CheckboxChangeEvent) => {
try {
base.enabled = e.target.checked
await $api.base.update(base.project_id as string, base.id as string, {
id: base.id,
project_id: base.project_id,
enabled: base.enabled,
})
await loadProject()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const moveBase = async (base: BaseType, direction: 'up' | 'down') => {
try {
if (!base.order) {
// empty update call to reorder bases (migration)
await $api.base.update(base.project_id as string, base.id as string, {
id: base.id,
project_id: base.project_id,
})
message.info('Bases are migrated. Please try again.')
} else {
direction === 'up' ? base.order-- : base.order++
await $api.base.update(base.project_id as string, base.id as string, {
id: base.id,
project_id: base.project_id,
order: base.order,
})
}
await loadProject()
await loadBases()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
onMounted(async () => { onMounted(async () => {
if (sources.length === 0) { if (sources.length === 0) {
await loadBases() await loadBases()
@ -151,6 +190,32 @@ watch(
bordered bordered
> >
<template #emptyText> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> </template> <template #emptyText> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> </template>
<a-table-column key="enabled" data-index="enabled" :width="30">
<template #default="{ record }">
<div v-if="!record.is_meta" class="flex items-center gap-1">
<a-tooltip>
<template #title>Show in UI</template>
<a-checkbox :checked="record.enabled ? true : false" @change="toggleBase(record, $event)"></a-checkbox>
</a-tooltip>
</div>
</template>
</a-table-column>
<a-table-column key="order" width="60px">
<template #default="{ record, index }">
<div class="flex items-center gap-1 text-gray-600 font-light">
<MdiArrowUpBox
v-if="!record.is_meta && index !== 1"
class="text-lg group-hover:text-accent"
@click="moveBase(record, 'up')"
/>
<MdiArrowDownBox
v-if="!record.is_meta && index !== sources.length - 1"
class="text-lg group-hover:text-accent"
@click="moveBase(record, 'down')"
/>
</div>
</template>
</a-table-column>
<a-table-column key="alias" title="Name" data-index="alias"> <a-table-column key="alias" title="Name" data-index="alias">
<template #default="{ text, record }"> <template #default="{ text, record }">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">

4
packages/nc-gui/composables/useProject.ts

@ -98,7 +98,9 @@ const [setup, use] = useInjectionState(() => {
includeM2M: includeM2M.value, includeM2M: includeM2M.value,
}) })
if (tablesResponse.list) tables.value = tablesResponse.list if (tablesResponse.list) {
tables.value = tablesResponse.list.filter((table) => bases.value.find((base) => base.id === table.base_id)?.enabled)
}
} }
} }

2
packages/nocodb-sdk/src/lib/Api.ts

@ -84,6 +84,8 @@ export interface BaseType {
updated_at?: any; updated_at?: any;
inflection_column?: string; inflection_column?: string;
inflection_table?: string; inflection_table?: string;
order?: number;
enabled?: boolean;
} }
export interface BaseReqType { export interface BaseReqType {

8
packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts

@ -10,7 +10,7 @@ import * as nc_019_add_meta_in_meta_tables from './v2/nc_019_add_meta_in_meta_ta
import * as nc_020_kanban_view from './v2/nc_020_kanban_view'; import * as nc_020_kanban_view from './v2/nc_020_kanban_view';
import * as nc_021_add_fields_in_token from './v2/nc_021_add_fields_in_token'; import * as nc_021_add_fields_in_token from './v2/nc_021_add_fields_in_token';
import * as nc_022_qr_code_column_type from './v2/nc_022_qr_code_column_type'; import * as nc_022_qr_code_column_type from './v2/nc_022_qr_code_column_type';
import * as nc_023_add_base_id_in_sync_source from './v2/nc_023_add_base_id_in_sync_source'; import * as nc_023_multiple_source from './v2/nc_023_multiple_source';
// Create a custom migration source class // Create a custom migration source class
export default class XcMigrationSourcev2 { export default class XcMigrationSourcev2 {
@ -32,7 +32,7 @@ export default class XcMigrationSourcev2 {
'nc_020_kanban_view', 'nc_020_kanban_view',
'nc_021_add_fields_in_token', 'nc_021_add_fields_in_token',
'nc_022_qr_code_column_type', 'nc_022_qr_code_column_type',
'nc_023_add_base_id_in_sync_source' 'nc_023_multiple_source'
]); ]);
} }
@ -66,8 +66,8 @@ export default class XcMigrationSourcev2 {
return nc_021_add_fields_in_token; return nc_021_add_fields_in_token;
case 'nc_022_qr_code_column_type': case 'nc_022_qr_code_column_type':
return nc_022_qr_code_column_type; return nc_022_qr_code_column_type;
case 'nc_023_add_base_id_in_sync_source': case 'nc_023_multiple_source':
return nc_023_add_base_id_in_sync_source; return nc_023_multiple_source;
} }
} }
} }

10
packages/nocodb/src/lib/migrations/v2/nc_023_add_base_id_in_sync_source.ts → packages/nocodb/src/lib/migrations/v2/nc_023_multiple_source.ts

@ -6,12 +6,22 @@ const up = async (knex: Knex) => {
table.string('base_id', 20); table.string('base_id', 20);
table.foreign('base_id').references(`${MetaTable.BASES}.id`); table.foreign('base_id').references(`${MetaTable.BASES}.id`);
}); });
await knex.schema.alterTable(MetaTable.BASES, (table) => {
table.boolean('enabled').defaultTo(true);
table.float('order');
});
}; };
const down = async (knex) => { const down = async (knex) => {
await knex.schema.alterTable(MetaTable.SYNC_SOURCE, (table) => { await knex.schema.alterTable(MetaTable.SYNC_SOURCE, (table) => {
table.dropColumn('base_id'); table.dropColumn('base_id');
}); });
await knex.schema.alterTable(MetaTable.BASES, (table) => {
table.dropColumn('enabled');
table.dropColumn('order');
});
}; };
export { up, down }; export { up, down };

137
packages/nocodb/src/lib/models/Base.ts

@ -25,6 +25,8 @@ export default class Base implements BaseType {
updated_at?: any; updated_at?: any;
inflection_column?: string; inflection_column?: string;
inflection_table?: string; inflection_table?: string;
order?: number;
enabled?: boolean;
constructor(base: Partial<Base>) { constructor(base: Partial<Base>) {
Object.assign(this, base); Object.assign(this, base);
@ -44,6 +46,8 @@ export default class Base implements BaseType {
'updated_at', 'updated_at',
'inflection_column', 'inflection_column',
'inflection_table', 'inflection_table',
'order',
'enabled',
]); ]);
insertObj.config = CryptoJS.AES.encrypt( insertObj.config = CryptoJS.AES.encrypt(
JSON.stringify(base.config), JSON.stringify(base.config),
@ -56,13 +60,19 @@ export default class Base implements BaseType {
MetaTable.BASES, MetaTable.BASES,
insertObj insertObj
); );
await NocoCache.appendToList( await NocoCache.appendToList(
CacheScope.BASE, CacheScope.BASE,
[base.projectId], [base.projectId],
`${CacheScope.BASE}:${id}` `${CacheScope.BASE}:${id}`
); );
// call before reorder to update cache
const returnBase = await this.get(id, ncMeta);
await this.reorderBases(base.projectId);
return this.get(id, ncMeta); return returnBase;
} }
public static async updateBase( public static async updateBase(
@ -81,7 +91,7 @@ export default class Base implements BaseType {
await NocoCache.deepDel( await NocoCache.deepDel(
CacheScope.BASE, CacheScope.BASE,
`${CacheScope.BASE}:${baseId}`, `${CacheScope.BASE}:${baseId}`,
CacheDelDirection.PARENT_TO_CHILD CacheDelDirection.CHILD_TO_PARENT
); );
const insertObj = extractProps(base, [ const insertObj = extractProps(base, [
@ -94,37 +104,48 @@ export default class Base implements BaseType {
'updated_at', 'updated_at',
'inflection_column', 'inflection_column',
'inflection_table', 'inflection_table',
'order',
'enabled',
]); ]);
insertObj.config = CryptoJS.AES.encrypt(
JSON.stringify(base.config), if (insertObj.config) {
Noco.getConfig()?.auth?.jwt?.secret insertObj.config = CryptoJS.AES.encrypt(
).toString(); JSON.stringify(base.config),
Noco.getConfig()?.auth?.jwt?.secret
).toString();
}
// type property is undefined even if not provided
if (!insertObj.type) {
insertObj.type = oldBase.type;
}
// add missing (not updated) fields
const finalInsertObj = {
...oldBase,
...insertObj,
};
const { id } = await ncMeta.metaInsert2( const { id } = await ncMeta.metaInsert2(
base.projectId, base.projectId,
null, null,
MetaTable.BASES, MetaTable.BASES,
insertObj finalInsertObj
); );
await NocoCache.appendToList( await NocoCache.appendToList(
CacheScope.BASE, CacheScope.BASE,
[base.projectId], [base.projectId],
`${CacheScope.BASE}:${id}` `${CacheScope.BASE}:${id}`
); );
return this.get(id, ncMeta); // call before reorder to update cache
} const returnBase = await this.get(id, ncMeta);
/* await this.reorderBases(base.projectId, id, ncMeta);
await ncMeta.metaDelete(null, null, MetaTable.COL_LOOKUP, {
fk_column_id: colId, return returnBase;
}); }
await NocoCache.deepDel(
CacheScope.COL_LOOKUP,
`${CacheScope.COL_LOOKUP}:${colId}`,
CacheDelDirection.CHILD_TO_PARENT
);
*/
static async list( static async list(
args: { projectId: string }, args: { projectId: string },
@ -137,10 +158,22 @@ export default class Base implements BaseType {
baseDataList = await ncMeta.metaList2( baseDataList = await ncMeta.metaList2(
args.projectId, args.projectId,
null, null,
MetaTable.BASES MetaTable.BASES,
{
orderBy: {
order: 'asc',
},
}
); );
await NocoCache.setList(CacheScope.BASE, [args.projectId], baseDataList); await NocoCache.setList(CacheScope.BASE, [args.projectId], baseDataList);
} }
baseDataList.sort(
(a, b) =>
(a.order != null ? a.order : Infinity) -
(b.order != null ? b.order : Infinity)
);
return baseDataList?.map((baseData) => { return baseDataList?.map((baseData) => {
return new Base(baseData); return new Base(baseData);
}); });
@ -160,6 +193,70 @@ export default class Base implements BaseType {
return baseData && new Base(baseData); return baseData && new Base(baseData);
} }
static async reorderBases(projectId: string, keepBase?: string, ncMeta = Noco.ncMeta) {
const bases = await this.list({ projectId: projectId }, ncMeta);
// order list for bases
const orders = [];
const takenOrders = bases.map((base) => base.order);
if (keepBase) {
bases.find((base) => {
if (base.id === keepBase) {
orders.push({ id: base.id, order: base.order });
}
});
}
for (const b of bases) {
if (b.id === keepBase) continue;
let tempIndex = b.order;
if (!b.order || orders.find((o) => o.order === tempIndex)) {
tempIndex = 1;
while (takenOrders.includes(tempIndex)) {
tempIndex++;
}
}
// use index as order if order is not set
orders.push({ id: b.id, order: tempIndex });
}
orders.sort((a, b) => a.order - b.order);
// update order for bases
for (const [i, o] of Object.entries(orders)) {
const fnd = bases.find((b) => b.id === o.id);
if (fnd && (!fnd.order || fnd.order != parseInt(i) + 1)) {
await ncMeta.metaDelete(null, null, MetaTable.BASES, {
id: fnd.id,
});
await NocoCache.deepDel(
CacheScope.BASE,
`${CacheScope.BASE}:${fnd.id}`,
CacheDelDirection.CHILD_TO_PARENT
);
fnd.order = parseInt(i) + 1;
const { id } = await ncMeta.metaInsert2(
fnd.project_id,
null,
MetaTable.BASES,
fnd
);
await NocoCache.appendToList(
CacheScope.BASE,
[fnd.project_id],
`${CacheScope.BASE}:${id}`
);
await NocoCache.set(`${CacheScope.BASE}:${id}`, fnd);
}
}
}
public getConnectionConfig(): any { public getConnectionConfig(): any {
if (this.is_meta) { if (this.is_meta) {
const metaConfig = Noco.getConfig()?.meta?.db; const metaConfig = Noco.getConfig()?.meta?.db;

6
scripts/sdk/swagger.json

@ -7262,6 +7262,12 @@
}, },
"inflection_table": { "inflection_table": {
"type": "string" "type": "string"
},
"order": {
"type": "number"
},
"enabled": {
"type": "boolean"
} }
} }
}, },

Loading…
Cancel
Save