mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-01 05:37:00 +00:00
740 lines
26 KiB
TypeScript
740 lines
26 KiB
TypeScript
import {
|
|
type ColumnType,
|
|
CommonAggregations,
|
|
type FilterType,
|
|
type LinkToAnotherRecordType,
|
|
type LookupType,
|
|
type TableType,
|
|
UITypes,
|
|
type ViewType,
|
|
} from 'nocodb-sdk'
|
|
import { createGroupUniqueIdentifier, generateGroupPath } from '../components/smartsheet/grid/canvas/utils/groupby'
|
|
import type { CanvasGroup } from '#imports'
|
|
import { groupKeysManager } from '#imports'
|
|
|
|
const GROUP_CHUNK_SIZE = 100
|
|
const MAX_GROUP_CACHE_SIZE = 100
|
|
|
|
const getSortParams = (sort: string) => {
|
|
if (sort === 'asc') return '+'
|
|
if (sort === 'desc') return '-'
|
|
if (sort === 'count-asc') return '~+'
|
|
if (sort === 'count-desc') return '~-'
|
|
return '+'
|
|
}
|
|
|
|
export const useInfiniteGroups = (
|
|
view: Ref<ViewType | undefined>,
|
|
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
|
|
where: ComputedRef<string | undefined>,
|
|
callbacks: {
|
|
syncVisibleData: () => void
|
|
},
|
|
) => {
|
|
const { gridViewCols } = useViewColumnsOrThrow()
|
|
const baseStore = useBase()
|
|
const { base } = storeToRefs(baseStore)
|
|
const { $api } = useNuxtApp()
|
|
const { getMeta, metas } = useMetas()
|
|
const { appInfo } = useGlobal()
|
|
const { nestedFilters, sorts } = useSmartsheetStoreOrThrow()
|
|
const { fetchBulkAggregatedData, sharedView } = useSharedView()
|
|
const router = useRouter()
|
|
const isPublic = inject(IsPublicInj, ref(false))
|
|
const sharedViewPassword = inject(SharedViewPasswordInj, ref(null))
|
|
|
|
const routeQuery = computed(() => router.currentRoute.value.query as Record<string, string>)
|
|
|
|
const columnsById = computed(() => {
|
|
if (!meta.value?.columns?.length) return {}
|
|
return meta.value?.columns.reduce((acc, column) => {
|
|
acc[column.id!] = column
|
|
return acc
|
|
}, {} as Record<string, ColumnType>)
|
|
})
|
|
|
|
const gridViewColByTitle = computed(() => {
|
|
return Object.values(gridViewCols.value).reduce((prev, curr) => {
|
|
const title = curr.title
|
|
prev[title] = curr
|
|
return prev
|
|
}, {})
|
|
})
|
|
|
|
const { groupBy: injectedGroupBy, hideEmptyGroups } = useViewGroupByOrThrow()
|
|
|
|
const groupByColumns = computed(() => injectedGroupBy.value)
|
|
|
|
const appendHideEmptyWhere = (colTitle: string | undefined, existingWhere?: string) => {
|
|
if (!hideEmptyGroups?.value || !colTitle) return existingWhere
|
|
const hideFilter = `(${colTitle},notblank)`
|
|
return existingWhere ? `${existingWhere}~and${hideFilter}` : hideFilter
|
|
}
|
|
|
|
const cachedGroups = ref<Map<number, CanvasGroup>>(new Map())
|
|
const totalGroups = ref(0)
|
|
const chunkStates = ref<Array<'loading' | 'loaded' | undefined>>([])
|
|
|
|
const getGroupChunkIndex = (offset: number) => Math.floor(offset / GROUP_CHUNK_SIZE)
|
|
|
|
const colors = ref(enumColor.light)
|
|
|
|
const nextGroupColor = ref(colors.value[0])
|
|
|
|
const getNextColor = () => {
|
|
const tempColor = nextGroupColor.value
|
|
const index = colors.value.indexOf(nextGroupColor.value)
|
|
if (index === colors.value.length - 1) {
|
|
nextGroupColor.value = colors.value[0]
|
|
} else {
|
|
nextGroupColor.value = colors.value[index + 1]
|
|
}
|
|
return tempColor
|
|
}
|
|
|
|
const fetchGroupChunk = async (chunkId: number, parentGroup?: CanvasGroup, force = false) => {
|
|
const targetChunkStates = parentGroup ? parentGroup.chunkStates : chunkStates.value
|
|
|
|
if (targetChunkStates[chunkId] === 'loading' || (targetChunkStates[chunkId] === 'loaded' && !force)) return
|
|
|
|
targetChunkStates[chunkId] = 'loading'
|
|
const offset = chunkId * GROUP_CHUNK_SIZE
|
|
const level = parentGroup ? findGroupLevel(parentGroup) : 0
|
|
const groupCol = groupByColumns.value[level]
|
|
|
|
if (!groupCol || !view.value?.id || !base.value?.id) return
|
|
|
|
try {
|
|
const nestedGrpWhereArr = buildNestedFilterArr(parentGroup) ?? []
|
|
|
|
const effectiveWhere = appendHideEmptyWhere(groupCol.column.title, where.value)
|
|
|
|
const response = isPublic.value
|
|
? await $api.public.dataGroupBy(
|
|
sharedView.value!.uuid!,
|
|
{
|
|
offset,
|
|
limit: GROUP_CHUNK_SIZE,
|
|
where: effectiveWhere,
|
|
sort: `${getSortParams(groupCol.sort)}${groupCol.column.title}` as any,
|
|
column_name: groupCol.column.title,
|
|
subGroupColumnName: groupByColumns.value[level + 1]?.column.title,
|
|
sortArrJson: JSON.stringify(sorts.value),
|
|
filterArrJson: JSON.stringify([...(nestedFilters.value ?? []), ...nestedGrpWhereArr]),
|
|
},
|
|
{
|
|
headers: {
|
|
'xc-password': sharedViewPassword.value,
|
|
},
|
|
},
|
|
)
|
|
: await $api.dbViewRow.groupBy('noco', base.value.id, view.value.fk_model_id, view.value.id, {
|
|
offset,
|
|
limit: GROUP_CHUNK_SIZE,
|
|
where: effectiveWhere,
|
|
sort: `${getSortParams(groupCol.sort)}${groupCol.column.title}` as any,
|
|
column_name: groupCol.column.title,
|
|
sortArrJson: JSON.stringify(sorts.value),
|
|
filterArrJson: JSON.stringify([...(nestedFilters.value || []), ...nestedGrpWhereArr]),
|
|
subGroupColumnName: groupByColumns.value[level + 1]?.column.title,
|
|
})
|
|
|
|
const groups: CanvasGroup[] = []
|
|
for (const item of response.list) {
|
|
let group: CanvasGroup = {} as any
|
|
|
|
if (groupCol.column.uidt === UITypes.LinkToAnotherRecord) {
|
|
const colOpts = groupCol.column.colOptions as LinkToAnotherRecordType
|
|
const relatedBaseId = colOpts?.fk_related_base_id || (base.value?.id as string)
|
|
const relatedTableMeta = await getMeta(relatedBaseId, colOpts.fk_related_model_id as string)
|
|
if (!relatedTableMeta) continue
|
|
group.relatedTableMeta = relatedTableMeta
|
|
const col = relatedTableMeta.columns?.find((c) => c.pv) || relatedTableMeta.columns?.[0]
|
|
group.relatedColumn = col
|
|
group.displayValueProp = col?.title
|
|
}
|
|
|
|
if (groupCol.column.uidt === UITypes.Lookup) {
|
|
const relationColumn = meta.value?.columns?.find(
|
|
(c: ColumnType) => c.id === (groupCol.column?.colOptions as LookupType)?.fk_relation_column_id,
|
|
)
|
|
if (!relationColumn) continue
|
|
|
|
const relColOpts = relationColumn.colOptions as LinkToAnotherRecordType
|
|
const relatedBaseId = relColOpts?.fk_related_base_id || (base.value?.id as string)
|
|
const relatedTableMeta = await getMeta(relatedBaseId, relColOpts.fk_related_model_id as string)
|
|
if (!relatedTableMeta) continue
|
|
|
|
const lookupColumn = relatedTableMeta.columns?.find(
|
|
(c) => c.id === (groupCol.column.colOptions as LookupType)?.fk_lookup_column_id,
|
|
)
|
|
if (!lookupColumn) continue
|
|
|
|
let finalTableMeta = relatedTableMeta
|
|
let finalColumn = lookupColumn
|
|
|
|
// Resolve nested lookups (Lookup → Lookup → ... → target column)
|
|
while (finalColumn?.uidt === UITypes.Lookup) {
|
|
const nestedRelCol = finalTableMeta.columns?.find(
|
|
(c: ColumnType) => c.id === (finalColumn!.colOptions as LookupType)?.fk_relation_column_id,
|
|
)
|
|
if (!nestedRelCol) break
|
|
|
|
const nestedRelOpts = nestedRelCol.colOptions as LinkToAnotherRecordType
|
|
const nestedBaseId = nestedRelOpts?.fk_related_base_id || (base.value?.id as string)
|
|
const nestedTableMeta = await getMeta(nestedBaseId, nestedRelOpts.fk_related_model_id as string)
|
|
if (!nestedTableMeta) break
|
|
|
|
const nestedLookupCol = nestedTableMeta.columns?.find(
|
|
(c) => c.id === (finalColumn!.colOptions as LookupType)?.fk_lookup_column_id,
|
|
)
|
|
if (!nestedLookupCol) break
|
|
|
|
finalTableMeta = nestedTableMeta
|
|
finalColumn = nestedLookupCol
|
|
}
|
|
|
|
// Check if the final column is a LinkToAnotherRecord
|
|
if (finalColumn?.uidt === UITypes.LinkToAnotherRecord) {
|
|
const lookupColOpts = finalColumn.colOptions as LinkToAnotherRecordType
|
|
const targetBaseId = lookupColOpts?.fk_related_base_id || (base.value?.id as string)
|
|
const targetTableMeta = await getMeta(targetBaseId, lookupColOpts.fk_related_model_id as string)
|
|
if (targetTableMeta) {
|
|
finalTableMeta = targetTableMeta
|
|
finalColumn = targetTableMeta.columns?.find((c) => c.pv) || targetTableMeta.columns?.[0]
|
|
}
|
|
}
|
|
|
|
group.relatedTableMeta = finalTableMeta
|
|
group.relatedColumn = finalColumn
|
|
group.displayValueProp = finalColumn?.title
|
|
}
|
|
|
|
const index: number = response.list.indexOf(item)
|
|
const value = valueToTitle(
|
|
item[groupCol.column.title!] ?? item[groupCol.column.column_name!],
|
|
groupCol.column,
|
|
group?.displayValueProp,
|
|
)
|
|
const groupIndex = offset + index
|
|
|
|
group = {
|
|
...group,
|
|
groupIndex,
|
|
column: groupCol.column,
|
|
groups: new Map(),
|
|
chunkStates: [],
|
|
count: +item.count,
|
|
groupCount: +item.__sub_group_count__,
|
|
isExpanded: false,
|
|
color: findKeyColor(value, groupCol.column, getNextColor),
|
|
expandedGroups: 0,
|
|
value,
|
|
nestedIn: parentGroup
|
|
? [
|
|
...parentGroup.nestedIn,
|
|
{
|
|
title: groupCol.column.title!,
|
|
column_name: groupCol.column.title!,
|
|
key: value,
|
|
column_uidt: group.relatedColumn?.uidt ?? groupCol.column.uidt,
|
|
column_id: groupCol.column.id,
|
|
groupIndex,
|
|
},
|
|
]
|
|
: [
|
|
{
|
|
title: groupCol.column.title!,
|
|
column_name: groupCol.column.title!,
|
|
key: value,
|
|
column_uidt: group.relatedColumn?.uidt ?? groupCol.column.uidt,
|
|
column_id: groupCol.column.id,
|
|
groupIndex,
|
|
},
|
|
],
|
|
aggregations: {},
|
|
}
|
|
|
|
const groupPath = generateGroupPath(group)
|
|
|
|
let routePath = (routeQuery.value?.path?.split('-') ?? []).map((c) => +c)
|
|
|
|
routePath = [
|
|
...routePath.slice(0, group.nestedIn.length),
|
|
...Array(Math.max(0, group.nestedIn.length - routePath.length)).fill(''),
|
|
]
|
|
|
|
const isExpanded = groupPath.join('-') === routePath.join('-')
|
|
|
|
const nestedKey = group.nestedIn.map((n) => `${n.key}-${n.column_name}`).join('_') || 'default'
|
|
|
|
group.isExpanded = groupKeysManager.hasKey(view.value.id!, nestedKey) || isExpanded
|
|
|
|
// Create useInfiniteData for leaf groups
|
|
if (level === groupByColumns.value.length - 1) {
|
|
group.path = groupPath
|
|
}
|
|
|
|
if (parentGroup) {
|
|
parentGroup.groups.set(groupIndex, group)
|
|
} else {
|
|
cachedGroups.value.set(groupIndex, group)
|
|
}
|
|
groups.push(group)
|
|
}
|
|
|
|
if (groups.length && !appInfo.value.disableGroupByAggregation) {
|
|
const aggregationAliasMapper = new AliasMapper()
|
|
|
|
const aggregation = Object.values(gridViewCols.value)
|
|
.map((f) => ({
|
|
field: f.fk_column_id!,
|
|
type: f.aggregation ?? CommonAggregations.None,
|
|
}))
|
|
.filter((f) => f.type !== CommonAggregations.None)
|
|
|
|
const aggregationParams = groups.map((group) => ({
|
|
where: where?.value,
|
|
alias: aggregationAliasMapper.generateAlias(group.value),
|
|
filterArrJson: JSON.stringify([...nestedFilters.value, ...buildNestedFilterArr(group)]),
|
|
}))
|
|
let aggResponse = {}
|
|
|
|
if (aggregation.length) {
|
|
aggResponse = !isPublic.value
|
|
? await $api.internal.postOperation(
|
|
(meta.value as any)!.fk_workspace_id!,
|
|
meta.value!.base_id!,
|
|
{
|
|
operation: 'bulkAggregate',
|
|
tableId: meta.value!.id,
|
|
viewId: view.value!.id,
|
|
baseId: meta.value!.base_id!,
|
|
aggregation,
|
|
filterArrJson: JSON.stringify(nestedFilters.value),
|
|
},
|
|
aggregationParams,
|
|
)
|
|
: await fetchBulkAggregatedData(
|
|
{
|
|
aggregation,
|
|
filterArrJson: JSON.stringify(nestedFilters.value),
|
|
},
|
|
aggregationParams,
|
|
)
|
|
|
|
await aggregationAliasMapper.process(aggResponse, (originalKey, value) => {
|
|
const group = groups.find((g) => g.value.toString() === originalKey.toString())
|
|
|
|
Object.keys(value).forEach((key) => {
|
|
const field = gridViewColByTitle.value[key]
|
|
const col = columnsById.value[field.fk_column_id]
|
|
value[key] =
|
|
getFormattedAggrationValue(field.aggregation, value[key], col, [originalKey.toString()], {
|
|
col,
|
|
meta: meta.value as TableType,
|
|
metas: metas.value,
|
|
isMysql: baseStore.isMysql,
|
|
isPg: baseStore.isPg,
|
|
}) ?? ''
|
|
})
|
|
|
|
if (group) {
|
|
Object.assign(group.aggregations, value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
if (!parentGroup) {
|
|
totalGroups.value = response.pageInfo.totalRows || totalGroups.value
|
|
chunkStates.value[chunkId] = 'loaded'
|
|
} else {
|
|
targetChunkStates[chunkId] = 'loaded'
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error fetching group chunk at level ${level}:`, error)
|
|
targetChunkStates[chunkId] = undefined
|
|
}
|
|
}
|
|
|
|
function buildNestedWhere(group: CanvasGroup, existing = ''): string {
|
|
// Use nestedIn array instead of traversing parents
|
|
if (!group?.nestedIn?.length) return existing
|
|
|
|
const sanitiseValue = (value: string) => {
|
|
return `"${value}"` // .replace(/"/g, '\\"')}`
|
|
}
|
|
|
|
return group.nestedIn.reduce((acc, curr) => {
|
|
if (curr.key === GROUP_BY_VARS.NULL) {
|
|
acc += `${acc.length ? '~and' : '@'}(${curr.title},gb_null)`
|
|
} else if (curr.column_uidt === UITypes.Checkbox) {
|
|
acc += `${acc.length ? '~and' : '@'}(${curr.title},${curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked'})`
|
|
} else if (
|
|
[UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(curr.column_uidt as UITypes)
|
|
) {
|
|
acc += `${acc.length ? '~and' : '@'}(${curr.title},gb_eq,exactDate,${sanitiseValue(curr.key)})`
|
|
} else if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(curr.column_uidt as UITypes)) {
|
|
try {
|
|
const value = JSON.parse(curr.key)
|
|
acc += `${acc.length ? '~and' : '@'}(${curr.title},gb_eq,${sanitiseValue(
|
|
(Array.isArray(value) ? value : [value]).map((v: any) => v.id).join(','),
|
|
)})`
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
} else {
|
|
acc += `${acc.length ? '~and' : '@'}(${curr.title},gb_eq,${sanitiseValue(curr.key)})`
|
|
}
|
|
return acc
|
|
}, existing)
|
|
}
|
|
|
|
function buildNestedFilterArr(group: CanvasGroup, existing: FilterType[] = []): FilterType[] {
|
|
// Use nestedIn array instead of traversing parents
|
|
if (!group?.nestedIn?.length) return existing
|
|
|
|
return group.nestedIn.reduce((acc, curr) => {
|
|
if (curr.key === GROUP_BY_VARS.NULL) {
|
|
// acc += `${acc.length ? '~and' : '@'}(${curr.title},gb_null)`
|
|
acc.push({
|
|
fk_column_id: curr.column_id,
|
|
comparison_op: 'gb_null',
|
|
})
|
|
} else if (curr.column_uidt === UITypes.Checkbox) {
|
|
acc.push({
|
|
fk_column_id: curr.column_id,
|
|
comparison_op: curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked',
|
|
})
|
|
} else if (
|
|
[UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(curr.column_uidt as UITypes)
|
|
) {
|
|
acc.push({
|
|
fk_column_id: curr.column_id,
|
|
comparison_op: 'gb_eq',
|
|
comparison_sub_op: 'exactDate',
|
|
value: curr.key,
|
|
})
|
|
} else if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(curr.column_uidt as UITypes)) {
|
|
try {
|
|
const value = JSON.parse(curr.key)
|
|
|
|
acc.push({
|
|
fk_column_id: curr.column_id,
|
|
comparison_op: 'gb_eq',
|
|
value: (Array.isArray(value) ? value : [value]).map((v: any) => v.id).join(','),
|
|
})
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
} else {
|
|
acc.push({
|
|
fk_column_id: curr.column_id,
|
|
comparison_op: 'gb_eq',
|
|
value: curr.key,
|
|
})
|
|
}
|
|
return acc ?? []
|
|
}, existing)
|
|
}
|
|
|
|
function findGroupLevel(group: CanvasGroup): number {
|
|
return group.nestedIn?.length || 0
|
|
}
|
|
|
|
const fetchMissingGroupChunks = async (startIndex: number, endIndex: number, parentGroup?: CanvasGroup, force = false) => {
|
|
const firstChunkId = getGroupChunkIndex(startIndex)
|
|
const lastChunkId = getGroupChunkIndex(endIndex)
|
|
|
|
const targetChunkStates = parentGroup ? parentGroup.chunkStates : chunkStates.value
|
|
const chunksToFetch = Array.from({ length: lastChunkId - firstChunkId + 1 }, (_, i) => firstChunkId + i).filter(
|
|
(chunkId) => !targetChunkStates[chunkId] || force,
|
|
)
|
|
|
|
await Promise.all(chunksToFetch.map((chunkId) => fetchGroupChunk(chunkId, parentGroup, force)))
|
|
callbacks?.syncVisibleData()
|
|
|
|
// if found empty chunk, remove all chunks after it and fetch all chunks again
|
|
if (force) {
|
|
let foundEmptyChunk = false
|
|
for (let i = startIndex; i <= endIndex; i++) {
|
|
const targetGroup = cachedGroups.value.get(i)
|
|
if (targetGroup?.count === 0) {
|
|
foundEmptyChunk = true
|
|
}
|
|
|
|
if (foundEmptyChunk) {
|
|
cachedGroups.value.delete(i)
|
|
}
|
|
}
|
|
}
|
|
|
|
await Promise.all(chunksToFetch.map((chunkId) => fetchGroupChunk(chunkId, parentGroup, force)))
|
|
}
|
|
|
|
const clearGroupCache = (startIndex: number, endIndex: number, parentGroup?: CanvasGroup) => {
|
|
if (startIndex === Number.NEGATIVE_INFINITY && endIndex === Number.POSITIVE_INFINITY) {
|
|
cachedGroups.value = new Map()
|
|
chunkStates.value = []
|
|
return
|
|
}
|
|
|
|
const targetGroups = parentGroup ? parentGroup.groups : cachedGroups.value
|
|
if (targetGroups.size <= MAX_GROUP_CACHE_SIZE) return
|
|
|
|
const safeStartIndex = Math.max(0, startIndex)
|
|
const safeEndIndex = Math.min((parentGroup ? parentGroup.count : totalGroups.value) - 1, endIndex)
|
|
const newGroups = new Map<number, CanvasGroup>()
|
|
|
|
for (let i = safeStartIndex; i <= safeEndIndex; i++) {
|
|
const group = targetGroups.get(i)
|
|
if (group) newGroups.set(i, group)
|
|
}
|
|
|
|
if (parentGroup) {
|
|
parentGroup.groups = newGroups
|
|
parentGroup.chunkStates = parentGroup.chunkStates.map((state, index) =>
|
|
index >= getGroupChunkIndex(safeStartIndex) && index <= getGroupChunkIndex(safeEndIndex) ? state : undefined,
|
|
)
|
|
} else {
|
|
cachedGroups.value = newGroups
|
|
chunkStates.value = chunkStates.value.map((state, index) =>
|
|
index >= getGroupChunkIndex(safeStartIndex) && index <= getGroupChunkIndex(safeEndIndex) ? state : undefined,
|
|
)
|
|
}
|
|
}
|
|
|
|
async function syncCount(group?: CanvasGroup, throwError = false, showToastMessage = false) {
|
|
if (!view.value || !meta.value?.columns?.length) return
|
|
|
|
try {
|
|
if (!group) {
|
|
const groupCol = groupByColumns.value?.[0]
|
|
if (!groupCol) return
|
|
|
|
const effectiveWhere = appendHideEmptyWhere(groupCol.column.title, where?.value)
|
|
|
|
totalGroups.value = isPublic.value
|
|
? await $api.public.dataGroupByCount(
|
|
sharedView.value!.uuid!,
|
|
{
|
|
where: effectiveWhere,
|
|
column_name: groupCol.column.title,
|
|
filterArrJson: JSON.stringify(nestedFilters.value),
|
|
},
|
|
{
|
|
headers: {
|
|
'xc-password': sharedViewPassword.value,
|
|
},
|
|
},
|
|
)
|
|
: await $api.dbViewRow.groupByCount('noco', base.value.id!, view.value.fk_model_id, view.value.id!, {
|
|
where: effectiveWhere,
|
|
column_name: groupCol.column.title,
|
|
})
|
|
} else {
|
|
const groupCol = groupByColumns.value?.[group.nestedIn.length]
|
|
|
|
if (!groupCol) return
|
|
|
|
const groupFilterArr = buildNestedFilterArr(group) ?? []
|
|
const effectiveWhere = appendHideEmptyWhere(groupCol.column.title, where?.value)
|
|
|
|
group.groupCount = isPublic.value
|
|
? await $api.public.dataGroupByCount(
|
|
sharedView.value!.uuid!,
|
|
{
|
|
where: effectiveWhere,
|
|
column_name: groupCol.column.title,
|
|
filterArrJson: JSON.stringify([...(nestedFilters.value || []), ...groupFilterArr]),
|
|
},
|
|
{
|
|
headers: {
|
|
'xc-password': sharedViewPassword.value,
|
|
},
|
|
},
|
|
)
|
|
: await $api.dbViewRow.groupByCount('noco', base.value.id!, view.value.fk_model_id, view.value.id!, {
|
|
where: effectiveWhere,
|
|
column_name: groupCol.column.title,
|
|
filterArrJson: JSON.stringify(groupFilterArr),
|
|
})
|
|
}
|
|
} catch (e: any) {
|
|
if (showToastMessage) {
|
|
const errorMessage = await extractSdkResponseErrorMsg(e)
|
|
message.error(`Failed to sync count: ${errorMessage}`)
|
|
}
|
|
|
|
if (throwError) {
|
|
throw e
|
|
}
|
|
}
|
|
}
|
|
|
|
async function updateGroupAggregations(
|
|
groups: CanvasGroup[],
|
|
fields?: Array<{
|
|
title: string
|
|
aggregation?: string
|
|
}>,
|
|
) {
|
|
if (appInfo.value.disableGroupByAggregation) return
|
|
|
|
const BATCH_SIZE = 100
|
|
const aggregationAliasMapper = new AliasMapper()
|
|
|
|
const aggregation = fields
|
|
? fields
|
|
.map((f) => {
|
|
const col = gridViewColByTitle.value[f.title]
|
|
return col
|
|
? {
|
|
field: col.fk_column_id!,
|
|
type: f.aggregation ?? col.aggregation ?? CommonAggregations.None,
|
|
}
|
|
: null
|
|
})
|
|
.filter(Boolean)
|
|
: Object.values(gridViewCols.value)
|
|
.map((f) => ({
|
|
field: f.fk_column_id!,
|
|
type: f.aggregation ?? CommonAggregations.None,
|
|
}))
|
|
.filter((f) => f.type !== CommonAggregations.None)
|
|
|
|
if (!aggregation.length) return
|
|
|
|
const fieldAggregationMap = new Map<string, string>()
|
|
if (fields) {
|
|
fields.forEach((f) => {
|
|
const col = gridViewColByTitle.value[f.title]
|
|
if (col?.fk_column_id) {
|
|
fieldAggregationMap.set(col.fk_column_id, f.aggregation ?? col.aggregation ?? CommonAggregations.None)
|
|
}
|
|
})
|
|
}
|
|
|
|
for (let i = 0; i < groups.length; i += BATCH_SIZE) {
|
|
const batchGroups = groups.slice(i, i + BATCH_SIZE)
|
|
|
|
const aggregationParams = batchGroups.map((group) => ({
|
|
where: where?.value,
|
|
alias: aggregationAliasMapper.generateAlias(createGroupUniqueIdentifier(group)),
|
|
filterArrJson: JSON.stringify([...(nestedFilters.value || []), ...buildNestedFilterArr(group)]),
|
|
}))
|
|
|
|
try {
|
|
const aggResponse = !isPublic.value
|
|
? await $api.internal.postOperation(
|
|
(meta.value as any)!.fk_workspace_id!,
|
|
meta.value!.base_id!,
|
|
{
|
|
operation: 'bulkAggregate',
|
|
tableId: meta.value!.id,
|
|
viewId: view.value!.id,
|
|
baseId: meta.value!.base_id!,
|
|
aggregation,
|
|
filterArrJson: JSON.stringify(nestedFilters.value),
|
|
},
|
|
aggregationParams,
|
|
)
|
|
: await fetchBulkAggregatedData(
|
|
{
|
|
aggregation,
|
|
filterArrJson: JSON.stringify(nestedFilters.value),
|
|
},
|
|
aggregationParams,
|
|
)
|
|
|
|
await aggregationAliasMapper.process(aggResponse, (originalKey, value) => {
|
|
const group = batchGroups.find((g) => createGroupUniqueIdentifier(g) === originalKey.toString())
|
|
|
|
if (!group) return
|
|
|
|
Object.keys(value).forEach((key) => {
|
|
const field = gridViewColByTitle.value[key]
|
|
const col = columnsById.value[field.fk_column_id]
|
|
const aggregationType = fieldAggregationMap.get(field.fk_column_id) ?? field.aggregation
|
|
value[key] =
|
|
getFormattedAggrationValue(aggregationType, value[key], col, [originalKey.toString()], {
|
|
col,
|
|
meta: meta.value as TableType,
|
|
metas: metas.value,
|
|
isMysql: baseStore.isMysql,
|
|
isPg: baseStore.isPg,
|
|
}) ?? ''
|
|
})
|
|
|
|
Object.assign(group.aggregations, value)
|
|
})
|
|
} catch (error) {
|
|
console.error('Error refreshing group aggregations batch:', error)
|
|
}
|
|
}
|
|
callbacks?.syncVisibleData()
|
|
}
|
|
|
|
const toggleExpand = async (group: CanvasGroup) => {
|
|
group.isExpanded = !group.isExpanded
|
|
const nestedKey = group.nestedIn.map((n) => `${n.key}-${n.column_name}`).join('_') || 'default'
|
|
|
|
if (!view.value?.id) return
|
|
groupKeysManager.toggleKey(view.value.id, nestedKey, group.isExpanded)
|
|
}
|
|
|
|
const toggleExpandAll = async (path: number[], expand: boolean) => {
|
|
let targetGroups: Map<number, CanvasGroup>
|
|
if (!path?.length) {
|
|
path = [0]
|
|
}
|
|
|
|
if (path.length === 1) {
|
|
targetGroups = cachedGroups.value
|
|
} else {
|
|
let currentGroups = cachedGroups.value
|
|
|
|
for (let i = 0; i < path.length - 1; i++) {
|
|
const group = currentGroups.get(path[i])
|
|
if (!group || !group.groups) return
|
|
currentGroups = group.groups
|
|
}
|
|
|
|
targetGroups = currentGroups
|
|
}
|
|
|
|
if (!view.value?.id) return
|
|
|
|
targetGroups.forEach((group) => {
|
|
const nestedKey = group.nestedIn.map((n) => `${n.key}-${n.column_name}`).join('_') || 'default'
|
|
group.isExpanded = expand
|
|
groupKeysManager.toggleKey(view.value.id!, nestedKey, expand)
|
|
})
|
|
|
|
callbacks?.syncVisibleData()
|
|
}
|
|
|
|
const isGroupBy = computed(() => !!groupByColumns.value.length)
|
|
|
|
return {
|
|
isGroupBy,
|
|
cachedGroups,
|
|
groupByColumns,
|
|
totalGroups,
|
|
chunkStates,
|
|
syncCount,
|
|
fetchMissingGroupChunks,
|
|
clearGroupCache,
|
|
toggleExpand,
|
|
GROUP_CHUNK_SIZE,
|
|
buildNestedWhere,
|
|
buildNestedFilterArr,
|
|
CHUNK_SIZE: 50,
|
|
columnsById,
|
|
gridViewColByTitle,
|
|
updateGroupAggregations,
|
|
toggleExpandAll,
|
|
}
|
|
}
|