Skip to content

Commit 2d96122

Browse files
authored
Merge pull request #479 from devtron-labs/refactor/user-permission-select
refactor: replace select picker and button in user permission and permission groups
2 parents b87c21f + c4b1dcf commit 2d96122

File tree

7 files changed

+131
-38
lines changed

7 files changed

+131
-38
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "1.3.7",
3+
"version": "1.3.8",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",

src/Pages/GlobalConfigurations/Authorization/shared/components/UserRoleGroupsTable/UserRoleGroupTableRow.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
*/
1616

1717
import { Link } from 'react-router-dom'
18-
import Tippy from '@tippyjs/react'
1918

2019
import { URLS } from '@Common/Constants'
2120
import { getRandomColor } from '@Common/Helper'
21+
import { ComponentSizeType } from '@Shared/constants'
22+
import { Button, ButtonStyleType, ButtonVariantType } from '@Shared/Components'
2223
import { UserRoleGroupsTableProps } from './types'
2324
import { ReactComponent as TrashIcon } from '../../../../../../Assets/Icon/ic-delete-interactive.svg'
2425
import { UserRoleGroup } from '../../../types'
@@ -77,20 +78,19 @@ const UserRoleGroupTableRow = ({
7778
showDropdownBorder={false}
7879
breakLinesForTemporaryAccess
7980
showTooltipWhenDisabled
81+
size={ComponentSizeType.medium}
8082
/>
8183
)}
8284
{showDelete && (
83-
<Tippy className="default-tt" arrow={false} placement="top" content="Delete">
84-
<button
85-
type="button"
86-
className="dc__transparent flex p-4"
87-
data-testid="user-role-groups__delete-button icon-delete"
88-
onClick={_handleDelete}
89-
aria-label="Delete row"
90-
>
91-
<TrashIcon className="scn-6 icon-dim-16 icon-delete" />
92-
</button>
93-
</Tippy>
85+
<Button
86+
icon={<TrashIcon />}
87+
ariaLabel="Delete"
88+
dataTestId="user-role-groups__delete-button"
89+
onClick={_handleDelete}
90+
size={ComponentSizeType.xs}
91+
variant={ButtonVariantType.borderLess}
92+
style={ButtonStyleType.negativeGrey}
93+
/>
9494
)}
9595
</div>
9696
)

