mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:31:43 +00:00
Batch Changes: Replace MultiSelect with wildcard MultiCombobox UI (#46450)
* Add first version of MultiCombobox UI * Fix problem with value set in combobox input * Replace multiselect with MultiCombobox UI on the batch changes list page * Remove react-select package * Add custom styles to batch changes list filter UI * Bring Input and Select types back * Fix no state option * Fix by review comments * Clear out the search input after you picked option
This commit is contained in:
parent
03f776c4a8
commit
da745613cb
@ -1,7 +1,5 @@
|
||||
import { Optional } from 'utility-types'
|
||||
|
||||
import { MultiSelectState } from '@sourcegraph/wildcard'
|
||||
|
||||
import { BatchChangeState } from '../../graphql-operations'
|
||||
|
||||
import { DiffMode } from './diffMode'
|
||||
@ -9,6 +7,14 @@ import { RecentSearch } from './recentSearches'
|
||||
import { SectionID, NoResultsSectionID } from './searchSidebar'
|
||||
import { TourListState } from './tourState'
|
||||
|
||||
// Prior to this type we store in settings list of MultiSelectState
|
||||
// we no longer use MultiSelect UI but for backward compatibility we still
|
||||
// have to store and parse the old version of batch changes filters
|
||||
export interface LegacyBatchChangesFilter {
|
||||
label: string
|
||||
value: BatchChangeState
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for temporary settings.
|
||||
*/
|
||||
@ -31,7 +37,7 @@ export interface TemporarySettingsSchema {
|
||||
'user.themePreference': string
|
||||
'signup.finishedWelcomeFlow': boolean
|
||||
'homepage.userInvites.tab': number
|
||||
'batches.defaultListFilters': MultiSelectState<BatchChangeState>
|
||||
'batches.defaultListFilters': LegacyBatchChangesFilter[]
|
||||
'batches.downloadSpecModalDismissed': boolean
|
||||
'codeintel.badge.used': boolean
|
||||
'codeintel.referencePanel.redesign.ctaDismissed': boolean
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
// Normalize font-weight within Label element
|
||||
font-weight: normal;
|
||||
}
|
||||
@ -1,37 +1,96 @@
|
||||
import React from 'react'
|
||||
import { FC, useCallback, useId, useState } from 'react'
|
||||
|
||||
import { H3, H4, MultiSelect, MultiSelectOption, MultiSelectProps } from '@sourcegraph/wildcard'
|
||||
import classNames from 'classnames'
|
||||
import { upperFirst } from 'lodash'
|
||||
|
||||
import {
|
||||
H3,
|
||||
H4,
|
||||
Label,
|
||||
MultiCombobox,
|
||||
MultiComboboxInput,
|
||||
MultiComboboxPopover,
|
||||
MultiComboboxList,
|
||||
MultiComboboxEmptyList,
|
||||
MultiComboboxOption,
|
||||
} from '@sourcegraph/wildcard'
|
||||
|
||||
import { BatchChangeState } from '../../../graphql-operations'
|
||||
|
||||
export const OPEN_STATUS: MultiSelectOption<BatchChangeState> = { label: 'Open', value: BatchChangeState.OPEN }
|
||||
export const DRAFT_STATUS: MultiSelectOption<BatchChangeState> = { label: 'Draft', value: BatchChangeState.DRAFT }
|
||||
export const CLOSED_STATUS: MultiSelectOption<BatchChangeState> = { label: 'Closed', value: BatchChangeState.CLOSED }
|
||||
import styles from './BatchChangeListFilter.module.scss'
|
||||
|
||||
export const STATUS_OPTIONS: MultiSelectOption<BatchChangeState>[] = [OPEN_STATUS, DRAFT_STATUS, CLOSED_STATUS]
|
||||
// Drafts are a new feature of severside execution that for now should not be shown if
|
||||
// execution is not enabled.
|
||||
const STATUS_OPTIONS_NO_DRAFTS: MultiSelectOption<BatchChangeState>[] = [OPEN_STATUS, CLOSED_STATUS]
|
||||
/** Returns string with capitalized first letter */
|
||||
const format = (filter: BatchChangeState): string => upperFirst(filter.toLowerCase())
|
||||
|
||||
interface BatchChangeListFiltersProps
|
||||
extends Required<Pick<MultiSelectProps<MultiSelectOption<BatchChangeState>>, 'onChange' | 'value'>> {
|
||||
interface BatchChangeListFiltersProps {
|
||||
filters: BatchChangeState[]
|
||||
selectedFilters: BatchChangeState[]
|
||||
onFiltersChange: (filters: BatchChangeState[]) => void
|
||||
className?: string
|
||||
isExecutionEnabled: boolean
|
||||
}
|
||||
|
||||
export const BatchChangeListFilters: React.FunctionComponent<React.PropsWithChildren<BatchChangeListFiltersProps>> = ({
|
||||
isExecutionEnabled,
|
||||
...props
|
||||
}) => (
|
||||
<>
|
||||
{/* TODO: This should be a proper label. MultiSelect currently doesn't support that being inline though, so this is for later. */}
|
||||
<H4 as={H3} className="mb-0 mr-2">
|
||||
Status
|
||||
</H4>
|
||||
<MultiSelect
|
||||
{...props}
|
||||
options={isExecutionEnabled ? STATUS_OPTIONS : STATUS_OPTIONS_NO_DRAFTS}
|
||||
aria-label="Select batch change status to filter."
|
||||
/>
|
||||
</>
|
||||
)
|
||||
export const BatchChangeListFilters: FC<BatchChangeListFiltersProps> = props => {
|
||||
const { filters, selectedFilters, onFiltersChange, className } = props
|
||||
|
||||
const id = useId()
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(newFilters: BatchChangeState[]) => {
|
||||
if (newFilters.length > selectedFilters.length) {
|
||||
// Reset value when we add new filter
|
||||
// see https://github.com/sourcegraph/sourcegraph/pull/46450#discussion_r1070840089
|
||||
setSearchTerm('')
|
||||
}
|
||||
|
||||
onFiltersChange(newFilters)
|
||||
},
|
||||
[selectedFilters, onFiltersChange]
|
||||
)
|
||||
|
||||
// Render only non-selected filters and filters that match with search term value
|
||||
const suggestions = filters.filter(
|
||||
filter => !selectedFilters.includes(filter) && filter.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
return (
|
||||
<Label htmlFor={id} className={classNames(className, styles.root)}>
|
||||
<H4 as={H3} className="mb-0 mr-2">
|
||||
Status
|
||||
</H4>
|
||||
|
||||
<MultiCombobox
|
||||
selectedItems={selectedFilters}
|
||||
getItemName={format}
|
||||
getItemKey={format}
|
||||
onSelectedItemsChange={handleFilterChange}
|
||||
aria-label="Select batch change status to filter."
|
||||
>
|
||||
<MultiComboboxInput
|
||||
id={id}
|
||||
value={searchTerm}
|
||||
autoCorrect="false"
|
||||
autoComplete="off"
|
||||
placeholder="Select filter..."
|
||||
onChange={event => setSearchTerm(event.target.value)}
|
||||
/>
|
||||
|
||||
<MultiComboboxPopover>
|
||||
<MultiComboboxList items={suggestions}>
|
||||
{filters =>
|
||||
filters.map((filter, index) => (
|
||||
<MultiComboboxOption key={filter.toString()} value={format(filter)} index={index} />
|
||||
))
|
||||
}
|
||||
</MultiComboboxList>
|
||||
|
||||
{suggestions.length === 0 && (
|
||||
<MultiComboboxEmptyList>
|
||||
{!searchTerm ? <>All filters are selected</> : <>No options</>}
|
||||
</MultiComboboxEmptyList>
|
||||
)}
|
||||
</MultiComboboxPopover>
|
||||
</MultiCombobox>
|
||||
</Label>
|
||||
)
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ export const BatchChangeListPage: React.FunctionComponent<React.PropsWithChildre
|
||||
|
||||
const isExecutionEnabled = isBatchChangesExecutionEnabled(settingsCascade)
|
||||
|
||||
const { selectedFilters, setSelectedFilters, selectedStates } = useBatchChangeListFilters()
|
||||
const { selectedFilters, setSelectedFilters, availableFilters } = useBatchChangeListFilters({ isExecutionEnabled })
|
||||
const [selectedTab, setSelectedTab] = useState<SelectedTab>(
|
||||
openTab ?? (isSourcegraphDotCom ? 'gettingStarted' : 'batchChanges')
|
||||
)
|
||||
@ -118,7 +118,7 @@ export const BatchChangeListPage: React.FunctionComponent<React.PropsWithChildre
|
||||
query: namespaceID ? BATCH_CHANGES_BY_NAMESPACE : BATCH_CHANGES,
|
||||
variables: {
|
||||
namespaceID,
|
||||
states: selectedStates,
|
||||
states: selectedFilters,
|
||||
first: BATCH_CHANGES_PER_PAGE_COUNT,
|
||||
after: null,
|
||||
viewerCanAdminister: null,
|
||||
@ -206,10 +206,10 @@ export const BatchChangeListPage: React.FunctionComponent<React.PropsWithChildre
|
||||
)}
|
||||
|
||||
<BatchChangeListFilters
|
||||
filters={availableFilters}
|
||||
selectedFilters={selectedFilters}
|
||||
onFiltersChange={setSelectedFilters}
|
||||
className="m-0"
|
||||
isExecutionEnabled={isExecutionEnabled}
|
||||
value={selectedFilters}
|
||||
onChange={setSelectedFilters}
|
||||
/>
|
||||
</div>
|
||||
{error && <ConnectionError errors={[error.message]} />}
|
||||
|
||||
@ -1,64 +1,85 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { lowerCase } from 'lodash'
|
||||
import { useHistory } from 'react-router'
|
||||
|
||||
import { LegacyBatchChangesFilter } from '@sourcegraph/shared/src/settings/temporary/TemporarySettings'
|
||||
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary/useTemporarySetting'
|
||||
import { MultiSelectState } from '@sourcegraph/wildcard'
|
||||
|
||||
import { BatchChangeState } from '../../../graphql-operations'
|
||||
|
||||
import { STATUS_OPTIONS } from './BatchChangeListFilters'
|
||||
const STATUS_OPTIONS = [BatchChangeState.OPEN, BatchChangeState.DRAFT, BatchChangeState.CLOSED]
|
||||
|
||||
const statesToFilters = (states: string[]): MultiSelectState<BatchChangeState> =>
|
||||
STATUS_OPTIONS.filter(option => states.map(lowerCase).includes(option.value.toLowerCase()))
|
||||
// Drafts are a new feature of serverside execution that for now should not be shown if
|
||||
// execution is not enabled.
|
||||
const STATUS_OPTIONS_NO_DRAFTS: BatchChangeState[] = [BatchChangeState.OPEN, BatchChangeState.CLOSED]
|
||||
|
||||
const filtersToStates = (filters: MultiSelectState<BatchChangeState>): BatchChangeState[] =>
|
||||
filters.map(filter => filter.value)
|
||||
const fromLegacyFilters = (legacyFilters: LegacyBatchChangesFilter[]): BatchChangeState[] =>
|
||||
legacyFilters.map(legacyFilter => legacyFilter.value)
|
||||
|
||||
const toLegacyFilters = (filters: BatchChangeState[]): LegacyBatchChangesFilter[] =>
|
||||
filters.map(filter => ({ label: filter.toString(), value: filter }))
|
||||
|
||||
interface UseBatchChangeListFiltersProps {
|
||||
isExecutionEnabled: boolean
|
||||
}
|
||||
|
||||
interface UseBatchChangeListFiltersResult {
|
||||
/** State representing the different filters selected in the `MultiSelect` UI. */
|
||||
selectedFilters: MultiSelectState<BatchChangeState>
|
||||
selectedFilters: BatchChangeState[]
|
||||
|
||||
/** Method to set the filters selected in the `MultiSelect`. */
|
||||
setSelectedFilters: (filters: MultiSelectState<BatchChangeState>) => void
|
||||
setSelectedFilters: (filters: BatchChangeState[]) => void
|
||||
|
||||
/**
|
||||
* Array of raw `BatchChangeState`s corresponding to `selectedFilters`, i.e. for
|
||||
* passing in GraphQL connection query parameters.
|
||||
* List of available batch changes filters, it may be different based on
|
||||
* {@link isExecutionEnabled} prop
|
||||
*/
|
||||
selectedStates: BatchChangeState[]
|
||||
availableFilters: BatchChangeState[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing, persisting, and transforming the state options selected from
|
||||
* the `MultiSelect` UI to filter a list of batch changes.
|
||||
* Custom hook for managing and persisting filter options selected from
|
||||
* the MultiCombobox UI to filter a list of batch changes.
|
||||
*/
|
||||
export const useBatchChangeListFilters = (): UseBatchChangeListFiltersResult => {
|
||||
export const useBatchChangeListFilters = (props: UseBatchChangeListFiltersProps): UseBatchChangeListFiltersResult => {
|
||||
const { isExecutionEnabled } = props
|
||||
|
||||
const history = useHistory()
|
||||
const availableFilters = isExecutionEnabled ? STATUS_OPTIONS : STATUS_OPTIONS_NO_DRAFTS
|
||||
|
||||
// NOTE: Fetching this setting is an async operation, so we can't use it as the
|
||||
// initial value for `useState`. Instead, we will set the value of the filter state in
|
||||
// a `useEffect` hook once we've loaded it.
|
||||
const [defaultFilters, setDefaultFilters] = useTemporarySetting('batches.defaultListFilters', [])
|
||||
|
||||
const [selectedFilters, setSelectedFiltersRaw] = useState<MultiSelectState<BatchChangeState>>(() => {
|
||||
const [hasModifiedFilters, setHasModifiedFilters] = useState(false)
|
||||
const [selectedFilters, setSelectedFiltersRaw] = useState<BatchChangeState[]>(() => {
|
||||
const searchParameters = new URLSearchParams(history.location.search).get('states')
|
||||
|
||||
if (searchParameters) {
|
||||
return statesToFilters(searchParameters.split(','))
|
||||
const loweredCaseFilters = new Set(availableFilters.map(filter => filter.toLowerCase()))
|
||||
const urlFilters = searchParameters.split(',').map(option => option.toLowerCase())
|
||||
|
||||
return urlFilters
|
||||
.filter(urlFilter => loweredCaseFilters.has(urlFilter))
|
||||
.map(urlFilters => urlFilters.toUpperCase() as BatchChangeState)
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
const [hasModifiedFilters, setHasModifiedFilters] = useState(false)
|
||||
|
||||
const setSelectedFilters = useCallback(
|
||||
(filters: MultiSelectState<BatchChangeState>) => {
|
||||
setHasModifiedFilters(true)
|
||||
setSelectedFiltersRaw(filters)
|
||||
setDefaultFilters(filters)
|
||||
|
||||
(filters: BatchChangeState[]) => {
|
||||
const searchParameters = new URLSearchParams(history.location.search)
|
||||
|
||||
if (filters.length > 0) {
|
||||
searchParameters.set('states', filtersToStates(filters).join(',').toLowerCase())
|
||||
searchParameters.set(
|
||||
'states',
|
||||
filters
|
||||
.map(filter => filter.toLowerCase())
|
||||
.join(',')
|
||||
.toLowerCase()
|
||||
)
|
||||
} else {
|
||||
searchParameters.delete('states')
|
||||
}
|
||||
@ -66,6 +87,10 @@ export const useBatchChangeListFilters = (): UseBatchChangeListFiltersResult =>
|
||||
if (history.location.search !== searchParameters.toString()) {
|
||||
history.replace({ ...history.location, search: searchParameters.toString() })
|
||||
}
|
||||
|
||||
setHasModifiedFilters(true)
|
||||
setSelectedFiltersRaw(filters)
|
||||
setDefaultFilters(toLegacyFilters(filters))
|
||||
},
|
||||
[setDefaultFilters, history]
|
||||
)
|
||||
@ -77,15 +102,13 @@ export const useBatchChangeListFilters = (): UseBatchChangeListFiltersResult =>
|
||||
const searchParameters = new URLSearchParams(history.location.search).get('states')
|
||||
|
||||
if (defaultFilters && !hasModifiedFilters && !searchParameters) {
|
||||
setSelectedFiltersRaw(defaultFilters)
|
||||
setSelectedFiltersRaw(fromLegacyFilters(defaultFilters))
|
||||
}
|
||||
}, [defaultFilters, hasModifiedFilters, history.location.search])
|
||||
|
||||
const selectedStates = useMemo<BatchChangeState[]>(() => filtersToStates(selectedFilters), [selectedFilters])
|
||||
|
||||
return {
|
||||
selectedFilters,
|
||||
setSelectedFilters,
|
||||
selectedStates,
|
||||
availableFilters,
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,3 +93,10 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.zero-state {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -172,6 +172,7 @@ export const MultiComboboxInput = forwardRef<HTMLInputElement, MultiComboboxInpu
|
||||
selectOnClick={false}
|
||||
autocomplete={false}
|
||||
value={value.toString()}
|
||||
byPassValue={value.toString()}
|
||||
{...attributes}
|
||||
/>
|
||||
)
|
||||
@ -179,12 +180,13 @@ export const MultiComboboxInput = forwardRef<HTMLInputElement, MultiComboboxInpu
|
||||
|
||||
interface MultiValueInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
status?: InputStatus | `${InputStatus}`
|
||||
byPassValue: string
|
||||
}
|
||||
|
||||
// Forward ref doesn't support function components with generic,
|
||||
// so we have to cast a proper FC types with generic props
|
||||
const MultiValueInput = forwardRef((props: MultiValueInputProps, ref: Ref<HTMLInputElement>) => {
|
||||
const { onKeyDown, onFocus, onBlur, value, ...attributes } = props
|
||||
const { onKeyDown, onFocus, onBlur, byPassValue, value, ...attributes } = props
|
||||
|
||||
const {
|
||||
setInputElement,
|
||||
@ -201,7 +203,7 @@ const MultiValueInput = forwardRef((props: MultiValueInputProps, ref: Ref<HTMLIn
|
||||
const listRef = useMergeRefs<HTMLUListElement>([setInputElement])
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (value === '' && event.key === Key.Backspace) {
|
||||
if (byPassValue === '' && event.key === Key.Backspace) {
|
||||
onSelectedItemsChange(selectedItems.slice(0, -1))
|
||||
|
||||
// Prevent any single combobox UI state machine updates
|
||||
@ -265,6 +267,7 @@ const MultiValueInput = forwardRef((props: MultiValueInputProps, ref: Ref<HTMLIn
|
||||
))}
|
||||
<Input
|
||||
{...attributes}
|
||||
value={byPassValue}
|
||||
ref={inputRef}
|
||||
className={styles.inputContainer}
|
||||
inputClassName={styles.input}
|
||||
@ -326,6 +329,14 @@ export function MultiComboboxList<T>(props: MultiComboboxListProps<T>): ReactEle
|
||||
)
|
||||
}
|
||||
|
||||
interface MultiComboboxEmptyListProps extends HTMLAttributes<HTMLSpanElement> {}
|
||||
|
||||
export function MultiComboboxEmptyList(props: MultiComboboxEmptyListProps): ReactElement {
|
||||
const { className, ...attributes } = props
|
||||
|
||||
return <span {...attributes} className={classNames(className, styles.zeroState)} />
|
||||
}
|
||||
|
||||
interface MultiComboboxOptionProps extends ComboboxOptionProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ export {
|
||||
MultiComboboxInput,
|
||||
MultiComboboxPopover,
|
||||
MultiComboboxList,
|
||||
MultiComboboxEmptyList,
|
||||
MultiComboboxOptionGroup,
|
||||
MultiComboboxOption,
|
||||
MultiComboboxOptionText,
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
import { mdiClose } from '@mdi/js'
|
||||
import { components, ClearIndicatorProps } from 'react-select'
|
||||
|
||||
import { Icon } from '../../Icon'
|
||||
|
||||
import { MultiSelectOption } from './types'
|
||||
|
||||
import styles from './MultiSelect.module.scss'
|
||||
|
||||
// Overwrite the clear indicator with `CloseIcon`
|
||||
export const ClearIndicator = <OptionValue extends unknown = unknown>(
|
||||
props: ClearIndicatorProps<MultiSelectOption<OptionValue>, true>
|
||||
): ReactElement => (
|
||||
<components.ClearIndicator {...props}>
|
||||
<Icon className={styles.clearIcon} svgPath={mdiClose} inline={false} aria-hidden={true} />
|
||||
</components.ClearIndicator>
|
||||
)
|
||||
@ -1,24 +0,0 @@
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
import { mdiChevronDown } from '@mdi/js'
|
||||
import { components, DropdownIndicatorProps } from 'react-select'
|
||||
|
||||
import { Icon } from '../../Icon'
|
||||
|
||||
import { MultiSelectOption } from './types'
|
||||
|
||||
import styles from './MultiSelect.module.scss'
|
||||
|
||||
// Overwrite the dropdown indicator with `ChevronDownIcon`
|
||||
export const DropdownIndicator = <OptionValue extends unknown = unknown>(
|
||||
props: DropdownIndicatorProps<MultiSelectOption<OptionValue>, true>
|
||||
): ReactElement => (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<Icon
|
||||
className={props.isDisabled ? styles.dropdownIconDisabled : styles.dropdownIcon}
|
||||
svgPath={mdiChevronDown}
|
||||
inline={false}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
</components.DropdownIndicator>
|
||||
)
|
||||
@ -1,62 +0,0 @@
|
||||
.multi-select {
|
||||
min-width: 6.5rem;
|
||||
|
||||
:global(.theme-light) & {
|
||||
--react-select-neutral0: var(--white);
|
||||
--react-select-neutral5: var(--gray-01);
|
||||
--react-select-neutral10: var(--gray-01);
|
||||
--react-select-neutral20: var(--gray-02);
|
||||
--react-select-neutral30: var(--gray-03);
|
||||
--react-select-neutral40: var(--gray-04);
|
||||
--react-select-neutral50: var(--gray-05);
|
||||
--react-select-neutral60: var(--gray-06);
|
||||
--react-select-neutral70: var(--gray-07);
|
||||
--react-select-neutral80: var(--gray-08);
|
||||
--react-select-neutral90: var(--gray-09);
|
||||
--select-button-border-color: var(--secondary-3);
|
||||
}
|
||||
|
||||
:global(.theme-dark) & {
|
||||
--react-select-neutral0: var(--black);
|
||||
--react-select-neutral5: var(--gray-09);
|
||||
--react-select-neutral10: var(--gray-08);
|
||||
--react-select-neutral20: var(--gray-07);
|
||||
--react-select-neutral30: var(--gray-06);
|
||||
--react-select-neutral40: var(--gray-05);
|
||||
--react-select-neutral50: var(--gray-04);
|
||||
--react-select-neutral60: var(--gray-03);
|
||||
--react-select-neutral70: var(--gray-02);
|
||||
--react-select-neutral80: var(--gray-01);
|
||||
--react-select-neutral90: var(--white);
|
||||
--select-button-border-color: var(--secondary);
|
||||
}
|
||||
|
||||
input:focus-within {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
color: var(--icon-color);
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
color: var(--icon-color);
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.dropdown-icon-disabled {
|
||||
color: var(--text-disabled);
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.multi-value-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Meta, Story } from '@storybook/react'
|
||||
|
||||
import { H1, H2 } from '../..'
|
||||
import { BrandedStory } from '../../../stories/BrandedStory'
|
||||
import { Grid } from '../../Grid/Grid'
|
||||
|
||||
import { MultiSelect, MultiSelectProps, MultiSelectState, MultiSelectOption } from '.'
|
||||
|
||||
const config: Meta = {
|
||||
title: 'wildcard/MultiSelect',
|
||||
|
||||
decorators: [story => <BrandedStory>{() => <div className="container mt-3">{story()}</div>}</BrandedStory>],
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
type OptionValue = 'chocolate' | 'strawberry' | 'vanilla' | 'green tea' | 'rocky road' | 'really long'
|
||||
|
||||
const OPTIONS: MultiSelectOption<OptionValue>[] = [
|
||||
{ value: 'chocolate', label: 'Chocolate' },
|
||||
{ value: 'strawberry', label: 'Strawberry' },
|
||||
{ value: 'vanilla', label: 'Vanilla' },
|
||||
{ value: 'green tea', label: 'Green Tea' },
|
||||
{ value: 'rocky road', label: 'Rocky Road' },
|
||||
{ value: 'really long', label: 'A really really really REALLY long ice cream flavor' },
|
||||
]
|
||||
|
||||
const BaseSelect = (props: Partial<Pick<MultiSelectProps, 'isValid' | 'isDisabled'>>) => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<MultiSelectState<OptionValue>>([])
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
options={OPTIONS}
|
||||
defaultValue={selectedOptions}
|
||||
onChange={setSelectedOptions}
|
||||
message="I am a message"
|
||||
label="Select your favorite ice cream flavors."
|
||||
aria-label="Select your favorite ice cream flavors."
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SelectWithValues = () => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<MultiSelectState<OptionValue>>([OPTIONS[5], OPTIONS[1]])
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
options={OPTIONS}
|
||||
defaultValue={selectedOptions}
|
||||
onChange={setSelectedOptions}
|
||||
message="I am a message"
|
||||
label="Select your favorite ice cream flavors."
|
||||
aria-label="Select your favorite ice cream flavors."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const MultiSelectExamples: Story = () => (
|
||||
<>
|
||||
<H1>Multi Select</H1>
|
||||
<Grid columnCount={4}>
|
||||
<div>
|
||||
<H2>Standard</H2>
|
||||
<BaseSelect />
|
||||
</div>
|
||||
<div>
|
||||
<H2>Valid</H2>
|
||||
<BaseSelect isValid={true} />
|
||||
</div>
|
||||
<div>
|
||||
<H2>Invalid</H2>
|
||||
<BaseSelect isValid={false} />
|
||||
</div>
|
||||
<div>
|
||||
<H2>Disabled</H2>
|
||||
<BaseSelect isDisabled={true} />
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<H2>Pre-selected values (300px wide container)</H2>
|
||||
<div style={{ width: '300px ' }}>
|
||||
<SelectWithValues />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
MultiSelectExamples.parameters = {
|
||||
chromatic: {
|
||||
enableDarkMode: true,
|
||||
disableSnapshot: false,
|
||||
},
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import Select, { Props as SelectProps, StylesConfig, GroupBase } from 'react-select'
|
||||
|
||||
import { AccessibleFieldProps } from '../internal/AccessibleFieldType'
|
||||
import { FormFieldLabel } from '../internal/FormFieldLabel'
|
||||
import { FormFieldMessage } from '../internal/FormFieldMessage'
|
||||
import { getValidStyle } from '../internal/utils'
|
||||
|
||||
import { ClearIndicator } from './ClearIndicator'
|
||||
import { DropdownIndicator } from './DropdownIndicator'
|
||||
import { MultiValueContainer } from './MultiValueContainer'
|
||||
import { MultiValueLabel } from './MultiValueLabel'
|
||||
import { MultiValueRemove } from './MultiValueRemove'
|
||||
import { STYLES } from './styles'
|
||||
import { THEME } from './theme'
|
||||
import { MultiSelectOption } from './types'
|
||||
|
||||
import selectStyles from '../Select/Select.module.scss'
|
||||
import styles from './MultiSelect.module.scss'
|
||||
|
||||
export type MultiSelectProps<Option = unknown> = AccessibleFieldProps<
|
||||
SelectProps<Option, true> & { options: SelectProps<Option, true>['options'] } & {
|
||||
// Require options
|
||||
/**
|
||||
* Optional label position. Default is 'inline'
|
||||
*/
|
||||
labelVariant?: 'inline' | 'block'
|
||||
}
|
||||
>
|
||||
|
||||
/**
|
||||
* A wrapper around `react-select`'s `Select` component for producing multiselect dropdown
|
||||
* components.
|
||||
*
|
||||
* `MultiSelect` should be used to provide a user with a list of options from which they
|
||||
* can select none to many, within a form.
|
||||
*
|
||||
* @param options An array of the `Option`s to be listed from the dropdown.
|
||||
*/
|
||||
export const MultiSelect = <OptionValue extends unknown = unknown>({
|
||||
className,
|
||||
labelVariant,
|
||||
message,
|
||||
options,
|
||||
...props
|
||||
}: MultiSelectProps<MultiSelectOption<OptionValue>>): ReactElement => (
|
||||
<div className={classNames('form-group', className)}>
|
||||
{'label' in props && (
|
||||
<FormFieldLabel
|
||||
htmlFor={props.id}
|
||||
className={labelVariant === 'block' ? selectStyles.labelBlock : undefined}
|
||||
>
|
||||
{props.label}
|
||||
</FormFieldLabel>
|
||||
)}
|
||||
<Select<MultiSelectOption<OptionValue>, true, GroupBase<MultiSelectOption<OptionValue>>>
|
||||
isMulti={true}
|
||||
className={classNames(styles.multiSelect, getValidStyle(props.isValid))}
|
||||
options={options}
|
||||
theme={THEME}
|
||||
styles={STYLES as StylesConfig<MultiSelectOption<OptionValue>>}
|
||||
hideSelectedOptions={false}
|
||||
components={{
|
||||
ClearIndicator,
|
||||
DropdownIndicator,
|
||||
MultiValueContainer,
|
||||
MultiValueLabel,
|
||||
MultiValueRemove,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
{message && <FormFieldMessage isValid={props.isValid}>{message}</FormFieldMessage>}
|
||||
</div>
|
||||
)
|
||||
@ -1,16 +0,0 @@
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
import { MultiValueGenericProps } from 'react-select'
|
||||
|
||||
import { Badge } from '../../Badge'
|
||||
|
||||
import { MultiSelectOption } from './types'
|
||||
|
||||
// Overwrite the multi value container with Wildcard `Badge`
|
||||
export const MultiValueContainer = <OptionValue extends unknown = unknown>({
|
||||
innerProps: _innerProps,
|
||||
selectProps: _selectProps,
|
||||
...props
|
||||
}: MultiValueGenericProps<MultiSelectOption<OptionValue>, true>): ReactElement => (
|
||||
<Badge variant="secondary" className={_innerProps.className} {...props} />
|
||||
)
|
||||
@ -1,16 +0,0 @@
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
import { MultiValueGenericProps } from 'react-select'
|
||||
|
||||
import { MultiSelectOption } from './types'
|
||||
|
||||
import styles from './MultiSelect.module.scss'
|
||||
|
||||
// Remove extra wrappers around multi value label
|
||||
export const MultiValueLabel = <OptionValue extends unknown = unknown>({
|
||||
innerProps: _innerProps,
|
||||
selectProps: _selectProps,
|
||||
...props
|
||||
}: MultiValueGenericProps<MultiSelectOption<OptionValue>, true>): ReactElement => (
|
||||
<span className={styles.multiValueLabel} {...props} />
|
||||
)
|
||||
@ -1,19 +0,0 @@
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
import { mdiClose } from '@mdi/js'
|
||||
import { components, MultiValueRemoveProps } from 'react-select'
|
||||
|
||||
import { Icon } from '../../Icon'
|
||||
|
||||
import { MultiSelectOption } from './types'
|
||||
|
||||
import styles from './MultiSelect.module.scss'
|
||||
|
||||
// Overwrite the multi value remove indicator with `CloseIcon`
|
||||
export const MultiValueRemove = <OptionValue extends unknown = unknown>(
|
||||
props: MultiValueRemoveProps<MultiSelectOption<OptionValue>, true>
|
||||
): ReactElement => (
|
||||
<components.MultiValueRemove {...props}>
|
||||
<Icon className={styles.removeIcon} svgPath={mdiClose} inline={false} aria-hidden={true} />
|
||||
</components.MultiValueRemove>
|
||||
)
|
||||
@ -1,2 +0,0 @@
|
||||
export * from './MultiSelect'
|
||||
export * from './types'
|
||||
@ -1,129 +0,0 @@
|
||||
import { StylesConfig } from 'react-select'
|
||||
|
||||
export const STYLES: StylesConfig = {
|
||||
clearIndicator: provided => ({
|
||||
...provided,
|
||||
padding: '0.125rem 0',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
'&:hover': {
|
||||
background: 'var(--secondary-3)',
|
||||
},
|
||||
}),
|
||||
control: (provided, state) => ({
|
||||
...provided,
|
||||
// Styles here replicate the styles of `wildcard/Select`
|
||||
backgroundColor: state.isDisabled ? 'var(--input-disabled-bg)' : 'var(--input-bg)',
|
||||
borderColor: state.selectProps.isValid
|
||||
? 'var(--success)'
|
||||
: state.selectProps.isValid === false
|
||||
? 'var(--danger)'
|
||||
: state.isFocused
|
||||
? state.theme.colors.primary
|
||||
: 'var(--input-border-color)',
|
||||
boxShadow: state.isFocused
|
||||
? // These are stolen from `wildcard/Input` and `wildcard/Select`, which come from `client/wildcard/src/global-styles/forms.scss`
|
||||
state.selectProps.isValid
|
||||
? 'var(--input-focus-box-shadow-valid)'
|
||||
: state.selectProps.isValid === false
|
||||
? 'var(--input-focus-box-shadow-invalid)'
|
||||
: 'var(--input-focus-box-shadow)'
|
||||
: undefined,
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
borderColor: undefined,
|
||||
},
|
||||
}),
|
||||
dropdownIndicator: provided => ({
|
||||
...provided,
|
||||
padding: '0.125rem 0',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
'&:hover': {
|
||||
background: 'var(--secondary-3)',
|
||||
},
|
||||
}),
|
||||
indicatorSeparator: (provided, state) => ({
|
||||
...provided,
|
||||
backgroundColor: state.hasValue ? 'var(--input-border-color)' : 'transparent',
|
||||
}),
|
||||
input: provided => ({
|
||||
...provided,
|
||||
color: 'var(--input-color)',
|
||||
margin: '0 0.125rem',
|
||||
padding: 0,
|
||||
}),
|
||||
menu: provided => ({
|
||||
...provided,
|
||||
background: 'var(--dropdown-bg)',
|
||||
padding: 0,
|
||||
margin: '0.125rem 0 0',
|
||||
dropShadow: 'var(--dropdown-shadow)',
|
||||
// This is to prevent item edges from sticking out of the rounded dropdown container
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
menuList: provided => ({
|
||||
...provided,
|
||||
padding: 0,
|
||||
}),
|
||||
multiValue: (provided, state) => ({
|
||||
display: 'flex',
|
||||
maxWidth: '100%',
|
||||
alignItems: 'center',
|
||||
padding: '0 0 0 0.5rem',
|
||||
margin: '0.125rem',
|
||||
background: state.isFocused ? 'var(--secondary-3)' : 'var(--secondary)',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px',
|
||||
borderColor: state.isFocused ? 'var(--select-button-border-color)' : 'transparent',
|
||||
'&:hover': {
|
||||
background: 'var(--secondary-3)',
|
||||
borderColor: 'var(--select-button-border-color)',
|
||||
},
|
||||
}),
|
||||
multiValueRemove: (provided, state) => ({
|
||||
...provided,
|
||||
padding: '4px',
|
||||
marginLeft: '0.25rem',
|
||||
borderRadius: '0 var(--border-radius) var(--border-radius) 0',
|
||||
background: state.isFocused ? 'var(--secondary-3)' : undefined,
|
||||
':hover': {
|
||||
...provided[':hover'],
|
||||
background: 'var(--secondary-3)',
|
||||
color: undefined,
|
||||
},
|
||||
}),
|
||||
noOptionsMessage: provided => ({
|
||||
...provided,
|
||||
color: 'var(--input-placeholder-color)',
|
||||
}),
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
backgroundColor: state.isSelected
|
||||
? state.isFocused
|
||||
? 'var(--primary-3)'
|
||||
: state.theme.colors.primary
|
||||
: state.isFocused
|
||||
? 'var(--dropdown-link-hover-bg)'
|
||||
: undefined,
|
||||
color: state.isSelected ? 'var(--light-text)' : undefined,
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
':active': {
|
||||
backgroundColor: state.isSelected
|
||||
? state.isFocused
|
||||
? 'var(--primary-3)'
|
||||
: state.theme.colors.primary
|
||||
: state.isFocused
|
||||
? 'var(--dropdown-link-hover-bg)'
|
||||
: undefined,
|
||||
},
|
||||
}),
|
||||
placeholder: (provided, state) => ({
|
||||
...provided,
|
||||
color: state.isDisabled ? 'var(--gray-06)' : 'var(--input-placeholder-color)',
|
||||
}),
|
||||
valueContainer: provided => ({
|
||||
...provided,
|
||||
padding: '0.125rem 0.125rem 0.125rem 0.75rem',
|
||||
}),
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
import { ThemeConfig } from 'react-select'
|
||||
|
||||
export const THEME: ThemeConfig = theme => ({
|
||||
...theme,
|
||||
borderRadius: 3,
|
||||
// Each identifiable instance of `Select` components using a theme color has been
|
||||
// overwritten by `STYLES` in order to switch to light mode/dark mode. These colors
|
||||
// defined here only serve as a fallback in case of unexpected or missed color usage.
|
||||
colors: {
|
||||
primary: 'var(--primary)',
|
||||
// Never used.
|
||||
primary75: 'var(--primary)',
|
||||
// Used for `option` background color, which is overwritten in `STYLES`.
|
||||
primary50: 'var(--primary-2)',
|
||||
// Used for `option` background color, which is overwritten in `STYLES`.
|
||||
primary25: 'var(--primary-2)',
|
||||
// Used for `multiValueRemove` color, which is replaced with a custom component.
|
||||
danger: 'var(--danger)',
|
||||
// Used for `multiValueRemove` background color, which is overwritten in `STYLES`.
|
||||
dangerLight: 'var(--danger)',
|
||||
// Used for `menu` and `control` background color as well as `option` color, all
|
||||
// of which are overwritten in `STYLES`.
|
||||
neutral0: 'var(--react-select-neutral0)',
|
||||
// Used for `control` background color and `placeholder` color, both of which are
|
||||
// overwritten in `STYLES`.
|
||||
neutral5: 'var(--react-select-neutral5)',
|
||||
// Used for `indicatorSeparator` background color and `control` border color, both
|
||||
// of which are overwritten in `STYLES`, and `multiValue` background color, which
|
||||
// is replaced with a custom component.
|
||||
neutral10: 'var(--react-select-neutral10)',
|
||||
// Used for `indicatorSeparator` background color, `control` border color, and
|
||||
// `option` color, all of which are overwritten in `STYLES`.
|
||||
neutral20: 'var(--react-select-neutral20)',
|
||||
// Used for `control` border color, which is overwritten in `STYLES`.
|
||||
neutral30: 'var(--react-select-neutral30)',
|
||||
// Used by components that aren't used for `isMulti=true` or have been replaced
|
||||
// with custom ones.
|
||||
neutral40: 'var(--react-select-neutral40)',
|
||||
// Used for `placeholder` color, which is overwritten in `STYLES`.
|
||||
neutral50: 'var(--react-select-neutral50)',
|
||||
// Used by components that have been replaced with custom ones.
|
||||
neutral60: 'var(--react-select-neutral60)',
|
||||
// Never used.
|
||||
neutral70: 'var(--react-select-neutral70)',
|
||||
// Used for `input` and `multiValue` color, which are both overwritten in
|
||||
// `STYLES`, as well as components that aren't used for `isMulti=true` or have
|
||||
// been replaced with custom ones.
|
||||
neutral80: 'var(--react-select-neutral80)',
|
||||
// Never used.
|
||||
neutral90: 'var(--react-select-neutral90)',
|
||||
},
|
||||
})
|
||||
@ -1,31 +0,0 @@
|
||||
import { GroupBase } from 'react-select'
|
||||
|
||||
import { AccessibleFieldProps } from '../internal/AccessibleFieldType'
|
||||
|
||||
/**
|
||||
* Generic type for an option to be listed from the `MultiSelect` dropdown.
|
||||
*
|
||||
* @param OptionValue The type of the value of the option, i.e. a union set of all
|
||||
* possible values.
|
||||
*/
|
||||
export interface MultiSelectOption<OptionValue = unknown> {
|
||||
value: OptionValue
|
||||
label: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic type for the state a consumer of `MultiSelect` should expect to manage.
|
||||
*
|
||||
* @param OptionValue The type of the value of the option, i.e. a union set of all
|
||||
* possible values.
|
||||
*/
|
||||
export type MultiSelectState<OptionValue = unknown> = readonly MultiSelectOption<OptionValue>[]
|
||||
|
||||
// We use module augmentation to make TS aware of custom props available from `Select`
|
||||
// custom components, styles, theme, etc.
|
||||
// See: https://react-select.com/typescript#custom-select-props
|
||||
declare module 'react-select/dist/declarations/src/Select' {
|
||||
export interface Props<Option, IsMulti extends boolean, Group extends GroupBase<Option>> {
|
||||
isValid?: AccessibleFieldProps<{}>['isValid']
|
||||
}
|
||||
}
|
||||
@ -5,5 +5,4 @@ export * from './Input'
|
||||
export * from './LoaderInput'
|
||||
export * from './RadioButton'
|
||||
export * from './Select'
|
||||
export * from './MultiSelect'
|
||||
export * from './TextArea'
|
||||
|
||||
@ -24,7 +24,6 @@ export {
|
||||
LoaderInput,
|
||||
RadioButton,
|
||||
Select,
|
||||
MultiSelect,
|
||||
TextArea,
|
||||
InputStatus,
|
||||
getInputStatus,
|
||||
@ -76,6 +75,7 @@ export {
|
||||
MultiComboboxInput,
|
||||
MultiComboboxPopover,
|
||||
MultiComboboxList,
|
||||
MultiComboboxEmptyList,
|
||||
MultiComboboxOptionGroup,
|
||||
MultiComboboxOption,
|
||||
MultiComboboxOptionText,
|
||||
@ -87,9 +87,9 @@ export {
|
||||
*/
|
||||
export type { FeedbackPromptSubmitEventHandler } from './Feedback'
|
||||
export type { AlertProps, AlertLinkProps } from './Alert'
|
||||
export type { MultiSelectProps, MultiSelectOption, MultiSelectState, SelectProps, InputProps } from './Form'
|
||||
export type { ButtonProps } from './Button'
|
||||
export type { ButtonLinkProps } from './ButtonLink'
|
||||
export type { SelectProps, InputProps } from './Form'
|
||||
export type { Series, SeriesLikeChart, CategoricalLikeChart, LineChartProps, BarChartProps } from './Charts'
|
||||
export type { LinkProps } from './Link'
|
||||
export type { PopoverOpenEvent, Rectangle } from './Popover'
|
||||
|
||||
@ -457,7 +457,6 @@
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-dom-v5-compat": "^6.3.0",
|
||||
"react-select": "^5.2.2",
|
||||
"react-spring": "^9.4.2",
|
||||
"react-sticky-box": "1.0.2",
|
||||
"react-visibility-sensor": "^5.1.1",
|
||||
|
||||
153
pnpm-lock.yaml
153
pnpm-lock.yaml
@ -340,7 +340,6 @@ importers:
|
||||
react-router: ^5.2.0
|
||||
react-router-dom: ^5.2.0
|
||||
react-router-dom-v5-compat: ^6.3.0
|
||||
react-select: ^5.2.2
|
||||
react-spring: ^9.4.2
|
||||
react-sticky-box: 1.0.2
|
||||
react-visibility-sensor: ^5.1.1
|
||||
@ -520,7 +519,6 @@ importers:
|
||||
react-router: 5.2.0_react@18.1.0
|
||||
react-router-dom: 5.2.0_react@18.1.0
|
||||
react-router-dom-v5-compat: 6.3.0_xjeikfjhclro5pp2abrahotxli
|
||||
react-select: 5.2.2_ceda52rm5e5anx7ab4xo6b2wjy
|
||||
react-spring: 9.4.2_ssv3vkwxg74iun7wj74vdfwzhu
|
||||
react-sticky-box: 1.0.2_react@18.1.0
|
||||
react-visibility-sensor: 5.1.1_ef5jwxihqo6n7gxfmzogljlgcm
|
||||
@ -2739,95 +2737,6 @@ packages:
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
dev: true
|
||||
|
||||
/@emotion/babel-plugin/11.7.2_@babel+core@7.20.5:
|
||||
resolution: {integrity: sha512-6mGSCWi9UzXut/ZAN6lGFu33wGR3SJisNl3c0tvlmb8XChH1b2SUvxvnOh7hvLpqyRdHHU9AiazV3Cwbk5SXKQ==}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
dependencies:
|
||||
'@babel/core': 7.20.5
|
||||
'@babel/helper-module-imports': 7.18.6
|
||||
'@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.20.5
|
||||
'@babel/runtime': 7.20.6
|
||||
'@emotion/hash': 0.8.0
|
||||
'@emotion/memoize': 0.7.5
|
||||
'@emotion/serialize': 1.0.2
|
||||
babel-plugin-macros: 2.8.0
|
||||
convert-source-map: 1.7.0
|
||||
escape-string-regexp: 4.0.0
|
||||
find-root: 1.1.0
|
||||
source-map: 0.5.7
|
||||
stylis: 4.0.13
|
||||
dev: false
|
||||
|
||||
/@emotion/cache/11.7.1:
|
||||
resolution: {integrity: sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==}
|
||||
dependencies:
|
||||
'@emotion/memoize': 0.7.5
|
||||
'@emotion/sheet': 1.1.0
|
||||
'@emotion/utils': 1.1.0
|
||||
'@emotion/weak-memoize': 0.2.5
|
||||
stylis: 4.0.13
|
||||
dev: false
|
||||
|
||||
/@emotion/hash/0.8.0:
|
||||
resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==}
|
||||
dev: false
|
||||
|
||||
/@emotion/memoize/0.7.5:
|
||||
resolution: {integrity: sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==}
|
||||
dev: false
|
||||
|
||||
/@emotion/react/11.8.1_kle6j4c75ehjy3nd72aprojeqy:
|
||||
resolution: {integrity: sha512-XGaie4nRxmtP1BZYBXqC5JGqMYF2KRKKI7vjqNvQxyRpekVAZhb6QqrElmZCAYXH1L90lAelADSVZC4PFsrJ8Q==}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
'@types/react': '*'
|
||||
react: '>=16.8.0'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.20.5
|
||||
'@babel/runtime': 7.20.6
|
||||
'@emotion/babel-plugin': 11.7.2_@babel+core@7.20.5
|
||||
'@emotion/cache': 11.7.1
|
||||
'@emotion/serialize': 1.0.2
|
||||
'@emotion/sheet': 1.1.0
|
||||
'@emotion/utils': 1.1.0
|
||||
'@emotion/weak-memoize': 0.2.5
|
||||
'@types/react': 18.0.8
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 18.1.0
|
||||
dev: false
|
||||
|
||||
/@emotion/serialize/1.0.2:
|
||||
resolution: {integrity: sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==}
|
||||
dependencies:
|
||||
'@emotion/hash': 0.8.0
|
||||
'@emotion/memoize': 0.7.5
|
||||
'@emotion/unitless': 0.7.5
|
||||
'@emotion/utils': 1.1.0
|
||||
csstype: 3.1.0
|
||||
dev: false
|
||||
|
||||
/@emotion/sheet/1.1.0:
|
||||
resolution: {integrity: sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==}
|
||||
dev: false
|
||||
|
||||
/@emotion/unitless/0.7.5:
|
||||
resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==}
|
||||
dev: false
|
||||
|
||||
/@emotion/utils/1.1.0:
|
||||
resolution: {integrity: sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==}
|
||||
dev: false
|
||||
|
||||
/@emotion/weak-memoize/0.2.5:
|
||||
resolution: {integrity: sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==}
|
||||
dev: false
|
||||
|
||||
/@esbuild/android-arm/0.16.10:
|
||||
resolution: {integrity: sha512-RmJjQTRrO6VwUWDrzTBLmV4OJZTarYsiepLGlF2rYTVB701hSorPywPGvP6d8HCuuRibyXa5JX4s3jN2kHEtjQ==}
|
||||
engines: {node: '>=12'}
|
||||
@ -8595,12 +8504,6 @@ packages:
|
||||
'@types/react': 18.0.8
|
||||
dev: true
|
||||
|
||||
/@types/react-transition-group/4.4.4:
|
||||
resolution: {integrity: sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==}
|
||||
dependencies:
|
||||
'@types/react': 18.0.8
|
||||
dev: false
|
||||
|
||||
/@types/react/17.0.43:
|
||||
resolution: {integrity: sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==}
|
||||
dependencies:
|
||||
@ -10253,14 +10156,6 @@ packages:
|
||||
require-package-name: 2.0.1
|
||||
dev: true
|
||||
|
||||
/babel-plugin-macros/2.8.0:
|
||||
resolution: {integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.20.6
|
||||
cosmiconfig: 6.0.0
|
||||
resolve: 1.22.0
|
||||
dev: false
|
||||
|
||||
/babel-plugin-macros/3.0.1:
|
||||
resolution: {integrity: sha512-CKt4+Oy9k2wiN+hT1uZzOw7d8zb1anbQpf7KLwaaXRCi/4pzKdFKHf7v5mvoPmjkmxshh7eKZQuRop06r5WP4w==}
|
||||
engines: {node: '>=10', npm: '>=6'}
|
||||
@ -11559,7 +11454,7 @@ packages:
|
||||
color-name: 1.1.4
|
||||
|
||||
/color-name/1.1.3:
|
||||
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
||||
resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=}
|
||||
|
||||
/color-name/1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
@ -13340,13 +13235,6 @@ packages:
|
||||
'@babel/runtime': 7.20.6
|
||||
dev: false
|
||||
|
||||
/dom-helpers/5.2.0:
|
||||
resolution: {integrity: sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.20.6
|
||||
csstype: 3.1.0
|
||||
dev: false
|
||||
|
||||
/dom-serializer/0.2.2:
|
||||
resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
|
||||
dependencies:
|
||||
@ -14882,10 +14770,6 @@ packages:
|
||||
make-dir: 3.1.0
|
||||
pkg-dir: 4.2.0
|
||||
|
||||
/find-root/1.1.0:
|
||||
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
|
||||
dev: false
|
||||
|
||||
/find-up/1.1.2:
|
||||
resolution: {integrity: sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -22747,26 +22631,6 @@ packages:
|
||||
react: 18.1.0
|
||||
dev: false
|
||||
|
||||
/react-select/5.2.2_ceda52rm5e5anx7ab4xo6b2wjy:
|
||||
resolution: {integrity: sha512-miGS2rT1XbFNjduMZT+V73xbJEeMzVkJOz727F6MeAr2hKE0uUSA8Ff7vD44H32x2PD3SRB6OXTY/L+fTV3z9w==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.20.6
|
||||
'@emotion/cache': 11.7.1
|
||||
'@emotion/react': 11.8.1_kle6j4c75ehjy3nd72aprojeqy
|
||||
'@types/react-transition-group': 4.4.4
|
||||
memoize-one: 5.0.4
|
||||
prop-types: 15.8.1
|
||||
react: 18.1.0
|
||||
react-dom: 18.1.0_react@18.1.0
|
||||
react-transition-group: 4.4.1_ef5jwxihqo6n7gxfmzogljlgcm
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@types/react'
|
||||
dev: false
|
||||
|
||||
/react-shallow-renderer/16.15.0_react@18.1.0:
|
||||
resolution: {integrity: sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==}
|
||||
peerDependencies:
|
||||
@ -22891,20 +22755,6 @@ packages:
|
||||
react-lifecycles-compat: 3.0.4
|
||||
dev: false
|
||||
|
||||
/react-transition-group/4.4.1_ef5jwxihqo6n7gxfmzogljlgcm:
|
||||
resolution: {integrity: sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==}
|
||||
peerDependencies:
|
||||
react: '>=16.6.0'
|
||||
react-dom: '>=16.6.0'
|
||||
dependencies:
|
||||
'@babel/runtime': 7.20.6
|
||||
dom-helpers: 5.2.0
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
react: 18.1.0
|
||||
react-dom: 18.1.0_react@18.1.0
|
||||
dev: false
|
||||
|
||||
/react-use-measure/2.1.1_ef5jwxihqo6n7gxfmzogljlgcm:
|
||||
resolution: {integrity: sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==}
|
||||
peerDependencies:
|
||||
@ -24993,6 +24843,7 @@ packages:
|
||||
|
||||
/stylis/4.0.13:
|
||||
resolution: {integrity: sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==}
|
||||
dev: true
|
||||
|
||||
/sudo-prompt/9.2.1:
|
||||
resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user