Browse Source

Merge pull request #6573 from nocodb/nc-feat/grid-opt

feat: grid optimizations
pull/6578/head
mertmit 1 year ago committed by GitHub
parent
commit
c21b190f86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      packages/nc-gui/components/nc/Pagination.vue
  2. 7
      packages/nc-gui/components/smartsheet/Pagination.vue
  3. 303
      packages/nc-gui/components/smartsheet/grid/Table.vue
  4. 7
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  5. 3
      packages/nc-gui/components/virtual-cell/HasMany.vue
  6. 7
      packages/nc-gui/components/virtual-cell/Links.vue
  7. 3
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  8. 26
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  9. 33
      packages/nc-gui/composables/useViewData.ts
  10. 2
      tests/playwright/pages/Dashboard/Grid/index.ts

14
packages/nc-gui/components/nc/Pagination.vue

@ -23,8 +23,10 @@ const { isMobileMode } = useGlobal()
const mode = computed(() => props.mode || (isMobileMode.value ? 'simple' : 'full'))
const changePage = ({ increase }: { increase: boolean }) => {
if (increase && current.value < totalPages.value) {
const changePage = ({ increase, set }: { increase?: boolean; set?: number }) => {
if (set) {
current.value = set
} else if (increase && current.value < totalPages.value) {
current.value = current.value + 1
} else if (current.value > 0) {
current.value = current.value - 1
@ -38,6 +40,10 @@ const goToLastPage = () => {
const goToFirstPage = () => {
current.value = 1
}
const pagesList = computed(() => {
return Array.from({ length: totalPages.value }, (_, i) => i + 1)
})
</script>
<template>
@ -65,7 +71,9 @@ const goToFirstPage = () => {
<GeneralIcon icon="arrowLeft" />
</NcButton>
<div class="text-gray-600">
<span class="active"> {{ current }} </span>
<a-select v-model:value="current" class="!mr-[2px]" virtual>
<a-select-option v-for="p of pagesList" :key="`p-${p}`" @click="changePage({ set: p })">{{ p }}</a-select-option>
</a-select>
<span class="mx-1"> {{ mode !== 'full' ? '/' : 'of' }} </span>
<span class="total">
{{ totalPages }}

7
packages/nc-gui/components/smartsheet/Pagination.vue

@ -1,4 +1,5 @@
<script setup lang="ts">
import axios from 'axios'
import type { PaginatedType } from 'nocodb-sdk'
import { IsGroupByInj, computed, iconMap, inject, isRtlLang, useI18n } from '#imports'
import type { Language } from '#imports'
@ -47,9 +48,11 @@ const page = computed({
isViewDataLoading.value = true
try {
await changePage?.(p)
isViewDataLoading.value = false
} catch (e) {
console.error(e)
} finally {
if (axios.isCancel(e)) {
return
}
isViewDataLoading.value = false
}
},

303
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -195,8 +195,6 @@ const isAddNewRecordGridMode = ref(true)
const switchingTab = ref(false)
const showLoading = ref(true)
const isView = false
const columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null)
@ -973,7 +971,9 @@ eventBus.on(async (event, payload) => {
}
})
async function reloadViewDataHandler(shouldShowLoading: boolean | void) {
async function reloadViewDataHandler(_shouldShowLoading: boolean | void) {
isViewDataLoading.value = true
if (predictedNextColumn.value?.length) {
const fieldsAvailable = meta.value?.columns?.map((c) => c.title)
predictedNextColumn.value = predictedNextColumn.value.filter((c) => !fieldsAvailable?.includes(c.title))
@ -981,11 +981,9 @@ async function reloadViewDataHandler(shouldShowLoading: boolean | void) {
// save any unsaved data before reload
await saveOrUpdateRecords()
// set value if spinner should be hidden
showLoading.value = !!shouldShowLoading
await loadData?.()
// reset to default (showing spinner on load)
showLoading.value = true
isViewDataLoading.value = false
}
useEventListener(scrollWrapper, 'scroll', () => {
@ -1147,11 +1145,32 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
_handleCellClick(event, row, col)
}
const loaderText = computed(() => {
if (isViewDataLoading.value) {
if (paginationDataRef.value?.totalRows && paginationDataRef.value?.pageSize) {
return `Loading page<br/>${paginationDataRef.value.page} of ${Math.ceil(
paginationDataRef.value?.totalRows / paginationDataRef.value?.pageSize,
)}`
} else {
return t('general.loading')
}
}
})
</script>
<template>
<div class="flex flex-col" :class="`${headerOnly !== true ? 'h-full w-full' : ''}`">
<div ref="gridWrapper" class="nc-grid-wrapper min-h-0 flex-1 relative" :class="gridWrapperClass">
<div
v-show="showSkeleton && !isPaginationLoading"
class="flex items-center justify-center absolute l-0 t-0 w-full h-full z-10 pb-10"
>
<div class="flex flex-col justify-center gap-2">
<GeneralLoader size="xlarge" />
<span class="text-center" v-html="loaderText"></span>
</div>
</div>
<NcDropdown
v-model:visible="contextMenu"
:trigger="isSqlView ? [] : ['contextmenu']"
@ -1168,7 +1187,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
@contextmenu="showContextMenu"
>
<thead v-show="hideHeader !== true" ref="tableHeadEl">
<tr v-if="showSkeleton">
<tr v-if="showSkeleton && isPaginationLoading">
<td
v-for="(col, colIndex) of dummyDataForLoading"
:key="colIndex"
@ -1184,7 +1203,7 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
/>
</td>
</tr>
<tr v-else class="nc-grid-header">
<tr v-show="!isPaginationLoading" class="nc-grid-header">
<th class="w-[85px] min-w-[85px]" data-testid="grid-id-column" @dblclick="() => {}">
<div class="w-full h-full flex pl-5 pr-1 items-center" data-testid="nc-check-all">
<template v-if="!readOnly">
@ -1341,142 +1360,146 @@ const handleCellClick = (event: MouseEvent, row: number, col: number) => {
></td>
</tr>
</template>
<template v-else>
<LazySmartsheetRow v-for="(row, rowIndex) of dataRef" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr
class="nc-grid-row !xs:h-14"
:style="{ height: rowHeight ? `${rowHeight * 1.8}rem` : `1.8rem` }"
:data-testid="`grid-row-${rowIndex}`"
<LazySmartsheetRow
v-for="(row, rowIndex) of dataRef"
v-show="!showSkeleton"
ref="rowRefs"
:key="rowIndex"
:row="row"
>
<template #default="{ state }">
<tr
class="nc-grid-row !xs:h-14"
:style="{ height: rowHeight ? `${rowHeight * 1.8}rem` : `1.8rem` }"
:data-testid="`grid-row-${rowIndex}`"
>
<td
key="row-index"
class="caption nc-grid-cell pl-5 pr-1"
:data-testid="`cell-Id-${rowIndex}`"
@contextmenu="contextMenuTarget = null"
>
<td
key="row-index"
class="caption nc-grid-cell pl-5 pr-1"
:data-testid="`cell-Id-${rowIndex}`"
@contextmenu="contextMenuTarget = null"
>
<div class="items-center flex gap-1 min-w-[60px]">
<div
v-if="!readOnly || !isLocked || isMobileMode"
class="nc-row-no sm:min-w-4 text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
>
{{ ((paginationDataRef?.page ?? 1) - 1) * (paginationDataRef?.pageSize ?? 25) + rowIndex + 1 }}
</div>
<div
v-if="!readOnly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
</div>
<span class="flex-1" />
<div
v-if="isUIAllowed('expandedForm')"
class="nc-expand"
:data-testid="`nc-expand-${rowIndex}`"
:class="{ 'nc-comment': row.rowMeta?.commentCount }"
>
<a-spin
v-if="row.rowMeta.saving"
class="!flex items-center"
:data-testid="`row-save-spinner-${rowIndex}`"
/>
<template v-else-if="!isLocked">
<span
v-if="row.rowMeta?.commentCount && expandForm"
v-e="['c:expanded-form:open']"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }"
@click="expandAndLooseFocus(row, state)"
>
{{ row.rowMeta.commentCount }}
</span>
<div
v-else
class="cursor-pointer flex items-center border-1 border-gray-100 active:ring rounded p-1 hover:(bg-gray-50)"
>
<component
:is="iconMap.expand"
v-if="expandForm"
v-e="['c:row-expand:open']"
class="select-none transform hover:(text-black scale-120) nc-row-expand"
@click="expandAndLooseFocus(row, state)"
/>
</div>
</template>
</div>
<div class="items-center flex gap-1 min-w-[60px]">
<div
v-if="!readOnly || !isLocked || isMobileMode"
class="nc-row-no sm:min-w-4 text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
>
{{ ((paginationDataRef?.page ?? 1) - 1) * (paginationDataRef?.pageSize ?? 25) + rowIndex + 1 }}
</div>
</td>
<SmartsheetTableDataCell
v-for="(columnObj, colIndex) of fields"
:key="columnObj.id"
class="cell relative nc-grid-cell"
:class="{
'cursor-pointer': hasEditPermission,
'active': hasEditPermission && isCellSelected(rowIndex, colIndex),
'active-cell':
hasEditPermission &&
((activeCell.row === rowIndex && activeCell.col === colIndex) ||
(selectedRange._start?.row === rowIndex && selectedRange._start?.col === colIndex)),
'last-cell':
rowIndex === (isNaN(selectedRange.end.row) ? activeCell.row : selectedRange.end.row) &&
colIndex === (isNaN(selectedRange.end.col) ? activeCell.col : selectedRange.end.col),
'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row) && !isPublicView,
'align-middle': !rowHeight || rowHeight === 1,
'align-top': rowHeight && rowHeight !== 1,
'filling': isCellInFillRange(rowIndex, colIndex),
'readonly':
(isLookup(columnObj) || isRollup(columnObj) || isFormula(columnObj)) &&
hasEditPermission &&
isCellSelected(rowIndex, colIndex),
}"
:data-testid="`cell-${columnObj.title}-${rowIndex}`"
:data-key="`data-key-${rowIndex}-${columnObj.id}`"
:data-col="columnObj.id"
:data-title="columnObj.title"
:data-row-index="rowIndex"
:data-col-index="colIndex"
@mousedown="handleMouseDown($event, rowIndex, colIndex)"
@mouseover="handleMouseOver($event, rowIndex, colIndex)"
@click="handleCellClick($event, rowIndex, colIndex)"
@dblclick="makeEditable(row, columnObj)"
@contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })"
>
<div v-if="!switchingTab" class="w-full h-full">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(columnObj) && columnObj.title"
v-model="row.row[columnObj.title]"
:column="columnObj"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
:row="row"
:read-only="readOnly"
@navigate="onNavigate"
@save="updateOrSaveRow?.(row, '', state)"
/>
<div
v-if="!readOnly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
</div>
<span class="flex-1" />
<LazySmartsheetCell
v-else-if="columnObj.title"
v-model="row.row[columnObj.title]"
:column="columnObj"
:edit-enabled="
!!hasEditPermission && !!editEnabled && activeCell.col === colIndex && activeCell.row === rowIndex
"
:row-index="rowIndex"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
:read-only="readOnly"
@update:edit-enabled="editEnabled = $event"
@save="updateOrSaveRow?.(row, columnObj.title, state)"
@navigate="onNavigate"
@cancel="editEnabled = false"
<div
v-if="isUIAllowed('expandedForm')"
class="nc-expand"
:data-testid="`nc-expand-${rowIndex}`"
:class="{ 'nc-comment': row.rowMeta?.commentCount }"
>
<a-spin
v-if="row.rowMeta.saving"
class="!flex items-center"
:data-testid="`row-save-spinner-${rowIndex}`"
/>
<template v-else-if="!isLocked">
<span
v-if="row.rowMeta?.commentCount && expandForm"
v-e="['c:expanded-form:open']"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }"
@click="expandAndLooseFocus(row, state)"
>
{{ row.rowMeta.commentCount }}
</span>
<div
v-else
class="cursor-pointer flex items-center border-1 border-gray-100 active:ring rounded p-1 hover:(bg-gray-50)"
>
<component
:is="iconMap.expand"
v-if="expandForm"
v-e="['c:row-expand:open']"
class="select-none transform hover:(text-black scale-120) nc-row-expand"
@click="expandAndLooseFocus(row, state)"
/>
</div>
</template>
</div>
</SmartsheetTableDataCell>
</tr>
</template>
</LazySmartsheetRow>
</template>
</div>
</td>
<SmartsheetTableDataCell
v-for="(columnObj, colIndex) of fields"
:key="columnObj.id"
class="cell relative nc-grid-cell"
:class="{
'cursor-pointer': hasEditPermission,
'active': hasEditPermission && isCellSelected(rowIndex, colIndex),
'active-cell':
hasEditPermission &&
((activeCell.row === rowIndex && activeCell.col === colIndex) ||
(selectedRange._start?.row === rowIndex && selectedRange._start?.col === colIndex)),
'last-cell':
rowIndex === (isNaN(selectedRange.end.row) ? activeCell.row : selectedRange.end.row) &&
colIndex === (isNaN(selectedRange.end.col) ? activeCell.col : selectedRange.end.col),
'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row) && !isPublicView,
'align-middle': !rowHeight || rowHeight === 1,
'align-top': rowHeight && rowHeight !== 1,
'filling': isCellInFillRange(rowIndex, colIndex),
'readonly':
(isLookup(columnObj) || isRollup(columnObj) || isFormula(columnObj)) &&
hasEditPermission &&
isCellSelected(rowIndex, colIndex),
}"
:data-testid="`cell-${columnObj.title}-${rowIndex}`"
:data-key="`data-key-${rowIndex}-${columnObj.id}`"
:data-col="columnObj.id"
:data-title="columnObj.title"
:data-row-index="rowIndex"
:data-col-index="colIndex"
@mousedown="handleMouseDown($event, rowIndex, colIndex)"
@mouseover="handleMouseOver($event, rowIndex, colIndex)"
@click="handleCellClick($event, rowIndex, colIndex)"
@dblclick="makeEditable(row, columnObj)"
@contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })"
>
<div v-if="!switchingTab" class="w-full h-full">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(columnObj) && columnObj.title"
v-model="row.row[columnObj.title]"
:column="columnObj"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
:row="row"
:read-only="readOnly"
@navigate="onNavigate"
@save="updateOrSaveRow?.(row, '', state)"
/>
<LazySmartsheetCell
v-else-if="columnObj.title"
v-model="row.row[columnObj.title]"
:column="columnObj"
:edit-enabled="
!!hasEditPermission && !!editEnabled && activeCell.col === colIndex && activeCell.row === rowIndex
"
:row-index="rowIndex"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
:read-only="readOnly"
@update:edit-enabled="editEnabled = $event"
@save="updateOrSaveRow?.(row, columnObj.title, state)"
@navigate="onNavigate"
@cancel="editEnabled = false"
/>
</div>
</SmartsheetTableDataCell>
</tr>
</template>
</LazySmartsheetRow>
<tr
v-if="isAddingEmptyRowAllowed && !isGroupBy"

7
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -113,7 +113,12 @@ const belongsToColumn = computed(
/>
</div>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="belongsToColumn" @attach-record="listItemsDlg = true" />
<LazyVirtualCellComponentsListItems
v-if="listItemsDlg"
v-model="listItemsDlg"
:column="belongsToColumn"
@attach-record="listItemsDlg = true"
/>
</div>
</template>

3
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -136,9 +136,10 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
/>
</div>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="hasManyColumn" />
<LazyVirtualCellComponentsListItems v-if="listItemsDlg || childListDlg" v-model="listItemsDlg" :column="hasManyColumn" />
<LazyVirtualCellComponentsListChildItems
v-if="listItemsDlg || childListDlg"
v-model="childListDlg"
:cell-value="localCellValue"
:column="hasManyColumn"

7
packages/nc-gui/components/virtual-cell/Links.vue

@ -117,9 +117,14 @@ const localCellValue = computed<any[]>(() => {
/>
</div>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="relatedTableDisplayColumn" />
<LazyVirtualCellComponentsListItems
v-if="listItemsDlg || childListDlg"
v-model="listItemsDlg"
:column="relatedTableDisplayColumn"
/>
<LazyVirtualCellComponentsListChildItems
v-if="listItemsDlg || childListDlg"
v-model="childListDlg"
:column="relatedTableDisplayColumn"
:cell-value="localCellValue"

3
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -138,9 +138,10 @@ const m2mColumn = computed(
/>
</div>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="m2mColumn" />
<LazyVirtualCellComponentsListItems v-if="listItemsDlg || childListDlg" v-model="listItemsDlg" :column="m2mColumn" />
<LazyVirtualCellComponentsListChildItems
v-if="listItemsDlg || childListDlg"
v-model="childListDlg"
:cell-value="localCellValue"
:column="m2mColumn"

26
packages/nc-gui/components/virtual-cell/components/ListItems.vue

@ -83,17 +83,23 @@ const unlinkRow = async (row: Record<string, any>, id: number) => {
}
/** reload list on modal open */
watch(vModel, (nextVal, prevVal) => {
if (nextVal && !prevVal) {
/** reset query and limit */
childrenExcludedListPagination.query = ''
childrenExcludedListPagination.page = 1
if (!isForm.value) {
loadChildrenList()
watch(
vModel,
(nextVal, prevVal) => {
if (nextVal && !prevVal) {
/** reset query and limit */
childrenExcludedListPagination.query = ''
childrenExcludedListPagination.page = 1
if (!isForm.value) {
loadChildrenList()
}
loadChildrenExcludedList(rowState.value)
}
loadChildrenExcludedList(rowState.value)
}
})
},
{
immediate: true,
},
)
const expandedFormDlg = ref(false)

33
packages/nc-gui/composables/useViewData.ts

@ -1,4 +1,5 @@
import { ViewTypes } from 'nocodb-sdk'
import axios from 'axios'
import type { Api, ColumnType, FormColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import {
@ -166,16 +167,34 @@ export function useViewData(
}
}
const controller = ref()
async function loadData(params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) {
if ((!base?.value?.id || !metaId.value || !viewMeta.value?.id) && !isPublic.value) return
if (controller.value) {
controller.value.cancel()
}
const CancelToken = axios.CancelToken
controller.value = CancelToken.source()
const response = !isPublic.value
? await api.dbViewRow.list('noco', base.value.id!, metaId.value!, viewMeta.value!.id!, {
...queryParams.value,
...params,
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
where: where?.value,
} as any)
? await api.dbViewRow.list(
'noco',
base.value.id!,
metaId.value!,
viewMeta.value!.id!,
{
...queryParams.value,
...params,
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
where: where?.value,
} as any,
{ cancelToken: controller.value.token },
)
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value })
formattedData.value = formatData(response.list)

2
tests/playwright/pages/Dashboard/Grid/index.ts

@ -327,7 +327,7 @@ export class GridPage extends BasePage {
}
async verifyActivePage({ pageNumber }: { pageNumber: string }) {
await expect(this.get().locator(`.nc-pagination .active`)).toHaveText(pageNumber);
await expect(this.get().locator(`.nc-pagination .ant-select-selection-item`)).toHaveText(pageNumber);
}
async waitLoading() {

Loading…
Cancel
Save