|
|
|
import {
|
|
|
|
type ColumnType,
|
|
|
|
CommonAggregations,
|
|
|
|
type LinkToAnotherRecordType,
|
|
|
|
type LookupType,
|
|
|
|
type SelectOptionsType,
|
|
|
|
type TableType,
|
|
|
|
type ViewType,
|
|
|
|
} from 'nocodb-sdk'
|
|
|
|
import { UITypes } from 'nocodb-sdk'
|
|
|
|
import type { Ref } from 'vue'
|
|
|
|
import { message } from 'ant-design-vue'
|
|
|
|
import type { Group } from '../lib/types'
|
|
|
|
|
|
|
|
const excludedGroupingUidt = [UITypes.Attachment, UITypes.QrCode, UITypes.Barcode]
|
|
|
|
|
|
|
|
const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState(
|
|
|
|
(
|
|
|
|
view: Ref<ViewType | undefined>,
|
|
|
|
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
|
|
|
|
where?: ComputedRef<string | undefined>,
|
|
|
|
isPublic = false,
|
|
|
|
) => {
|
|
|
|
const groupByLimit: number = 3
|
|
|
|
|
|
|
|
const { api } = useApi()
|
|
|
|
|
|
|
|
const { appInfo } = useGlobal()
|
|
|
|
|
|
|
|
const { base } = storeToRefs(useBase())
|
|
|
|
|
|
|
|
const { sharedView, fetchSharedViewData, fetchBulkAggregatedData, fetchBulkListData, fetchBulkGroupData } = useSharedView()
|
|
|
|
|
|
|
|
const { gridViewCols } = useViewColumnsOrThrow()
|
|
|
|
|
|
|
|
const { getMeta } = useMetas()
|
|
|
|
|
|
|
|
const sharedViewPassword = inject(SharedViewPasswordInj, ref(null))
|
|
|
|
|
|
|
|
const groupBy = computed<{ column: ColumnType; sort: string; order?: number }[]>(() => {
|
|
|
|
const tempGroupBy: { column: ColumnType; sort: string; order?: number }[] = []
|
|
|
|
Object.values(gridViewCols.value).forEach((col) => {
|
|
|
|
if (col.group_by) {
|
|
|
|
const column = meta?.value?.columns?.find((f) => f.id === col.fk_column_id)
|
|
|
|
if (column) {
|
|
|
|
tempGroupBy.push({
|
|
|
|
column,
|
|
|
|
sort: col.group_by_sort || 'asc',
|
|
|
|
order: col.group_by_order || 1,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
tempGroupBy.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
|
|
|
|
return tempGroupBy
|
|
|
|
})
|
|
|
|
|
|
|
|
const isGroupBy = computed(() => !!groupBy.value.length)
|
|
|
|
|
|
|
|
const { isUIAllowed } = useRoles()
|
|
|
|
|
|
|
|
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
|
|
|
|
|
|
|
|
const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
|
|
|
|
|
|
|
|
const groupByGroupLimit = computed(() => {
|
|
|
|
return appInfo.value.defaultGroupByLimit?.limitGroup || 25
|
|
|
|
})
|
|
|
|
|
|
|
|
const groupByRecordLimit = computed(() => {
|
|
|
|
return appInfo.value.defaultGroupByLimit?.limitRecord || 10
|
|
|
|
})
|
|
|
|
|
|
|
|
const supportedLookups = ref<string[]>([])
|
|
|
|
|
|
|
|
const fieldsToGroupBy = computed(() =>
|
|
|
|
(meta?.value?.columns || []).filter((field) => {
|
|
|
|
if (excludedGroupingUidt.includes(field.uidt as UITypes)) return false
|
|
|
|
|
|
|
|
if (field.uidt === UITypes.Lookup) {
|
|
|
|
return field.id && supportedLookups.value.includes(field.id)
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
|
|
|
|
const rootGroup = ref<Group>({
|
|
|
|
key: 'root',
|
|
|
|
color: 'root',
|
|
|
|
count: 0,
|
|
|
|
column: {} as any,
|
|
|
|
nestedIn: [],
|
|
|
|
aggregations: {},
|
|
|
|
paginationData: { page: 1, pageSize: groupByGroupLimit.value },
|
|
|
|
nested: true,
|
|
|
|
children: [],
|
|
|
|
root: true,
|
|
|
|
})
|
|
|
|
|
|
|
|
async function groupWrapperChangePage(page: number, groupWrapper?: Group) {
|
|
|
|
groupWrapper = groupWrapper || rootGroup.value
|
|
|
|
|
|
|
|
if (!groupWrapper) return
|
|
|
|
|
|
|
|
groupWrapper.paginationData.page = page
|
|
|
|
await loadGroups(
|
|
|
|
{
|
|
|
|
offset: (page - 1) * (groupWrapper.paginationData.pageSize || groupByGroupLimit.value),
|
|
|
|
} as any,
|
|
|
|
groupWrapper,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const formatData = (list: Record<string, any>[]) =>
|
|
|
|
list.map((row) => ({
|
|
|
|
row: { ...row },
|
|
|
|
oldRow: { ...row },
|
|
|
|
rowMeta: {},
|
|
|
|
}))
|
|
|
|
|
|
|
|
const valueToTitle = (value: string, col: ColumnType, displayValueProp?: string) => {
|
|
|
|
if (col.uidt === UITypes.Checkbox) {
|
|
|
|
return value ? GROUP_BY_VARS.TRUE : GROUP_BY_VARS.FALSE
|
|
|
|
}
|
|
|
|
|
|
|
|
if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(col.uidt as UITypes)) {
|
|
|
|
if (!value) {
|
|
|
|
return GROUP_BY_VARS.NULL
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (col.uidt === UITypes.LinkToAnotherRecord && displayValueProp && value && typeof value === 'object') {
|
|
|
|
return value[displayValueProp] ?? GROUP_BY_VARS.NULL
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert to JSON string if non-string value
|
|
|
|
if (value && typeof value === 'object') {
|
|
|
|
value = JSON.stringify(value)
|
|
|
|
}
|
|
|
|
|
|
|
|
return value ?? GROUP_BY_VARS.NULL
|
|
|
|
}
|
|
|
|
|
|
|
|
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 findKeyColor = (key?: string, col?: ColumnType): string => {
|
|
|
|
if (col) {
|
|
|
|
switch (col.uidt) {
|
|
|
|
case UITypes.MultiSelect: {
|
|
|
|
const keys = key?.split(',') || []
|
|
|
|
const colors = []
|
|
|
|
for (const k of keys) {
|
|
|
|
const option = (col.colOptions as SelectOptionsType).options?.find((o) => o.title === k)
|
|
|
|
if (option) {
|
|
|
|
colors.push(option.color)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return colors.join(',')
|
|
|
|
}
|
|
|
|
case UITypes.SingleSelect: {
|
|
|
|
const option = (col.colOptions as SelectOptionsType).options?.find((o) => o.title === key)
|
|
|
|
if (option) {
|
|
|
|
return option.color || getNextColor()
|
|
|
|
}
|
|
|
|
return 'gray'
|
|
|
|
}
|
|
|
|
case UITypes.Checkbox: {
|
|
|
|
if (key) {
|
|
|
|
return themeColors.success
|
|
|
|
}
|
|
|
|
return themeColors.error
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return key ? getNextColor() : 'gray'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return key ? getNextColor() : 'gray'
|
|
|
|
}
|
|
|
|
|
|
|
|
const calculateNestedWhere = (nestedIn: GroupNestedIn[], existing = '') => {
|
|
|
|
return 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,${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,${(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,${curr.key})`
|
|
|
|
}
|
|
|
|
return acc
|
|
|
|
}, existing)
|
|
|
|
}
|
|
|
|
|
|
|
|
const getSortParams = (sort: string) => {
|
|
|
|
if (sort === 'asc') {
|
|
|
|
return '+'
|
|
|
|
} else if (sort === 'desc') {
|
|
|
|
return '-'
|
|
|
|
} else if (sort === 'count-asc') {
|
|
|
|
return '~+'
|
|
|
|
} else if (sort === 'count-desc') {
|
|
|
|
return '~-'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const processGroupData = async (response: any, group?: Group) => {
|
|
|
|
group = group || rootGroup.value
|
|
|
|
|
|
|
|
const groupby = groupBy.value[group.nestedIn.length]
|
|
|
|
|
|
|
|
if (!groupby) return group
|
|
|
|
|
|
|
|
const tempList: Group[] = response.list.reduce((acc: Group[], curr: Record<string, any>) => {
|
|
|
|
const keyExists = acc.find(
|
|
|
|
(a) => a.key === valueToTitle(curr[groupby.column.column_name!] ?? curr[groupby.column.title!], groupby.column),
|
|
|
|
)
|
|
|
|
if (keyExists) {
|
|
|
|
keyExists.count += +curr.count
|
|
|
|
keyExists.paginationData = {
|
|
|
|
page: 1,
|
|
|
|
pageSize: group.paginationData.pageSize || groupByGroupLimit.value,
|
|
|
|
totalRows: keyExists.count,
|
|
|
|
}
|
|
|
|
return acc
|
|
|
|
}
|
|
|
|
if (groupby.column.title && groupby.column.uidt) {
|
|
|
|
acc.push({
|
|
|
|
key: valueToTitle(curr[groupby.column.title!], groupby.column),
|
|
|
|
column: groupby.column,
|
|
|
|
count: +curr.count,
|
|
|
|
color: findKeyColor(curr[groupby.column.title!], groupby.column),
|
|
|
|
nestedIn: [
|
|
|
|
...group!.nestedIn,
|
|
|
|
{
|
|
|
|
title: groupby.column.title,
|
|
|
|
column_name: groupby.column.title!,
|
|
|
|
key: valueToTitle(curr[groupby.column.title!], groupby.column),
|
|
|
|
column_uidt: groupby.column.uidt,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
aggregations: curr.aggregations ?? {},
|
|
|
|
paginationData: {
|
|
|
|
page: 1,
|
|
|
|
pageSize:
|
|
|
|
group!.nestedIn.length < groupBy.value.length - 1
|
|
|
|
? group.paginationData.pageSize || groupByGroupLimit.value
|
|
|
|
: groupByRecordLimit.value,
|
|
|
|
totalRows: +curr.count,
|
|
|
|
},
|
|
|
|
nested: group!.nestedIn.length < groupBy.value.length - 1,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return acc
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
if (!group.children) group.children = []
|
|
|
|
|
|
|
|
for (const temp of tempList) {
|
|
|
|
const keyExists = group.children?.find((a) => a.key === temp.key)
|
|
|
|
if (keyExists) {
|
|
|
|
temp.paginationData = {
|
|
|
|
page: keyExists.paginationData.page || temp.paginationData.page,
|
|
|
|
pageSize: keyExists.paginationData.pageSize || temp.paginationData.pageSize,
|
|
|
|
totalRows: temp.count,
|
|
|
|
}
|
|
|
|
temp.color = keyExists.color
|
|
|
|
|
|
|
|
// update group
|
|
|
|
Object.assign(keyExists, temp)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
group.children.push(temp)
|
|
|
|
}
|
|
|
|
|
|
|
|
group.children = group.children.filter((c) => tempList.find((t) => t.key === c.key))
|
|
|
|
|
|
|
|
if (group.count <= (group.paginationData.pageSize ?? groupByGroupLimit.value)) {
|
|
|
|
group.children.sort((a, b) => {
|
|
|
|
const orderA = tempList.findIndex((t) => t.key === a.key)
|
|
|
|
const orderB = tempList.findIndex((t) => t.key === b.key)
|
|
|
|
return orderA - orderB
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
group.paginationData = response.pageInfo
|
|
|
|
|
|
|
|
// to cater the case like when querying with a non-zero offset
|
|
|
|
// the result page may point to the target page where the actual returned data don't display on
|
|
|
|
const expectedPage = Math.max(1, Math.ceil(group.paginationData.totalRows! / group.paginationData.pageSize!))
|
|
|
|
if (expectedPage < group.paginationData.page!) {
|
|
|
|
await groupWrapperChangePage(expectedPage, group)
|
|
|
|
}
|
|
|
|
|
|
|
|
return group
|
|
|
|
}
|
|
|
|
|
|
|
|
async function loadGroups(
|
|
|
|
params: any = {},
|
|
|
|
group?: Group,
|
|
|
|
options?: {
|
|
|
|
triggerChildOnly: boolean
|
|
|
|
},
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
group = group || rootGroup.value
|
|
|
|
if (!base?.value?.id || !view.value?.id || !view.value?.fk_model_id || !group) return
|
|
|
|
|
|
|
|
if (groupBy.value.length === 0) {
|
|
|
|
group.children = []
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (group.nestedIn.length > groupBy.value.length) return
|
|
|
|
|
|
|
|
if (group.nestedIn.length === 0) nextGroupColor.value = colors.value[0]
|
|
|
|
const groupby = groupBy.value[group.nestedIn.length]
|
|
|
|
|
|
|
|
const nestedWhere = calculateNestedWhere(group.nestedIn, where?.value)
|
|
|
|
if (!groupby || !groupby.column.title) return
|
|
|
|
|
|
|
|
if (isPublic && !sharedView.value?.uuid) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (groupby.column.uidt === UITypes.LinkToAnotherRecord) {
|
|
|
|
const relatedTableMeta = await getMeta(
|
|
|
|
(groupby.column.colOptions as LinkToAnotherRecordType).fk_related_model_id as string,
|
|
|
|
)
|
|
|
|
if (!relatedTableMeta) return
|
|
|
|
group.displayValueProp = (relatedTableMeta.columns?.find((c) => c.pv) || relatedTableMeta.columns?.[0])?.title || ''
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!options?.triggerChildOnly) {
|
|
|
|
const response = !isPublic
|
|
|
|
? await api.dbViewRow.groupBy('noco', base.value.id, view.value.fk_model_id, view.value.id, {
|
|
|
|
offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByGroupLimit.value),
|
|
|
|
limit: group.paginationData.pageSize ?? groupByGroupLimit.value,
|
|
|
|
...params,
|
|
|
|
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
|
|
|
|
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
|
|
|
|
where: `${nestedWhere}`,
|
|
|
|
sort: `${getSortParams(groupby.sort)}${groupby.column.title}`,
|
|
|
|
column_name: groupby.column.title,
|
|
|
|
} as any)
|
|
|
|
: await api.public.dataGroupBy(
|
|
|
|
sharedView.value!.uuid!,
|
|
|
|
{
|
|
|
|
offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByGroupLimit.value),
|
|
|
|
limit: group.paginationData.pageSize ?? groupByGroupLimit.value,
|
|
|
|
...params,
|
|
|
|
where: nestedWhere,
|
|
|
|
sort: `${getSortParams(groupby.sort)}${groupby.column.title}`,
|
|
|
|
column_name: groupby.column.title,
|
|
|
|
sortsArr: sorts.value,
|
|
|
|
filtersArr: nestedFilters.value,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
headers: {
|
|
|
|
'xc-password': sharedViewPassword.value,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
group = await processGroupData(response, group)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (appInfo.value.ee) {
|
|
|
|
const aggregationMap = new Map<string, string>()
|
|
|
|
|
|
|
|
const aggregationParams = (group.children ?? []).map((child) => {
|
|
|
|
let key = child.key
|
|
|
|
|
|
|
|
if (!key?.length || key.startsWith(' ') || key.endsWith(' ')) {
|
|
|
|
key = Math.random().toString(36).substring(7)
|
|
|
|
aggregationMap.set(key, child.key)
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
key = JSON.parse(key)
|
|
|
|
if (typeof key === 'object') {
|
|
|
|
key = Math.random().toString(36).substring(7)
|
|
|
|
aggregationMap.set(key, child.key)
|
|
|
|
return {
|
|
|
|
where: calculateNestedWhere(child.nestedIn, where?.value),
|
|
|
|
alias: key,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
|
|
return {
|
|
|
|
where: calculateNestedWhere(child.nestedIn, where?.value),
|
|
|
|
alias: key,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const aggResponse = !isPublic
|
|
|
|
? await api.dbDataTableBulkAggregate.dbDataTableBulkAggregate(
|
|
|
|
meta.value!.id,
|
|
|
|
{
|
|
|
|
viewId: view.value!.id,
|
|
|
|
},
|
|
|
|
aggregationParams,
|
|
|
|
)
|
|
|
|
: await fetchBulkAggregatedData({}, aggregationParams)
|
|
|
|
|
|
|
|
Object.entries(aggResponse).forEach(([key, value]) => {
|
|
|
|
const child = (group?.children ?? []).find((c) => c.key.toString() === key.toString())
|
|
|
|
if (child) {
|
|
|
|
Object.assign(child.aggregations, value)
|
|
|
|
} else {
|
|
|
|
const originalKey = aggregationMap.get(key)
|
|
|
|
const child = (group?.children ?? []).find((c) => c.key.toString() === originalKey.toString())
|
|
|
|
if (child) {
|
|
|
|
Object.assign(child.aggregations, value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if (group?.children && group.nestedIn.length === groupBy.value.length - 1) {
|
|
|
|
const aliasMap = new Map<string, string>()
|
|
|
|
|
|
|
|
const childViewFilters = group?.children?.map((childGroup) => {
|
|
|
|
let key = childGroup.key
|
|
|
|
|
|
|
|
if (!key?.length || key.startsWith(' ') || key.endsWith(' ')) {
|
|
|
|
key = Math.random().toString(36).substring(7)
|
|
|
|
aliasMap.set(key, childGroup.key)
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
key = JSON.parse(key)
|
|
|
|
|
|
|
|
if (typeof key === 'object') {
|
|
|
|
key = Math.random().toString(36).substring(7)
|
|
|
|
aliasMap.set(key, childGroup.key)
|
|
|
|
}
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
|
|
return {
|
|
|
|
alias: key,
|
|
|
|
where: calculateNestedWhere(childGroup.nestedIn, where?.value),
|
|
|
|
offset:
|
|
|
|
((childGroup.paginationData.page ?? 0) - 1) * (childGroup.paginationData.pageSize ?? groupByRecordLimit.value),
|
|
|
|
limit: childGroup.paginationData.pageSize ?? groupByRecordLimit.value,
|
|
|
|
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
|
|
|
|
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (childViewFilters.length > 0) {
|
|
|
|
const bulkData = !isPublic
|
|
|
|
? await api.dbDataTableBulkList.dbDataTableBulkList(
|
|
|
|
meta.value.id,
|
|
|
|
{
|
|
|
|
viewId: view.value.id,
|
|
|
|
},
|
|
|
|
childViewFilters,
|
|
|
|
{},
|
|
|
|
)
|
|
|
|
: await fetchBulkListData({}, childViewFilters)
|
|
|
|
|
|
|
|
Object.entries(bulkData).forEach(([key, value]: { key: string; value: any }) => {
|
|
|
|
const child = (group?.children ?? []).find((c) => c.key.toString() === key.toString())
|
|
|
|
if (child) {
|
|
|
|
child.count = value.pageInfo.totalRows ?? 0
|
|
|
|
child.rows = formatData(value.list)
|
|
|
|
child.paginationData = value.pageInfo
|
|
|
|
} else {
|
|
|
|
const originalKey = aliasMap.get(key)
|
|
|
|
const child = (group?.children ?? []).find((c) => c.key.toString() === originalKey.toString())
|
|
|
|
if (child) {
|
|
|
|
child.count = value.pageInfo.totalRows ?? 0
|
|
|
|
child.rows = formatData(value.list)
|
|
|
|
child.paginationData = value.pageInfo
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (group?.children && group.nestedIn.length < groupBy.value.length - 1) {
|
|
|
|
const aliasMap = new Map<string, string>()
|
|
|
|
|
|
|
|
const childGroupFilters = group?.children?.map((childGroup) => {
|
|
|
|
const childGroupBy = groupBy.value[childGroup.nestedIn.length]
|
|
|
|
const childNestedWhere = calculateNestedWhere(childGroup.nestedIn, where?.value)
|
|
|
|
|
|
|
|
let key = childGroup.key
|
|
|
|
|
|
|
|
if (!key?.length || key.startsWith(' ') || key.endsWith(' ')) {
|
|
|
|
key = Math.random().toString(36).substring(7)
|
|
|
|
aliasMap.set(key, childGroup.key)
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
key = JSON.parse(key)
|
|
|
|
if (typeof key === 'object') {
|
|
|
|
key = Math.random().toString(36).substring(7)
|
|
|
|
aliasMap.set(key, childGroup.key)
|
|
|
|
}
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
|
|
return {
|
|
|
|
alias: key,
|
|
|
|
offset:
|
|
|
|
((childGroup.paginationData.page ?? 0) - 1) * (childGroup.paginationData.pageSize ?? groupByGroupLimit.value),
|
|
|
|
limit: childGroup.paginationData.pageSize ?? groupByGroupLimit.value,
|
|
|
|
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
|
|
|
|
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
|
|
|
|
where: `${childNestedWhere}`,
|
|
|
|
sort: `${getSortParams(childGroupBy.sort)}${childGroupBy.column.title}`,
|
|
|
|
column_name: childGroupBy.column.title,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (childGroupFilters.length > 0) {
|
|
|
|
const bulkGroupData = !isPublic
|
|
|
|
? await api.dbDataTableBulkGroupList.dbDataTableBulkGroupList(
|
|
|
|
meta.value.id,
|
|
|
|
{
|
|
|
|
viewId: view.value.id,
|
|
|
|
},
|
|
|
|
childGroupFilters,
|
|
|
|
)
|
|
|
|
: await fetchBulkGroupData({}, childGroupFilters)
|
|
|
|
|
|
|
|
for (const [key, value] of Object.entries(bulkGroupData)) {
|
|
|
|
let child = (group?.children ?? []).find((c) => c.key.toString() === key.toString())
|
|
|
|
if (!child) {
|
|
|
|
const originalKey = aliasMap.get(key)
|
|
|
|
child = (group?.children ?? []).find((c) => c.key.toString() === originalKey.toString())!
|
|
|
|
}
|
|
|
|
Object.assign(child, await processGroupData(value, child))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.log(e)
|
|
|
|
message.error(await extractSdkResponseErrorMsg(e))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function loadGroupData(group: Group, force = false, params: any = {}) {
|
|
|
|
try {
|
|
|
|
if (!base?.value?.id || !view.value?.id || !view.value?.fk_model_id) return
|
|
|
|
|
|
|
|
if (group.children && !force) return
|
|
|
|
|
|
|
|
if (!group.paginationData) {
|
|
|
|
group.paginationData = { page: 1, pageSize: groupByRecordLimit.value }
|
|
|
|
}
|
|
|
|
|
|
|
|
const nestedWhere = calculateNestedWhere(group.nestedIn, where?.value)
|
|
|
|
|
|
|
|
const query = {
|
|
|
|
offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByRecordLimit.value),
|
|
|
|
limit: group.paginationData.pageSize ?? groupByRecordLimit.value,
|
|
|
|
where: `${nestedWhere}`,
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = !isPublic
|
|
|
|
? await api.dbViewRow.list('noco', base.value.id, view.value.fk_model_id, view.value.id, {
|
|
|
|
...query,
|
|
|
|
...params,
|
|
|
|
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
|
|
|
|
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
|
|
|
|
} as any)
|
|
|
|
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value, ...query }, { isGroupBy: true })
|
|
|
|
|
|
|
|
group.count = response.pageInfo.totalRows ?? 0
|
|
|
|
group.rows = formatData(response.list)
|
|
|
|
group.paginationData = response.pageInfo
|
|
|
|
} catch (e) {
|
|
|
|
message.error(await extractSdkResponseErrorMsg(e))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function loadGroupAggregation(
|
|
|
|
group: Group,
|
|
|
|
fields?: Array<{
|
|
|
|
field: string
|
|
|
|
type: string
|
|
|
|
}>,
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
if (!meta?.value?.id || !view.value?.id || !view.value?.fk_model_id || !appInfo.value.ee) return
|
|
|
|
|
|
|
|
const filteredFields = fields?.filter((x) => x.type !== CommonAggregations.None)
|
|
|
|
|
|
|
|
if (filteredFields && !filteredFields?.length) return
|
|
|
|
|
|
|
|
const aggregationMap = new Map<string, string>()
|
|
|
|
|
|
|
|
const aggregationParams = (group.children ?? []).map((child) => {
|
|
|
|
let key = child.key
|
|
|
|
|
|
|
|
if (!key?.length || key.startsWith(' ') || key.endsWith(' ')) {
|
|
|
|
key = Math.random().toString(36).substring(7)
|
|
|
|
aggregationMap.set(key, child.key)
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
key = JSON.parse(child.key)
|
|
|
|
if (typeof key === 'object') {
|
|
|
|
key = Math.random().toString(36).substring(7)
|
|
|
|
aggregationMap.set(key, child.key)
|
|
|
|
}
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
|
|
return {
|
|
|
|
where: calculateNestedWhere(child.nestedIn, where?.value),
|
|
|
|
alias: key,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const response = !isPublic
|
|
|
|
? await api.dbDataTableBulkAggregate.dbDataTableBulkAggregate(
|
|
|
|
meta.value!.id,
|
|
|
|
{
|
|
|
|
viewId: view.value!.id,
|
|
|
|
...(filteredFields ? { aggregation: filteredFields } : {}),
|
|
|
|
},
|
|
|
|
aggregationParams,
|
|
|
|
)
|
|
|
|
: await fetchBulkAggregatedData(
|
|
|
|
{
|
|
|
|
...(filteredFields ? { aggregation: filteredFields } : {}),
|
|
|
|
},
|
|
|
|
aggregationParams,
|
|
|
|
)
|
|
|
|
|
|
|
|
Object.entries(response).forEach(([key, value]) => {
|
|
|
|
const child = (group.children ?? []).find((c) => c.key.toString() === key.toString())
|
|
|
|
if (child) {
|
|
|
|
Object.assign(child.aggregations, value)
|
|
|
|
} else {
|
|
|
|
const originalKey = aggregationMap.get(key)
|
|
|
|
if (originalKey) {
|
|
|
|
const child = (group.children ?? []).find((c) => c.key.toString() === originalKey.toString())
|
|
|
|
if (child) {
|
|
|
|
Object.assign(child.aggregations, value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} catch (e) {
|
|
|
|
message.error(await extractSdkResponseErrorMsg(e))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const loadGroupPage = async (group: Group, p: number) => {
|
|
|
|
if (!group.paginationData) {
|
|
|
|
group.paginationData = { page: 1, pageSize: groupByRecordLimit.value }
|
|
|
|
}
|
|
|
|
group.paginationData.page = p
|
|
|
|
await loadGroupData(group, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
const refreshNested = (group?: Group, nestLevel = 0) => {
|
|
|
|
group = group || rootGroup.value
|
|
|
|
if (!group) return
|
|
|
|
|
|
|
|
if (nestLevel < groupBy.value.length) {
|
|
|
|
group.nested = true
|
|
|
|
} else {
|
|
|
|
group.nested = false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (group.nested) {
|
|
|
|
if (group?.rows) {
|
|
|
|
group.rows = []
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (group?.children) {
|
|
|
|
group.children = []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nestLevel > groupBy.value.length) return
|
|
|
|
|
|
|
|
for (const child of group.children || []) {
|
|
|
|
refreshNested(child, nestLevel + 1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
watch(
|
|
|
|
() => groupBy.value.length,
|
|
|
|
async () => {
|
|
|
|
if (groupBy.value.length > 0) {
|
|
|
|
rootGroup.value.paginationData = { page: 1, pageSize: groupByGroupLimit.value }
|
|
|
|
rootGroup.value.column = {} as any
|
|
|
|
refreshNested()
|
|
|
|
nextTick(() => reloadViewDataHook?.trigger())
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
const findGroupByNestedIn = (nestedIn: GroupNestedIn[], group?: Group, nestLevel = 0): Group => {
|
|
|
|
group = group || rootGroup.value
|
|
|
|
if (nestLevel >= nestedIn.length) return group
|
|
|
|
const child = group.children?.find((g) => g.key === nestedIn[nestLevel].key)
|
|
|
|
if (child) {
|
|
|
|
if (child.nested) {
|
|
|
|
return findGroupByNestedIn(nestedIn, child, nestLevel + 1)
|
|
|
|
}
|
|
|
|
return child
|
|
|
|
}
|
|
|
|
return group
|
|
|
|
}
|
|
|
|
|
|
|
|
const parentGroup = (group: Group) => {
|
|
|
|
const parent = findGroupByNestedIn(group.nestedIn.slice(0, -1))
|
|
|
|
return parent
|
|
|
|
}
|
|
|
|
|
|
|
|
const modifyCount = (group: Group, countEffect: number) => {
|
|
|
|
if (!group) return
|
|
|
|
group.count += countEffect
|
|
|
|
// remove group if count is 0
|
|
|
|
if (group.count === 0) {
|
|
|
|
const parent = parentGroup(group)
|
|
|
|
if (parent) {
|
|
|
|
parent.children = parent.children?.filter((c) => c.key !== group.key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (group.root) return
|
|
|
|
modifyCount(parentGroup(group), countEffect)
|
|
|
|
}
|
|
|
|
|
|
|
|
const findGroupForRow = (row: Row, group?: Group, nestLevel = 0): { found: boolean; group: Group } => {
|
|
|
|
group = group || rootGroup.value
|
|
|
|
if (group.nested) {
|
|
|
|
const child = group.children?.find((g) => {
|
|
|
|
if (!groupBy.value[nestLevel].column.title) return undefined
|
|
|
|
|
|
|
|
return (
|
|
|
|
g.key ===
|
|
|
|
valueToTitle(row.row[groupBy.value[nestLevel].column.title!], groupBy.value[nestLevel].column, group.displayValueProp)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
if (child) {
|
|
|
|
return findGroupForRow(row, child, nestLevel + 1)
|
|
|
|
}
|
|
|
|
return { found: false, group }
|
|
|
|
}
|
|
|
|
return { found: true, group }
|
|
|
|
}
|
|
|
|
|
|
|
|
const redistributeRows = (group?: Group) => {
|
|
|
|
group = group || rootGroup.value
|
|
|
|
if (!group) return
|
|
|
|
if (!group.nested && group.rows) {
|
|
|
|
group.rows.forEach((row) => {
|
|
|
|
const properGroup = findGroupForRow(row)
|
|
|
|
if (properGroup.found) {
|
|
|
|
if (properGroup.group !== group) {
|
|
|
|
if (properGroup.group) {
|
|
|
|
properGroup.group.rows?.push(row)
|
|
|
|
modifyCount(properGroup.group, 1)
|
|
|
|
}
|
|
|
|
if (group) {
|
|
|
|
group.rows?.splice(group!.rows.indexOf(row), 1)
|
|
|
|
modifyCount(group, -1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (group) {
|
|
|
|
group.rows?.splice(group!.rows.indexOf(row), 1)
|
|
|
|
modifyCount(group, -1)
|
|
|
|
} else {
|
|
|
|
rootGroup.value.rows?.splice(rootGroup.value.rows!.indexOf(row), 1)
|
|
|
|
}
|
|
|
|
// if (properGroup.group?.children) loadGroups({}, properGroup.group)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
group.children?.forEach((g) => redistributeRows(g))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const loadAllowedLookups = async () => {
|
|
|
|
const filteredLookupCols = []
|
|
|
|
try {
|
|
|
|
for (const col of meta?.value?.columns || []) {
|
|
|
|
if (col.uidt !== UITypes.Lookup) continue
|
|
|
|
|
|
|
|
let nextCol: ColumnType = col
|
|
|
|
// check the lookup column is supported type or not
|
|
|
|
while (nextCol && nextCol.uidt === UITypes.Lookup) {
|
|
|
|
const lookupRelation = (await getMeta(nextCol.fk_model_id as string))?.columns?.find(
|
|
|
|
(c) => c.id === (nextCol?.colOptions as LookupType).fk_relation_column_id,
|
|
|
|
)
|
|
|
|
|
|
|
|
if (!lookupRelation?.colOptions) break
|
|
|
|
|
|
|
|
const relatedTableMeta = await getMeta(
|
|
|
|
(lookupRelation?.colOptions as LinkToAnotherRecordType).fk_related_model_id as string,
|
|
|
|
)
|
|
|
|
|
|
|
|
nextCol = relatedTableMeta?.columns?.find(
|
|
|
|
(c) => c.id === ((nextCol?.colOptions as LookupType).fk_lookup_column_id as string),
|
|
|
|
) as ColumnType
|
|
|
|
|
|
|
|
// if next column is same as root lookup column then break the loop
|
|
|
|
// since it's going to be a circular loop, and ignore the column
|
|
|
|
if (nextCol?.id === col.id) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nextCol?.uidt !== UITypes.Attachment && col.id) filteredLookupCols.push(col.id)
|
|
|
|
}
|
|
|
|
|
|
|
|
supportedLookups.value = filteredLookupCols
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
watch([() => view?.value?.id, () => meta.value?.columns], async ([newViewId]) => {
|
|
|
|
// reload only if view belongs to current table
|
|
|
|
if (newViewId && view.value?.fk_model_id === meta.value?.id) {
|
|
|
|
await loadAllowedLookups()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return {
|
|
|
|
rootGroup,
|
|
|
|
groupBy,
|
|
|
|
isGroupBy,
|
|
|
|
fieldsToGroupBy,
|
|
|
|
groupByLimit,
|
|
|
|
loadGroups,
|
|
|
|
loadGroupData,
|
|
|
|
loadGroupPage,
|
|
|
|
loadGroupAggregation,
|
|
|
|
groupWrapperChangePage,
|
|
|
|
redistributeRows,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
'useViewGroupBy',
|
|
|
|
)
|
|
|
|
|
|
|
|
export { useProvideViewGroupBy }
|
|
|
|
|
|
|
|
export function useViewGroupByOrThrow() {
|
|
|
|
const viewColumns = useViewGroupBy()
|
|
|
|
if (viewColumns == null) throw new Error('Please call `useProvideViewGroupBy` on the appropriate parent component')
|
|
|
|
return viewColumns
|
|
|
|
}
|