Merge pull request #11 from instat/OTT-91-country-list
commit
b548d1e607
|
After Width: | Height: | Size: 250 B |
@ -1,3 +1,4 @@ |
|||||||
export * from './routes' |
export * from './routes' |
||||||
export * from './pages' |
export * from './pages' |
||||||
export * from './authKeys' |
export * from './authKeys' |
||||||
|
export * from './procedures' |
||||||
|
|||||||
@ -0,0 +1,3 @@ |
|||||||
|
export const PROCEDURES = { |
||||||
|
lst_c_country: 'lst_c_country', |
||||||
|
} |
||||||
@ -1,2 +1,2 @@ |
|||||||
export const API_ROOT = '' |
export const API_ROOT = 'http://85.10.224.24:8080' |
||||||
export const DATA_URL = `${API_ROOT}/data` |
export const DATA_URL = `${API_ROOT}/data` |
||||||
|
|||||||
@ -0,0 +1,69 @@ |
|||||||
|
import { matchSort } from '..' |
||||||
|
|
||||||
|
it('does not sort items when query is empty', () => { |
||||||
|
const items = [ |
||||||
|
{ name: 'app' }, |
||||||
|
{ name: 'apple' }, |
||||||
|
] |
||||||
|
const query = '' |
||||||
|
const result = matchSort( |
||||||
|
items, |
||||||
|
'name', |
||||||
|
query, |
||||||
|
) |
||||||
|
expect(result).toEqual(items) |
||||||
|
}) |
||||||
|
|
||||||
|
it('non matching words omitted', () => { |
||||||
|
const items = [ |
||||||
|
{ name: 'banana' }, |
||||||
|
{ name: 'app' }, |
||||||
|
{ name: 'apple' }, |
||||||
|
] |
||||||
|
const query = 'app' |
||||||
|
const result = matchSort( |
||||||
|
items, |
||||||
|
'name', |
||||||
|
query, |
||||||
|
) |
||||||
|
expect(result).toEqual([ |
||||||
|
items[1], |
||||||
|
items[2], |
||||||
|
]) |
||||||
|
}) |
||||||
|
|
||||||
|
it('matching words go first', () => { |
||||||
|
const items = [ |
||||||
|
{ name: 'apple' }, |
||||||
|
{ name: 'app' }, |
||||||
|
] |
||||||
|
const query = 'app' |
||||||
|
const result = matchSort( |
||||||
|
items, |
||||||
|
'name', |
||||||
|
query, |
||||||
|
) |
||||||
|
expect(result).toEqual([ |
||||||
|
items[1], |
||||||
|
items[0], |
||||||
|
]) |
||||||
|
}) |
||||||
|
|
||||||
|
it('words starting with query word go second', () => { |
||||||
|
const items = [ |
||||||
|
{ name: 'apple' }, |
||||||
|
{ name: 'app' }, |
||||||
|
{ name: 'buggy app' }, |
||||||
|
] |
||||||
|
const query = 'app' |
||||||
|
const result = matchSort( |
||||||
|
items, |
||||||
|
'name', |
||||||
|
query, |
||||||
|
) |
||||||
|
expect(result).toEqual([ |
||||||
|
items[1], |
||||||
|
items[0], |
||||||
|
items[2], |
||||||
|
]) |
||||||
|
}) |
||||||
@ -0,0 +1,77 @@ |
|||||||
|
import startsWith from 'lodash/startsWith' |
||||||
|
import orderBy from 'lodash/orderBy' |
||||||
|
import toLower from 'lodash/toLower' |
||||||
|
import filter from 'lodash/filter' |
||||||
|
import split from 'lodash/split' |
||||||
|
import size from 'lodash/size' |
||||||
|
import some from 'lodash/some' |
||||||
|
|
||||||
|
type Item = { [key: string]: any } |
||||||
|
|
||||||
|
type Func = <T extends Item, K extends keyof T>( |
||||||
|
items: Array<T>, |
||||||
|
key: K, |
||||||
|
query: string, |
||||||
|
) => Array<T> |
||||||
|
|
||||||
|
const startsWithIgnore = (word: string, target: string) => ( |
||||||
|
startsWith(toLower(word), toLower(target)) |
||||||
|
) |
||||||
|
|
||||||
|
const hasManyWords = (word: string) => size(split(word, ' ')) > 1 |
||||||
|
|
||||||
|
const atLeastOneWordStartsWith = (word: string, target: string) => { |
||||||
|
const words = split(word, ' ') |
||||||
|
return some(words, (w) => startsWithIgnore(w, target)) |
||||||
|
} |
||||||
|
|
||||||
|
const filterMatching: Func = ( |
||||||
|
items, |
||||||
|
key, |
||||||
|
query, |
||||||
|
) => ( |
||||||
|
filter(items, (item) => { |
||||||
|
const value = item[key] |
||||||
|
if (startsWithIgnore(value, query)) return true |
||||||
|
if (hasManyWords(value)) return atLeastOneWordStartsWith(value, query) |
||||||
|
return false |
||||||
|
}) |
||||||
|
) |
||||||
|
|
||||||
|
const sortMatching: Func = ( |
||||||
|
items, |
||||||
|
key, |
||||||
|
query, |
||||||
|
) => ( |
||||||
|
orderBy( |
||||||
|
items, |
||||||
|
(item) => { |
||||||
|
if (item[key] === query) return 10 |
||||||
|
if (startsWithIgnore(item[key], query)) { |
||||||
|
return 5 |
||||||
|
} |
||||||
|
return 0 |
||||||
|
}, |
||||||
|
'desc', |
||||||
|
) |
||||||
|
) |
||||||
|
|
||||||
|
export const matchSort: Func = ( |
||||||
|
items, |
||||||
|
key, |
||||||
|
query, |
||||||
|
) => { |
||||||
|
if (!query) return items |
||||||
|
|
||||||
|
const filteredItems = filterMatching( |
||||||
|
items, |
||||||
|
key, |
||||||
|
query, |
||||||
|
) |
||||||
|
|
||||||
|
return sortMatching( |
||||||
|
filteredItems, |
||||||
|
key, |
||||||
|
query, |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
import type { ChangeEvent, FocusEvent } from 'react' |
||||||
|
import type { Countries } from 'requests' |
||||||
|
|
||||||
|
import { |
||||||
|
useEffect, |
||||||
|
useState, |
||||||
|
useCallback, |
||||||
|
} from 'react' |
||||||
|
|
||||||
|
import toLower from 'lodash/toLower' |
||||||
|
import slice from 'lodash/slice' |
||||||
|
import find from 'lodash/find' |
||||||
|
|
||||||
|
import { getCountries } from 'requests' |
||||||
|
|
||||||
|
import { matchSort } from './helpers' |
||||||
|
|
||||||
|
// временно, будем считывать из стора(контекста) лексики
|
||||||
|
const useCurrentLang = () => 'eng' |
||||||
|
|
||||||
|
const useCountriesList = () => { |
||||||
|
const [countries, setCountries] = useState<Countries>([]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
getCountries().then(setCountries) |
||||||
|
}, []) |
||||||
|
|
||||||
|
return countries |
||||||
|
} |
||||||
|
|
||||||
|
const useQuery = () => { |
||||||
|
const [query, setQuery] = useState('') |
||||||
|
|
||||||
|
const onQueryChange = useCallback(({ target }: ChangeEvent<HTMLInputElement>) => { |
||||||
|
setQuery(target.value) |
||||||
|
}, []) |
||||||
|
|
||||||
|
return { |
||||||
|
onQueryChange, |
||||||
|
query, |
||||||
|
setQuery, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const isOptionClicked = (target: HTMLElement) => target?.getAttribute('role') === 'option' |
||||||
|
|
||||||
|
export const useCountrySelector = () => { |
||||||
|
const lang = useCurrentLang() |
||||||
|
const countries = useCountriesList() |
||||||
|
const { |
||||||
|
onQueryChange, |
||||||
|
query, |
||||||
|
setQuery, |
||||||
|
} = useQuery() |
||||||
|
|
||||||
|
const keyToSortBy = `name_${lang}` as 'name_eng' |
||||||
|
|
||||||
|
const results = matchSort( |
||||||
|
countries, |
||||||
|
keyToSortBy, |
||||||
|
query, |
||||||
|
) |
||||||
|
|
||||||
|
const onBlur = (event: FocusEvent<HTMLInputElement>) => { |
||||||
|
const target = event.relatedTarget as HTMLElement | null |
||||||
|
// клик по элементу списка тоже вызывает onBlur
|
||||||
|
// если кликали элемент списка то событие обрабатывается onCountrySelect
|
||||||
|
if (target && isOptionClicked(target)) return |
||||||
|
|
||||||
|
const found = find( |
||||||
|
countries, |
||||||
|
(country) => toLower(country[keyToSortBy]) === toLower(query), |
||||||
|
) |
||||||
|
setQuery(found ? found[keyToSortBy] : '') |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
countries: slice( |
||||||
|
results, |
||||||
|
0, |
||||||
|
20, |
||||||
|
), |
||||||
|
onBlur, |
||||||
|
onCountrySelect: setQuery, |
||||||
|
onQueryChange, |
||||||
|
query, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,63 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import isEmpty from 'lodash/isEmpty' |
||||||
|
import map from 'lodash/map' |
||||||
|
import { |
||||||
|
ComboboxPopover, |
||||||
|
ComboboxList, |
||||||
|
ComboboxOption, |
||||||
|
} from '@reach/combobox' |
||||||
|
import '@reach/combobox/styles.css' |
||||||
|
|
||||||
|
import { Label } from 'features/Common/Input/styled' |
||||||
|
|
||||||
|
import { useCountrySelector } from './hooks' |
||||||
|
import { |
||||||
|
ComboboxStyled, |
||||||
|
ComboboxInputStyled, |
||||||
|
Arrow, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
labelWidth?: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const CountrySelector = ({ labelWidth }: Props) => { |
||||||
|
const { |
||||||
|
countries, |
||||||
|
onBlur, |
||||||
|
onCountrySelect, |
||||||
|
onQueryChange, |
||||||
|
query, |
||||||
|
} = useCountrySelector() |
||||||
|
|
||||||
|
return ( |
||||||
|
<ComboboxStyled onSelect={onCountrySelect}> |
||||||
|
<Label |
||||||
|
labelWidth={labelWidth} |
||||||
|
htmlFor='country' |
||||||
|
> |
||||||
|
Страна |
||||||
|
<Arrow /> |
||||||
|
</Label> |
||||||
|
<ComboboxInputStyled |
||||||
|
id='country' |
||||||
|
value={query} |
||||||
|
onChange={onQueryChange} |
||||||
|
onBlur={onBlur} |
||||||
|
/> |
||||||
|
{!isEmpty(countries) && ( |
||||||
|
<ComboboxPopover> |
||||||
|
<ComboboxList> |
||||||
|
{map(countries, ({ id, name_eng }) => ( |
||||||
|
<ComboboxOption |
||||||
|
key={id} |
||||||
|
value={name_eng} |
||||||
|
/> |
||||||
|
))} |
||||||
|
</ComboboxList> |
||||||
|
</ComboboxPopover> |
||||||
|
)} |
||||||
|
</ComboboxStyled> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
import { Combobox, ComboboxInput } from '@reach/combobox' |
||||||
|
|
||||||
|
import { wrapperStyles, inputStyles } from 'features/Common/Input/styled' |
||||||
|
|
||||||
|
export const ComboboxStyled = styled(Combobox)` |
||||||
|
${wrapperStyles} |
||||||
|
position: relative; |
||||||
|
` |
||||||
|
|
||||||
|
export const ComboboxInputStyled = styled(ComboboxInput)` |
||||||
|
${inputStyles} |
||||||
|
padding-right: 24px; |
||||||
|
` |
||||||
|
|
||||||
|
export const Arrow = styled.div` |
||||||
|
position: absolute; |
||||||
|
right: 20px; |
||||||
|
top: 50%; |
||||||
|
transform: translateY(-50%); |
||||||
|
width: 12px; |
||||||
|
height: 12px; |
||||||
|
background-image: url(/images/arrowDown.svg); |
||||||
|
background-position: center; |
||||||
|
background-repeat: no-repeat; |
||||||
|
` |
||||||
@ -1,3 +1,3 @@ |
|||||||
export const getResponseData = (proc: string) => (response: any) => ( |
export const getResponseData = (proc: string) => (response: any) => ( |
||||||
response?.data?.[0]?.[proc] |
response?.[proc] |
||||||
) |
) |
||||||
|
|||||||
@ -1 +1,2 @@ |
|||||||
export * from './callApi' |
export * from './callApi' |
||||||
|
export * from './callApi/getResponseData' |
||||||
|
|||||||
@ -0,0 +1,28 @@ |
|||||||
|
import { DATA_URL, PROCEDURES } from 'config' |
||||||
|
import { callApi, getResponseData } from 'helpers' |
||||||
|
|
||||||
|
const proc = PROCEDURES.lst_c_country |
||||||
|
|
||||||
|
export type Country = { |
||||||
|
id: number, |
||||||
|
iso_3166_1_alpha_2: string, |
||||||
|
iso_3166_1_alpha_3: string, |
||||||
|
name_eng: string, |
||||||
|
name_rus: string, |
||||||
|
} |
||||||
|
|
||||||
|
export type Countries = Array<Country> |
||||||
|
|
||||||
|
export const getCountries = (): Promise<Countries> => { |
||||||
|
const config = { |
||||||
|
body: { |
||||||
|
params: null, |
||||||
|
proc, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return callApi({ |
||||||
|
config, |
||||||
|
url: DATA_URL, |
||||||
|
}).then(getResponseData(proc)) |
||||||
|
} |
||||||
@ -1 +1,2 @@ |
|||||||
export * from './login' |
export * from './login' |
||||||
|
export * from './getCountries' |
||||||
|
|||||||
Loading…
Reference in new issue