Skip to content

Commit 6153206

Browse files
Zaimwa9tiagoapolo
andauthored
feat: use dropdown for values with context environment (#5832)
Co-authored-by: Tiago Paiva <tiago@tiagopaiva.me>
1 parent 475f744 commit 6153206

8 files changed

Lines changed: 361 additions & 119 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import Icon from 'components/Icon'
2+
import React from 'react'
3+
export interface OptionType {
4+
enabled?: boolean
5+
label: string
6+
value: string
7+
}
8+
9+
export interface GroupedOptionType {
10+
label: React.ReactNode
11+
options: OptionType[]
12+
}
13+
14+
export const GroupLabel = ({
15+
groupName,
16+
tooltipText,
17+
}: {
18+
groupName: string
19+
tooltipText?: string
20+
}) => {
21+
return (
22+
<div className='d-flex align-items-center gap-1'>
23+
<div>{groupName}</div>
24+
{tooltipText && (
25+
<Tooltip
26+
title={
27+
<h5 className='mb-1 cursor-pointer'>
28+
<Icon name='info-outlined' height={16} width={16} />
29+
</h5>
30+
}
31+
place='right'
32+
>
33+
{tooltipText}
34+
</Tooltip>
35+
)}
36+
</div>
37+
)
38+
}
39+
40+
interface SearchableDropdownProps {
41+
placeholder: string
42+
options: OptionType[] | GroupedOptionType[]
43+
value: string | number | null
44+
dataTest?: string
45+
isSearchable?: boolean
46+
displayedLabel?: string
47+
noOptionsMessage?: string
48+
maxMenuHeight?: number
49+
onBlur?: (e: OptionType | null) => void
50+
onInputChange?: (e: string, metadata: any) => void
51+
onChange?: (e: OptionType) => void
52+
isMulti?: boolean
53+
isClearable?: boolean
54+
components?: any
55+
}
56+
57+
const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
58+
components,
59+
dataTest,
60+
displayedLabel,
61+
isClearable = false,
62+
isMulti,
63+
isSearchable,
64+
maxMenuHeight,
65+
noOptionsMessage,
66+
onBlur,
67+
onChange,
68+
onInputChange,
69+
options,
70+
placeholder,
71+
value,
72+
}) => {
73+
return (
74+
<Select
75+
data-test={dataTest}
76+
placeholder={placeholder}
77+
value={value ? { label: displayedLabel || value, value: value } : null}
78+
onBlur={onBlur}
79+
isSearchable={isSearchable}
80+
onInputChange={onInputChange}
81+
onChange={onChange}
82+
options={options}
83+
maxMenuHeight={maxMenuHeight}
84+
isMulti={isMulti}
85+
{...(noOptionsMessage
86+
? { noOptionsMessage: () => noOptionsMessage }
87+
: {})}
88+
isClearable={isClearable}
89+
className={`react-select ${
90+
isClearable && value ? 'clearable-dropdown' : ''
91+
}`}
92+
components={components}
93+
/>
94+
)
95+
}
96+
97+
export default SearchableDropdown

frontend/web/components/modals/CreateSegment.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ const CreateSegment: FC<CreateSegmentType> = ({
374374
updateRule(0, i, v)
375375
}}
376376
errors={error?.data?.rules?.[0]?.rules?.[i]?.conditions}
377+
projectId={projectId}
377378
/>
378379
</div>
379380
)

frontend/web/components/segments/Rule/Rule.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ interface RuleProps {
2626
showDescription?: boolean
2727
'data-test'?: string
2828
errors: SegmentConditionsError[]
29+
projectId: number
2930
}
3031

3132
const Rule: React.FC<RuleProps> = ({
3233
'data-test': dataTest,
3334
errors,
3435
onChange,
3536
operators,
37+
projectId,
3638
readOnly,
3739
rule,
3840
showDescription,
@@ -90,7 +92,6 @@ const Rule: React.FC<RuleProps> = ({
9092

9193
const invalidPercentageSplit =
9294
condition?.value && isInvalidPercentageSplit(condition.value)
93-
9495
if (invalidPercentageSplit) {
9596
updates.value = ''
9697
} else {
@@ -106,7 +107,6 @@ const Rule: React.FC<RuleProps> = ({
106107
value: string | boolean,
107108
) => {
108109
const condition = rule.conditions[conditionIndex]
109-
110110
if (
111111
condition?.operator === 'PERCENTAGE_SPLIT' &&
112112
isInvalidPercentageSplit(value)
@@ -177,6 +177,7 @@ const Rule: React.FC<RuleProps> = ({
177177
addRule={addRule}
178178
rules={rules}
179179
data-test={`${dataTest}`}
180+
projectId={projectId}
180181
/>
181182
))}
182183
</div>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React, { useEffect, useState } from 'react'
2+
import SearchableDropdown, {
3+
GroupLabel,
4+
OptionType,
5+
} from 'components/base/SearchableDropdown'
6+
import { useGetEnvironmentsQuery } from 'common/services/useEnvironment'
7+
import Utils from 'common/utils/utils'
8+
9+
interface EnvironmentSelectDropdownProps {
10+
projectId: number
11+
dataTest?: string
12+
onChange?: (value: string) => void
13+
value?: string | number | boolean | null
14+
}
15+
const EnvironmentSelectDropdown: React.FC<EnvironmentSelectDropdownProps> = ({
16+
dataTest,
17+
onChange,
18+
projectId,
19+
value,
20+
}) => {
21+
const [localCurrentValue, setLocalCurrentValue] = useState(value || '')
22+
const { data } = useGetEnvironmentsQuery({ projectId: projectId?.toString() })
23+
const environments = data?.results
24+
25+
useEffect(() => {
26+
setLocalCurrentValue(value || '')
27+
}, [value])
28+
29+
const isEditing = localCurrentValue !== value
30+
const isExistingEnvironment = environments?.find(
31+
(environment) => environment.name === value,
32+
)
33+
34+
const customSelectionAsOption =
35+
localCurrentValue && (isEditing || !isExistingEnvironment)
36+
? [
37+
{
38+
label: <GroupLabel groupName='Custom selection' />,
39+
options: [
40+
{
41+
label: localCurrentValue?.toString(),
42+
value: localCurrentValue?.toString(),
43+
},
44+
],
45+
},
46+
]
47+
: []
48+
49+
const environmentOptions = [
50+
{
51+
label: <GroupLabel groupName='Environments' />,
52+
options:
53+
environments?.map((environment) => ({
54+
label: Utils.capitalize(environment.name),
55+
value: environment.name,
56+
})) || [],
57+
},
58+
]
59+
60+
const allOptions = [...customSelectionAsOption, ...environmentOptions]
61+
62+
return (
63+
<SearchableDropdown
64+
options={allOptions}
65+
value={value?.toString() || null}
66+
placeholder={'Environment'}
67+
noOptionsMessage={'No environment matches your search'}
68+
isClearable={true}
69+
maxMenuHeight={280}
70+
dataTest={dataTest}
71+
onInputChange={(e: string, metadata: any) => {
72+
if (metadata.action !== 'input-change') {
73+
return
74+
}
75+
setLocalCurrentValue(e)
76+
}}
77+
onBlur={() => {
78+
if (onChange && localCurrentValue !== value) {
79+
onChange(localCurrentValue?.toString() || '')
80+
}
81+
}}
82+
onChange={(e: OptionType) => {
83+
if (onChange) {
84+
onChange(Utils.safeParseEventValue(e?.value || ''))
85+
}
86+
}}
87+
/>
88+
)
89+
}
90+
91+
export default EnvironmentSelectDropdown

frontend/web/components/segments/Rule/components/RuleConditionPropertySelect.tsx

Lines changed: 46 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import React, { useEffect, useState } from 'react'
2+
import { components } from 'react-select/lib/components'
23
import Utils from 'common/utils/utils'
34
import { RuleContextValues } from 'common/types/rules.types'
45
import Constants from 'common/constants'
5-
import Icon from 'components/Icon'
6-
7-
export interface OptionType {
8-
enabled?: boolean
9-
label: string
10-
value: string
11-
}
6+
import { GroupLabel } from 'components/base/SearchableDropdown'
7+
import SearchableDropdown, {
8+
OptionType,
9+
} from 'components/base/SearchableDropdown'
1210

1311
interface RuleConditionPropertySelectProps {
1412
ruleIndex: number
@@ -24,32 +22,6 @@ interface RuleConditionPropertySelectProps {
2422
isValueFromContext: boolean
2523
}
2624

27-
const GroupLabel = ({
28-
groupName,
29-
tooltipText,
30-
}: {
31-
groupName: string
32-
tooltipText?: string
33-
}) => {
34-
return (
35-
<div className='d-flex align-items-center gap-1'>
36-
<div>{groupName}</div>
37-
{tooltipText && (
38-
<Tooltip
39-
title={
40-
<h5 className='mb-1 cursor-pointer'>
41-
<Icon name='info-outlined' height={16} width={16} />
42-
</h5>
43-
}
44-
place='right'
45-
>
46-
{tooltipText}
47-
</Tooltip>
48-
)}
49-
</div>
50-
)
51-
}
52-
5325
const RuleConditionPropertySelect = ({
5426
allowedContextValues,
5527
dataTest,
@@ -90,20 +62,21 @@ const RuleConditionPropertySelect = ({
9062
contextOptions.find((option) => option.value === propertyValue)?.label ||
9163
propertyValue
9264
const isEditing = localCurrentValue !== propertyValue
93-
const traitAsGroupedOptions =
65+
const showTraitOptions =
9466
localCurrentValue && (!isValueFromContext || isEditing)
95-
? [
96-
{
97-
label: (
98-
<GroupLabel
99-
groupName='Traits'
100-
tooltipText={Constants.strings.USER_PROPERTY_DESCRIPTION}
101-
/>
102-
),
103-
options: [{ label: localCurrentValue, value: localCurrentValue }],
104-
},
105-
]
106-
: []
67+
const traitAsGroupedOptions = showTraitOptions
68+
? [
69+
{
70+
label: (
71+
<GroupLabel
72+
groupName='Traits'
73+
tooltipText={Constants.strings.USER_PROPERTY_DESCRIPTION}
74+
/>
75+
),
76+
options: [{ label: localCurrentValue, value: localCurrentValue }],
77+
},
78+
]
79+
: []
10780

10881
const contextAsGroupedOptions =
10982
contextOptions?.length > 0
@@ -129,22 +102,20 @@ const RuleConditionPropertySelect = ({
129102
...contextAsGroupedOptions,
130103
]
131104

105+
const showTitle = !showTraitOptions && operator !== 'PERCENTAGE_SPLIT'
106+
132107
return (
133108
<>
134-
<Select
135-
data-test={dataTest}
109+
<SearchableDropdown
110+
dataTest={dataTest}
111+
value={propertyValue}
112+
isClearable={true}
136113
placeholder={'Trait / Context value'}
137-
value={
138-
localCurrentValue
139-
? { label: displayedLabel, value: propertyValue }
140-
: null
141-
}
114+
options={optionsWithTrait}
115+
noOptionsMessage={'Start typing to select a trait'}
142116
onBlur={() => {
143117
setRuleProperty(ruleIndex, 'property', { value: localCurrentValue })
144118
}}
145-
isSearchable={
146-
operator !== 'PERCENTAGE_SPLIT' || isContextPropertyEnabled
147-
}
148119
onInputChange={(e: string, metadata: any) => {
149120
if (metadata.action !== 'input-change') {
150121
return
@@ -156,10 +127,25 @@ const RuleConditionPropertySelect = ({
156127
value: Utils.safeParseEventValue(e?.value),
157128
})
158129
}}
159-
options={[...optionsWithTrait]}
160-
style={{ width: '200px' }}
161-
noOptionsMessage={() => {
162-
return 'Start typing to select a trait'
130+
displayedLabel={displayedLabel}
131+
components={{
132+
Menu: ({ ...props }: any) => {
133+
return (
134+
<components.Menu {...props}>
135+
<React.Fragment>
136+
{showTitle && (
137+
<p
138+
style={{ fontStyle: 'italic', paddingTop: 6 }}
139+
className='mb-0 faint text-center'
140+
>
141+
Pick a value or type a trait name
142+
</p>
143+
)}
144+
{props.children}
145+
</React.Fragment>
146+
</components.Menu>
147+
)
148+
},
163149
}}
164150
/>
165151
</>

0 commit comments

Comments
 (0)