Skip to content

Commit

Permalink
feat: ~LemonMulitSelect~ LemonInputSelect (#20948)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite authored Mar 18, 2024
1 parent ca35b3a commit 2778c6b
Show file tree
Hide file tree
Showing 58 changed files with 514 additions and 687 deletions.
2 changes: 1 addition & 1 deletion cypress/e2e/events.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('Events', () => {
cy.get('[data-attr="new-prop-filter-EventPropertyFilters.0"]').click()
cy.get('[data-attr=taxonomic-filter-searchfield]').click()
cy.get('[data-attr=prop-filter-event_properties-0]').click()
cy.get('[data-attr=prop-val] .ant-select-selector').click({ force: true })
cy.get('[data-attr=prop-val] .LemonInput').click({ force: true })
cy.wait('@getBrowserValues').then(() => {
cy.get('[data-attr=prop-val-0]').click()
cy.get('.DataTable').should('exist')
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/surveys.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('Surveys', () => {
// select the first property
cy.get('[data-attr="property-select-toggle-0"]').click()
cy.get('[data-attr="prop-filter-person_properties-0"]').click()
cy.get('[data-attr=prop-val] .ant-select-selector').click({ force: true })
cy.get('[data-attr=prop-val] .LemonInput').click({ force: true })
cy.get('[data-attr=prop-val-0]').click({ force: true })

cy.get('[data-attr="rollout-percentage"]').type('100')
Expand Down
8 changes: 0 additions & 8 deletions cypress/productAnalytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,6 @@ export const dashboard = {
cy.get('[data-attr="prop-val-0"]').click({ force: true })
cy.get('.PropertyFilterButton').should('have.length', 1)
},
addPropertyFilter(type: string = 'Browser', value: string = 'Chrome'): void {
cy.get('.PropertyFilterButton').should('have.length', 0)
cy.get('[data-attr="property-filter-0"]').click()
cy.get('[data-attr="taxonomic-filter-searchfield"]').click().type('Browser').wait(1000)
cy.get('[data-attr="prop-filter-event_properties-0"]').click({ force: true })
cy.get('.ant-select-selector').type(value)
cy.get('.ant-select-item-option-content').click({ force: true })
},
}

export function createInsight(insightName: string): void {
Expand Down
2 changes: 1 addition & 1 deletion frontend/@posthog/lemon-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export * from 'lib/lemon-ui/LemonModal'
export * from 'lib/lemon-ui/LemonRow'
export * from 'lib/lemon-ui/LemonSegmentedButton'
export * from 'lib/lemon-ui/LemonSelect'
export * from 'lib/lemon-ui/LemonSelectMultiple'
export * from 'lib/lemon-ui/LemonInputSelect'
export * from 'lib/lemon-ui/LemonSkeleton'
export * from 'lib/lemon-ui/LemonSnack'
export * from 'lib/lemon-ui/LemonSwitch'
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

This file was deleted.

221 changes: 39 additions & 182 deletions frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import './PropertyValue.scss'

import { AutoComplete } from 'antd'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { DateFilter } from 'lib/components/DateFilter/DateFilter'
import { DurationPicker } from 'lib/components/DurationPicker/DurationPicker'
import { PropertyFilterDatePicker } from 'lib/components/PropertyFilters/components/PropertyFilterDatePicker'
import { propertyFilterTypeToPropertyDefinitionType } from 'lib/components/PropertyFilters/utils'
import { dayjs } from 'lib/dayjs'
import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple'
import { LemonInputSelect } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect'
import { formatDate, isOperatorDate, isOperatorFlag, isOperatorMulti, toString } from 'lib/utils'
import { useEffect, useRef, useState } from 'react'
import { useEffect } from 'react'

import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel'
import { PropertyFilterType, PropertyOperator, PropertyType } from '~/types'
Expand All @@ -20,43 +16,26 @@ export interface PropertyValueProps {
type: PropertyFilterType
endpoint?: string // Endpoint to fetch options from
placeholder?: string
className?: string
onSet: CallableFunction
value?: string | number | Array<string | number> | null
operator: PropertyOperator
autoFocus?: boolean
allowCustom?: boolean
eventNames?: string[]
addRelativeDateTimeOptions?: boolean
}

function matchesLowerCase(needle?: string, haystack?: string): boolean {
if (typeof haystack !== 'string' || typeof needle !== 'string') {
return false
}
return haystack.toLowerCase().indexOf(needle.toLowerCase()) > -1
}

export function PropertyValue({
propertyKey,
type,
endpoint = undefined,
placeholder = undefined,
className,
onSet,
value,
operator,
autoFocus = false,
allowCustom = true,
eventNames = [],
addRelativeDateTimeOptions = false,
}: PropertyValueProps): JSX.Element {
// what the human has typed into the box
const [input, setInput] = useState(Array.isArray(value) ? '' : toString(value) ?? '')

const [shouldBlur, setShouldBlur] = useState(false)
const autoCompleteRef = useRef<HTMLElement>(null)

const { formatPropertyValueForDisplay, describeProperty, options } = useValues(propertyDefinitionsModel)
const { loadPropertyValues } = useActions(propertyDefinitionsModel)

Expand All @@ -67,20 +46,6 @@ export function PropertyValue({
const isDurationProperty =
propertyKey && describeProperty(propertyKey, propertyDefinitionType) === PropertyType.Duration

// update the input field if passed a new `value` prop
useEffect(() => {
if (value == null) {
setInput('')
} else if (!Array.isArray(value) && toString(value) !== input) {
const valueObject = options[propertyKey]?.values?.find((v) => v.id === value)
if (valueObject) {
setInput(toString(valueObject.name))
} else {
setInput(toString(value))
}
}
}, [value])

const load = (newInput: string | undefined): void => {
loadPropertyValues({
endpoint,
Expand All @@ -91,114 +56,26 @@ export function PropertyValue({
})
}

function setValue(newValue: PropertyValueProps['value']): void {
onSet(newValue)
if (isMultiSelect) {
setInput('')
}
}
const setValue = (newValue: PropertyValueProps['value']): void => onSet(newValue)

useEffect(() => {
load('')
}, [propertyKey])

useEffect(() => {
if (input === '' && shouldBlur) {
;(document.activeElement as HTMLElement)?.blur()
setShouldBlur(false)
}
}, [input, shouldBlur])

const displayOptions = (options[propertyKey]?.values || []).filter(
(option) => input === '' || matchesLowerCase(input, toString(option?.name))
)
const displayOptions = options[propertyKey]?.values || []

const commonInputProps = {
onSearch: (newInput: string) => {
setInput(newInput)
if (!Object.keys(options).includes(newInput) && !(operator && isOperatorFlag(operator))) {
load(newInput.trim())
}
},
['data-attr']: 'prop-val',
dropdownMatchSelectWidth: 350,
placeholder,
allowClear: Boolean(value),
onKeyDown: (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
setInput('')
setShouldBlur(true)
return
}
if (!isMultiSelect && e.key === 'Enter') {
// We have not explicitly selected a dropdown item by pressing the up/down keys; or the ref is unavailable
if (
!autoCompleteRef.current ||
autoCompleteRef.current?.querySelectorAll?.('.ant-select-item-option-active')?.length === 0
) {
setValue(input)
}
}
},
handleBlur: () => {
if (input != '') {
if (Array.isArray(value) && !value.includes(input)) {
setValue([...value, ...[input]])
} else if (!Array.isArray(value)) {
setValue(input)
}
setInput('')
}
},
const onSearchTextChange = (newInput: string): void => {
if (!Object.keys(options).includes(newInput) && !(operator && isOperatorFlag(operator))) {
load(newInput.trim())
}
}

if (isMultiSelect) {
const formattedValues = (
value === null || value === undefined ? [] : Array.isArray(value) ? value : [value]
).map((label) => String(formatPropertyValueForDisplay(propertyKey, label)))
return (
<LemonSelectMultiple
loading={options[propertyKey]?.status === 'loading'}
{...commonInputProps}
selectClassName={clsx(className, 'property-filters-property-value', 'w-full')}
value={formattedValues}
mode="multiple-custom"
onChange={(nextVal: string[]) => {
setValue(nextVal)
}}
onBlur={commonInputProps.handleBlur}
// TODO: When LemonSelectMultiple is free of AntD, add footnote that pressing comma applies the value
options={Object.fromEntries([
...displayOptions.map(({ name: _name }, index) => {
const name = toString(_name)
return [
name,
{
label: name,
labelComponent: (
<span
key={name}
data-attr={'prop-val-' + index}
className="ph-no-capture"
title={name}
>
{name === '' ? (
<i>(empty string)</i>
) : (
formatPropertyValueForDisplay(propertyKey, name)
)}
</span>
),
},
]
}),
])}
/>
)
if (isDurationProperty) {
return <DurationPicker autoFocus={autoFocus} value={value as number} onChange={setValue} />
}

if (isDateTimeProperty && addRelativeDateTimeOptions) {
if (operator === PropertyOperator.IsDateExact) {
if (isDateTimeProperty) {
if (!addRelativeDateTimeOptions || operator === PropertyOperator.IsDateExact) {
return (
<PropertyFilterDatePicker autoFocus={autoFocus} operator={operator} value={value} setValue={setValue} />
)
Expand Down Expand Up @@ -241,52 +118,32 @@ export function PropertyValue({
)
}

return isDateTimeProperty ? (
<PropertyFilterDatePicker autoFocus={autoFocus} operator={operator} value={value} setValue={setValue} />
) : isDurationProperty ? (
<DurationPicker autoFocus={autoFocus} value={value as number} onChange={setValue} />
) : (
<AutoComplete
{...commonInputProps}
autoFocus={autoFocus}
value={input}
className="h-10 w-full property-filters-property-value"
onClear={() => {
setInput('')
setValue('')
}}
onChange={(val) => {
setInput(toString(val))
}}
onSelect={(val, option) => {
setInput(option.title)
setValue(toString(val).trim())
}}
ref={autoCompleteRef}
>
{[
...(input && allowCustom && !displayOptions.some(({ name }) => input === toString(name))
? [
<AutoComplete.Option key="@@@specify-value" value={input} className="ph-no-capture">
Specify: {input}
</AutoComplete.Option>,
]
: []),
...displayOptions.map(({ name: _name, id }, index) => {
const name = toString(_name)
return (
<AutoComplete.Option
key={id ? toString(id) : name}
value={id ? toString(id) : name}
data-attr={'prop-val-' + index}
className="ph-no-capture"
title={name}
>
{name}
</AutoComplete.Option>
)
}),
]}
</AutoComplete>
const formattedValues = (value === null || value === undefined ? [] : Array.isArray(value) ? value : [value]).map(
(label) => String(formatPropertyValueForDisplay(propertyKey, label))
)

return (
<LemonInputSelect
data-attr="prop-val"
loading={options[propertyKey]?.status === 'loading'}
value={formattedValues}
mode={isMultiSelect ? 'multiple' : 'single'}
allowCustomValues
onChange={(nextVal) => setValue(nextVal)}
onInputChange={onSearchTextChange}
placeholder={placeholder}
options={displayOptions.map(({ name: _name }, index) => {
const name = toString(_name)
return {
key: name,
label: name,
labelComponent: (
<span key={name} data-attr={'prop-val-' + index} className="ph-no-capture" title={name}>
{name === '' ? <i>(empty string)</i> : formatPropertyValueForDisplay(propertyKey, name)}
</span>
),
}
})}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const subscriptionLogic = kea<subscriptionLogicType>([
: undefined,
memberOfSlackChannel:
target_type == 'slack'
? !values.isMemberOfSlackChannel(target_value)
? target_value && !values.isMemberOfSlackChannel(target_value)
? 'Please add the PostHog Slack App to the selected channel'
: undefined
: undefined,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/lib/components/Subscriptions/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IconLetter } from '@posthog/icons'
import { LemonSelectOptions } from '@posthog/lemon-ui'
import { IconSlack, IconSlackExternal } from 'lib/lemon-ui/icons'
import { LemonSelectMultipleOptionItem } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple'
import { LemonInputSelectOption } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect'
import { range } from 'lib/utils'
import { urls } from 'scenes/urls'

Expand Down Expand Up @@ -84,7 +84,7 @@ export const timeOptions: LemonSelectOptions<string> = range(0, 24).map((x) => (
export const getSlackChannelOptions = (
value: string,
slackChannels?: SlackChannelType[] | null
): LemonSelectMultipleOptionItem[] => {
): LemonInputSelectOption[] => {
return slackChannels
? slackChannels.map((x) => ({
key: `${x.id}|#${x.name}`,
Expand Down
Loading

0 comments on commit 2778c6b

Please sign in to comment.