-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement filtering by browser in new ui (#593)
* feat: implement filtering by browser in new ui
- Loading branch information
Showing
7 changed files
with
253 additions
and
11 deletions.
There are no files selected for viewing
37 changes: 37 additions & 0 deletions
37
lib/static/new-ui/features/suites/components/BrowsersSelect/BrowserIcon.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import {isString} from 'lodash'; | ||
import React, {ReactNode} from 'react'; | ||
|
||
const valueToIcon = { | ||
google: 'chrome', | ||
chrome: 'chrome', | ||
firefox: 'firefox', | ||
safari: 'safari', | ||
edge: 'edge', | ||
yandex: 'yandex', | ||
yabro: 'yandex', | ||
ie: 'internet-explorer', | ||
explorer: 'internet-explorer', | ||
opera: 'opera', | ||
phone: 'mobile', | ||
mobile: 'mobile', | ||
tablet: 'tablet', | ||
ipad: 'tablet' | ||
} as const; | ||
|
||
export function BrowserIcon({name: browser}: {name: string}): ReactNode { | ||
const getIcon = (iconName: string): ReactNode => <i className={`fa fa-${iconName}`} aria-hidden="true" />; | ||
|
||
if (!isString(browser)) { | ||
return getIcon('browser'); | ||
} | ||
|
||
const lowerValue = browser.toLowerCase(); | ||
|
||
for (const pattern in valueToIcon) { | ||
if (lowerValue.includes(pattern)) { | ||
return getIcon(valueToIcon[pattern as keyof typeof valueToIcon]); | ||
} | ||
} | ||
|
||
return getIcon('browser'); | ||
} |
21 changes: 21 additions & 0 deletions
21
lib/static/new-ui/features/suites/components/BrowsersSelect/index.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
:global(.g-popup) { | ||
z-index: 9999; | ||
} | ||
|
||
.browserlist__filter { | ||
padding: 4px; | ||
} | ||
|
||
.action-button { | ||
width: 60px; | ||
margin-left: auto; | ||
} | ||
|
||
.browserlist__popup { | ||
--g-color-text-info: var(--g-color-private-color-500-solid); | ||
} | ||
|
||
.browserlist__popup :global(.g-select-list__option) { | ||
gap: 8px; | ||
flex-direction: row-reverse; | ||
} |
180 changes: 180 additions & 0 deletions
180
lib/static/new-ui/features/suites/components/BrowsersSelect/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
import {Globe} from '@gravity-ui/icons'; | ||
import {Button, Flex, Select, SelectRenderControlProps, SelectRenderOption} from '@gravity-ui/uikit'; | ||
import React, {useState, useEffect, ReactNode} from 'react'; | ||
import {connect} from 'react-redux'; | ||
import {bindActionCreators} from 'redux'; | ||
|
||
import * as actions from '@/static/modules/actions'; | ||
import {BrowserIcon} from '@/static/new-ui/features/suites/components/BrowsersSelect/BrowserIcon'; | ||
import {State} from '@/static/new-ui/types/store'; | ||
import {BrowserItem} from '@/types'; | ||
import styles from './index.module.css'; | ||
|
||
// In the onUpdate callback we only have access to array of selected strings. That's why we need to serialize | ||
// id/version in string. Encoding to avoid errors if id/version contains delimiter. | ||
// The other approach would be to use mapping, but in practice it makes things even more complex. | ||
const DELIMITER = '/'; | ||
|
||
const serializeBrowserData = (id: string, version: string): string => | ||
`${encodeURIComponent(id)}${DELIMITER}${encodeURIComponent(version)}`; | ||
|
||
const deserializeBrowserData = (data: string): {id: string; version: string} => { | ||
const [idEncoded, versionEncoded] = data.split(DELIMITER); | ||
return {id: decodeURIComponent(idEncoded), version: decodeURIComponent(versionEncoded)}; | ||
}; | ||
|
||
interface BrowsersSelectProps { | ||
browsers: BrowserItem[]; | ||
filteredBrowsers: BrowserItem[]; | ||
actions: typeof actions; | ||
} | ||
|
||
function BrowsersSelectInternal({browsers, filteredBrowsers, actions}: BrowsersSelectProps): ReactNode { | ||
const [selectedBrowsers, setSelectedBrowsers] = useState<BrowserItem[]>([]); | ||
|
||
useEffect(() => { | ||
setSelectedBrowsers(filteredBrowsers); | ||
}, [browsers, filteredBrowsers]); | ||
|
||
const renderFilter = (): React.JSX.Element => { | ||
return ( | ||
<div className={styles['browserlist__filter']}> | ||
<Button onClick={(): void => setSelectedBrowsers(browsers)} width='max'> | ||
Select All | ||
</Button> | ||
</div> | ||
); | ||
}; | ||
|
||
const onUpdate = (values: string[]): void => { | ||
const selectedItems: BrowserItem[] = []; | ||
|
||
values.forEach(encodedBrowserData => { | ||
const {id, version} = deserializeBrowserData(encodedBrowserData); | ||
const existingBrowser = selectedItems.find(browser => browser.id === id); | ||
|
||
if (existingBrowser) { | ||
if (!existingBrowser.versions.includes(version)) { | ||
existingBrowser.versions.push(version); | ||
} | ||
} else { | ||
selectedItems.push({id, versions: [version]}); | ||
} | ||
}); | ||
|
||
setSelectedBrowsers(selectedItems); | ||
}; | ||
|
||
const renderOptions = (): React.JSX.Element | React.JSX.Element[] => { | ||
const browsersWithMultipleVersions = browsers.filter(browser => browser.versions.length > 1); | ||
const browsersWithSingleVersion = browsers.filter(browser => browser.versions.length === 1); | ||
|
||
const getOptionProps = (browser: BrowserItem, version: string): {value: string; content: string; data: Record<string, unknown>} => ({ | ||
value: serializeBrowserData(browser.id, version), | ||
content: browser.id, | ||
data: {id: browser.id, version} | ||
}); | ||
|
||
if (browsersWithMultipleVersions.length === 0) { | ||
// If there are no browsers with multiple versions, we want to render a simple plain list | ||
return browsers.map(browser => <Select.Option | ||
key={browser.id} | ||
{...getOptionProps(browser, browser.versions[0])} | ||
/>); | ||
} else { | ||
// Otherwise render browser version groups and place all browsers with single version into "Other" group | ||
return ( | ||
<> | ||
{browsersWithMultipleVersions.map(browser => ( | ||
<Select.OptionGroup key={browser.id} label={browser.id}> | ||
{browser.versions.map(version => ( | ||
<Select.Option | ||
key={version} | ||
{...getOptionProps(browser, version)} | ||
/> | ||
))} | ||
</Select.OptionGroup> | ||
))} | ||
<Select.OptionGroup label="Other"> | ||
{browsersWithSingleVersion.map(browser => ( | ||
<Select.Option | ||
key={browser.id} | ||
{...getOptionProps(browser, browser.versions[0])} | ||
/> | ||
))} | ||
</Select.OptionGroup> | ||
</> | ||
); | ||
} | ||
}; | ||
|
||
const renderControl = ({onClick, onKeyDown, ref}: SelectRenderControlProps): React.JSX.Element => { | ||
return <Button ref={ref} onClick={onClick} extraProps={{onKeyDown}} view={'outlined'} style={{width: 28}}> | ||
<Globe/> | ||
</Button>; | ||
}; | ||
|
||
const selected = selectedBrowsers.flatMap(browser => browser.versions.map(version => serializeBrowserData(browser.id, version))); | ||
|
||
const onClose = (): void => { | ||
actions.selectBrowsers(selectedBrowsers.filter(browser => browser.versions.length > 0)); | ||
}; | ||
|
||
const onFocus = (): void => { | ||
if (selected.length === 0) { | ||
setSelectedBrowsers(browsers); | ||
} | ||
}; | ||
|
||
const renderOption: SelectRenderOption<{id: string; version: string}> = (option) => { | ||
const isTheOnlySelected = selected.includes(option.value) && selected.length === 1; | ||
const selectOnly = (e: React.MouseEvent<HTMLElement>): void => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
setSelectedBrowsers([{id: option.data?.id as string, versions: [option.data?.version as string]}]); | ||
}; | ||
const selectExcept = (e: React.MouseEvent<HTMLElement>): void => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
setSelectedBrowsers(browsers.map(browser => ({ | ||
id: browser.id, | ||
versions: browser.versions.filter(version => browser.id !== option.data?.id || version !== option.data?.version) | ||
}))); | ||
}; | ||
return ( | ||
<Flex alignItems="center" width={'100%'} gap={2}> | ||
<Flex height={16} width={16} alignItems={'center'} justifyContent={'center'}><BrowserIcon name={option.data?.id as string} /></Flex> | ||
<div className="browser-name">{option.content}</div> | ||
<Button size="s" onClick={isTheOnlySelected ? selectExcept : selectOnly} className={styles.actionButton}>{isTheOnlySelected ? 'Except' : 'Only'}</Button> | ||
</Flex> | ||
); | ||
}; | ||
|
||
return ( | ||
<Select | ||
disablePortal | ||
value={selected} | ||
multiple={true} | ||
hasCounter | ||
filterable | ||
renderFilter={renderFilter} | ||
renderOption={renderOption} | ||
renderControl={renderControl} | ||
popupClassName={styles['browserlist__popup']} | ||
className='browserlist' | ||
onUpdate={onUpdate} | ||
onFocus={onFocus} | ||
onClose={onClose} | ||
> | ||
{renderOptions()} | ||
</Select> | ||
); | ||
} | ||
|
||
export const BrowsersSelect = connect( | ||
(state: State) => ({ | ||
filteredBrowsers: state.view.filteredBrowsers, | ||
browsers: state.browsers | ||
}), | ||
(dispatch) => ({actions: bindActionCreators(actions, dispatch)}) | ||
)(BrowsersSelectInternal); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters