Files
nocodb/packages/nc-gui/components/nc/List/AggregationSelector.vue
2026-02-24 14:03:22 +00:00

211 lines
6.0 KiB
Vue

<script setup lang="ts">
import { CommonAggregations, UITypes, getAvailableAggregations } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
interface Props {
baseId?: string
tableId?: string
columnId?: string
value?: string
forceLayout?: 'vertical' | 'horizontal'
filterAggregation?: (aggregation: string) => boolean
forceLoadColumnMeta?: boolean
disableLabel?: boolean
autoSelect?: boolean
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
forceLoadColumnMeta: false,
disableLabel: false,
autoSelect: false,
disabled: false,
})
const emit = defineEmits<{
'update:value': [value: string | undefined]
}>()
const { t } = useI18n()
const { getMeta } = useMetas()
const modelValue = useVModel(props, 'value', emit)
const isOpenAggregationSelectDropdown = ref(false)
const handleValueUpdate = (value: any) => {
const stringValue = String(value)
modelValue.value = stringValue
}
const column = ref<ColumnType | null>(null)
const isLoading = ref(false)
const aggregationList = ref<Array<{ label: string; value: string; ncItemDisabled: boolean; ncItemTooltip: string }>>([])
const loadAggregationList = async () => {
if (!props.tableId || !props.columnId) {
isLoading.value = false
aggregationList.value = []
return
}
try {
isLoading.value = true
const tableMeta = await getMeta(props.baseId, props.tableId, undefined, false, true)
if (!tableMeta) {
aggregationList.value = []
return
}
const columnMeta = tableMeta.columns?.find((col) => col.id === props.columnId)
if (!columnMeta) {
aggregationList.value = []
return
}
column.value = columnMeta
let availableAggregations: string[]
if (columnMeta.uidt === UITypes.Formula && columnMeta.colOptions && 'parsed_tree' in columnMeta.colOptions) {
availableAggregations = getAvailableAggregations(columnMeta.uidt, (columnMeta.colOptions as any).parsed_tree)
} else {
availableAggregations = getAvailableAggregations(columnMeta.uidt!)
}
// Filter out None aggregation
availableAggregations = availableAggregations.filter((agg) => agg !== CommonAggregations.None)
// Apply custom filter if provided
if (props.filterAggregation) {
availableAggregations = availableAggregations.filter(props.filterAggregation)
}
aggregationList.value = availableAggregations.map((agg) => {
return {
label: t(`aggregation_type.${agg}`),
value: agg,
ncItemDisabled: false,
ncItemTooltip: '',
}
})
} catch (error) {
aggregationList.value = []
} finally {
isLoading.value = false
}
}
watch([() => props.tableId, () => props.columnId], () => {
loadAggregationList()
})
const aggregationListMap = computed(() => {
if (!aggregationList.value || aggregationList.value.length === 0) return new Map()
return new Map(aggregationList.value.map((agg) => [agg.value, agg]))
})
const selectedAggregation = computed(() => {
if (!aggregationListMap.value || aggregationListMap.value.size === 0) return undefined
return aggregationListMap.value.get(modelValue.value) || undefined
})
watch(
aggregationList,
(newAggregationList) => {
if (newAggregationList && newAggregationList.length > 0) {
const newAggregationListMap = new Map(newAggregationList.map((agg) => [agg.value, agg]))
// Check if current value exists in the new aggregation list
if (modelValue.value && !newAggregationListMap.has(modelValue.value)) {
// Current value is not in the list, set null to clear it
modelValue.value = undefined
return
}
// Auto-select logic (only if autoSelect is enabled and no current value)
if (!modelValue.value && props.autoSelect) {
const newAggregationValue = newAggregationList[0]?.value
modelValue.value = newAggregationValue
}
}
},
{ immediate: true },
)
onMounted(() => {
loadAggregationList()
})
defineExpose({
modelValue,
selectedAggregation,
isOpenAggregationSelectDropdown,
aggregationList,
aggregationListMap,
})
</script>
<template>
<a-form-item
name="aggregationId"
class="!mb-0 nc-aggregation-selector"
:class="`nc-force-layout-${forceLayout}`"
:validate-status="selectedAggregation?.ncItemDisabled ? 'error' : ''"
:help="selectedAggregation?.ncItemDisabled ? [selectedAggregation.ncItemTooltip] : []"
@click.stop
@dblclick.stop
>
<template v-if="!disableLabel" #label>
<div>
<slot name="label">{{ t('general.aggregation') }}</slot>
</div>
</template>
<NcListDropdown
v-model:is-open="isOpenAggregationSelectDropdown"
:disabled="isLoading || disabled"
:has-error="!!selectedAggregation?.ncItemDisabled"
>
<div class="flex-1 flex items-center gap-2 min-w-0">
<NcTooltip hide-on-click class="flex-1 truncate" show-on-truncate-only>
<span
v-if="selectedAggregation"
:key="selectedAggregation?.value"
class="text-sm flex-1 truncate"
:class="{ 'text-nc-content-gray-muted': !selectedAggregation }"
>
{{ selectedAggregation?.label }}
</span>
<span v-else class="text-sm flex-1 truncate text-nc-content-gray-muted">-- Select aggregation --</span>
<template #title>
{{ selectedAggregation?.label || 'Select aggregation' }}
</template>
</NcTooltip>
<GeneralIcon
icon="ncChevronDown"
class="flex-none h-4 w-4 transition-transform opacity-70"
:class="{ 'transform rotate-180': isOpenAggregationSelectDropdown }"
/>
</div>
<template #overlay="{ onEsc }">
<NcList
v-model:open="isOpenAggregationSelectDropdown"
:value="modelValue || selectedAggregation?.value || ''"
:list="aggregationList"
variant="medium"
class="!w-auto"
wrapper-class-name="!h-auto"
@update:value="handleValueUpdate"
@escape="onEsc"
>
</NcList>
</template>
</NcListDropdown>
</a-form-item>
</template>