Browse Source

Merge pull request #7528 from nocodb/nc-fix/views-reorder

Nc feat: base reorder and fixed views reorder issue
pull/7559/head
Raju Udava 10 months ago committed by GitHub
parent
commit
3753cf7eda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      packages/nc-gui/components/dashboard/TreeView/TableList.vue
  2. 28
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  3. 69
      packages/nc-gui/components/dashboard/TreeView/index.vue
  4. 1
      packages/nc-gui/lib/acl.ts
  5. 61
      packages/nc-gui/store/bases.ts
  6. 33
      packages/nocodb/src/models/Base.ts
  7. 32
      packages/nocodb/src/models/BaseUser.ts
  8. 12
      packages/nocodb/src/schema/swagger-v2.json
  9. 12
      packages/nocodb/src/schema/swagger.json
  10. 5
      packages/nocodb/src/services/bases.service.ts

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

@ -22,6 +22,8 @@ const source = computed(() => base.value?.sources?.[sourceIndex.value])
const { isMobileMode } = useGlobal()
const { isUIAllowed } = useRoles()
const { baseTables } = storeToRefs(useTablesStore())
const tables = computed(() => baseTables.value.get(base.value.id!) ?? [])
@ -114,7 +116,7 @@ const initSortable = (el: Element) => {
}
watchEffect(() => {
if (menuRefs.value) {
if (menuRefs.value && isUIAllowed('viewCreateOrEdit')) {
if (menuRefs.value instanceof HTMLElement) {
initSortable(menuRefs.value)
} else {

28
packages/nc-gui/components/dashboard/TreeView/ViewsList.vue

@ -22,7 +22,15 @@ import {
} from '#imports'
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string }): void
(
event: 'openModal',
data: {
type: ViewTypes
title?: string
copyViewId?: string
groupingFieldColumnId?: string
},
): void
(event: 'deleted'): void
}
@ -119,7 +127,10 @@ async function onSortEnd(evt: SortableEvent, undo = false) {
if (views.value.length < 2) return
const { newIndex = 0, oldIndex = 0 } = evt
let { newIndex = 0, oldIndex = 0 } = evt
newIndex = newIndex - 1
oldIndex = oldIndex - 1
if (newIndex === oldIndex) return
@ -149,7 +160,10 @@ async function onSortEnd(evt: SortableEvent, undo = false) {
})
}
const children = evt.to.children as unknown as HTMLLIElement[]
const children = Array.from(evt.to.children as unknown as HTMLLIElement[])
// remove `Create View` children from list
children.shift()
const previousEl = children[newIndex - 1]
const nextEl = children[newIndex + 1]
@ -196,7 +210,7 @@ const initSortable = (el: HTMLElement) => {
})
}
onMounted(() => menuRef.value && initSortable(menuRef.value.$el))
onMounted(() => menuRef.value && isUIAllowed('viewCreateOrEdit') && initSortable(menuRef.value.$el))
/** Navigate to view by changing url param */
async function changeView(view: ViewType) {
@ -279,7 +293,11 @@ function openDeleteDialog(view: ViewType) {
emits('deleted')
removeFromRecentViews({ viewId: view.id, tableId: view.fk_model_id, baseId: base.value.id })
removeFromRecentViews({
viewId: view.id,
tableId: view.fk_model_id,
baseId: base.value.id,
})
refreshCommandPalette()
if (activeView.value?.id === view.id) {
navigateToTable({

69
packages/nc-gui/components/dashboard/TreeView/index.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import type { TableType } from 'nocodb-sdk'
import ProjectWrapper from './ProjectWrapper.vue'
import {
@ -32,7 +32,7 @@ const route = router.currentRoute
const basesStore = useBases()
const { createProject: _createProject } = basesStore
const { createProject: _createProject, updateProject } = basesStore
const { bases, basesList, activeProjectId } = storeToRefs(basesStore)
@ -188,6 +188,38 @@ const scrollTableNode = () => {
activeTableDom?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
const onMove = async (_event: { moved: { newIndex: number; oldIndex: number; element: NcProject } }) => {
const {
moved: { newIndex = 0, oldIndex = 0, element },
} = _event
if (!element?.id) return
let nextOrder: number
// set new order value based on the new order of the items
if (basesList.value.length - 1 === newIndex) {
// If moving to the end, set nextOrder greater than the maximum order in the list
nextOrder = Math.max(...basesList.value.map((item) => item?.order ?? 0)) + 1
} else if (newIndex === 0) {
// If moving to the beginning, set nextOrder smaller than the minimum order in the list
nextOrder = Math.min(...basesList.value.map((item) => item?.order ?? 0)) / 2
} else {
nextOrder =
(parseFloat(String(basesList.value[newIndex - 1]?.order ?? 0)) +
parseFloat(String(basesList.value[newIndex + 1]?.order ?? 0))) /
2
}
const _nextOrder = !isNaN(Number(nextOrder)) ? nextOrder : oldIndex
await updateProject(element.id, {
order: _nextOrder,
})
$e('a:base:reorder')
}
watch(
() => _activeTable.value?.id,
() => {
@ -224,11 +256,24 @@ watch(
<div class="nc-treeview-container flex flex-col justify-between select-none">
<div v-if="!isSharedBase" class="text-gray-500 font-medium pl-3.5 mb-1">{{ $t('objects.projects') }}</div>
<div mode="inline" class="nc-treeview pb-0.5 flex-grow min-h-50 overflow-x-hidden">
<template v-if="basesList?.length">
<ProjectWrapper v-for="base of basesList" :key="base.id" :base-role="base.project_role" :base="base">
<DashboardTreeViewProjectNode />
</ProjectWrapper>
</template>
<div v-if="basesList?.length">
<Draggable
:model-value="basesList"
:disabled="!isUIAllowed('baseReorder') || basesList?.length < 2"
item-key="id"
handle=".base-title-node"
ghost-class="ghost"
@change="onMove($event)"
>
<template #item="{ element: base }">
<div :key="base.id">
<ProjectWrapper :base-role="base.project_role" :base="base">
<DashboardTreeViewProjectNode />
</ProjectWrapper>
</div>
</template>
</Draggable>
</div>
<WorkspaceEmptyPlaceholder v-else-if="!isWorkspaceLoading" />
</div>
@ -236,4 +281,12 @@ watch(
</div>
</template>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.ghost,
.ghost > * {
@apply pointer-events-none;
}
.ghost {
@apply bg-primary-selected;
}
</style>

1
packages/nc-gui/lib/acl.ts

@ -30,6 +30,7 @@ const rolePermissions = {
tableRename: true,
tableDelete: true,
viewCreateOrEdit: true,
baseReorder: true,
},
},
[OrgUserRoles.VIEWER]: {

61
packages/nc-gui/store/bases.ts

@ -9,9 +9,15 @@ import type { NcProject, User } from '#imports'
export const useBases = defineStore('basesStore', () => {
const { $api } = useNuxtApp()
const { isUIAllowed } = useRoles()
const bases = ref<Map<string, NcProject>>(new Map())
const basesList = computed<NcProject[]>(() => Array.from(bases.value.values()).sort((a, b) => a.updated_at - b.updated_at))
const basesList = computed<NcProject[]>(() =>
Array.from(bases.value.values()).sort(
(a, b) => (a.order != null ? a.order : Infinity) - (b.order != null ? b.order : Infinity),
),
)
const basesUser = ref<Map<string, User[]>>(new Map())
const router = useRouter()
@ -146,6 +152,8 @@ export const useBases = defineStore('basesStore', () => {
})
return acc
}, new Map())
await updateIfBaseOrderIsNullOrDuplicate()
} catch (e) {
console.error(e)
message.error(e.message)
@ -214,8 +222,17 @@ export const useBases = defineStore('basesStore', () => {
}
const updateProject = async (baseId: string, baseUpdatePayload: BaseType) => {
const existingProject = bases.value.get(baseId) ?? ({} as any)
const base = {
...existingProject,
...baseUpdatePayload,
}
bases.value.set(baseId, base)
await api.base.update(baseId, baseUpdatePayload)
// todo: update base in store
await loadProject(baseId, true)
}
@ -287,6 +304,46 @@ export const useBases = defineStore('basesStore', () => {
await navigateTo(`/nc/${baseId}`)
}
async function updateIfBaseOrderIsNullOrDuplicate() {
if (!isUIAllowed('baseReorder')) return
const basesArray = Array.from(bases.value.values())
const baseOrderSet = new Set()
let hasNullOrDuplicates = false
// Check if basesArray contains null or duplicate order
for (const base of basesArray) {
if (base.order === null || baseOrderSet.has(base.order)) {
hasNullOrDuplicates = true
break
}
baseOrderSet.add(base.order)
}
if (!hasNullOrDuplicates) return
// update the local state and return updated bases payload
const updatedBasesOrder = basesArray.map((base, i) => {
bases.value.set(base.id!, { ...base, order: i + 1 })
return {
id: base.id,
order: i + 1,
}
})
try {
await Promise.all(
updatedBasesOrder.map(async (base) => {
await api.base.update(base.id!, { order: base.order })
}),
)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
onMounted(() => {
if (!activeProjectId.value) return
if (isProjectPopulated(activeProjectId.value)) return

33
packages/nocodb/src/models/Base.ts

@ -54,8 +54,14 @@ export default class Base implements BaseType {
'status',
'meta',
'color',
'order',
]);
if (!insertObj.order) {
// get order value
insertObj.order = await ncMeta.metaGetNextOrder(MetaTable.PROJECT, {});
}
const { id: baseId } = await ncMeta.metaInsert2(
null,
null,
@ -110,15 +116,31 @@ export default class Base implements BaseType {
},
],
},
orderBy: {
order: 'asc',
},
});
await NocoCache.setList(CacheScope.PROJECT, [], baseList);
}
baseList = baseList.filter(
(p) => p.deleted === 0 || p.deleted === false || p.deleted === null,
);
const castedProjectList = baseList.map((m) => this.castType(m));
await Promise.all(castedProjectList.map((base) => base.getSources(ncMeta)));
const promises = [];
const castedProjectList = baseList
.filter(
(p) => p.deleted === 0 || p.deleted === false || p.deleted === null,
)
.sort(
(a, b) =>
(a.order != null ? a.order : Infinity) -
(b.order != null ? b.order : Infinity),
)
.map((p) => {
const base = this.castType(p);
promises.push(base.getSources(ncMeta));
return base;
});
await Promise.all(promises);
return castedProjectList;
}
@ -253,6 +275,7 @@ export default class Base implements BaseType {
'password',
'roles',
]);
// get existing cache
const key = `${CacheScope.PROJECT}:${baseId}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);

32
packages/nocodb/src/models/BaseUser.ts

@ -352,20 +352,30 @@ export default class BaseUser {
qb.whereNot(`${MetaTable.PROJECT}.deleted`, true);
const baseList = await qb;
if (baseList?.length) {
// parse meta
for (const base of baseList) {
base.meta = parseMetaProp(base);
}
}
const castedProjectList = baseList
.filter((p) => !params?.type || p.type === params.type)
.map((m) => Base.castType(m));
if (baseList && baseList?.length) {
const promises = [];
const castedProjectList = baseList
.filter((p) => !params?.type || p.type === params.type)
.sort(
(a, b) =>
(a.order != null ? a.order : Infinity) -
(b.order != null ? b.order : Infinity),
)
.map((p) => {
const base = Base.castType(p);
base.meta = parseMetaProp(base);
promises.push(base.getSources(ncMeta));
return base;
});
await Promise.all(castedProjectList.map((base) => base.getSources(ncMeta)));
await Promise.all(promises);
return castedProjectList;
return castedProjectList;
} else {
return [];
}
}
static async updateOrInsert(

12
packages/nocodb/src/schema/swagger-v2.json

@ -1163,7 +1163,8 @@
"value": {
"color": "#24716E",
"meta": null,
"title": "My Base"
"title": "My Base",
"order": 1
}
}
}
@ -16011,7 +16012,8 @@
{
"color": "#24716E",
"meta": null,
"title": "My Base"
"title": "My Base",
"order": 1
}
],
"title": "Base Update Request Model",
@ -16048,6 +16050,12 @@
"items": {
"type": "string"
}
},
"order": {
"type": "number",
"description": "The order of the list of projects.",
"example": 1,
"minimum": 0
}
}
},

12
packages/nocodb/src/schema/swagger.json

@ -2785,7 +2785,8 @@
"value": {
"color": "#24716E",
"meta": null,
"title": "My Base"
"title": "My Base",
"order": 1
}
}
}
@ -21266,7 +21267,8 @@
{
"color": "#24716E",
"meta": null,
"title": "My Base"
"title": "My Base",
"order": 1
}
],
"title": "Base Update Request Model",
@ -21303,6 +21305,12 @@
"items": {
"type": "string"
}
},
"order": {
"type": "number",
"description": "The order of the list of projects.",
"example": 1,
"minimum": 0
}
}
},

5
packages/nocodb/src/services/bases.service.ts

@ -78,9 +78,14 @@ export class BasesService {
'meta',
'color',
'status',
'order',
]);
await this.validateProjectTitle(data, base);
if (data?.order !== undefined) {
data.order = !isNaN(+data.order) ? +data.order : 0;
}
const result = await Base.update(param.baseId, data);
this.appHooksService.emit(AppEvents.PROJECT_UPDATE, {

Loading…
Cancel
Save