src/Shared/Components/SelectPicker/FilterSelectPicker.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ const FilterSelectPicker = ({
2222
...props
2323
}: FilterSelectPickerProps) => {
2424
const [isMenuOpen, setIsMenuOpen] = useState(false)
25-
const [inputValue, setInputValue] = useState('')
2625

2726
const [selectedOptions, setSelectedOptions] = useState<SelectPickerOptionType[]>(
2827
structuredClone(appliedFilterOptions ?? []),
@@ -50,7 +49,6 @@ const FilterSelectPicker = ({
5049
}
5150

5251
const handleSelectOnChange: SelectPickerProps<number | string, true>['onChange'] = (selectedOptionsToUpdate) => {
53-
setInputValue(inputValue)
5452
setSelectedOptions(structuredClone(selectedOptionsToUpdate) as SelectPickerOptionType[])
5553
}
5654

@@ -111,8 +109,6 @@ const FilterSelectPicker = ({
111109
isClearable={false}
112110
customSelectedOptionsCount={appliedFiltersCount}
113111
icon={filterIcon}
114-
inputValue={inputValue}
115-
onInputChange={setInputValue}
116112
/>
117113
</div>
118114
)

src/Shared/Components/SelectPicker/SelectPicker.component.tsx

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ import {
2323
Props as ReactSelectProps,
2424
} from 'react-select'
2525
import CreatableSelect from 'react-select/creatable'
26-
import { ReactElement, useCallback, useMemo } from 'react'
26+
import { ReactElement, useCallback, useMemo, useState } from 'react'
2727

2828
import { ReactComponent as ErrorIcon } from '@Icons/ic-warning.svg'
2929
import { ReactComponent as ICInfoFilledOverride } from '@Icons/ic-info-filled-override.svg'
3030
import { ComponentSizeType } from '@Shared/constants'
3131
import { ConditionalWrap } from '@Common/Helper'
3232
import Tippy from '@tippyjs/react'
33+
import { isNullOrUndefined } from '@Shared/Helpers'
3334
import { getCommonSelectStyle, getSelectPickerOptionByValue } from './utils'
3435
import {
3536
SelectPickerMultiValueLabel,
@@ -208,16 +209,31 @@ const SelectPicker = <OptionValue, IsMulti extends boolean>({
208209
fullWidth = false,
209210
customSelectedOptionsCount = null,
210211
renderMenuListFooter,
211-
inputValue,
212-
onInputChange,
213212
isCreatable = false,
214213
onCreateOption,
215214
closeMenuOnSelect = false,
216215
shouldShowNoOptionsMessage = true,
216+
shouldHideMenu = false,
217217
...props
218218
}: SelectPickerProps<OptionValue, IsMulti>) => {
219-
const { inputId, required, isDisabled, controlShouldRenderValue = true, value, options, getOptionValue } = props
220-
const { isGroupHeadingSelectable = false, getIsOptionValid = () => true } = multiSelectProps
219+
const [isFocussed, setIsFocussed] = useState(false)
220+
const [inputValue, setInputValue] = useState('')
221+
222+
const {
223+
inputId,
224+
required,
225+
isDisabled,
226+
controlShouldRenderValue: _controlShouldRenderValue = true,
227+
value,
228+
options,
229+
getOptionValue,
230+
} = props
231+
const {
232+
isGroupHeadingSelectable = false,
233+
getIsOptionValid = () => true,
234+
customDisplayText = null,
235+
} = multiSelectProps
236+
const controlShouldRenderValue = _controlShouldRenderValue && !customDisplayText
221237

222238
// Only large variant is supported for multi select picker
223239
const selectSize = isMulti && controlShouldRenderValue ? ComponentSizeType.large : size
@@ -265,9 +281,10 @@ const SelectPicker = <OptionValue, IsMulti extends boolean>({
265281
{...valueContainerProps}
266282
showSelectedOptionsCount={showSelectedOptionsCount}
267283
customSelectedOptionsCount={customSelectedOptionsCount}
284+
isFocussed={isFocussed}
268285
/>
269286
),
270-
[showSelectedOptionsCount, customSelectedOptionsCount],
287+
[showSelectedOptionsCount, customSelectedOptionsCount, isFocussed],
271288
)
272289

273290
const renderOption = useCallback(
@@ -315,11 +332,40 @@ const SelectPicker = <OptionValue, IsMulti extends boolean>({
315332
}
316333
}
317334

335+
const handleInputChange: ReactSelectProps['onInputChange'] = (updatedInputValue, actionMeta) => {
336+
// If onInputChange is provided, then the input state should be controlled externally
337+
if (props.onInputChange) {
338+
props.onInputChange(updatedInputValue, actionMeta)
339+
return
340+
}
341+
setInputValue(updatedInputValue)
342+
}
343+
318344
const handleKeyDown: ReactSelectProps['onKeyDown'] = (e) => {
319345
// Prevent the option from being selected if meta or control key is pressed
320346
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
321347
e.preventDefault()
322348
}
349+
props.onKeyDown?.(e)
350+
}
351+
352+
const handleFocus: ReactSelectProps['onFocus'] = () => {
353+
setIsFocussed(true)
354+
}
355+
356+
const handleBlur: ReactSelectProps['onFocus'] = (e) => {
357+
setIsFocussed(false)
358+
359+
props.onBlur?.(e)
360+
}
361+
362+
const handleChange: ReactSelectProps<SelectPickerOptionType<OptionValue>, IsMulti>['onChange'] = (...params) => {
363+
// Retain the input value on selection change
364+
if (isMulti && isNullOrUndefined(props.inputValue)) {
365+
setInputValue(inputValue)
366+
}
367+
368+
props.onChange?.(...params)
323369
}
324370

325371
const commonProps = useMemo(
@@ -352,6 +398,7 @@ const SelectPicker = <OptionValue, IsMulti extends boolean>({
352398
labelId,
353399
shouldRenderCustomOptions,
354400
controlShouldRenderValue,
401+
customDisplayText,
355402
],
356403
)
357404

@@ -396,18 +443,27 @@ const SelectPicker = <OptionValue, IsMulti extends boolean>({
396443
MultiValueRemove: SelectPickerMultiValueRemove,
397444
GroupHeading: renderGroupHeading,
398445
NoOptionsMessage: renderNoOptionsMessage,
446+
...(shouldHideMenu && {
447+
Menu: () => null,
448+
DropdownIndicator: () => null,
449+
}),
399450
}}
400451
closeMenuOnSelect={!isMulti || closeMenuOnSelect}
401452
allowCreateWhileLoading={false}
402453
isValidNewOption={isValidNewOption}
403454
createOptionPosition="first"
404455
onCreateOption={handleCreateOption}
405456
renderMenuListFooter={!optionListError && renderMenuListFooter}
406-
inputValue={inputValue}
407-
onInputChange={onInputChange}
457+
inputValue={props.inputValue ?? inputValue}
458+
onInputChange={handleInputChange}
408459
icon={icon}
409460
showSelectedOptionIcon={shouldShowSelectedOptionIcon}
410461
onKeyDown={handleKeyDown}
462+
customDisplayText={customDisplayText}
463+
onFocus={handleFocus}
464+
onBlur={handleBlur}
465+
onChange={handleChange}
466+
controlShouldRenderValue={controlShouldRenderValue}
411467
/>
412468
</div>
413469
</ConditionalWrap>

src/Shared/Components/SelectPicker/common.tsx

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { Progressing } from '@Common/Progressing'
3030
import { ReactComponent as ICCaretDown } from '@Icons/ic-caret-down.svg'
3131
import { ReactComponent as ICClose } from '@Icons/ic-close.svg'
3232
import { ReactComponent as ICErrorExclamation } from '@Icons/ic-error-exclamation.svg'
33-
import { ChangeEvent } from 'react'
33+
import { ChangeEvent, Children } from 'react'
3434
import { noop } from '@Common/Helper'
3535
import { CHECKBOX_VALUE } from '@Common/Types'
3636
import { Checkbox } from '@Common/Checkbox'
@@ -107,18 +107,43 @@ export const SelectPickerControl = <OptionValue,>(props: ControlProps<SelectPick
107107
export const SelectPickerValueContainer = <OptionValue, IsMulti extends boolean>({
108108
showSelectedOptionsCount,
109109
customSelectedOptionsCount,
110+
isFocussed,
110111
...props
111112
}: ValueContainerProps<SelectPickerOptionType<OptionValue>> &
112-
Pick<SelectPickerProps<OptionValue, IsMulti>, 'showSelectedOptionsCount' | 'customSelectedOptionsCount'>) => {
113-
const { getValue } = props
113+
Pick<SelectPickerProps<OptionValue, IsMulti>, 'showSelectedOptionsCount' | 'customSelectedOptionsCount'> & {
114+
isFocussed: boolean
115+
}) => {
116+
const {
117+
getValue,
118+
selectProps: { customDisplayText },
119+
children,
120+
} = props
114121
const selectedOptionsLength = isNullOrUndefined(customSelectedOptionsCount)
115122
? (getValue() ?? []).length
116123
: customSelectedOptionsCount
124+
const childrenLength = Children.count(children)
117125

118126
return (
119127
<div className="flex left dc__gap-8 flex-grow-1">
120-
<div className="flex left">
121-
<components.ValueContainer {...props} />
128+
<div className="flex left flex-grow-1">
129+
<components.ValueContainer {...props}>
130+
{customDisplayText && selectedOptionsLength > 0 && !isFocussed ? (
131+
<>
132+
<p className="m-0 fs-13 fw-4 lh-20 cn-9 dc__truncate">{customDisplayText}</p>
133+
<div className="dc__position-abs">
134+
{Children.map(children, (child, index) => {
135+
if (index === childrenLength - 1) {
136+
return child
137+
}
138+
139+
return null
140+
})}
141+
</div>
142+
</>
143+
) : (
144+
children
145+
)}
146+
</components.ValueContainer>
122147
</div>
123148
{/* Size will not work here need to go in details later when prioritized */}
124149
{showSelectedOptionsCount && selectedOptionsLength > 0 && (
@@ -175,9 +200,13 @@ export const SelectPickerOption = <OptionValue, IsMulti extends boolean>({
175200
<div className="dc__no-shrink icon-dim-16 flex dc__fill-available-space">{startIcon}</div>
176201
)}
177202
<div className="flex-grow-1">
178-
<h4 className={`m-0 fs-13 ${isCreatableOption ? 'cb-5' : 'cn-9'} fw-4 lh-20 dc__truncate`}>
179-
{label}
180-
</h4>
203+
<Tooltip content={label} placement="right">
204+
<h4
205+
className={`m-0 fs-13 ${isCreatableOption ? 'cb-5' : 'cn-9'} fw-4 lh-20 dc__truncate`}
206+
>
207+
{label}
208+
</h4>
209+
</Tooltip>
181210
{/* Add support for custom ellipsis if required */}
182211
{showDescription &&
183212
(typeof description === 'string' ? (
@@ -261,11 +290,11 @@ export const SelectPickerMultiValueLabel = <OptionValue, IsMulti extends boolean
261290
}
262291

263292
export const SelectPickerMultiValueRemove = (props: MultiValueRemoveProps) => (
264-
<components.MultiValueLabel {...props}>
293+
<components.MultiValueRemove {...props}>
265294
<span className="flex dc__no-shrink">
266295
<ICClose className="icon-dim-12 icon-use-fill-n6" />
267296
</span>
268-
</components.MultiValueLabel>
297+
</components.MultiValueRemove>
269298
)
270299

271300
export const SelectPickerGroupHeading = <OptionValue,>({

src/Shared/Components/SelectPicker/type.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ declare module 'react-select/base' {
8484
* @default 'true'
8585
*/
8686
showSelectedOptionIcon?: boolean
87+
/**
88+
* If provided, the custom display text is shown in the value container
89+
*
90+
* @default null
91+
*/
92+
customDisplayText?: string
8793
/** Render function for the footer at the end of the options list. */
8894
renderOptionsFooter?: () => ReactNode
8995
}
@@ -241,10 +247,16 @@ export type SelectPickerProps<OptionValue = number | string, IsMulti extends boo
241247
* @default true
242248
*/
243249
shouldShowNoOptionsMessage?: boolean
250+
/**
251+
* If true, the menu list and the dropdown indicator are hidden. Suitable for use cases like multi-inputs
252+
*
253+
* @default false
254+
*/
255+
shouldHideMenu?: boolean
244256
} & (IsMulti extends true
245257
? {
246258
isMulti: IsMulti | boolean
247-
multiSelectProps?: {
259+
multiSelectProps?: Partial<Pick<SelectProps<OptionValue, IsMulti>, 'customDisplayText'>> & {
248260
/**
249261
* If true, the group heading can be selected
250262
*

0 commit comments

Comments
 (0)