diff --git a/packages/frontend-2/components/viewer/filters/filter/string/Checkboxes.vue b/packages/frontend-2/components/viewer/filters/filter/string/Checkboxes.vue index 0d69408ee..c24779614 100644 --- a/packages/frontend-2/components/viewer/filters/filter/string/Checkboxes.vue +++ b/packages/frontend-2/components/viewer/filters/filter/string/Checkboxes.vue @@ -2,9 +2,8 @@
@@ -37,27 +36,13 @@
@@ -81,14 +66,7 @@ const props = defineProps<{ searchQuery?: string }>() -const { - toggleActiveFilterValue, - updateActiveFilterValues, - isActiveFilterValueSelected, - getFilterValueColor, - getAvailableFilterValues, - filters -} = useFilterUtilities() +const { toggleActiveFilterValue, getFilteredFilterValues } = useFilterUtilities() const showSortMenu = ref(false) const sortMode = ref<'selected-first' | 'alphabetical'>('alphabetical') @@ -108,87 +86,31 @@ const sortMenuItems = computed(() => [ ] ]) -// Handle sort option selection -const onSortOptionChosen = ({ item }: { item: LayoutMenuItem; event: MouseEvent }) => { - sortMode.value = item.id as 'selected-first' | 'alphabetical' - showSortMenu.value = false -} - -const isValueSelected = (value: string): boolean => { - return isActiveFilterValueSelected(props.filter.id, value) -} - -const getValueCount = (_value: string): number => { - return 1 -} - -const getValueColor = (value: string): string | null => { - if (filters.activeColorFilterId.value !== props.filter.id) { - return null - } - return getFilterValueColor(value) -} - -const toggleValue = (value: string) => { - toggleActiveFilterValue(props.filter.id, value) -} - -const selectAll = (selected: boolean) => { - if (!isStringFilter(props.filter) || !props.filter.filter) return - - const allAvailableValues = getAvailableFilterValues(props.filter.filter) - if (selected) { - updateActiveFilterValues(props.filter.id, allAvailableValues) - } else { - updateActiveFilterValues(props.filter.id, []) - } -} - -const availableValues = computed(() => { +const filteredValues = computed(() => { if (isStringFilter(props.filter) && props.filter.filter) { - return getAvailableFilterValues(props.filter.filter) + return getFilteredFilterValues(props.filter.filter, { + sortMode: sortMode.value, + filterId: props.filter.id + }) } return [] }) -const filteredValues = computed(() => { - let values = availableValues.value - - if (props.searchQuery?.trim()) { - const searchTerm = props.searchQuery.toLowerCase().trim() - values = values.filter((value: string) => value.toLowerCase().includes(searchTerm)) - } - - if (sortMode.value === 'selected-first') { - // Sort: selected first, then alphabetical - const selectedValues = values.filter((value: string) => isValueSelected(value)) - const unselectedValues = values.filter((value: string) => !isValueSelected(value)) - - // Sort each group alphabetically - const sortedSelectedValues = selectedValues.sort((a, b) => a.localeCompare(b)) - const sortedUnselectedValues = unselectedValues.sort((a, b) => a.localeCompare(b)) - - return [...sortedSelectedValues, ...sortedUnselectedValues] - } else { - // Sort: pure alphabetical - return values.sort((a, b) => a.localeCompare(b)) - } -}) - -const selectedCount = computed(() => { - return filteredValues.value.filter((value) => isValueSelected(value)).length -}) - const itemHeight = 28 // Height of each checkbox item in pixels const maxHeight = 240 +const { list, containerProps } = useVirtualList(filteredValues, { + itemHeight: 28, + overscan: 5 +}) + const containerHeight = computed(() => { const contentHeight = filteredValues.value.length * itemHeight return `${Math.min(contentHeight, maxHeight)}px` }) -const { list, containerProps } = useVirtualList(filteredValues, { - itemHeight, - overscan: 5 -}) +const onSortOptionChosen = ({ item }: { item: LayoutMenuItem; event: MouseEvent }) => { + sortMode.value = item.id as 'selected-first' | 'alphabetical' + showSortMenu.value = false +} diff --git a/packages/frontend-2/components/viewer/filters/filter/string/SelectAll.vue b/packages/frontend-2/components/viewer/filters/filter/string/SelectAll.vue index ed4203cf1..5818133d9 100644 --- a/packages/frontend-2/components/viewer/filters/filter/string/SelectAll.vue +++ b/packages/frontend-2/components/viewer/filters/filter/string/SelectAll.vue @@ -26,30 +26,55 @@ diff --git a/packages/frontend-2/components/viewer/filters/filter/string/ValueItem.vue b/packages/frontend-2/components/viewer/filters/filter/string/ValueItem.vue index c46fb6015..76b63f192 100644 --- a/packages/frontend-2/components/viewer/filters/filter/string/ValueItem.vue +++ b/packages/frontend-2/components/viewer/filters/filter/string/ValueItem.vue @@ -15,7 +15,7 @@ 'opacity-50 dark:!bg-transparent !border !border-outline-5 !group-hover:border-outline-5': isDefaultSelected }" - :name="`filter-${filterId}-${value}`" + :name="`filter-${filter.id}-${value}`" :model-value="isSelected" hide-label /> @@ -24,7 +24,7 @@
-
+
{{ count }}
import { FormCheckbox } from '@speckle/ui-components' +import { useFilterUtilities } from '~~/lib/viewer/composables/filtering' +import { isStringFilter, type FilterData } from '~/lib/viewer/helpers/filters/types' -defineProps<{ - filterId: string +const props = defineProps<{ + filter: FilterData value: string - isSelected: boolean - count: number - color?: string | null - isDefaultSelected?: boolean }>() defineEmits<{ toggle: [] }>() + +const { + isActiveFilterValueSelected, + getFilterValueColor, + getPropertyValueCounts, + filters +} = useFilterUtilities() + +const isSelected = computed(() => + isActiveFilterValueSelected(props.filter.id, props.value) +) + +const count = computed(() => { + if (!props.filter.filter?.key) return null + const counts = getPropertyValueCounts(props.filter.filter.key) + return counts[props.value] || 0 +}) + +const color = computed(() => { + if (filters.activeColorFilterId.value !== props.filter.id) { + return null + } + return getFilterValueColor(props.value) +}) + +const isDefaultSelected = computed(() => { + return ( + isStringFilter(props.filter) && + props.filter.isDefaultAllSelected && + isSelected.value + ) +}) diff --git a/packages/frontend-2/lib/viewer/composables/filtering.ts b/packages/frontend-2/lib/viewer/composables/filtering.ts index 594ed3117..37fa097ba 100644 --- a/packages/frontend-2/lib/viewer/composables/filtering.ts +++ b/packages/frontend-2/lib/viewer/composables/filtering.ts @@ -406,7 +406,9 @@ function createFilteringDataStore() { currentFilterLogic, setGhostMode, ghostMode, - dataSlices + dataSlices, + dataSources, + buildPropertyIndex } } @@ -534,6 +536,43 @@ export function useFilterUtilities( return [] } + /** + * Gets counts for all values of a property at once (performance optimized) + */ + const getPropertyValueCounts = (propertyKey: string): Record => { + const valueCounts: Record = {} + + for (const dataSource of dataStore.dataSources.value) { + const propertyIndex = dataStore.buildPropertyIndex(dataSource, propertyKey) + + for (const [value, objectIds] of Object.entries(propertyIndex)) { + if (!valueCounts[value]) { + valueCounts[value] = 0 + } + valueCounts[value] += objectIds.length + } + } + + return valueCounts + } + + /** + * Gets the count of objects that have a specific value for a property + * Note: For better performance when getting multiple counts, use getPropertyValueCounts + */ + const getPropertyValueCount = (propertyKey: string, value: string): number => { + let totalCount = 0 + + for (const dataSource of dataStore.dataSources.value) { + const propertyIndex = dataStore.buildPropertyIndex(dataSource, propertyKey) + if (propertyIndex && propertyIndex[value]) { + totalCount += propertyIndex[value].length + } + } + + return totalCount + } + /** * Creates a properly typed FilterData object from PropertyInfo */ @@ -1204,6 +1243,50 @@ export function useFilterUtilities( return color.startsWith('#') ? color : `#${color}` } + /** + * Gets filtered and sorted values for a string filter with search and sorting options + */ + const getFilteredFilterValues = ( + filter: PropertyInfo, + options?: { + searchQuery?: string + sortMode?: 'alphabetical' | 'selected-first' + filterId?: string + } + ): string[] => { + const { searchQuery, sortMode = 'alphabetical', filterId } = options || {} + + let values = getAvailableFilterValues(filter) + + // Apply search filtering + if (searchQuery?.trim()) { + const searchTerm = searchQuery.toLowerCase().trim() + values = values.filter((value: string) => + value.toLowerCase().includes(searchTerm) + ) + } + + // Apply sorting + if (sortMode === 'selected-first' && filterId) { + // Sort: selected first, then alphabetical + const selectedValues = values.filter((value: string) => + isActiveFilterValueSelected(filterId, value) + ) + const unselectedValues = values.filter( + (value: string) => !isActiveFilterValueSelected(filterId, value) + ) + + // Sort each group alphabetically + const sortedSelectedValues = selectedValues.sort((a, b) => a.localeCompare(b)) + const sortedUnselectedValues = unselectedValues.sort((a, b) => a.localeCompare(b)) + + return [...sortedSelectedValues, ...sortedUnselectedValues] + } else { + // Sort: pure alphabetical + return values.sort((a, b) => a.localeCompare(b)) + } + } + return { isolateObjects, unIsolateObjects, @@ -1211,6 +1294,8 @@ export function useFilterUtilities( showObjects, filters, getAvailableFilterValues, + getPropertyValueCount, + getPropertyValueCounts, addActiveFilter, updateFilterProperty, removeActiveFilter, @@ -1243,6 +1328,8 @@ export function useFilterUtilities( toggleColorFilter, getFilterColorGroups, getFilterValueColor, + // Filtered values + getFilteredFilterValues, // Numeric range filtering setNumericRange, // Filter logic