Design updates
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
</template>
|
||||
|
||||
<!-- Filter Logic Selection -->
|
||||
<ViewerFiltersFilterLogicSelector
|
||||
<ViewerFiltersLogicSelector
|
||||
v-if="propertyFilters.length > 0"
|
||||
v-model="filterLogic"
|
||||
@update:model-value="handleFilterLogicChange"
|
||||
@@ -43,7 +43,6 @@
|
||||
:key="filter.id"
|
||||
:filter="filter"
|
||||
collapsed
|
||||
@select-condition="(val) => handleConditionSelect(filter.id, val)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,7 +70,6 @@ import {
|
||||
} from '~~/lib/viewer/composables/setup'
|
||||
import type {
|
||||
PropertySelectOption,
|
||||
ConditionOption,
|
||||
FilterLogicOption
|
||||
} from '~/lib/viewer/helpers/filters/types'
|
||||
import { FilterLogic } from '~/lib/viewer/helpers/filters/types'
|
||||
@@ -84,7 +82,6 @@ const {
|
||||
filters: { propertyFilters },
|
||||
getRelevantFilters,
|
||||
addActiveFilter,
|
||||
updateFilterCondition,
|
||||
resetFilters,
|
||||
setFilterLogic
|
||||
} = useFilterUtilities()
|
||||
@@ -177,10 +174,6 @@ const selectProperty = (propertyKey: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleConditionSelect = (filterId: string, conditionOption: ConditionOption) => {
|
||||
updateFilterCondition(filterId, conditionOption.value)
|
||||
}
|
||||
|
||||
const handleFilterLogicChange = (logicOption: FilterLogicOption) => {
|
||||
filterLogic.value = logicOption.value
|
||||
}
|
||||
|
||||
@@ -1,47 +1,23 @@
|
||||
<template>
|
||||
<div class="border border-outline-2 rounded-lg">
|
||||
<div class="p-1" :class="{ 'border-b border-outline-3 pb-0.5': !collapsed }">
|
||||
<div class="p-1" :class="{ 'border-b border-outline-3': !collapsed }">
|
||||
<ViewerFiltersFilterHeader v-model:collapsed="collapsed" :filter="filter" />
|
||||
|
||||
<ViewerFiltersFilterConditionSelector
|
||||
v-if="!collapsed"
|
||||
:filter="filter"
|
||||
@select-condition="$emit('selectCondition', $event)"
|
||||
/>
|
||||
|
||||
<ViewerSearchInput
|
||||
v-if="filter.type === FilterType.String && !collapsed"
|
||||
v-model="searchQuery"
|
||||
placeholder="Search for a value..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="filter.filter && !collapsed">
|
||||
<ViewerFiltersFilterValuesNumericRange
|
||||
v-if="isNumericFilter(filter)"
|
||||
:filter="filter"
|
||||
/>
|
||||
|
||||
<ViewerFiltersFilterValuesStringCheckboxes
|
||||
v-else
|
||||
:filter="filter"
|
||||
:search-query="searchQuery"
|
||||
/>
|
||||
<ViewerFiltersFilterNumeric v-if="isNumericFilter(filter)" :filter="filter" />
|
||||
<ViewerFiltersFilterString v-else :filter="filter" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FilterData } from '~/lib/viewer/helpers/filters/types'
|
||||
import { FilterType, isNumericFilter } from '~/lib/viewer/helpers/filters/types'
|
||||
import { isNumericFilter } from '~/lib/viewer/helpers/filters/types'
|
||||
|
||||
defineProps<{
|
||||
filter: FilterData
|
||||
}>()
|
||||
|
||||
const collapsed = ref(true)
|
||||
|
||||
defineEmits(['selectCondition'])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const collapsed = ref(false)
|
||||
</script>
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
<template>
|
||||
<div class="pl-8 -mb-0.5">
|
||||
<LayoutMenu
|
||||
v-model:open="showMenu"
|
||||
:items="menuItems"
|
||||
show-ticks="right"
|
||||
:custom-menu-items-classes="['!w-24 !text-body-2xs']"
|
||||
@chosen="onConditionChosen"
|
||||
<LayoutMenu
|
||||
v-model:open="showMenu"
|
||||
:items="menuItems"
|
||||
show-ticks="right"
|
||||
:custom-menu-items-classes="[
|
||||
'!text-body-2xs',
|
||||
filter.type === FilterType.Numeric ? '!w-36' : '!w-24'
|
||||
]"
|
||||
@chosen="onConditionChosen"
|
||||
>
|
||||
<FormButton
|
||||
class="-ml-2"
|
||||
color="subtle"
|
||||
size="sm"
|
||||
:class="showMenu ? '!bg-highlight-2' : ''"
|
||||
@click="showMenu = !showMenu"
|
||||
>
|
||||
<FormButton
|
||||
class="-ml-2"
|
||||
color="subtle"
|
||||
size="sm"
|
||||
:class="showMenu ? '!bg-highlight-2' : ''"
|
||||
@click="showMenu = !showMenu"
|
||||
>
|
||||
<span class="text-foreground-2 font-medium text-body-2xs">
|
||||
{{ selectedConditionLabel }}
|
||||
</span>
|
||||
</FormButton>
|
||||
</LayoutMenu>
|
||||
</div>
|
||||
<span class="text-foreground-2 font-medium text-body-2xs">
|
||||
{{ selectedConditionLabel }}
|
||||
</span>
|
||||
</FormButton>
|
||||
</LayoutMenu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
import type {
|
||||
FilterCondition,
|
||||
type FilterData,
|
||||
type ConditionOption
|
||||
FilterData,
|
||||
ConditionOption
|
||||
} from '~/lib/viewer/helpers/filters/types'
|
||||
import {
|
||||
getConditionsForType,
|
||||
getConditionLabel,
|
||||
FilterType
|
||||
} from '~/lib/viewer/helpers/filters/types'
|
||||
import { LayoutMenu, FormButton, type LayoutMenuItem } from '@speckle/ui-components'
|
||||
|
||||
@@ -38,37 +44,37 @@ const emit = defineEmits(['selectCondition'])
|
||||
|
||||
const showMenu = ref(false)
|
||||
|
||||
const getConditionLabel = (condition: FilterCondition): string => {
|
||||
switch (condition) {
|
||||
case FilterCondition.Is:
|
||||
return 'is'
|
||||
case FilterCondition.IsNot:
|
||||
return 'is not'
|
||||
default:
|
||||
return 'is'
|
||||
}
|
||||
}
|
||||
// Get condition options based on filter type
|
||||
const conditionOptions = computed<ConditionOption[]>(() => {
|
||||
const availableConditions = getConditionsForType(props.filter.type)
|
||||
return availableConditions.map((condition) => ({
|
||||
value: condition,
|
||||
label: getConditionLabel(condition)
|
||||
}))
|
||||
})
|
||||
|
||||
const menuItems = computed<LayoutMenuItem[][]>(() => [
|
||||
Object.values(FilterCondition).map((condition) => ({
|
||||
id: condition,
|
||||
title: getConditionLabel(condition),
|
||||
active: condition === (props.filter.condition || FilterCondition.Is)
|
||||
conditionOptions.value.map((conditionOption) => ({
|
||||
id: conditionOption.value,
|
||||
title: conditionOption.label,
|
||||
active: conditionOption.value === props.filter.condition
|
||||
}))
|
||||
])
|
||||
|
||||
const selectedConditionLabel = computed(() => {
|
||||
return getConditionLabel(props.filter.condition || FilterCondition.Is)
|
||||
return getConditionLabel(props.filter.condition)
|
||||
})
|
||||
|
||||
const onConditionChosen = ({ item }: { item: LayoutMenuItem }) => {
|
||||
const onConditionChosen = ({ item }: { item: LayoutMenuItem; event: MouseEvent }) => {
|
||||
// Since we control the menu items, we know item.id is a FilterCondition
|
||||
const condition = item.id as FilterCondition
|
||||
const conditionOption: ConditionOption = {
|
||||
value: condition,
|
||||
label: getConditionLabel(condition)
|
||||
const conditionOption = conditionOptions.value.find(
|
||||
(option) => option.value === condition
|
||||
)
|
||||
|
||||
if (conditionOption) {
|
||||
emit('selectCondition', conditionOption)
|
||||
}
|
||||
emit('selectCondition', conditionOption)
|
||||
showMenu.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
+19
-5
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="flex flex-col p-3">
|
||||
<div class="flex flex-col px-2 py-1">
|
||||
<FormDualRange
|
||||
v-model:min-value="currentMin"
|
||||
v-model:max-value="currentMax"
|
||||
:name="`range-${filter.id}`"
|
||||
:min="(filter.filter as NumericPropertyInfo).min"
|
||||
:max="(filter.filter as NumericPropertyInfo).max"
|
||||
:min="filterMin"
|
||||
:max="filterMax"
|
||||
:step="0.01"
|
||||
show-fields
|
||||
/>
|
||||
@@ -14,8 +14,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormDualRange } from '@speckle/ui-components'
|
||||
import type { NumericPropertyInfo } from '@speckle/viewer'
|
||||
import { useFilterUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { useFilterUtilities } from '~~/lib/viewer/composables/filtering'
|
||||
import { isNumericFilter, type FilterData } from '~/lib/viewer/helpers/filters/types'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -24,6 +23,21 @@ const props = defineProps<{
|
||||
|
||||
const { setNumericRange } = useFilterUtilities()
|
||||
|
||||
// Get the filter's min/max bounds
|
||||
const filterMin = computed(() => {
|
||||
if (isNumericFilter(props.filter)) {
|
||||
return props.filter.filter.min
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const filterMax = computed(() => {
|
||||
if (isNumericFilter(props.filter)) {
|
||||
return props.filter.filter.max
|
||||
}
|
||||
return 100
|
||||
})
|
||||
|
||||
const currentMin = computed({
|
||||
get: () => {
|
||||
if (isNumericFilter(props.filter)) {
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="p-1">
|
||||
<ViewerFiltersFilterConditionSelector
|
||||
:filter="filter"
|
||||
class="pl-4"
|
||||
@select-condition="handleConditionSelect"
|
||||
/>
|
||||
|
||||
<ViewerFiltersFilterNumericBetween
|
||||
v-if="filter.condition === NumericFilterCondition.IsBetween"
|
||||
:filter="filter"
|
||||
/>
|
||||
|
||||
<ViewerFiltersFilterNumericSingle v-else :filter="filter" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FilterData, ConditionOption } from '~/lib/viewer/helpers/filters/types'
|
||||
import { NumericFilterCondition } from '~/lib/viewer/helpers/filters/types'
|
||||
import { useFilterUtilities } from '~~/lib/viewer/composables/filtering'
|
||||
|
||||
const props = defineProps<{
|
||||
filter: FilterData
|
||||
}>()
|
||||
|
||||
const { updateFilterCondition } = useFilterUtilities()
|
||||
|
||||
const handleConditionSelect = (conditionOption: ConditionOption) => {
|
||||
updateFilterCondition(props.filter.id, conditionOption.value)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="flex flex-col px-2 py-1 gap-1">
|
||||
<FormRange
|
||||
v-if="showRangeSlider"
|
||||
v-model="singleValueReactive"
|
||||
:min="filterMin"
|
||||
:max="filterMax"
|
||||
:step="0.01"
|
||||
name="singleValueRange"
|
||||
:label="rangeLabel"
|
||||
hide-header
|
||||
class="-mt-1.5"
|
||||
/>
|
||||
|
||||
<FormTextInput
|
||||
:model-value="String(singleValue)"
|
||||
name="singleValue"
|
||||
size="sm"
|
||||
type="number"
|
||||
placeholder="Enter value"
|
||||
@update:model-value="updateSingleValue"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormTextInput, FormRange } from '@speckle/ui-components'
|
||||
import { useFilterUtilities } from '~~/lib/viewer/composables/filtering'
|
||||
import {
|
||||
isNumericFilter,
|
||||
NumericFilterCondition,
|
||||
type FilterData
|
||||
} from '~/lib/viewer/helpers/filters/types'
|
||||
|
||||
const props = defineProps<{
|
||||
filter: FilterData
|
||||
}>()
|
||||
|
||||
const { setNumericRange } = useFilterUtilities()
|
||||
|
||||
// Get the filter's min/max bounds for range inputs
|
||||
const filterMin = computed(() => {
|
||||
if (isNumericFilter(props.filter)) {
|
||||
return props.filter.filter.min
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const filterMax = computed(() => {
|
||||
if (isNumericFilter(props.filter)) {
|
||||
return props.filter.filter.max
|
||||
}
|
||||
return 100
|
||||
})
|
||||
|
||||
const singleValue = computed(() => props.filter.numericRange.min)
|
||||
|
||||
// Show range slider for greater than / less than conditions
|
||||
const showRangeSlider = computed(() => {
|
||||
return (
|
||||
props.filter.condition === NumericFilterCondition.IsGreaterThan ||
|
||||
props.filter.condition === NumericFilterCondition.IsLessThan
|
||||
)
|
||||
})
|
||||
|
||||
const rangeLabel = computed(() => {
|
||||
return props.filter.condition === NumericFilterCondition.IsGreaterThan
|
||||
? 'Greater than'
|
||||
: 'Less than'
|
||||
})
|
||||
|
||||
// Reactive value for FormRange v-model
|
||||
const singleValueReactive = computed({
|
||||
get: () => props.filter.numericRange.min,
|
||||
set: (value: number) => {
|
||||
setNumericRange(props.filter.id, value, value)
|
||||
}
|
||||
})
|
||||
|
||||
const updateSingleValue = (value: string) => {
|
||||
const numericValue = parseFloat(value) || 0
|
||||
// For single value conditions, set both min and max to the same value
|
||||
setNumericRange(props.filter.id, numericValue, numericValue)
|
||||
}
|
||||
</script>
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<ViewerFiltersFilterValuesSelectAllCheckbox
|
||||
<ViewerFiltersFilterStringSelectAll
|
||||
:selected-count="selectedCount"
|
||||
:total-count="filteredValues.length"
|
||||
@select-all="selectAll"
|
||||
@@ -23,7 +23,7 @@
|
||||
transform: `translateY(${index * itemHeight}px)`
|
||||
}"
|
||||
>
|
||||
<ViewerFiltersFilterValuesFilterValueItem
|
||||
<ViewerFiltersFilterStringValueItem
|
||||
:filter-id="filter.id"
|
||||
:value="value"
|
||||
:is-selected="isValueSelected(value)"
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="pt-1">
|
||||
<ViewerFiltersFilterConditionSelector
|
||||
:filter="filter"
|
||||
class="pl-9"
|
||||
@select-condition="handleConditionSelect"
|
||||
/>
|
||||
|
||||
<ViewerSearchInput
|
||||
v-if="!collapsed"
|
||||
v-model="searchQuery"
|
||||
placeholder="Search for a value..."
|
||||
class="pl-1 -mt-0.5"
|
||||
/>
|
||||
|
||||
<ViewerFiltersFilterStringCheckboxes :filter="filter" :search-query="searchQuery" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FilterData, ConditionOption } from '~/lib/viewer/helpers/filters/types'
|
||||
import { useFilterUtilities } from '~~/lib/viewer/composables/filtering'
|
||||
|
||||
const props = defineProps<{
|
||||
filter: FilterData
|
||||
}>()
|
||||
|
||||
const collapsed = ref(false)
|
||||
const searchQuery = ref('')
|
||||
|
||||
const { updateFilterCondition } = useFilterUtilities()
|
||||
|
||||
const handleConditionSelect = (conditionOption: ConditionOption) => {
|
||||
updateFilterCondition(props.filter.id, conditionOption.value)
|
||||
}
|
||||
</script>
|
||||
@@ -67,6 +67,7 @@ import { Ellipsis } from 'lucide-vue-next'
|
||||
import { useFilterUtilities } from '~~/lib/viewer/composables/filtering'
|
||||
import { useInjectedViewer } from '~~/lib/viewer/composables/setup'
|
||||
import type { KeyValuePair } from '~/components/viewer/selection/types'
|
||||
import { isNumericPropertyInfo } from '~/lib/viewer/helpers/sceneExplorer'
|
||||
|
||||
const props = defineProps<{
|
||||
kvp: KeyValuePair
|
||||
@@ -78,7 +79,8 @@ const {
|
||||
findFilterByKvp,
|
||||
addActiveFilter,
|
||||
updateActiveFilterValues,
|
||||
toggleFilterApplied
|
||||
toggleFilterApplied,
|
||||
setNumericRange
|
||||
} = useFilterUtilities()
|
||||
|
||||
const {
|
||||
@@ -109,9 +111,20 @@ const handleAddToFilters = (kvp: KeyValuePair) => {
|
||||
const filter = findFilterByKvp(kvp, availableFilters.value)
|
||||
if (filter && kvp.value !== null && kvp.value !== undefined) {
|
||||
const filterId = addActiveFilter(filter)
|
||||
const values = [String(kvp.value)]
|
||||
updateActiveFilterValues(filterId, values)
|
||||
toggleFilterApplied(filterId)
|
||||
|
||||
if (isNumericPropertyInfo(filter)) {
|
||||
// For numeric filters, set the specific numeric value
|
||||
const numericValue =
|
||||
typeof kvp.value === 'number' ? kvp.value : parseFloat(String(kvp.value))
|
||||
if (!isNaN(numericValue)) {
|
||||
setNumericRange(filterId, numericValue, numericValue)
|
||||
}
|
||||
} else {
|
||||
// For string filters, use the selectedValues array
|
||||
const values = [String(kvp.value)]
|
||||
updateActiveFilterValues(filterId, values)
|
||||
toggleFilterApplied(filterId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
isNumericPropertyInfo
|
||||
} from '~/lib/viewer/helpers/sceneExplorer'
|
||||
import {
|
||||
FilterCondition,
|
||||
type FilterCondition,
|
||||
FilterLogic,
|
||||
FilterType,
|
||||
type FilterData,
|
||||
@@ -34,7 +34,10 @@ import {
|
||||
type ResourceInfo,
|
||||
type CreateFilterParams,
|
||||
isNumericFilter,
|
||||
isStringFilter
|
||||
isStringFilter,
|
||||
NumericFilterCondition,
|
||||
StringFilterCondition,
|
||||
getConditionLabel
|
||||
} from '~/lib/viewer/helpers/filters/types'
|
||||
import { useOnViewerLoadComplete } from '~~/lib/viewer/composables/viewer'
|
||||
import { arraysEqual } from '~~/lib/common/helpers/utils'
|
||||
@@ -140,28 +143,52 @@ function createFilteringDataStore() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle numeric conditions
|
||||
if (criteria.minValue !== undefined || criteria.maxValue !== undefined) {
|
||||
const minValue = criteria.minValue ?? -Infinity
|
||||
const maxValue = criteria.maxValue ?? Infinity
|
||||
|
||||
for (const [value, objectIds] of Object.entries(propertyIndex)) {
|
||||
const numericValue = Number(value)
|
||||
if (
|
||||
!isNaN(numericValue) &&
|
||||
numericValue >= minValue &&
|
||||
numericValue <= maxValue
|
||||
) {
|
||||
matchingIds.push(...objectIds)
|
||||
if (!isNaN(numericValue)) {
|
||||
let shouldInclude = false
|
||||
|
||||
switch (criteria.condition) {
|
||||
case NumericFilterCondition.IsBetween:
|
||||
shouldInclude = numericValue >= minValue && numericValue <= maxValue
|
||||
break
|
||||
case NumericFilterCondition.IsGreaterThan:
|
||||
shouldInclude = numericValue > minValue
|
||||
break
|
||||
case NumericFilterCondition.IsLessThan:
|
||||
shouldInclude = numericValue < maxValue
|
||||
break
|
||||
case NumericFilterCondition.IsEqualTo:
|
||||
// For numeric "is", check if the value is within the range
|
||||
shouldInclude = numericValue >= minValue && numericValue <= maxValue
|
||||
break
|
||||
case NumericFilterCondition.IsNotEqualTo:
|
||||
// For numeric "is not", exclude values within the range
|
||||
shouldInclude = numericValue < minValue || numericValue > maxValue
|
||||
break
|
||||
default:
|
||||
// Default to range behavior for backward compatibility
|
||||
shouldInclude = numericValue >= minValue && numericValue <= maxValue
|
||||
}
|
||||
|
||||
if (shouldInclude) {
|
||||
matchingIds.push(...objectIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (criteria.condition === FilterCondition.Is) {
|
||||
} else if (criteria.condition === StringFilterCondition.Is) {
|
||||
for (const value of criteria.values) {
|
||||
const objectIds = propertyIndex[value]
|
||||
if (objectIds) {
|
||||
matchingIds.push(...objectIds)
|
||||
}
|
||||
}
|
||||
} else if (criteria.condition === FilterCondition.IsNot) {
|
||||
} else if (criteria.condition === StringFilterCondition.IsNot) {
|
||||
const excludeValues = new Set(criteria.values)
|
||||
for (const [value, objectIds] of Object.entries(propertyIndex)) {
|
||||
if (!excludeValues.has(value)) {
|
||||
@@ -330,6 +357,7 @@ export function useFilterUtilities(
|
||||
dataStore.finalObjectIds,
|
||||
(newObjectIds, oldObjectIds) => {
|
||||
if (preventFilterWatchers) return
|
||||
|
||||
if (arraysEqual(newObjectIds, oldObjectIds || [])) return
|
||||
|
||||
withWatchersDisabled(() => {
|
||||
@@ -444,16 +472,12 @@ export function useFilterUtilities(
|
||||
const createFilterData = (params: CreateFilterParams): FilterData => {
|
||||
const { filter, id, availableValues } = params
|
||||
|
||||
const baseData = {
|
||||
id,
|
||||
isApplied: false,
|
||||
selectedValues: [...availableValues],
|
||||
condition: FilterCondition.Is
|
||||
}
|
||||
|
||||
if (isNumericPropertyInfo(filter)) {
|
||||
return {
|
||||
...baseData,
|
||||
id,
|
||||
isApplied: false,
|
||||
selectedValues: [...availableValues],
|
||||
condition: NumericFilterCondition.IsEqualTo,
|
||||
type: FilterType.Numeric,
|
||||
filter: filter as NumericPropertyInfo,
|
||||
numericRange: {
|
||||
@@ -463,7 +487,10 @@ export function useFilterUtilities(
|
||||
} satisfies NumericFilterData
|
||||
} else {
|
||||
return {
|
||||
...baseData,
|
||||
id,
|
||||
isApplied: false,
|
||||
selectedValues: [...availableValues],
|
||||
condition: StringFilterCondition.Is,
|
||||
type: FilterType.String,
|
||||
filter: filter as StringPropertyInfo,
|
||||
numericRange: { min: 0, max: 100 } // Default range for consistency
|
||||
@@ -505,6 +532,9 @@ export function useFilterUtilities(
|
||||
removeColorFilter()
|
||||
}
|
||||
filters.propertyFilters.value.splice(index, 1)
|
||||
|
||||
// Update viewer to reflect filter removal
|
||||
updateDataStoreSlices()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,6 +545,9 @@ export function useFilterUtilities(
|
||||
const filter = filters.propertyFilters.value.find((f) => f.id === filterId)
|
||||
if (filter) {
|
||||
filter.isApplied = !filter.isApplied
|
||||
|
||||
// Update viewer to reflect filter state change
|
||||
updateDataStoreSlices()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,6 +558,9 @@ export function useFilterUtilities(
|
||||
const filter = filters.propertyFilters.value.find((f) => f.id === filterId)
|
||||
if (filter) {
|
||||
filter.selectedValues = [...values]
|
||||
|
||||
// Update viewer to reflect filter value changes
|
||||
updateDataStoreSlices()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,8 +620,8 @@ export function useFilterUtilities(
|
||||
|
||||
const slice: DataSlice = {
|
||||
id: `filter-${filter.id}`,
|
||||
name: `${getPropertyName(
|
||||
filter.filter.key
|
||||
name: `${getPropertyName(filter.filter.key)} ${getConditionLabel(
|
||||
filter.condition
|
||||
)} (${filter.numericRange.min.toFixed(2)} - ${filter.numericRange.max.toFixed(
|
||||
2
|
||||
)})`,
|
||||
@@ -605,7 +641,7 @@ export function useFilterUtilities(
|
||||
const slice: DataSlice = {
|
||||
id: `filter-${filter.id}`,
|
||||
name: `${getPropertyName(filter.filter.key)} ${
|
||||
filter.condition === FilterCondition.Is ? 'is' : 'is not'
|
||||
filter.condition === StringFilterCondition.Is ? 'is' : 'is not'
|
||||
} ${filter.selectedValues.join(', ')}`,
|
||||
objectIds: matchingObjectIds
|
||||
}
|
||||
|
||||
@@ -4,11 +4,52 @@ import type {
|
||||
StringPropertyInfo
|
||||
} from '@speckle/viewer'
|
||||
|
||||
export enum FilterCondition {
|
||||
export enum NumericFilterCondition {
|
||||
IsBetween = 'is_between',
|
||||
IsEqualTo = 'is_equal_to',
|
||||
IsNotEqualTo = 'is_not_equal_to',
|
||||
IsGreaterThan = 'is_greater_than',
|
||||
IsLessThan = 'is_less_than'
|
||||
}
|
||||
|
||||
export enum StringFilterCondition {
|
||||
Is = 'is',
|
||||
IsNot = 'is_not'
|
||||
}
|
||||
|
||||
export type FilterCondition = NumericFilterCondition | StringFilterCondition
|
||||
|
||||
// Centralized condition configuration
|
||||
export const CONDITION_CONFIG: Record<FilterCondition, { label: string }> = {
|
||||
// String conditions
|
||||
[StringFilterCondition.Is]: { label: 'is' },
|
||||
[StringFilterCondition.IsNot]: { label: 'is not' },
|
||||
// Numeric conditions
|
||||
[NumericFilterCondition.IsEqualTo]: { label: 'is equal to' },
|
||||
[NumericFilterCondition.IsNotEqualTo]: { label: 'is not equal to' },
|
||||
[NumericFilterCondition.IsGreaterThan]: { label: 'is greater than' },
|
||||
[NumericFilterCondition.IsLessThan]: { label: 'is less than' },
|
||||
[NumericFilterCondition.IsBetween]: { label: 'is between' }
|
||||
} as const
|
||||
|
||||
// Helper to get available conditions for a filter type
|
||||
export const getConditionsForType = (filterType: FilterType): FilterCondition[] => {
|
||||
if (filterType === FilterType.Numeric) {
|
||||
return Object.values(NumericFilterCondition)
|
||||
} else {
|
||||
return Object.values(StringFilterCondition)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get condition label
|
||||
export const getConditionLabel = (condition: FilterCondition): string => {
|
||||
return CONDITION_CONFIG[condition]?.label || 'is'
|
||||
}
|
||||
|
||||
export type FilterConditionLabel = {
|
||||
[key in FilterCondition]: string
|
||||
}
|
||||
|
||||
export enum FilterLogic {
|
||||
All = 'all',
|
||||
Any = 'any'
|
||||
|
||||
Reference in New Issue
Block a user