diff --git a/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx b/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx
index fa05cbb2ae2f3..3d1348293a0c7 100644
--- a/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx
+++ b/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react'
import { PropertyOperator } from '~/types'
import { Col, Select, SelectProps } from 'antd'
-import { isOperatorFlag, isOperatorMulti, operatorMap } from 'lib/utils'
+import { isMobile, isOperatorFlag, isOperatorMulti, operatorMap } from 'lib/utils'
import { PropertyValue } from './PropertyValue'
import { ColProps } from 'antd/lib/col'
@@ -70,6 +70,7 @@ export function OperatorValueSelect({
onSet={(newValue: string | number | string[] | null) => {
onChange(currentOperator || PropertyOperator.Exact, newValue)
}}
+ autoFocus={!isMobile()}
/>
)}
diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.js b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.js
deleted file mode 100644
index 2440e4b10e83e..0000000000000
--- a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import React, { useState, useEffect } from 'react'
-import { AutoComplete, Select } from 'antd'
-import { useThrottledCallback } from 'use-debounce'
-import api from 'lib/api'
-import { isMobile, isOperatorFlag, isOperatorMulti, isOperatorRegex, isValidRegex } from 'lib/utils'
-import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow'
-
-export function PropertyValue({
- propertyKey,
- type,
- endpoint = undefined,
- placeholder = undefined,
- style = {},
- bordered = true,
- onSet,
- value,
- operator,
- outerOptions = undefined,
-}) {
- const isMultiSelect = isOperatorMulti(operator)
- const [input, setInput] = useState(isMultiSelect ? '' : value)
- const [optionsCache, setOptionsCache] = useState({})
- const [options, setOptions] = useState({})
-
- const loadPropertyValues = useThrottledCallback((newInput) => {
- if (type === 'cohort') {
- return
- }
- let key = propertyKey.split('__')[0]
- setOptions({ [propertyKey]: { ...options[propertyKey], status: 'loading' }, ...options })
- setOptionsCache({ ...optionsCache, [newInput]: 'loading' })
- if (outerOptions) {
- setOptions({
- [propertyKey]: { values: [...new Set([...outerOptions.map((option) => option)])], status: true },
- ...options,
- })
- setOptionsCache({ ...optionsCache, [newInput]: true })
- } else {
- api.get(endpoint || 'api/' + type + '/values/?key=' + key + (newInput ? '&value=' + newInput : '')).then(
- (propValues) => {
- setOptions({
- [propertyKey]: { values: [...new Set([...propValues.map((option) => option)])], status: true },
- ...options,
- })
- setOptionsCache({ ...optionsCache, [newInput]: true })
- }
- )
- }
- }, 300)
-
- function setValue(newValue) {
- onSet(newValue)
- setInput('')
- }
-
- useEffect(() => {
- loadPropertyValues('')
- }, [propertyKey])
-
- let displayOptions
- displayOptions = ((options[propertyKey] && options[propertyKey].values) || []).filter(
- (option) => input === '' || (option && option.name?.toLowerCase().indexOf(input?.toLowerCase()) > -1)
- )
-
- const validationError = getValidationError(operator, value)
-
- const commonInputProps = {
- autoFocus: !value && !isMobile(),
- style: { width: '100%', ...style },
- value: (isMultiSelect ? value : input) || placeholder,
- loading: optionsCache[input] === 'loading',
- onSearch: (newInput) => {
- setInput(newInput)
- if (!optionsCache[newInput] && !isOperatorFlag(operator)) {
- loadPropertyValues(newInput)
- }
- },
- ['data-attr']: 'prop-val',
- dropdownMatchSelectWidth: 350,
- bordered,
- placeholder,
- allowClear: value,
- onKeyDown: (e) => {
- if (e.key === 'Escape') {
- e.target.blur()
- }
- if (!isMultiSelect && e.key === 'Enter') {
- setValue(input)
- }
- },
- }
-
- return (
- <>
- {!isMultiSelect ? (
- {
- setInput(val ?? null)
- }}
- onSelect={(val) => {
- setValue(val ?? null)
- }}
- >
- {input && (
-
- Specify: {input}
-
- )}
- {displayOptions.map(({ name, id }, index) => (
-
- {name === true && 'true'}
- {name === false && 'false'}
- {name}
-
- ))}
-
- ) : (
- {
- if (isMultiSelect && payload.length > 0) {
- setValue(val)
- } else {
- setValue(payload?.value ?? null)
- }
- }}
- >
- {input && (
-
- Specify: {input}
-
- )}
- {displayOptions.map(({ name, id }, index) => (
-
- {name === true && 'true'}
- {name === false && 'false'}
- {name}
-
- ))}
-
- )}
- {validationError &&
{validationError}
}
- >
- )
-}
-
-function getValidationError(operator, value) {
- if (isOperatorRegex(operator) && !isValidRegex(value)) {
- return 'Value is not a valid regular expression'
- }
-
- return null
-}
diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx
new file mode 100644
index 0000000000000..3985f59c9c4dd
--- /dev/null
+++ b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx
@@ -0,0 +1,241 @@
+import React, { useState, useEffect, useRef } from 'react'
+import { AutoComplete, Select } from 'antd'
+import { useThrottledCallback } from 'use-debounce'
+import api from 'lib/api'
+import { isOperatorFlag, isOperatorMulti, isOperatorRegex, isValidRegex, toString } from 'lib/utils'
+import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow'
+import { PropertyOperator } from '~/types'
+
+type PropValue = {
+ id?: number
+ name?: string | boolean
+}
+
+type Option = {
+ label?: string
+ name?: string
+ status?: 'loading' | 'loaded'
+ values?: PropValue[]
+}
+
+interface PropertyValueProps {
+ propertyKey: string
+ type: string
+ endpoint?: string // Endpoint to fetch options from
+ placeholder?: string
+ style?: Partial
+ bordered?: boolean
+ onSet: CallableFunction
+ value?: string | number | Array | null
+ operator?: PropertyOperator
+ outerOptions?: Option[] // If no endpoint provided, options are given here
+ autoFocus?: boolean
+ allowCustom?: boolean
+}
+
+function matchesLowerCase(needle?: string, haystack?: string): boolean {
+ if (typeof haystack !== 'string' || typeof needle !== 'string') {
+ return false
+ }
+ return haystack.toLowerCase().indexOf(needle.toLowerCase()) > -1
+}
+
+function getValidationError(operator: PropertyOperator, value: any): string | null {
+ if (isOperatorRegex(operator) && !isValidRegex(value)) {
+ return 'Value is not a valid regular expression'
+ }
+ return null
+}
+
+export function PropertyValue({
+ propertyKey,
+ type,
+ endpoint = undefined,
+ placeholder = undefined,
+ style = {},
+ bordered = true,
+ onSet,
+ value,
+ operator,
+ outerOptions = undefined,
+ autoFocus = false,
+ allowCustom = true,
+}: PropertyValueProps): JSX.Element {
+ const isMultiSelect = operator && isOperatorMulti(operator)
+ const [input, setInput] = useState(isMultiSelect ? '' : toString(value))
+ const [options, setOptions] = useState({} as Record)
+ const autoCompleteRef = useRef(null)
+
+ // update the input field if passed a new `value` prop
+ useEffect(() => {
+ if (!value) {
+ setInput('')
+ } else if (value !== input) {
+ const valueObject = options[propertyKey]?.values?.find((v) => v.id === value)
+ if (valueObject) {
+ setInput(toString(valueObject.name))
+ } else {
+ setInput(toString(value))
+ }
+ }
+ }, [value])
+
+ const loadPropertyValues = useThrottledCallback((newInput) => {
+ if (type === 'cohort') {
+ return
+ }
+ const key = propertyKey.split('__')[0]
+ setOptions({ [propertyKey]: { ...options[propertyKey], status: 'loading' }, ...options })
+ if (outerOptions) {
+ setOptions({
+ [propertyKey]: {
+ values: [...Array.from(new Set(outerOptions))],
+ status: 'loaded',
+ },
+ ...options,
+ })
+ } else {
+ api.get(endpoint || 'api/' + type + '/values/?key=' + key + (newInput ? '&value=' + newInput : '')).then(
+ (propValues: PropValue[]) => {
+ setOptions({
+ [propertyKey]: {
+ values: [...Array.from(new Set(propValues))],
+ status: 'loaded',
+ },
+ ...options,
+ })
+ }
+ )
+ }
+ }, 300)
+
+ function setValue(newValue: PropertyValueProps['value']): void {
+ onSet(newValue)
+ if (isMultiSelect) {
+ setInput('')
+ }
+ }
+
+ useEffect(() => {
+ loadPropertyValues('')
+ }, [propertyKey])
+
+ const displayOptions = (options[propertyKey]?.values || []).filter(
+ (option) => input === '' || matchesLowerCase(input, toString(option?.name))
+ )
+
+ const validationError = operator ? getValidationError(operator, value) : null
+
+ const commonInputProps = {
+ style: { width: '100%', ...style },
+ loading: options[input]?.status === 'loading',
+ onSearch: (newInput: string) => {
+ setInput(newInput)
+ if (!Object.keys(options).includes(newInput) && !(operator && isOperatorFlag(operator))) {
+ loadPropertyValues(newInput)
+ }
+ },
+ ['data-attr']: 'prop-val',
+ dropdownMatchSelectWidth: 350,
+ bordered,
+ placeholder,
+ allowClear: Boolean(value),
+ onKeyDown: (e: React.KeyboardEvent) => {
+ if (e.key === 'Escape' && e.target instanceof HTMLElement) {
+ e.target.blur()
+ }
+ if (!isMultiSelect && e.key === 'Enter') {
+ // We have not explicitly selected a dropdown item by pressing the up/down keys
+ if (autoCompleteRef.current?.querySelectorAll('.ant-select-item-option-active')?.length === 0) {
+ setValue(input)
+ }
+ }
+ },
+ }
+
+ return (
+ <>
+ {isMultiSelect ? (
+ {
+ if (Array.isArray(payload) && payload.length > 0) {
+ setValue(val)
+ } else if (payload instanceof Option) {
+ setValue(payload?.value ?? [])
+ } else {
+ setValue([])
+ }
+ }}
+ >
+ {input && !displayOptions.some(({ name }) => input === toString(name)) && (
+
+ Specify: {input}
+
+ )}
+ {displayOptions.map(({ name: _name }, index) => {
+ const name = toString(_name)
+ return (
+
+ {name}
+
+ )
+ })}
+
+ ) : (
+ {
+ setInput('')
+ setValue('')
+ }}
+ onChange={(val) => {
+ setInput(toString(val))
+ }}
+ onSelect={(val, option) => {
+ setInput(option.title)
+ setValue(toString(val))
+ }}
+ ref={autoCompleteRef}
+ >
+ {[
+ ...(input && allowCustom && !displayOptions.some(({ name }) => input === toString(name))
+ ? [
+
+ Specify: {input}
+ ,
+ ]
+ : []),
+ ...displayOptions.map(({ name: _name, id }, index) => {
+ const name = toString(_name)
+ return (
+
+ {name}
+
+ )
+ }),
+ ]}
+
+ )}
+ {validationError && {validationError}
}
+ >
+ )
+}
diff --git a/frontend/src/lib/components/SelectGradientOverflow.tsx b/frontend/src/lib/components/SelectGradientOverflow.tsx
index f52a1416bb1c2..019e6125ca641 100644
--- a/frontend/src/lib/components/SelectGradientOverflow.tsx
+++ b/frontend/src/lib/components/SelectGradientOverflow.tsx
@@ -1,8 +1,9 @@
import React, { ReactElement, RefObject, useEffect, useRef, useState } from 'react'
import { Select, Tag, Tooltip } from 'antd'
import { RefSelectProps, SelectProps } from 'antd/lib/select'
-import './SelectGradientOverflow.scss'
import { CloseButton } from './CloseButton'
+import { toString } from 'lib/utils'
+import './SelectGradientOverflow.scss'
interface DropdownGradientRendererProps {
updateScrollGradient: () => void
@@ -28,7 +29,7 @@ type CustomTagProps = Parameters['tagRender'], undefine
function CustomTag({ label, onClose, value }: CustomTagProps): JSX.Element {
return (
-
+
{label}
diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx
index 25aacc73b3a19..3b9b6390c4765 100644
--- a/frontend/src/lib/constants.tsx
+++ b/frontend/src/lib/constants.tsx
@@ -39,11 +39,6 @@ export const annotationScopeToName = new Map([
export const PERSON_DISTINCT_ID_MAX_SIZE = 3
// Event constants
-export const PAGEVIEW = '$pageview'
-export const AUTOCAPTURE = '$autocapture'
-export const SCREEN = '$screen'
-export const CUSTOM_EVENT = 'custom_event'
-
export const ACTION_TYPE = 'action_type'
export const EVENT_TYPE = 'event_type'
diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx
index 94c05c5a72bc0..d16874ac8d5b8 100644
--- a/frontend/src/lib/utils.tsx
+++ b/frontend/src/lib/utils.tsx
@@ -261,7 +261,7 @@ export function isOperatorRegex(operator: string): boolean {
return ['regex', 'not_regex'].includes(operator)
}
-export function isValidRegex(value: string): boolean {
+export function isValidRegex(value: any): boolean {
try {
new RegExp(value)
return true
@@ -855,3 +855,7 @@ export function maybeAddCommasToInteger(value: any): any {
const internationalNumberFormat = new Intl.NumberFormat('en-US')
return internationalNumberFormat.format(value)
}
+
+export function toString(input?: any | null): string {
+ return input?.toString() || ''
+}
diff --git a/frontend/src/scenes/insights/InsightTabs/PathTab.tsx b/frontend/src/scenes/insights/InsightTabs/PathTab.tsx
index 3620c3660b3fa..182d9432d7d35 100644
--- a/frontend/src/scenes/insights/InsightTabs/PathTab.tsx
+++ b/frontend/src/scenes/insights/InsightTabs/PathTab.tsx
@@ -1,14 +1,7 @@
import React from 'react'
import { useValues, useActions } from 'kea'
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
-import {
- PAGEVIEW,
- AUTOCAPTURE,
- CUSTOM_EVENT,
- pathOptionsToLabels,
- pathOptionsToProperty,
- pathsLogic,
-} from 'scenes/paths/pathsLogic'
+import { pathOptionsToLabels, pathOptionsToProperty, pathsLogic } from 'scenes/paths/pathsLogic'
import { Select } from 'antd'
import { PropertyValue } from 'lib/components/PropertyFilters'
import { TestAccountFilter } from '../TestAccountFilter'
@@ -17,6 +10,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { PathTabHorizontal } from './PathTabHorizontal'
import { FEATURE_FLAGS } from 'lib/constants'
import { BaseTabProps } from '../Insights'
+import { PathType } from '~/types'
export function PathTab(props: BaseTabProps): JSX.Element {
const { featureFlags } = useValues(featureFlagLogic)
@@ -32,8 +26,8 @@ function DefaultPathTab(): JSX.Element {
<>
Path Type