Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions frontend/web/components/base/SearchableDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Icon from 'components/Icon'
Comment thread
Zaimwa9 marked this conversation as resolved.
import React from 'react'
export interface OptionType {
enabled?: boolean
label: string
value: string
}

export interface GroupedOptionType {
label: React.ReactNode
options: OptionType[]
}

export const GroupLabel = ({
groupName,
tooltipText,
}: {
groupName: string
tooltipText?: string
}) => {
return (
<div className='d-flex align-items-center gap-1'>
<div>{groupName}</div>
{tooltipText && (
<Tooltip
title={
<h5 className='mb-1 cursor-pointer'>
<Icon name='info-outlined' height={16} width={16} />
</h5>
}
place='right'
>
{tooltipText}
</Tooltip>
)}
</div>
)
}

interface SearchableDropdownProps {
placeholder: string
options: OptionType[] | GroupedOptionType[]
value: string | number | null
dataTest?: string
isSearchable?: boolean
displayedLabel?: string
noOptionsMessage?: string
maxMenuHeight?: number
onBlur?: (e: OptionType | null) => void
onInputChange?: (e: string, metadata: any) => void
onChange?: (e: OptionType) => void
isMulti?: boolean
isClearable?: boolean
components?: any
}

const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
components,
dataTest,
displayedLabel,
isClearable = false,
isMulti,
isSearchable,
maxMenuHeight,
noOptionsMessage,
onBlur,
onChange,
onInputChange,
options,
placeholder,
value,
}) => {
return (
<Select
data-test={dataTest}
placeholder={placeholder}
value={value ? { label: displayedLabel || value, value: value } : null}
onBlur={onBlur}
isSearchable={isSearchable}
onInputChange={onInputChange}
onChange={onChange}
options={options}
maxMenuHeight={maxMenuHeight}
isMulti={isMulti}
{...(noOptionsMessage
? { noOptionsMessage: () => noOptionsMessage }
: {})}
isClearable={isClearable}
className={`react-select ${
isClearable && value ? 'clearable-dropdown' : ''
}`}
components={components}
/>
)
}

export default SearchableDropdown
1 change: 1 addition & 0 deletions frontend/web/components/modals/CreateSegment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ const CreateSegment: FC<CreateSegmentType> = ({
updateRule(0, i, v)
}}
errors={error?.data?.rules?.[0]?.rules?.[i]?.conditions}
projectId={projectId}
/>
</div>
)
Expand Down
5 changes: 3 additions & 2 deletions frontend/web/components/segments/Rule/Rule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ interface RuleProps {
showDescription?: boolean
'data-test'?: string
errors: SegmentConditionsError[]
projectId: number
}

