Style changes. Scrollbar improvments
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="px-3 pt-3">
|
||||
<div class="px-3 py-2 flex items-center justify-between gap-3">
|
||||
<FormSelectBase
|
||||
name="filter-logic"
|
||||
label="Filter Logic"
|
||||
@@ -21,19 +21,33 @@
|
||||
<span class="text-foreground text-body-2xs">{{ item.label }}</span>
|
||||
</template>
|
||||
</FormSelectBase>
|
||||
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<Ghost class="h-4 w-4" />
|
||||
<FormCheckbox
|
||||
:model-value="ghostMode"
|
||||
name="ghost-mode"
|
||||
hide-label
|
||||
size="sm"
|
||||
@update:model-value="handleGhostModeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FilterLogic } from '~/lib/viewer/helpers/filters/types'
|
||||
import { FormSelectBase } from '@speckle/ui-components'
|
||||
import { FormSelectBase, FormCheckbox } from '@speckle/ui-components'
|
||||
import { useFilterUtilities } from '~~/lib/viewer/composables/filtering'
|
||||
import { Ghost } from 'lucide-vue-next'
|
||||
import type { Optional } from '@speckle/shared'
|
||||
|
||||
defineProps<{
|
||||
modelValue: FilterLogic
|
||||
}>()
|
||||
|
||||
const { setFilterLogicAndUpdate } = useFilterUtilities()
|
||||
const { setFilterLogicAndUpdate, setGhostModeAndUpdate, ghostMode } =
|
||||
useFilterUtilities()
|
||||
|
||||
const filterLogicOptions = ref([
|
||||
{ value: FilterLogic.All, label: 'Match all rules' },
|
||||
@@ -50,4 +64,10 @@ const handleLogicChange = (
|
||||
setFilterLogicAndUpdate(option.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleGhostModeChange = (enabled: Optional<string | true> | string[]) => {
|
||||
// Convert the checkbox value to boolean
|
||||
const isEnabled = Array.isArray(enabled) ? enabled.length > 0 : !!enabled
|
||||
setGhostModeAndUpdate(isEnabled)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ViewerLayoutSidePanel>
|
||||
<ViewerLayoutSidePanel disable-scrollbar>
|
||||
<template #title>Filters</template>
|
||||
<template #actions>
|
||||
<div class="flex gap-x-0.5 items-center">
|
||||
@@ -34,9 +34,9 @@
|
||||
<!-- Active Filters Section -->
|
||||
<div
|
||||
v-if="propertyFilters.length > 0"
|
||||
class="flex-1 overflow-y-scroll simple-scrollbar"
|
||||
class="flex-1 overflow-y-auto simple-scrollbar"
|
||||
>
|
||||
<div class="flex flex-col gap-3 p-3">
|
||||
<div class="flex flex-col gap-3 p-3 pt-0">
|
||||
<ViewerFiltersFilterCard
|
||||
v-for="filter in propertyFilters"
|
||||
:key="filter.id"
|
||||
@@ -98,19 +98,26 @@ const relevantFilters = computed(() => {
|
||||
})
|
||||
|
||||
const propertySelectOptions = computed((): PropertySelectOption[] => {
|
||||
const allOptions: PropertySelectOption[] = relevantFilters.value.map((filter) => {
|
||||
const pathParts = filter.key.split('.')
|
||||
const propertyName = pathParts[pathParts.length - 1] // Last part (e.g., "name")
|
||||
const parentPath = pathParts.slice(0, -1).join('.') // Everything except last part (e.g., "ab")
|
||||
// Get keys of already added filters
|
||||
const existingFilterKeys = new Set(
|
||||
propertyFilters.value.map((f) => f.filter?.key).filter(Boolean)
|
||||
)
|
||||
|
||||
return {
|
||||
value: filter.key,
|
||||
label: propertyName, // Clean property name for main display
|
||||
parentPath, // Full path without the property name
|
||||
type: filter.type,
|
||||
hasParent: parentPath.length > 0
|
||||
}
|
||||
})
|
||||
const allOptions: PropertySelectOption[] = relevantFilters.value
|
||||
.filter((filter) => !existingFilterKeys.has(filter.key)) // Exclude already added filters
|
||||
.map((filter) => {
|
||||
const pathParts = filter.key.split('.')
|
||||
const propertyName = pathParts[pathParts.length - 1] // Last part (e.g., "name")
|
||||
const parentPath = pathParts.slice(0, -1).join('.') // Everything except last part (e.g., "ab")
|
||||
|
||||
return {
|
||||
value: filter.key,
|
||||
label: propertyName, // Clean property name for main display
|
||||
parentPath, // Full path without the property name
|
||||
type: filter.type,
|
||||
hasParent: parentPath.length > 0
|
||||
}
|
||||
})
|
||||
|
||||
// Sort: root properties first, then grouped by parent
|
||||
const sortedOptions = allOptions.sort((a, b) => {
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="shouldShowAppliedSection"
|
||||
class="bg-highlight-1 rounded-md mx-2 my-1 p-1"
|
||||
>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="value in displayedSelectedValues"
|
||||
:key="value"
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 bg-highlight-3 text-foreground text-body-3xs rounded"
|
||||
>
|
||||
{{ value }}
|
||||
<button
|
||||
class="text-foreground-2 hover:text-foreground"
|
||||
@click="() => toggleValue(value)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
v-if="remainingCount > 0"
|
||||
class="inline-flex items-center px-2 py-0.5 bg-highlight-3 text-foreground-2 text-body-3xs rounded"
|
||||
>
|
||||
and {{ remainingCount }} more
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ViewerFiltersFilterStringSelectAll
|
||||
:selected-count="selectedCount"
|
||||
:total-count="filteredValues.length"
|
||||
@@ -29,6 +56,11 @@
|
||||
:is-selected="isValueSelected(value)"
|
||||
:count="getValueCount(value)"
|
||||
:color="getValueColor(value)"
|
||||
:is-default-selected="
|
||||
isStringFilter(filter) &&
|
||||
filter.isDefaultAllSelected &&
|
||||
isValueSelected(value)
|
||||
"
|
||||
@toggle="() => toggleValue(value)"
|
||||
/>
|
||||
</div>
|
||||
@@ -89,6 +121,8 @@ const selectAll = (selected: boolean) => {
|
||||
// Deselect all - set to empty array in one operation
|
||||
updateActiveFilterValues(props.filter.id, [])
|
||||
}
|
||||
|
||||
// Note: default state clearing is now handled in updateActiveFilterValues
|
||||
}
|
||||
|
||||
// Get available values from the filter
|
||||
@@ -111,6 +145,35 @@ const filteredValues = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
// Get selected values
|
||||
const selectedValues = computed(() => {
|
||||
return props.filter.selectedValues || []
|
||||
})
|
||||
|
||||
// Check if we should show the applied section (not when select all is active)
|
||||
const shouldShowAppliedSection = computed(() => {
|
||||
return selectedValues.value.length > 0 && !isSelectAllActive.value
|
||||
})
|
||||
|
||||
// Check if select all is active (when all available values are selected)
|
||||
const isSelectAllActive = computed(() => {
|
||||
return (
|
||||
selectedValues.value.length === availableValues.value.length &&
|
||||
availableValues.value.length > 0
|
||||
)
|
||||
})
|
||||
|
||||
// Limit displayed values to 8 items
|
||||
const maxDisplayedItems = 5
|
||||
const displayedSelectedValues = computed(() => {
|
||||
return selectedValues.value.slice(0, maxDisplayedItems)
|
||||
})
|
||||
|
||||
// Count of remaining items not displayed
|
||||
const remainingCount = computed(() => {
|
||||
return Math.max(0, selectedValues.value.length - maxDisplayedItems)
|
||||
})
|
||||
|
||||
// Select all logic
|
||||
const selectedCount = computed(() => {
|
||||
return filteredValues.value.filter((value) => isValueSelected(value)).length
|
||||
@@ -118,7 +181,7 @@ const selectedCount = computed(() => {
|
||||
|
||||
// Virtual list setup
|
||||
const itemHeight = 28 // Height of each checkbox item in pixels
|
||||
const maxHeight = 144 // 36 * 4px (h-36 equivalent)
|
||||
const maxHeight = 210
|
||||
|
||||
const containerHeight = computed(() => {
|
||||
const contentHeight = filteredValues.value.length * itemHeight
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
: 'bg-foundation border-highlight-3 hover:border-foreground-2'
|
||||
]"
|
||||
>
|
||||
<Minus v-if="areAllValuesSelected" class="h-3 w-3" />
|
||||
<Check v-else-if="areSomeValuesSelected" class="h-3 w-3" />
|
||||
<Check v-if="areAllValuesSelected" class="h-3 w-3" />
|
||||
<Minus v-else-if="areSomeValuesSelected" class="h-3 w-3" />
|
||||
</div>
|
||||
<span class="text-foreground ml-px">Select all</span>
|
||||
<div class="text-foreground-2 text-body-3xs ml-1">
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<div class="px-1">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 text-body-3xs py-0.5 px-2 hover:bg-highlight-1 rounded cursor-pointer"
|
||||
:class="{ 'opacity-50': isDefaultSelected }"
|
||||
@click="$emit('toggle')"
|
||||
>
|
||||
<div class="flex items-center min-w-0">
|
||||
@@ -41,6 +42,7 @@ defineProps<{
|
||||
isSelected: boolean
|
||||
count: number
|
||||
color?: string | null
|
||||
isDefaultSelected?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: `${itemHeight}px`,
|
||||
transform: `translateY(${index * itemHeight}px)`
|
||||
height: `${getItemHeight(property)}px`,
|
||||
transform: `translateY(${getItemOffset(index)}px)`
|
||||
}"
|
||||
>
|
||||
<div class="px-1">
|
||||
<button
|
||||
class="w-full h-full py-1.5 px-2 text-foreground rounded hover:bg-highlight-1 text-left flex items-center gap-2"
|
||||
class="w-full h-full px-2 text-foreground rounded hover:bg-highlight-1 text-left flex items-center gap-2"
|
||||
:class="!property.parentPath ? 'py-1.5' : 'py-1'"
|
||||
@click="$emit('selectProperty', property.value)"
|
||||
>
|
||||
<Hash v-if="property.type === 'number'" class="h-3 w-3" />
|
||||
@@ -34,8 +35,11 @@
|
||||
<div class="text-body-2xs font-medium text-foreground truncate">
|
||||
{{ property.label }}
|
||||
</div>
|
||||
<div class="text-body-3xs text-foreground-2 truncate -mt-0.5">
|
||||
{{ property.parentPath || '-' }}
|
||||
<div
|
||||
v-if="property.parentPath"
|
||||
class="text-body-3xs text-foreground-2 truncate -mt-0.5"
|
||||
>
|
||||
{{ property.parentPath }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -82,17 +86,39 @@ const filteredOptions = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
// Virtual list setup
|
||||
const itemHeight = 42
|
||||
// Virtual list setup with dynamic heights
|
||||
const smallItemHeight = 28 // Height for items without parent
|
||||
const largeItemHeight = 38 // Height for items with parent
|
||||
const maxHeight = 300 // Maximum height for the container
|
||||
|
||||
// Helper function to get height for a specific property
|
||||
const getItemHeight = (property: PropertyOption) => {
|
||||
return property.parentPath && property.parentPath !== '-'
|
||||
? largeItemHeight
|
||||
: smallItemHeight
|
||||
}
|
||||
|
||||
// Helper function to calculate offset for a specific index
|
||||
const getItemOffset = (index: number) => {
|
||||
let offset = 0
|
||||
for (let i = 0; i < index; i++) {
|
||||
const property = filteredOptions.value[i]
|
||||
if (property) {
|
||||
offset += getItemHeight(property)
|
||||
}
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
const containerHeight = computed(() => {
|
||||
const contentHeight = filteredOptions.value.length * itemHeight
|
||||
const contentHeight = filteredOptions.value.reduce((total, property) => {
|
||||
return total + getItemHeight(property)
|
||||
}, 0)
|
||||
return `${Math.min(contentHeight, maxHeight)}px`
|
||||
})
|
||||
|
||||
const { list, containerProps } = useVirtualList(filteredOptions, {
|
||||
itemHeight,
|
||||
itemHeight: largeItemHeight, // Use larger height as base for virtual list calculations
|
||||
overscan: 5
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div :class="['flex flex-col h-full', disableScrollbar ? '' : 'overflow-hidden']">
|
||||
<div
|
||||
:class="[
|
||||
'flex flex-col h-full max-h-[calc(100dvh-5rem)]',
|
||||
disableScrollbar ? '' : 'overflow-hidden'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="flex shrink-0 justify-between items-center border-b border-outline-3 h-10 pl-4 pr-2.5"
|
||||
>
|
||||
|
||||
@@ -45,6 +45,7 @@ function createFilteringDataStore() {
|
||||
const dataSourcesMap: Ref<Record<string, DataSource>> = ref({})
|
||||
const dataSources = computed(() => Object.values(dataSourcesMap.value))
|
||||
const currentFilterLogic = ref<FilterLogic>(FilterLogic.All)
|
||||
const ghostMode = ref<boolean>(true) // Default to ghosting enabled
|
||||
const dataSlices: Ref<DataSlice[]> = ref([])
|
||||
|
||||
const extractNestedProperties = (
|
||||
@@ -330,6 +331,10 @@ function createFilteringDataStore() {
|
||||
currentFilterLogic.value = logic
|
||||
}
|
||||
|
||||
const setGhostMode = (enabled: boolean) => {
|
||||
ghostMode.value = enabled
|
||||
}
|
||||
|
||||
const updateViewer = (
|
||||
instance: Viewer,
|
||||
filters: {
|
||||
@@ -340,32 +345,42 @@ function createFilteringDataStore() {
|
||||
const objectIds = getFinalObjectIds()
|
||||
const filteringExtension = instance.getExtension(FilteringExtension)
|
||||
|
||||
// Always clear existing filters first to prevent accumulation
|
||||
filteringExtension.resetFilters()
|
||||
|
||||
// Check if there are any applied filters
|
||||
const hasAppliedFilters = filters.propertyFilters.value.some(
|
||||
(filter) => filter.isApplied
|
||||
)
|
||||
|
||||
if (objectIds.length > 0) {
|
||||
// Clear existing property-filter isolation first to prevent accumulation
|
||||
filteringExtension.resetFilters()
|
||||
// Isolate the matching objects (ghost parameter controls transparency vs hiding)
|
||||
filteringExtension.isolateObjects(
|
||||
objectIds,
|
||||
'property-filters',
|
||||
true,
|
||||
ghostMode.value
|
||||
)
|
||||
} else if (hasAppliedFilters) {
|
||||
// When no objects match but filters are applied, isolate a fake object ID to ghost/hide everything
|
||||
// This provides better visual feedback than completely hiding everything
|
||||
filteringExtension.isolateObjects(
|
||||
['no-match-ghost-all'],
|
||||
'property-filters',
|
||||
true,
|
||||
ghostMode.value
|
||||
)
|
||||
}
|
||||
// If no applied filters and no objects match, do nothing - return to unfiltered state
|
||||
|
||||
filteringExtension.isolateObjects(objectIds, 'property-filters', true, true)
|
||||
} else {
|
||||
// Preserve color filter when clearing isolation
|
||||
const currentColorFilterId = filters.activeColorFilterId.value
|
||||
let activeColorFilter = null
|
||||
|
||||
if (currentColorFilterId) {
|
||||
const activeFilter = filters.propertyFilters.value.find(
|
||||
(f: FilterData) => f.id === currentColorFilterId
|
||||
)
|
||||
if (activeFilter?.filter) {
|
||||
activeColorFilter = activeFilter.filter
|
||||
}
|
||||
}
|
||||
|
||||
// Reset all filters (including isolation)
|
||||
filteringExtension.resetFilters()
|
||||
|
||||
// Restore color filter if it was active
|
||||
if (activeColorFilter && currentColorFilterId) {
|
||||
filteringExtension.setColorFilter(activeColorFilter)
|
||||
filters.activeColorFilterId.value = currentColorFilterId
|
||||
// Restore color filter if it was active
|
||||
const currentColorFilterId = filters.activeColorFilterId.value
|
||||
if (currentColorFilterId) {
|
||||
const activeFilter = filters.propertyFilters.value.find(
|
||||
(f: FilterData) => f.id === currentColorFilterId
|
||||
)
|
||||
if (activeFilter?.filter) {
|
||||
filteringExtension.setColorFilter(activeFilter.filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,6 +398,8 @@ function createFilteringDataStore() {
|
||||
clearPropertyIndexCache,
|
||||
setFilterLogic,
|
||||
currentFilterLogic,
|
||||
setGhostMode,
|
||||
ghostMode,
|
||||
dataSlices
|
||||
}
|
||||
}
|
||||
@@ -515,7 +532,7 @@ export function useFilterUtilities(
|
||||
* Creates a properly typed FilterData object from PropertyInfo
|
||||
*/
|
||||
const createFilterData = (params: CreateFilterParams): FilterData => {
|
||||
const { filter, id } = params
|
||||
const { filter, id, availableValues } = params
|
||||
|
||||
if (isNumericPropertyInfo(filter)) {
|
||||
return {
|
||||
@@ -534,11 +551,12 @@ export function useFilterUtilities(
|
||||
return {
|
||||
id,
|
||||
isApplied: true,
|
||||
selectedValues: [],
|
||||
selectedValues: [...availableValues], // Select all values by default
|
||||
condition: StringFilterCondition.Is,
|
||||
type: FilterType.String,
|
||||
filter: filter as StringPropertyInfo,
|
||||
numericRange: { min: 0, max: 100 } // Default range for consistency
|
||||
numericRange: { min: 0, max: 100 }, // Default range for consistency
|
||||
isDefaultAllSelected: true // Track that this is the initial "all selected" state
|
||||
} satisfies StringFilterData
|
||||
}
|
||||
}
|
||||
@@ -607,6 +625,12 @@ export function useFilterUtilities(
|
||||
const filter = filters.propertyFilters.value.find((f) => f.id === filterId)
|
||||
if (filter) {
|
||||
filter.selectedValues = [...values]
|
||||
|
||||
// Clear the default all-selected state when user updates values
|
||||
if (!isNumericFilter(filter) && filter.isDefaultAllSelected) {
|
||||
filter.isDefaultAllSelected = false
|
||||
}
|
||||
|
||||
updateDataStoreSlices()
|
||||
}
|
||||
}
|
||||
@@ -647,6 +671,11 @@ export function useFilterUtilities(
|
||||
updateDataStoreSlices()
|
||||
}
|
||||
|
||||
const setGhostModeAndUpdate = (enabled: boolean) => {
|
||||
dataStore.setGhostMode(enabled)
|
||||
updateDataStoreSlices()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates data store slices based on current filter state (optimized)
|
||||
*/
|
||||
@@ -681,7 +710,7 @@ export function useFilterUtilities(
|
||||
}
|
||||
dataStore.dataSlices.value.push(slice)
|
||||
}
|
||||
// Handle string filters with selected values
|
||||
// Handle string filters - only create slice if filter is enabled AND has selected values
|
||||
else if (
|
||||
!isNumericFilter(filter) &&
|
||||
filter.isApplied &&
|
||||
@@ -722,14 +751,21 @@ export function useFilterUtilities(
|
||||
const index = filter.selectedValues.indexOf(value)
|
||||
const wasSelected = index > -1
|
||||
|
||||
if (wasSelected) {
|
||||
filter.selectedValues.splice(index, 1)
|
||||
// Special behavior for default all-selected state: first click selects only that item
|
||||
if (!isNumericFilter(filter) && filter.isDefaultAllSelected) {
|
||||
filter.selectedValues = [value] // Select only this item
|
||||
filter.isDefaultAllSelected = false // Clear default state
|
||||
} else {
|
||||
filter.selectedValues.push(value)
|
||||
// Normal toggle behavior
|
||||
if (wasSelected) {
|
||||
filter.selectedValues.splice(index, 1)
|
||||
} else {
|
||||
filter.selectedValues.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure filter is marked as applied when values are selected
|
||||
filter.isApplied = filter.selectedValues.length > 0
|
||||
// Don't change isApplied here - that's controlled by the visibility toggle
|
||||
// isApplied represents whether the filter is enabled, not whether it has values
|
||||
|
||||
updateDataStoreSlices()
|
||||
}
|
||||
@@ -743,6 +779,23 @@ export function useFilterUtilities(
|
||||
return filter ? filter.selectedValues.includes(value) : false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a filter has content (selected values or numeric range)
|
||||
*/
|
||||
const filterHasContent = (filter: FilterData): boolean => {
|
||||
if (isNumericFilter(filter)) {
|
||||
// For numeric filters, check if range is different from default
|
||||
const defaultMin = filter.filter.min
|
||||
const defaultMax = filter.filter.max
|
||||
return (
|
||||
filter.numericRange.min !== defaultMin || filter.numericRange.max !== defaultMax
|
||||
)
|
||||
} else {
|
||||
// For string filters, check if any values are selected
|
||||
return filter.selectedValues.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all currently applied filters
|
||||
*/
|
||||
@@ -1140,9 +1193,11 @@ export function useFilterUtilities(
|
||||
updateFilterCondition,
|
||||
|
||||
setFilterLogicAndUpdate,
|
||||
setGhostModeAndUpdate,
|
||||
updateDataStoreSlices,
|
||||
toggleActiveFilterValue,
|
||||
isActiveFilterValueSelected,
|
||||
filterHasContent,
|
||||
getAppliedFilters,
|
||||
resetFilters,
|
||||
resetExplode,
|
||||
@@ -1172,6 +1227,8 @@ export function useFilterUtilities(
|
||||
// Filter logic
|
||||
setFilterLogic: dataStore.setFilterLogic,
|
||||
currentFilterLogic: dataStore.currentFilterLogic,
|
||||
getFinalObjectIds: dataStore.getFinalObjectIds
|
||||
getFinalObjectIds: dataStore.getFinalObjectIds,
|
||||
// Ghost mode
|
||||
ghostMode: dataStore.ghostMode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,7 +1109,9 @@ function setupInterfaceState(
|
||||
if (hiddenObjectIds.value.length) return true
|
||||
if (
|
||||
propertyFilters.value.some(
|
||||
(filter) => filter.selectedValues && filter.selectedValues.length > 0
|
||||
(filter) =>
|
||||
filter.isApplied ||
|
||||
(filter.selectedValues && filter.selectedValues.length > 0)
|
||||
)
|
||||
)
|
||||
return true
|
||||
|
||||
@@ -77,6 +77,7 @@ export type NumericFilterData = BaseFilterData & {
|
||||
export type StringFilterData = BaseFilterData & {
|
||||
type: FilterType.String
|
||||
filter: StringPropertyInfo
|
||||
isDefaultAllSelected?: boolean // Track initial "all selected" state
|
||||
}
|
||||
|
||||
// Union type for all filter data
|
||||
|
||||
Reference in New Issue
Block a user