多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

296 lines
10 KiB

feat: Field aggregation (#8786) * feat: basic ui for aggregation * feat: update aggregation in ui * feat: aggregation api implementation * feat: attachment aggregation.ts * fix: some changes * fix: rebase * feat: aggregation for links, rollup, ltar, formula, lookup * fix: type errors * fix: move from data-alias controller, service to data-table service, controller * chore: inline docs for aggregations * fix: handle edge cases * fix: ui bugs * feat: working ui aggregation * fix: minor issue * fix: rollup and links fix count * fix: handle ID Column * fix: minor fixes * fix: update aggregation on data change * fix: round to 2 decimal places * fix: stddev computation error replace with stddev_pop * fix: use pg age function * feat: new record layout * fix: shared view aggregations * feat: aggregations based on formula result * fix: temp pagination * feat: ncpagination v2 * feat: ncpagination v2 * fix: playwright tests * fix: pending changes * fix: failing tests * feat: mysql2 aggregations * fix: build * fix: record count * fix: cleanup * fix: disable count aggregation * feat: expiremental sqlite3 aggregation * fix: mysql2 median * fix:minor issues * refactor: rename column to column_query fix: remove default aggregations fix: disable aggregation for specific dbtype and Foreign Key * fix: remove unwanted else case * fix: aggregation not loading * fix: rebase * fix: rebase * fix: pagination fixed height * fix: respect locked mode for aggregations * fix: pagination component * fix: pagination component * fix: replace Math.random
5 months ago
<script setup lang="ts">
import axios from 'axios'
import { type PaginatedType, UITypes } from 'nocodb-sdk'
const props = defineProps<{
scrollLeft?: number
paginationData: PaginatedType
changePage: (page: number) => void
}>()
const emits = defineEmits(['update:paginationData'])
const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const isLocked = inject(IsLockedInj, ref(false))
const { changePage } = props
const vPaginationData = useVModel(props, 'paginationData', emits)
const { loadViewAggregate, updateAggregate, getAggregations, visibleFieldsComputed, displayFieldComputed } =
useViewAggregateOrThrow()
const scrollLeft = toRef(props, 'scrollLeft')
const reloadViewDataHook = inject(ReloadViewDataHookInj)
const containerElement = ref()
watch(
scrollLeft,
(value) => {
if (containerElement.value) {
containerElement.value.scrollLeft = value
}
},
{
immediate: true,
},
)
reloadViewDataHook?.on(async () => {
await loadViewAggregate()
})
const count = computed(() => vPaginationData.value?.totalRows ?? Infinity)
const page = computed({
get: () => vPaginationData?.value?.page ?? 1,
set: async (p) => {
isPaginationLoading.value = true
try {
await changePage?.(p)
isPaginationLoading.value = false
} catch (e) {
if (axios.isCancel(e)) {
return
}
isPaginationLoading.value = false
}
},
})
const size = computed({
get: () => vPaginationData.value?.pageSize ?? 25,
set: (size: number) => {
if (vPaginationData.value) {
// if there is no change in size then return
if (vPaginationData.value?.pageSize && vPaginationData.value?.pageSize === size) {
return
}
vPaginationData.value.pageSize = size
if (vPaginationData.value.totalRows && page.value * size < vPaginationData.value.totalRows) {
changePage?.(page.value)
} else {
changePage?.(1)
}
}
},
})
const renderAltOrOptlKey = () => {
return isMac() ? '⌥' : 'ALT'
}
onMounted(() => {
loadViewAggregate()
})
</script>
<template>
<div ref="containerElement" class="bg-gray-50 w-full pr-1 border-t-1 border-gray-200 overflow-x-hidden no-scrollbar flex h-9">
<div class="sticky flex items-center bg-gray-50 left-0">
<NcDropdown
:disabled="[UITypes.SpecificDBType, UITypes.ForeignKey].includes(displayFieldComputed.column?.uidt!) || isLocked"
overlay-class-name="max-h-96 relative scroll-container nc-scrollbar-thin overflow-auto"
>
<div
v-if="displayFieldComputed.field && displayFieldComputed.column?.id"
class="flex items-center overflow-x-hidden hover:bg-gray-100 cursor-pointer text-gray-500 justify-end transition-all transition-linear px-3 py-2"
:style="{
'min-width': displayFieldComputed?.width,
'max-width': displayFieldComputed?.width,
'width': displayFieldComputed?.width,
}"
>
<div class="flex relative justify-between gap-2 w-full">
<div v-if="isViewDataLoading" class="nc-pagination-skeleton flex justify-center item-center min-h-10 min-w-16 w-16">
<a-skeleton :active="true" :title="true" :paragraph="false" class="w-16 max-w-16" />
</div>
<NcTooltip v-else class="flex sticky items-center h-full">
<template #title> {{ count }} {{ count !== 1 ? $t('objects.records') : $t('objects.record') }} </template>
<span
data-testid="grid-pagination"
class="text-gray-500 text-ellipsis overflow-hidden pl-1 truncate nc-grid-row-count caption text-xs text-nowrap"
>
{{ Intl.NumberFormat('en', { notation: 'compact' }).format(count) }}
{{ count !== 1 ? $t('objects.records') : $t('objects.record') }}
</span>
</NcTooltip>
<template v-if="![UITypes.SpecificDBType, UITypes.ForeignKey].includes(displayFieldComputed.column?.uidt!)">
<div
v-if="!displayFieldComputed.field?.aggregation || displayFieldComputed.field?.aggregation === 'none'"
class="text-gray-500 opacity-0 transition group-hover:opacity-100"
>
<GeneralIcon class="text-gray-500" icon="arrowDown" />
<span class="text-[10px] font-semibold"> Summary </span>
</div>
<NcTooltip
v-else-if="displayFieldComputed.value !== undefined"
:style="{
maxWidth: `${displayFieldComputed?.width}`,
}"
>
<div style="direction: rtl" class="flex gap-2 text-nowrap truncate overflow-hidden items-center">
<span class="text-gray-600 text-[12px] font-semibold">
{{
formatAggregation(
displayFieldComputed.field.aggregation,
displayFieldComputed.value,
displayFieldComputed.column,
)
}}
</span>
<span class="text-gray-500 text-[12px] leading-4">
{{ $t(`aggregation.${displayFieldComputed.field.aggregation}`) }}
</span>
</div>
<template #title>
<div class="flex gap-2 text-nowrap overflow-hidden items-center">
<span class="text-[12px] leading-4">
{{ $t(`aggregation.${displayFieldComputed.field.aggregation}`) }}
</span>
<span class="text-[12px] font-semibold">
{{
formatAggregation(
displayFieldComputed.field.aggregation,
displayFieldComputed.value,
displayFieldComputed.column,
)
}}
</span>
</div>
</template>
</NcTooltip>
</template>
</div>
</div>
<template #overlay>
<NcMenu v-if="displayFieldComputed.field && displayFieldComputed.column?.id">
<NcMenuItem
v-for="(agg, index) in getAggregations(displayFieldComputed.column)"
:key="index"
@click="updateAggregate(displayFieldComputed.column.id, agg)"
>
<div class="flex !w-full text-[13px] text-gray-800 items-center justify-between">
{{ $t(`aggregation_type.${agg}`) }}
<GeneralIcon v-if="displayFieldComputed.field?.aggregation === agg" class="text-brand-500" icon="check" />
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</div>
<template v-for="({ field, width, column, value }, index) in visibleFieldsComputed" :key="index">
<NcDropdown
v-if="field && column?.id"
:disabled="[UITypes.SpecificDBType, UITypes.ForeignKey].includes(column?.uidt!) || isLocked"
overlay-class-name="max-h-96 relative scroll-container nc-scrollbar-thin overflow-auto"
>
<div
class="flex items-center overflow-x-hidden justify-end group hover:bg-gray-100 cursor-pointer text-gray-500 transition-all transition-linear px-3 py-2"
:style="{
'min-width': width,
'max-width': width,
'width': width,
}"
>
<template v-if="![UITypes.SpecificDBType, UITypes.ForeignKey].includes(column?.uidt!)">
<div
v-if="field?.aggregation === 'none' || field?.aggregation === null"
class="text-gray-500 opacity-0 transition group-hover:opacity-100"
>
<GeneralIcon class="text-gray-500" icon="arrowDown" />
<span class="text-[10px] font-semibold"> Summary </span>
</div>
<NcTooltip
v-else-if="value !== undefined"
:style="{
maxWidth: `${field?.width}px`,
}"
>
<div class="flex gap-2 truncate text-nowrap overflow-hidden items-center">
<span class="text-gray-500 text-[12px] leading-4">
{{ $t(`aggregation.${field.aggregation}`).replace('Percent ', '') }}
</span>
<span class="text-gray-600 font-semibold text-[12px]">
{{ formatAggregation(field.aggregation, value, column) }}
</span>
</div>
<template #title>
<div class="flex gap-2 text-nowrap overflow-hidden items-center">
<span class="text-[12px] leading-4">
{{ $t(`aggregation.${field.aggregation}`).replace('Percent ', '') }}
</span>
<span class="font-semibold text-[12px]">
{{ formatAggregation(field.aggregation, value, column) }}
</span>
</div>
</template>
</NcTooltip>
</template>
</div>
<template #overlay>
<NcMenu>
<NcMenuItem v-for="(agg, index) in getAggregations(column)" :key="index" @click="updateAggregate(column.id, agg)">
<div class="flex !w-full text-[13px] text-gray-800 items-center justify-between">
{{ $t(`aggregation_type.${agg}`) }}
<GeneralIcon v-if="field?.aggregation === agg" class="text-brand-500" icon="check" />
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</template>
<div class="!pl-8 pr-60 !w-8 h-1"></div>
<div class="fixed h-9 bg-white border-l-1 border-gray-200 px-1 flex items-center right-0">
<NcPaginationV2
v-if="count !== Infinity"
v-model:current="page"
v-model:page-size="size"
class="xs:(mr-2)"
:total="+count"
entity-name="grid"
:prev-page-tooltip="`${renderAltOrOptlKey()}+←`"
:next-page-tooltip="`${renderAltOrOptlKey()}+→`"
:first-page-tooltip="`${renderAltOrOptlKey()}+↓`"
:last-page-tooltip="`${renderAltOrOptlKey()}+↑`"
:show-size-changer="true"
/>
</div>
</div>
</template>
<style scoped lang="scss">
:deep(.nc-menu-item-inner) {
@apply w-full;
}
.nc-grid-pagination-wrapper {
.ant-pagination-item-active {
a {
@apply text-sm !text-gray-700 !hover:text-gray-800;
}
}
}
</style>
<style lang="scss"></style>