feat: add an operator allowlist to taxonomic filter (#36680)

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Paul D'Ambra
2025-08-19 13:05:33 +01:00
committed by GitHub
parent 60ccd762eb
commit 7f965950da
12 changed files with 101 additions and 28 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -5,7 +5,7 @@ import {
OperatorValueSelectProps,
} from 'lib/components/PropertyFilters/components/OperatorValueSelect'
import { PropertyDefinition, PropertyType } from '~/types'
import { PropertyDefinition, PropertyOperator, PropertyType } from '~/types'
const meta: Meta<typeof OperatorValueSelect> = {
title: 'Filters/PropertyFilters/OperatorValueSelect',
@@ -20,21 +20,27 @@ const makePropertyDefinition = (name: string, propertyType: PropertyType | undef
description: '',
})
const props = (type: PropertyType | undefined, editable: boolean): OperatorValueSelectProps => ({
const props = (overrides: {
type?: PropertyType | undefined
editable?: boolean
startVisible?: boolean
operatorAllowlist?: PropertyOperator[]
}): OperatorValueSelectProps => ({
type: undefined,
propertyKey: 'the_property',
onChange: () => {},
propertyDefinitions: [makePropertyDefinition('the_property', type)],
defaultOpen: true,
editable,
propertyDefinitions: [makePropertyDefinition('the_property', overrides.type)],
editable: overrides.editable ?? false,
startVisible: overrides.startVisible,
operatorAllowlist: overrides.operatorAllowlist,
})
export function OperatorValueWithStringProperty(): JSX.Element {
return (
<>
<h1>String Property</h1>
<OperatorValueSelect {...props(PropertyType.String, true)} />
<OperatorValueSelect {...props(PropertyType.String, false)} />
<OperatorValueSelect {...props({ type: PropertyType.String, editable: true })} />
<OperatorValueSelect {...props({ type: PropertyType.String, editable: false })} />
</>
)
}
@@ -43,8 +49,8 @@ export function OperatorValueWithDateTimeProperty(): JSX.Element {
return (
<>
<h1>Date Time Property</h1>
<OperatorValueSelect {...props(PropertyType.DateTime, true)} />
<OperatorValueSelect {...props(PropertyType.DateTime, false)} />
<OperatorValueSelect {...props({ type: PropertyType.DateTime, editable: true })} />
<OperatorValueSelect {...props({ type: PropertyType.DateTime, editable: false })} />
</>
)
}
@@ -53,8 +59,8 @@ export function OperatorValueWithNumericProperty(): JSX.Element {
return (
<>
<h1>Numeric Property</h1>
<OperatorValueSelect {...props(PropertyType.Numeric, true)} />
<OperatorValueSelect {...props(PropertyType.Numeric, false)} />
<OperatorValueSelect {...props({ type: PropertyType.Numeric, editable: true })} />
<OperatorValueSelect {...props({ type: PropertyType.Numeric, editable: false })} />
</>
)
}
@@ -63,8 +69,8 @@ export function OperatorValueWithBooleanProperty(): JSX.Element {
return (
<>
<h1>Boolean Property</h1>
<OperatorValueSelect {...props(PropertyType.Boolean, true)} />
<OperatorValueSelect {...props(PropertyType.Boolean, false)} />
<OperatorValueSelect {...props({ type: PropertyType.Boolean, editable: true })} />
<OperatorValueSelect {...props({ type: PropertyType.Boolean, editable: false })} />
</>
)
}
@@ -73,8 +79,8 @@ export function OperatorValueWithSelectorProperty(): JSX.Element {
return (
<>
<h1>CSS Selector Property</h1>
<OperatorValueSelect {...props(PropertyType.Selector, true)} />
<OperatorValueSelect {...props(PropertyType.Selector, false)} />
<OperatorValueSelect {...props({ type: PropertyType.Selector, editable: true })} />
<OperatorValueSelect {...props({ type: PropertyType.Selector, editable: false })} />
</>
)
}
@@ -83,8 +89,36 @@ export function OperatorValueWithUnknownProperty(): JSX.Element {
return (
<>
<h1>Property without specific type</h1>
<OperatorValueSelect {...props(undefined, true)} />
<OperatorValueSelect {...props(undefined, false)} />
<OperatorValueSelect {...props({ editable: true })} />
<OperatorValueSelect {...props({ editable: false })} />
</>
)
}
export function OperatorValueMenuOpen(): JSX.Element {
return (
<>
<h1>Showing the options</h1>
<OperatorValueSelect {...props({ editable: true, startVisible: true })} />
</>
)
}
export function OperatorValueMenuWithAllowlist(): JSX.Element {
return (
<>
<h1>Limiting the options to just three</h1>
<OperatorValueSelect
{...props({
startVisible: true,
editable: true,
operatorAllowlist: [
PropertyOperator.IContains,
PropertyOperator.Exact,
PropertyOperator.NotIContains,
],
})}
/>
</>
)
}

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { LemonSelect, LemonSelectProps } from '@posthog/lemon-ui'
import { LemonDropdownProps, LemonSelect, LemonSelectProps } from '@posthog/lemon-ui'
import { allOperatorsToHumanName } from 'lib/components/DefinitionPopover/utils'
import { dayjs } from 'lib/dayjs'
@@ -38,17 +38,23 @@ export interface OperatorValueSelectProps {
operatorSelectProps?: Partial<Omit<LemonSelectProps<any>, 'onChange'>>
eventNames?: string[]
propertyDefinitions: PropertyDefinition[]
defaultOpen?: boolean
addRelativeDateTimeOptions?: boolean
groupTypeIndex?: GroupTypeIndex
size?: 'xsmall' | 'small' | 'medium'
startVisible?: LemonDropdownProps['startVisible']
/**
* in some contexts you want to externally limit the available operators
* this won't add an operator if it isn't valid
* i.e. it limits the options shown from the options that would have been shown
* **/
operatorAllowlist?: Array<PropertyOperator>
}
interface OperatorSelectProps extends Omit<LemonSelectProps<any>, 'options'> {
operator: PropertyOperator
operators: Array<PropertyOperator>
onChange: (operator: PropertyOperator) => void
defaultOpen?: boolean
startVisible?: LemonDropdownProps['startVisible']
}
function getValidationError(operator: PropertyOperator, value: any, property?: string): string | null {
@@ -81,11 +87,12 @@ export function OperatorValueSelect({
operatorSelectProps,
propertyDefinitions = [],
eventNames = [],
defaultOpen,
addRelativeDateTimeOptions,
groupTypeIndex = undefined,
size,
editable,
startVisible,
operatorAllowlist,
}: OperatorValueSelectProps): JSX.Element {
const lookupKey = type === PropertyFilterType.DataWarehousePersonProperty ? 'id' : 'name'
const propertyDefinition = propertyDefinitions.find((pd) => pd[lookupKey] === propertyKey)
@@ -132,7 +139,9 @@ export function OperatorValueSelect({
const operatorMapping: Record<string, string> = chooseOperatorMap(propertyType)
const operators = Object.keys(operatorMapping) as Array<PropertyOperator>
const operators = (Object.keys(operatorMapping) as Array<PropertyOperator>).filter((op) => {
return !operatorAllowlist || operatorAllowlist.includes(op)
})
setOperators(operators)
if ((currentOperator !== operator && operators.includes(startingOperator)) || !propertyDefinition) {
setCurrentOperator(startingOperator)
@@ -147,7 +156,7 @@ export function OperatorValueSelect({
}
setCurrentOperator(defaultProperty)
}
}, [propertyDefinition, propertyKey, operator]) // oxlint-disable-line react-hooks/exhaustive-deps
}, [propertyDefinition, propertyKey, operator, operatorAllowlist]) // oxlint-disable-line react-hooks/exhaustive-deps
return (
<>
@@ -184,7 +193,7 @@ export function OperatorValueSelect({
}}
{...operatorSelectProps}
size={size}
defaultOpen={defaultOpen}
startVisible={startVisible}
/>
) : (
<span>{allOperatorsToHumanName(currentOperator)} </span>
@@ -232,7 +241,14 @@ export function OperatorValueSelect({
)
}
export function OperatorSelect({ operator, operators, onChange, className, size }: OperatorSelectProps): JSX.Element {
export function OperatorSelect({
operator,
operators,
onChange,
className,
size,
startVisible,
}: OperatorSelectProps): JSX.Element {
const operatorOptions = operators.map((op) => ({
label: <span className="operator-value-option">{allOperatorsMapping[op || PropertyOperator.Exact]}</span>,
value: op || PropertyOperator.Exact,
@@ -252,6 +268,7 @@ export function OperatorSelect({ operator, operators, onChange, className, size
menu={{
closeParentPopoverOnClickInside: false,
}}
startVisible={startVisible}
/>
)
}

View File

@@ -71,6 +71,7 @@ export function TaxonomicPropertyFilter({
hideBehavioralCohorts,
addFilterDocLink,
editable = true,
operatorAllowlist,
}: PropertyFilterInternalProps): JSX.Element {
const pageKey = useMemo(() => pageKeyInput || `filter-${uniqueMemoizedIndex++}`, [pageKeyInput])
const groupTypes = taxonomicGroupTypes || DEFAULT_TAXONOMIC_GROUP_TYPES
@@ -184,6 +185,7 @@ export function TaxonomicPropertyFilter({
? (filter?.group_type_index as GroupTypeIndex)
: undefined
}
operatorAllowlist={operatorAllowlist}
/>
)

View File

@@ -1,3 +1,4 @@
import { OperatorValueSelectProps } from 'lib/components/PropertyFilters/components/OperatorValueSelect'
import {
ExcludedProperties,
TaxonomicFilterGroup,
@@ -42,8 +43,10 @@ export interface PropertyFilterInternalProps {
filters: AnyPropertyFilter[]
setFilter: (index: number, property: AnyPropertyFilter) => void
editable?: boolean
operatorAllowlist?: OperatorValueSelectProps['operatorAllowlist']
taxonomicGroupTypes?: TaxonomicFilterGroupType[]
taxonomicFilterOptionsFromProp?: TaxonomicFilterProps['optionsFromProp']
propertyAllowList?: { [key in TaxonomicFilterGroupType]?: string[] }
eventNames?: string[]
schemaColumns?: DatabaseSchemaField[]
propertyGroupType?: FilterLogicalOperator | null
@@ -52,7 +55,6 @@ export interface PropertyFilterInternalProps {
size?: 'xsmall' | 'small' | 'medium'
hasRowOperator?: boolean
metadataSource?: AnyDataNode
propertyAllowList?: { [key in TaxonomicFilterGroupType]?: string[] }
excludedProperties?: ExcludedProperties
allowRelativeDateOptions?: boolean
exactMatchFeatureFlagCohortOperators?: boolean

View File

@@ -4,6 +4,8 @@ import { useState } from 'react'
import { IconPlusSmall } from '@posthog/icons'
import { LemonButton, LemonButtonProps, LemonDropdown, Popover } from '@posthog/lemon-ui'
import { OperatorValueSelectProps } from 'lib/components/PropertyFilters/components/OperatorValueSelect'
import { AnyDataNode } from '~/queries/schema/schema-general'
import { UniversalFilterValue, UniversalFiltersGroup } from '~/types'
@@ -79,6 +81,7 @@ const Value = ({
initiallyOpen = false,
metadataSource,
className,
operatorAllowlist,
}: {
index: number
filter: UniversalFilterValue
@@ -87,6 +90,7 @@ const Value = ({
initiallyOpen?: boolean
metadataSource?: AnyDataNode
className?: string
operatorAllowlist?: OperatorValueSelectProps['operatorAllowlist']
}): JSX.Element => {
const { rootKey, taxonomicPropertyFilterGroupTypes } = useValues(universalFiltersLogic)
@@ -124,6 +128,7 @@ const Value = ({
setFilter={(_, property) => onChange(property)}
disablePopover={false}
taxonomicGroupTypes={taxonomicPropertyFilterGroupTypes}
operatorAllowlist={operatorAllowlist}
/>
) : null
}

View File

@@ -4,6 +4,12 @@ import { Popover, PopoverOverlayContext, PopoverProps } from '../Popover'
export interface LemonDropdownProps extends Omit<PopoverProps, 'children' | 'visible'> {
visible?: boolean
/**
* Setting `visible` shifts the component to controlled mode.
* This lets you choose whether to start open (Defaults to false).
* Without having to take control of the visibility state.
* */
startVisible?: boolean
onVisibilityChange?: (visible: boolean) => void
/**
* Whether the dropdown should be closed on click inside.
@@ -34,6 +40,7 @@ export const LemonDropdown: React.FunctionComponent<LemonDropdownProps & React.R
closeOnClickInside = true,
trigger = 'click',
children,
startVisible,
...popoverProps
},
ref
@@ -41,7 +48,7 @@ export const LemonDropdown: React.FunctionComponent<LemonDropdownProps & React.R
const isControlled = visible !== undefined
const [, parentPopoverLevel] = useContext(PopoverOverlayContext)
const [localVisible, setLocalVisible] = useState(visible ?? false)
const [localVisible, setLocalVisible] = useState(visible ?? startVisible ?? false)
const floatingRef = useRef<HTMLDivElement>(null)
const referenceRef = useRef<HTMLSpanElement>(null)

View File

@@ -95,6 +95,7 @@ export interface LemonMenuProps
| 'className'
| 'onClickOutside'
| 'middleware'
| 'startVisible'
>,
LemonMenuOverlayProps {
/** Must support `ref` and `onKeyDown` for keyboard navigation. */

View File

@@ -3,6 +3,8 @@ import React, { useMemo } from 'react'
import { IconX } from '@posthog/icons'
import { LemonDropdownProps } from 'lib/lemon-ui/LemonDropdown'
import { LemonButton, LemonButtonProps } from '../LemonButton'
import {
LemonMenu,
@@ -77,7 +79,8 @@ export interface LemonSelectPropsBase<T>
placeholder?: string
size?: LemonButtonProps['size']
menu?: Pick<LemonMenuProps, 'className' | 'closeParentPopoverOnClickInside'>
visible?: boolean
visible?: LemonDropdownProps['visible']
startVisible?: LemonDropdownProps['startVisible']
}
export interface LemonSelectPropsClearable<T> extends LemonSelectPropsBase<T> {
@@ -115,6 +118,7 @@ export function LemonSelect<T extends string | number | boolean | null>({
menu,
renderButtonContent,
visible,
startVisible,
...buttonProps
}: LemonSelectProps<T>): JSX.Element {
const [items, allLeafOptions] = useMemo(
@@ -144,6 +148,7 @@ export function LemonSelect<T extends string | number | boolean | null>({
.findIndex((i) => (i as LemonMenuItem).active)}
closeParentPopoverOnClickInside={menu?.closeParentPopoverOnClickInside}
visible={visible}
startVisible={startVisible}
>
<LemonButton
className={clsx(className, 'LemonSelect')}