Skip to content

Commit

Permalink
4002 - TablePaginated: Filter by User Role + Admin users 'Role' (#4043)
Browse files Browse the repository at this point in the history
* 4002 - Tablepaginated filter by user role/admin users role - Integration (#4038)

* 4002 - Add Filter multi select type

* 4002 - Add draft multi select to Users table

* 2661 - Display all columns

* 4002 - TablePaginated: Filter by User Role + Admin users 'Role' - Multiselect UI (#4049)

* 4002 - Hide clear button in Text filter

* 4002 - Add counter label for multi select

* 4002 - Expose Select onMenuClose & onMenuOpen props

* 4002 - Add Filter multiselect styling and tooltip

* 4002 - Fix table overlapping select options

* 4002 - Add spacing between label and search input

* 4002 - Fix props order in Select

* 4002 - Rename isTooltipVisible to canDisplayTooltip

* 4002 - Rename MultiValue to MultiValueSummary

* 4002 - Rename multiLabelKey to multiLabelSummaryKey

* 4002 - Remove import as OriginalMultiValueProps

* 4002 - Render Text x button conditionally

* 4002 - Update useTooltipContent

* 4002 - Fix styling
  • Loading branch information
yaguzmang authored Oct 21, 2024
1 parent 89c7f81 commit 06a65aa
Show file tree
Hide file tree
Showing 21 changed files with 278 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import 'src/client/style/partials';

.select__multiValue-label {
padding-right: $spacing-xxxs;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import './MultiValueSummary.scss'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { MultiValueProps as OriginalMultiValueProps } from 'react-select'

import { SelectProps } from 'client/components/Inputs/Select/types'

type MultiValueProps = OriginalMultiValueProps & Pick<SelectProps, 'multiLabelSummaryKey'>

export const MultiValueSummary: React.FC<MultiValueProps> = (props) => {
const { getValue, index, multiLabelSummaryKey } = props
const count = getValue().length
const { t } = useTranslation()

const displayCountLabel = index === 0

if (displayCountLabel) {
return <span className="select__multiValue-label">{t(multiLabelSummaryKey, { count })}</span>
}

return null
}

export default MultiValueSummary
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MultiValueSummary } from './MultiValueSummary'
1 change: 1 addition & 0 deletions src/client/components/Inputs/Select/Indicators/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { ClearIndicator, DropdownIndicator, IndicatorsContainer } from './Indicators'
export { MultiSelectOption } from './MultiSelectOption'
export { MultiValueSummary } from './MultiValueSummary'
4 changes: 3 additions & 1 deletion src/client/components/Inputs/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useValue } from './hooks/useValue'
import { SelectProps } from './types'

const Select: React.FC<SelectProps> = (props) => {
const { disabled, isClearable, isMulti, maxMenuHeight, placeholder } = props
const { disabled, isClearable, isMulti, maxMenuHeight, onMenuClose, onMenuOpen, placeholder } = props

const classNames = useClassNames(props)
const components = useComponents(props)
Expand All @@ -32,6 +32,8 @@ const Select: React.FC<SelectProps> = (props) => {
menuPlacement="auto"
menuPosition="fixed"
onChange={onChange}
onMenuClose={onMenuClose}
onMenuOpen={onMenuOpen}
options={options}
placeholder={placeholder ?? ''}
value={value}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { useMemo } from 'react'
import { Props as ReactSelectProps } from 'react-select'
import React, { useMemo } from 'react'
import { MultiValueProps, Props as ReactSelectProps } from 'react-select'

import { Objects } from 'utils/objects'

import {
ClearIndicator,
DropdownIndicator,
IndicatorsContainer,
MultiSelectOption,
MultiValueSummary,
} from 'client/components/Inputs/Select/Indicators'
import { SelectProps } from 'client/components/Inputs/Select/types'

type Returned = ReactSelectProps['components']

export const useComponents = (props: SelectProps): Returned => {
const { isMulti } = props
const { isMulti, multiLabelSummaryKey } = props

return useMemo<Returned>(() => {
const components: Returned = {
Expand All @@ -22,7 +25,13 @@ export const useComponents = (props: SelectProps): Returned => {
IndicatorSeparator: null,
}
if (isMulti) components.Option = MultiSelectOption
if (isMulti && !Objects.isEmpty(multiLabelSummaryKey)) {
components.MultiValue = (originalMultiValueProps: MultiValueProps) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<MultiValueSummary {...originalMultiValueProps} multiLabelSummaryKey={multiLabelSummaryKey} />
)
}

return components
}, [isMulti])
}, [isMulti, multiLabelSummaryKey])
}
6 changes: 5 additions & 1 deletion src/client/components/Inputs/Select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ export type OptionsOrGroups = readonly (Option | OptionsGroup)[]

export type ValueInput = string | Array<string> | null

type SelectBaseProps = Pick<ReactSelectProps, 'isClearable' | 'isMulti' | 'maxMenuHeight' | 'placeholder'>
type SelectBaseProps = Pick<
ReactSelectProps,
'isClearable' | 'isMulti' | 'maxMenuHeight' | 'onMenuOpen' | 'onMenuClose' | 'placeholder'
>
type SelectClassNamesProps = {
classNames?: { container?: string }
}
export type SelectProps = SelectBaseProps &
SelectClassNamesProps & {
disabled?: boolean
multiLabelSummaryKey?: string
onChange: (value: string | Array<string> | null) => void
options: OptionsOrGroups
toggleAll?: boolean
Expand Down
1 change: 1 addition & 0 deletions src/client/components/TablePaginated/Filters/Filters.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
align-items: center;
display: flex;
gap: $spacing-xxs;
height: 30px;
width: 100%;

svg.icon_filter {
Expand Down
4 changes: 3 additions & 1 deletion src/client/components/TablePaginated/Filters/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import React from 'react'
import { TablePaginatedFilterType } from 'meta/tablePaginated'

import Icon from 'client/components/Icon'
import MultiSelect from 'client/components/TablePaginated/Filters/MultiSelect/MultiSelect'
import Text from 'client/components/TablePaginated/Filters/Text/Text'
import { TablePaginatedFilter } from 'client/components/TablePaginated/types'

const componentsByFilterType: Record<
TablePaginatedFilterType,
React.FC<TablePaginatedFilter<TablePaginatedFilterType> & { path: string }>
> = {
[TablePaginatedFilterType.TEXT]: Text,
[TablePaginatedFilterType.MULTI_SELECT]: MultiSelect,
[TablePaginatedFilterType.SWITCH]: () => null,
[TablePaginatedFilterType.TEXT]: Text,
}

type Props = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
@import 'src/client/style/partials';

$icon-size: 14px;

.filter-multiselect__tooltip-trigger {
height: 100%;
}

div.filter-multiselect__container {
border-radius: 2px;
border: 1px solid $ui-border;
font-size: $font-xxs;
min-width: #{3 * $spacing-xl};
padding: 0;
transition: all 0.2s ease;

div.select__control {
min-height: unset;
padding: 0 6px;

div.select__indicatorsContainer {
div.select__clearIndicator {
height: unset;
width: unset;

svg.icon {
color: rgba(0, 0, 0, 0.5);
height: $icon-size;
width: $icon-size;
}

&:hover {
background-color: unset;

svg.icon {
color: $ui-destructive;
}
}
}
}
}

div.select__dropdownIndicator {
svg {
height: $icon-size;
width: $icon-size;
}
}

&.active {
border: 1px solid $ui-accent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import './MultiSelect.scss'
import React from 'react'

import classNames from 'classnames'

import { TablePaginatedFilterType } from 'meta/tablePaginated'
import { TooltipId } from 'meta/tooltip'

import { useAppDispatch } from 'client/store'
import { TablePaginatedActions } from 'client/store/ui/tablePaginated'
import { useTablePaginatedFilterValue } from 'client/store/ui/tablePaginated/hooks'
import Select from 'client/components/Inputs/Select'
import { TablePaginatedFilter } from 'client/components/TablePaginated/types'

import { useTooltipContent } from './hooks/useTooltipContent'

type Props = TablePaginatedFilter<TablePaginatedFilterType.MULTI_SELECT> & {
path: string
}

const MultiSelect: React.FC<Props> = (props: Props) => {
const { fieldName, label, path, options } = props

const dispatch = useAppDispatch()

const { hideTooltip, showTooltip, tooltipContent } = useTooltipContent({ fieldName, options, path })

const filterValue = useTablePaginatedFilterValue<Array<string>>(path, fieldName)

const handleChange = (value: Array<string>) => {
dispatch(
TablePaginatedActions.setFilterValue({
fieldName,
path,
value,
})
)
}

return (
<div
className="filter-multiselect__tooltip-trigger"
data-tooltip-content={tooltipContent}
data-tooltip-id={TooltipId.info}
>
<Select
classNames={{
container: classNames('filter-multiselect__container', {
active: filterValue?.length > 0,
}),
}}
isMulti
multiLabelSummaryKey="admin.role"
onChange={handleChange}
onMenuClose={showTooltip}
onMenuOpen={hideTooltip}
options={options}
placeholder={label}
value={filterValue}
/>
</div>
)
}

export default MultiSelect
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useCallback, useMemo, useState } from 'react'

import { Objects } from 'utils/objects'

import { TablePaginatedFilterType } from 'meta/tablePaginated'

import { useTablePaginatedFilterValue } from 'client/store/ui/tablePaginated/hooks'
import { TablePaginatedFilter } from 'client/components/TablePaginated/types'

type Props = {
fieldName: string
options: TablePaginatedFilter<TablePaginatedFilterType.MULTI_SELECT>['options']
path: string
}

type Returned = {
hideTooltip: () => void
showTooltip: () => void
tooltipContent: string | null
}

export const useTooltipContent = (props: Props): Returned => {
const { fieldName, options, path } = props
const filterValue = useTablePaginatedFilterValue<Array<string>>(path, fieldName)
const [canDisplayTooltip, setCanDisplayTooltip] = useState(true)

const valueToLabelMap = useMemo<Record<string, string>>(() => {
return options.reduce<Record<string, string>>((acc, { value, label }) => {
return { ...acc, [value]: label }
}, {})
}, [options])

const tooltipContent = useMemo<string | null>(() => {
if (Objects.isEmpty(filterValue)) return null
if (!canDisplayTooltip) return null

const selectedLabels = filterValue.reduce<Array<string>>((acc, value) => {
const label = valueToLabelMap[value]
if (!Objects.isEmpty(label)) acc.push(label)
return acc
}, [])

if (selectedLabels.length === 0) {
return null
}
return selectedLabels.join(', ')
}, [canDisplayTooltip, filterValue, valueToLabelMap])

const hideTooltip = useCallback(() => setCanDisplayTooltip(false), [])
const showTooltip = useCallback(() => setCanDisplayTooltip(true), [])

return {
hideTooltip,
showTooltip,
tooltipContent,
}
}
6 changes: 1 addition & 5 deletions src/client/components/TablePaginated/Filters/Text/Text.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

.table-paginated-filter-input {
display: inline-block;
height: 100%;
position: relative;

input {
Expand Down Expand Up @@ -33,11 +34,6 @@
color: $ui-destructive;
}
}

&.disabled {
opacity: 0.6;
pointer-events: none;
}
}

&.active {
Expand Down
12 changes: 5 additions & 7 deletions src/client/components/TablePaginated/Filters/Text/Text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,11 @@ const Text = (props: Props) => {
return (
<div className={classNames('table-paginated-filter-input', { active: !Objects.isEmpty(filterValue) })}>
<InputText onChange={handleChange} placeholder={label} value={filterValue ?? ''} />
<button
className={classNames('clear-button icon', { disabled: Objects.isEmpty(filterValue) })}
onClick={handleClearInput}
type="button"
>
<Icon className="icon-sub" name="remove" />
</button>
{!Objects.isEmpty(filterValue) && (
<button className="clear-button icon" onClick={handleClearInput} type="button">
<Icon className="icon-sub" name="remove" />
</button>
)}
</div>
)
}
Expand Down
1 change: 1 addition & 0 deletions src/client/components/TablePaginated/TablePaginated.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
gap: $spacing-xs;
height: $spacing-l;
padding: $spacing-xxxs;
z-index: 1;
}

.table-paginated-datagrid {
Expand Down
15 changes: 14 additions & 1 deletion src/client/components/TablePaginated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,31 @@ export type TablePaginatedCounter = {
export type TablePaginatedEmptyListComponent = React.FC

type TablePaginatedFilterTypeMap = {
[TablePaginatedFilterType.MULTI_SELECT]: Array<string>
[TablePaginatedFilterType.SWITCH]: boolean
[TablePaginatedFilterType.TEXT]: string
}

export type TablePaginatedFilter<FilterType extends TablePaginatedFilterType> = {
type BaseTablePaginatedFilter<FilterType extends TablePaginatedFilterType> = {
defaultValue?: TablePaginatedFilterTypeMap[FilterType]
fieldName: string
hidden?: boolean
label: string
type: FilterType
}

type MultiSelectItem = {
label: string
value: string
}

type MultiSelectFilter = BaseTablePaginatedFilter<TablePaginatedFilterType.MULTI_SELECT> & {
options: Array<MultiSelectItem>
}

export type TablePaginatedFilter<FilterType extends TablePaginatedFilterType> =
FilterType extends TablePaginatedFilterType.MULTI_SELECT ? MultiSelectFilter : BaseTablePaginatedFilter<FilterType>

export type TablePaginatedSkeleton = {
baseColor: string
highlightColor: string
Expand Down
Loading

0 comments on commit 06a65aa

Please sign in to comment.