From c051c2389624e72dd496f92f823037c7ce7711d2 Mon Sep 17 00:00:00 2001 From: Nikolay <46225163+vigdorov@users.noreply.github.com> Date: Mon, 28 Dec 2020 00:31:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=85=D1=83=D0=BA=D0=BE=D0=B2=20useQuery,=20use?= =?UTF-8?q?Params,=20=D0=BF=D0=BE=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B0=D0=BC=D0=B8=20(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.json | 5 + src/app/components/top-menu/TopMenu.tsx | 6 +- src/core/consts/common.ts | 2 + src/core/hooks/usePageType.ts | 8 ++ src/core/hooks/useParams.ts | 25 +---- src/core/hooks/useQuery.ts | 67 +------------ .../utils/__test__/getParamFromUrl.test.ts | 30 ++++++ .../__test__/getQueryParamFromUrl.test.ts | 32 ++++++ src/core/utils/__test__/queryParsers.test.ts | 98 +++++++++++++++++++ src/core/utils/getParamFromUrl.ts | 15 +++ src/core/utils/getQueryFromUrl.ts | 19 ++++ src/core/utils/queryParsers.ts | 53 ++++++++++ 12 files changed, 273 insertions(+), 87 deletions(-) create mode 100644 src/core/hooks/usePageType.ts create mode 100644 src/core/utils/__test__/getParamFromUrl.test.ts create mode 100644 src/core/utils/__test__/getQueryParamFromUrl.test.ts create mode 100644 src/core/utils/__test__/queryParsers.test.ts create mode 100644 src/core/utils/getParamFromUrl.ts create mode 100644 src/core/utils/getQueryFromUrl.ts create mode 100644 src/core/utils/queryParsers.ts diff --git a/.eslintrc.json b/.eslintrc.json index 3c60a16..fe9c331 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,6 +4,11 @@ "es2021": true, "jest/globals": true }, + "settings": { + "react": { + "version": "detect" + } + }, "extends": [ "eslint:recommended", "plugin:react/recommended", diff --git a/src/app/components/top-menu/TopMenu.tsx b/src/app/components/top-menu/TopMenu.tsx index e654647..5e59187 100644 --- a/src/app/components/top-menu/TopMenu.tsx +++ b/src/app/components/top-menu/TopMenu.tsx @@ -8,10 +8,10 @@ import IconButton from '@material-ui/core/IconButton'; import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos'; import SearchIcon from '@material-ui/icons/Search'; import {Avatar} from '@material-ui/core'; -import {useParams} from '_hooks/useParams'; import {PageType} from '_enums/common'; import {PAGE_TITLE} from '_consts/common'; -import {buildPath} from '../../../core/utils/buildPath'; +import {usePageType} from '_hooks/usePageType'; +import {buildPath} from '_utils/buildPath'; const NO_NAME_AVATAR = 'https://d.newsweek.com/en/full/425257/02-10-putin-economy.jpg'; @@ -30,7 +30,7 @@ const useStyles = makeStyles(() => const TopMenu: React.FC = () => { const classes = useStyles(); - const {pageType} = useParams(); + const pageType = usePageType(); const history = useHistory(); const handleGoRoot = () => { diff --git a/src/core/consts/common.ts b/src/core/consts/common.ts index 47b25b2..d69fbec 100644 --- a/src/core/consts/common.ts +++ b/src/core/consts/common.ts @@ -21,3 +21,5 @@ export const PAGE_TITLE = { [PageType.Settings]: 'Settings', [PageType.SigIn]: 'SigIn', }; + +export const UTC_DATE_FORMAT = ''; diff --git a/src/core/hooks/usePageType.ts b/src/core/hooks/usePageType.ts new file mode 100644 index 0000000..33bb0d5 --- /dev/null +++ b/src/core/hooks/usePageType.ts @@ -0,0 +1,8 @@ +import {useMemo} from 'react'; +import {useLocation} from 'react-router-dom'; +import {getPageType} from '../utils/common'; + +export const usePageType = () => { + const location = useLocation(); + return useMemo(() => getPageType(location.pathname), [location.pathname]); +}; diff --git a/src/core/hooks/useParams.ts b/src/core/hooks/useParams.ts index f2149cc..056f821 100644 --- a/src/core/hooks/useParams.ts +++ b/src/core/hooks/useParams.ts @@ -1,25 +1,10 @@ import {useMemo} from 'react'; -import {useLocation, useParams as useReactParams} from 'react-router-dom'; -import {PageType} from '../enums/common'; -import {getPageType} from '../utils/common'; +import {useParams as useReactParams} from 'react-router-dom'; +import {getParamsFromUrl} from '../utils/getParamFromUrl'; +import {QueryParsers} from '../utils/getQueryFromUrl'; -type ParamsParser = (value?: string) => T; -export type ParamsParsers = Partial<{[K in keyof T]: ParamsParser}>; - -export function useParams(paramParsers: ParamsParsers = {}) { +export function useParams>(paramParsers: QueryParsers) { const params = useReactParams>(); - const {pathname} = useLocation(); - return useMemo(() => { - return Object.keys(paramParsers).reduce((memo, key) => { - const parser = paramParsers[key]; - - return { - ...memo, - [key]: parser?.(params[key]), - }; - }, { - pageType: getPageType(pathname), - } as T & {pageType: PageType}); - }, [params, paramParsers, pathname]); + return useMemo(() => getParamsFromUrl(paramParsers, params), [params, paramParsers]); } diff --git a/src/core/hooks/useQuery.ts b/src/core/hooks/useQuery.ts index 99b4dbb..64071df 100644 --- a/src/core/hooks/useQuery.ts +++ b/src/core/hooks/useQuery.ts @@ -1,70 +1,9 @@ -import {parse, ParsedUrlQuery} from 'querystring'; -import {head} from 'lodash'; import {useMemo} from 'react'; import {useLocation} from 'react-router-dom'; -import {toNumber} from '../utils/parsers'; +import {getQueryFromUrl, QueryParsers} from '../utils/getQueryFromUrl'; -type QueryParser = (value?: string | string[]) => T; -export type QueryParsers = Partial<{[K in keyof T]: QueryParser}>; - -export function stringParser(): QueryParser>; -export function stringParser(defaultValue: T): QueryParser; -export function stringParser(defaultValue?: T) { - return (val?: string | string[]) => { - const value = Array.isArray(val) ? head(val) : val; - - return value ?? defaultValue; - }; -} - -export function numberParser(): QueryParser>; -export function numberParser(defaultValue?: number): QueryParser; -export function numberParser(defaultValue?: number) { - return (val?: string | string[]) => { - const value = Array.isArray(val) ? head(val) : val; - - return toNumber(value) ?? defaultValue; - }; -} - -export function booleanParser(): QueryParser>; -export function booleanParser(defaultValue: boolean): QueryParser; -export function booleanParser(defaultValue?: boolean) { - return (val?: string | string[]) => { - const value = Array.isArray(val) ? head(val) : val; - - if (value === 'true') { - return true; - } - - if (value === 'false') { - return false; - } - - return defaultValue; - }; -} - -// Date parser - -// Array parser (должен уметь с enum) - -export function useQuery( - queryParsers: QueryParsers -): ParsedUrlQuery | Partial { +export function useQuery>(queryParsers: QueryParsers): T { const {search} = useLocation(); - return useMemo(() => { - const query = parse(search.slice(1)); - return queryParsers ? Object.keys(queryParsers).reduce((memo, key) => { - if (key in queryParsers) { - const parser = queryParsers[key]; - return { - ...memo, - [key]: parser?.(query[key]), - }; - } - return memo; - }, {} as T) : query; - }, [search, queryParsers]); + return useMemo(() => getQueryFromUrl(queryParsers, search), [search, queryParsers]); } diff --git a/src/core/utils/__test__/getParamFromUrl.test.ts b/src/core/utils/__test__/getParamFromUrl.test.ts new file mode 100644 index 0000000..ab700b8 --- /dev/null +++ b/src/core/utils/__test__/getParamFromUrl.test.ts @@ -0,0 +1,30 @@ +import {PageType} from '../../enums/common'; +import {getParamsFromUrl} from '../getParamFromUrl'; +import {QueryParsers} from '../getQueryFromUrl'; +import {booleanParser, numberParser, stringParser} from '../queryParsers'; + +describe('getParamsFromUrl', () => { + type Params = { + name?: string; + id?: number; + pageType?: PageType; + isNew: boolean; + }; + + it('Получение параметров', () => { + const parsers: QueryParsers = { + name: stringParser(), + id: numberParser(), + pageType: stringParser(), + isNew: booleanParser(false), + }; + expect(getParamsFromUrl(parsers, {olo: 't'})).toEqual({isNew: false}); + expect(getParamsFromUrl(parsers, {name: 't'})).toEqual({name: 't', isNew: false}); + expect(getParamsFromUrl(parsers, { + name: 't', + id: '6', + pageType: 'tags', + isNew: 'true', + })).toEqual({name: 't', id: 6, pageType: PageType.Tags, isNew: true}); + }); +}); diff --git a/src/core/utils/__test__/getQueryParamFromUrl.test.ts b/src/core/utils/__test__/getQueryParamFromUrl.test.ts new file mode 100644 index 0000000..ce29aeb --- /dev/null +++ b/src/core/utils/__test__/getQueryParamFromUrl.test.ts @@ -0,0 +1,32 @@ +import {PageType} from '../../enums/common'; +import {getQueryFromUrl, QueryParsers} from '../getQueryFromUrl'; +import {arrayParser, booleanParser, numberParser, stringParser} from '../queryParsers'; + +describe('getQueryFromUrl', () => { + type Query = { + name?: string; + id?: number; + pageType?: PageType; + isNew: boolean; + colors: string[]; + }; + + it('Получение параметров', () => { + const parsers: QueryParsers = { + name: stringParser(), + id: numberParser(), + pageType: stringParser(), + isNew: booleanParser(false), + colors: arrayParser([]), + }; + expect(getQueryFromUrl(parsers, '?olo=2')).toEqual({isNew: false, colors: []}); + expect(getQueryFromUrl(parsers, '?olo=2&name=t')).toEqual({name: 't', isNew: false, colors: []}); + expect(getQueryFromUrl(parsers, '?name=t&id=6&pageType=tags&isNew=true&colors=red&colors=blue')).toEqual({ + name: 't', + id: 6, + pageType: PageType.Tags, + isNew: true, + colors: ['red', 'blue'], + }); + }); +}); diff --git a/src/core/utils/__test__/queryParsers.test.ts b/src/core/utils/__test__/queryParsers.test.ts new file mode 100644 index 0000000..72ac163 --- /dev/null +++ b/src/core/utils/__test__/queryParsers.test.ts @@ -0,0 +1,98 @@ +import {arrayParser, booleanParser, numberParser, stringParser} from '../queryParsers'; + +describe('stringParser', () => { + it('Вернет значение', () => { + expect(stringParser()('trt')).toBe('trt'); + }); + + it('Вернет первое значение массива', () => { + expect(stringParser()(['trt', 'ggt'])).toBe('trt'); + }); + + it('Вернет undefined', () => { + expect(stringParser()()).toBeUndefined(); + expect(stringParser()([])).toBeUndefined(); + }); + + it('Вернет дефолт', () => { + expect(stringParser('def')()).toBe('def'); + expect(stringParser('def')([])).toBe('def'); + }); + + it('Вернет значение даже при наличии дефолта', () => { + expect(stringParser('def')(['trt'])).toBe('trt'); + expect(stringParser('def')('trt')).toBe('trt'); + }); +}); + +describe('numberParser', () => { + it('Вернет значение', () => { + expect(numberParser()('45')).toBe(45); + }); + + it('Вернет первое значение массива', () => { + expect(numberParser()(['45', '44'])).toBe(45); + }); + + it('Вернет undefined', () => { + expect(numberParser()()).toBeUndefined(); + expect(numberParser()([])).toBeUndefined(); + }); + + it('Вернет дефолт', () => { + expect(numberParser(33)()).toBe(33); + expect(numberParser(33)([])).toBe(33); + }); + + it('Вернет значение даже при наличии дефолта', () => { + expect(numberParser(33)(['45'])).toBe(45); + expect(numberParser(33)('45')).toBe(45); + }); +}); + +describe('booleanParser', () => { + it('Вернет значение', () => { + expect(booleanParser()('true')).toBe(true); + expect(booleanParser()('false')).toBe(false); + }); + + it('Вернет первое значение массива', () => { + expect(booleanParser()(['true', 'false'])).toBe(true); + }); + + it('Вернет undefined', () => { + expect(booleanParser()()).toBeUndefined(); + expect(booleanParser()([])).toBeUndefined(); + }); + + it('Вернет дефолт', () => { + expect(booleanParser(true)()).toBe(true); + expect(booleanParser(false)([])).toBe(false); + }); + + it('Вернет значение даже при наличии дефолта', () => { + expect(booleanParser(false)(['true'])).toBe(true); + expect(booleanParser(false)('true')).toBe(true); + }); +}); + +describe('arrayParser', () => { + it('Вернет значение', () => { + expect(arrayParser()('rtr')).toEqual(['rtr']); + expect(arrayParser()(['rtr', 'rtr'])).toEqual(['rtr', 'rtr']); + expect(arrayParser()([])).toEqual([]); + }); + + it('Вернет undefined', () => { + expect(arrayParser()()).toBeUndefined(); + }); + + it('Вернет дефолт', () => { + expect(arrayParser(['def'])()).toEqual(['def']); + }); + + it('Вернет значение даже при наличии дефолта', () => { + expect(arrayParser(['def'])('rtr')).toEqual(['rtr']); + expect(arrayParser(['def'])(['rtr'])).toEqual(['rtr']); + }); +}); diff --git a/src/core/utils/getParamFromUrl.ts b/src/core/utils/getParamFromUrl.ts new file mode 100644 index 0000000..1514664 --- /dev/null +++ b/src/core/utils/getParamFromUrl.ts @@ -0,0 +1,15 @@ +import {QueryParsers} from './getQueryFromUrl'; + +export const getParamsFromUrl = >( + paramParsers: QueryParsers, + params: Record +) => { + return Object.keys(paramParsers).reduce((memo, key) => { + const parser = paramParsers[key]; + + return { + ...memo, + [key]: parser?.(params[key]), + }; + }, {} as T); +}; diff --git a/src/core/utils/getQueryFromUrl.ts b/src/core/utils/getQueryFromUrl.ts new file mode 100644 index 0000000..d3a6930 --- /dev/null +++ b/src/core/utils/getQueryFromUrl.ts @@ -0,0 +1,19 @@ +import {decode} from 'querystring'; + +export type QueryParser = (value?: string | string[]) => T; +export type QueryParsers = {[K in keyof T]: QueryParser}; + +export const getQueryFromUrl = >(queryParsers: QueryParsers, search?: string) => { + const query = decode((search || location.search).slice(1)); + + return Object.keys(queryParsers).reduce((memo, key) => { + if (key in queryParsers) { + const parser = queryParsers[key]; + return { + ...memo, + [key]: parser?.(query[key]), + }; + } + return memo; + }, {} as T); +}; diff --git a/src/core/utils/queryParsers.ts b/src/core/utils/queryParsers.ts new file mode 100644 index 0000000..e8d3cfd --- /dev/null +++ b/src/core/utils/queryParsers.ts @@ -0,0 +1,53 @@ +import {head} from 'lodash'; +import {QueryParser} from './getQueryFromUrl'; +import {toNumber} from './parsers'; + +export function stringParser(): QueryParser>; +export function stringParser(defaultValue: T): QueryParser; +export function stringParser(defaultValue?: T) { + return (val?: string | string[]) => { + const value = Array.isArray(val) ? head(val) : val; + + return value ?? defaultValue; + }; +} + +export function numberParser(): QueryParser>; +export function numberParser(defaultValue?: number): QueryParser; +export function numberParser(defaultValue?: number) { + return (val?: string | string[]) => { + const value = Array.isArray(val) ? head(val) : val; + + return toNumber(value) ?? defaultValue; + }; +} + +export function booleanParser(): QueryParser>; +export function booleanParser(defaultValue: boolean): QueryParser; +export function booleanParser(defaultValue?: boolean) { + return (val?: string | string[]) => { + const value = Array.isArray(val) ? head(val) : val; + + if (value === 'true') { + return true; + } + + if (value === 'false') { + return false; + } + + return defaultValue; + }; +} + +export function arrayParser(): QueryParser>; +export function arrayParser(defaultValue: T[]): QueryParser; +export function arrayParser(defaultValue?: T[]) { + return (val?: string | string[]) => { + if (Array.isArray(val)) { + return val; + } + + return val === undefined ? defaultValue : [val]; + }; +}