const Rule: React.FC<RuleProps> = ({
'data-test': dataTest,
errors,
onChange,
operators,
projectId,
readOnly,
rule,
showDescription,
Expand Down Expand Up @@ -90,7 +92,6 @@ const Rule: React.FC<RuleProps> = ({

const invalidPercentageSplit =
condition?.value && isInvalidPercentageSplit(condition.value)

if (invalidPercentageSplit) {
updates.value = ''
} else {
Expand All @@ -106,7 +107,6 @@ const Rule: React.FC<RuleProps> = ({
value: string | boolean,
) => {
const condition = rule.conditions[conditionIndex]

if (
condition?.operator === 'PERCENTAGE_SPLIT' &&
isInvalidPercentageSplit(value)
Expand Down Expand Up @@ -177,6 +177,7 @@ const Rule: React.FC<RuleProps> = ({
addRule={addRule}
rules={rules}
data-test={`${dataTest}`}
projectId={projectId}
/>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react'
import SearchableDropdown, {
GroupLabel,
OptionType,
} from 'components/base/SearchableDropdown'
import { useGetEnvironmentsQuery } from 'common/services/useEnvironment'
import Utils from 'common/utils/utils'

interface EnvironmentSelectDropdownProps {
projectId: number
dataTest?: string
onChange?: (value: string) => void
value?: string | number | boolean | null
}
const EnvironmentSelectDropdown: React.FC<EnvironmentSelectDropdownProps> = ({
dataTest,
onChange,
projectId,
value,
}) => {
const [localCurrentValue, setLocalCurrentValue] = useState(value || '')
const { data } = useGetEnvironmentsQuery({ projectId: projectId?.toString() })
const environments = data?.results

useEffect(() => {
setLocalCurrentValue(value || '')
}, [value])

const isEditing = localCurrentValue !== value
const isExistingEnvironment = environments?.find(
(environment) => environment.name === value,
)

const customSelectionAsOption =
localCurrentValue && (isEditing || !isExistingEnvironment)
? [
{
label: <GroupLabel groupName='Custom selection' />,
options: [
{
label: localCurrentValue?.toString(),
value: localCurrentValue?.toString(),
},
],
},
]
: []

const environmentOptions = [
{
label: <GroupLabel groupName='Environments' />,
options:
environments?.map((environment) => ({
label: Utils.capitalize(environment.name),
value: environment.name,
})) || [],
},
]

const allOptions = [...customSelectionAsOption, ...environmentOptions]

return (
<SearchableDropdown
options={allOptions}
value={value?.toString() || null}
placeholder={'Environment'}
noOptionsMessage={'No environment matches your search'}
isClearable={true}
maxMenuHeight={280}
dataTest={dataTest}
onInputChange={(e: string, metadata: any) => {
if (metadata.action !== 'input-change') {
return
}
setLocalCurrentValue(e)
}}
onBlur={() => {
if (onChange && localCurrentValue !== value) {
onChange(localCurrentValue?.toString() || '')
}
}}
onChange={(e: OptionType) => {
if (onChange) {
onChange(Utils.safeParseEventValue(e?.value || ''))
}
}}
/>
)
}

export default EnvironmentSelectDropdown
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import React, { useEffect, useState } from 'react'
import { components } from 'react-select/lib/components'
import Utils from 'common/utils/utils'
import { RuleContextValues } from 'common/types/rules.types'
import Constants from 'common/constants'
import Icon from 'components/Icon'

export interface OptionType {
enabled?: boolean
label: string
value: string
}
import { GroupLabel } from 'components/base/SearchableDropdown'
import SearchableDropdown, {
OptionType,
} from 'components/base/SearchableDropdown'

interface RuleConditionPropertySelectProps {
ruleIndex: number
Expand All @@ -24,32 +22,6 @@ interface RuleConditionPropertySelectProps {
isValueFromContext: boolean
}

const GroupLabel = ({
groupName,
tooltipText,
}: {
groupName: string
tooltipText?: string
}) => {
return (
<div className='d-flex align-items-center gap-1'>
<div>{groupName}</div>
{tooltipText && (
<Tooltip
title={
<h5 className='mb-1 cursor-pointer'>
<Icon name='info-outlined' height={16} width={16} />
</h5>
}
place='right'
>
{tooltipText}
</Tooltip>
)}
</div>
)
}

const RuleConditionPropertySelect = ({
allowedContextValues,
dataTest,
Expand Down Expand Up @@ -90,20 +62,21 @@ const RuleConditionPropertySelect = ({
contextOptions.find((option) => option.value === propertyValue)?.label ||
propertyValue
const isEditing = localCurrentValue !== propertyValue
const traitAsGroupedOptions =
const showTraitOptions =
localCurrentValue && (!isValueFromContext || isEditing)
? [
{
label: (
<GroupLabel
groupName='Traits'
tooltipText={Constants.strings.USER_PROPERTY_DESCRIPTION}
/>
),
options: [{ label: localCurrentValue, value: localCurrentValue }],
},
]
: []
const traitAsGroupedOptions = showTraitOptions
? [
{
label: (
<GroupLabel
groupName='Traits'
tooltipText={Constants.strings.USER_PROPERTY_DESCRIPTION}
/>
),
options: [{ label: localCurrentValue, value: localCurrentValue }],
},
]
: []

const contextAsGroupedOptions =
contextOptions?.length > 0
Expand All @@ -129,22 +102,20 @@ const RuleConditionPropertySelect = ({
...contextAsGroupedOptions,
]

const showTitle = !showTraitOptions && operator !== 'PERCENTAGE_SPLIT'

return (
<>
<Select
data-test={dataTest}
<SearchableDropdown
dataTest={dataTest}
value={propertyValue}
isClearable={true}
placeholder={'Trait / Context value'}
value={
localCurrentValue
? { label: displayedLabel, value: propertyValue }
: null
}
options={optionsWithTrait}
noOptionsMessage={'Start typing to select a trait'}
onBlur={() => {
setRuleProperty(ruleIndex, 'property', { value: localCurrentValue })
}}
isSearchable={
operator !== 'PERCENTAGE_SPLIT' || isContextPropertyEnabled
}
onInputChange={(e: string, metadata: any) => {
if (metadata.action !== 'input-change') {
return
Expand All @@ -156,10 +127,25 @@ const RuleConditionPropertySelect = ({
value: Utils.safeParseEventValue(e?.value),
})
}}
options={[...optionsWithTrait]}
style={{ width: '200px' }}
noOptionsMessage={() => {
return 'Start typing to select a trait'
displayedLabel={displayedLabel}
components={{
Menu: ({ ...props }: any) => {
return (
<components.Menu {...props}>
<React.Fragment>
{showTitle && (
<p
style={{ fontStyle: 'italic', paddingTop: 6 }}
className='mb-0 faint text-center'
>
Pick a value or type a trait name
</p>
)}
{props.children}
</React.Fragment>
</components.Menu>
)
},
}}
/>
</>
Expand Down
Loading
Loading