From e42f1a13b3d70fb2557842161a5a897ecf408f9e Mon Sep 17 00:00:00 2001 From: Artem Zubkov Date: Thu, 31 Dec 2020 19:02:04 +0700 Subject: [PATCH] #74 Configured stage routing. --- .../Header/components/BranchStep/Body.jsx | 7 +- .../Header/components/CommitsStep/Body.jsx | 32 ++-- .../Header/components/RepositoryStep/Body.jsx | 7 +- .../Header/components/UserStep/Body.jsx | 8 +- src/components/StageController/index.js | 141 +++--------------- .../StageController/useSetSelected.js | 39 +++++ .../StageController/useStageBranches.js | 54 +++++++ .../StageController/useStageCommits.js | 46 ++++++ .../StageController/useStageProfiles.js | 43 ++++++ .../StageController/useStageRepositories.js | 39 +++++ src/models/UrlPartTypes.js | 7 + src/redux/modules/branches.js | 8 +- src/redux/modules/commits.js | 8 +- src/redux/modules/profiles.js | 18 +-- src/redux/modules/progress.js | 23 ++- src/redux/modules/repositories.js | 8 +- src/redux/utils/index.js | 12 +- src/routes/index.jsx | 2 +- src/shared/hooks/useRedirectTo.js | 47 ++++++ src/shared/hooks/useRouteMatches.js | 23 +++ 20 files changed, 406 insertions(+), 166 deletions(-) create mode 100644 src/components/StageController/useSetSelected.js create mode 100644 src/components/StageController/useStageBranches.js create mode 100644 src/components/StageController/useStageCommits.js create mode 100644 src/components/StageController/useStageProfiles.js create mode 100644 src/components/StageController/useStageRepositories.js create mode 100644 src/models/UrlPartTypes.js create mode 100644 src/shared/hooks/useRedirectTo.js create mode 100644 src/shared/hooks/useRouteMatches.js diff --git a/src/components/Header/components/BranchStep/Body.jsx b/src/components/Header/components/BranchStep/Body.jsx index a171185..0e33a16 100644 --- a/src/components/Header/components/BranchStep/Body.jsx +++ b/src/components/Header/components/BranchStep/Body.jsx @@ -1,10 +1,12 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { UrlPratTypes } from '@/models/UrlPartTypes'; import slice from '@/redux/modules/branches'; import repositoriesSlice from '@/redux/modules/repositories'; import Highlight from '@/shared/components/Highlight'; import LoadingOverlay from '@/shared/components/LoadingOverlay'; import { ScrollBarMixin } from '@/shared/components/ScrollBar'; import { useUIProperty } from '@/shared/hooks'; +import { useRedirectTo } from '@/shared/hooks/useRedirectTo'; import { Avatar, ListItem as ListItemOrigin, ListItemAvatar, ListSubheader, TextField } from '@material-ui/core'; import List from '@material-ui/core/List'; import ListItemText from '@material-ui/core/ListItemText'; @@ -65,6 +67,7 @@ const bySearch = (search) => (item) => { const Body = () => { const dispatch = useDispatch(); + const redirectTo = useRedirectTo(UrlPratTypes.branch); const inputRef = useRef(); const [search, setSearch] = useState(''); const { isFetching, items } = useSelector(slice.selectors.getState); @@ -90,10 +93,10 @@ const Body = () => { const onClick = useCallback( (item) => () => { - dispatch(slice.actions.setSelected(item)); setBodyOpen(false); + redirectTo(item.name); }, - [setBodyOpen, dispatch], + [setBodyOpen, redirectTo], ); const ListHeader = useMemo( diff --git a/src/components/Header/components/CommitsStep/Body.jsx b/src/components/Header/components/CommitsStep/Body.jsx index 55a786c..a0dfa26 100644 --- a/src/components/Header/components/CommitsStep/Body.jsx +++ b/src/components/Header/components/CommitsStep/Body.jsx @@ -1,6 +1,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { UrlPratTypes } from '@/models/UrlPartTypes'; import branchesSlice from '@/redux/modules/branches'; +import commitsSlice from '@/redux/modules/commits'; import { useUIProperty } from '@/shared/hooks'; +import { useRedirectTo } from '@/shared/hooks/useRedirectTo'; import Button from '@material-ui/core/Button'; import Grid from '@material-ui/core/Grid'; import Input from '@material-ui/core/Input'; @@ -9,7 +12,7 @@ import Typography from '@material-ui/core/Typography'; import HistoryIcon from 'mdi-react/HistoryIcon'; import PlayArrowIcon from 'mdi-react/PlayArrowIcon'; import StopIcon from 'mdi-react/StopIcon'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; const Container = styled.div` @@ -38,9 +41,11 @@ const SliderContainer = styled(Grid)` const valueLabelFormat = (value) => -value; const Body = () => { + const dispatch = useDispatch(); + const redirectTo = useRedirectTo(UrlPratTypes.commits); const [, setBodyOpen] = useUIProperty('bodyOpen'); - const [isAnalysing, setIsAnalysing] = useUIProperty('isAnalysing'); - + const [, setRefreshKey] = useUIProperty('refreshKey'); + const { isFetching } = useSelector(commitsSlice.selectors.getState); const { selected: branch } = useSelector(branchesSlice.selectors.getState); const { commits = 0 } = branch || {}; const [storedValue, setStoredValue] = useUIProperty('commitsCount', Math.min(100, commits)); @@ -85,10 +90,17 @@ const Body = () => { return; } - setIsAnalysing((prev) => !prev); setBodyOpen(false); + setRefreshKey(Date.now()); + redirectTo(-value); + }, + [redirectTo, setBodyOpen, setRefreshKey, value], + ); + const onStop = useCallback( + () => { + dispatch(commitsSlice.actions.cancel()); }, - [setBodyOpen, setIsAnalysing, value], + [dispatch], ); useEffect( @@ -118,7 +130,7 @@ const Body = () => { step: 1, type: 'number', }} - disabled={isAnalysing} + disabled={isFetching} /> @@ -138,16 +150,16 @@ const Body = () => { valueLabelFormat={valueLabelFormat} valueLabelDisplay="on" track="inverted" - disabled={isAnalysing} + disabled={isFetching} /> diff --git a/src/components/Header/components/RepositoryStep/Body.jsx b/src/components/Header/components/RepositoryStep/Body.jsx index d37f372..d9a9d7c 100644 --- a/src/components/Header/components/RepositoryStep/Body.jsx +++ b/src/components/Header/components/RepositoryStep/Body.jsx @@ -1,9 +1,11 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { UrlPratTypes } from '@/models/UrlPartTypes'; import slice from '@/redux/modules/repositories'; import Highlight from '@/shared/components/Highlight'; import LoadingOverlay from '@/shared/components/LoadingOverlay'; import { ScrollBarMixin } from '@/shared/components/ScrollBar'; import { useUIProperty } from '@/shared/hooks'; +import { useRedirectTo } from '@/shared/hooks/useRedirectTo'; import { Avatar, ListItem as ListItemOrigin, ListItemAvatar, ListSubheader, TextField, @@ -68,6 +70,7 @@ const bySearch = (search) => (item) => { }; const Body = () => { + const redirectTo = useRedirectTo(UrlPratTypes.repository); const dispatch = useDispatch(); const inputRef = useRef(); const [search, setSearch] = useState(''); @@ -92,10 +95,10 @@ const Body = () => { const onClick = useCallback( (item) => () => { - dispatch(slice.actions.setSelected(item)); setBodyOpen(false); + redirectTo(item.name); }, - [setBodyOpen, dispatch], + [setBodyOpen, redirectTo], ); const ListHeader = useMemo( diff --git a/src/components/Header/components/UserStep/Body.jsx b/src/components/Header/components/UserStep/Body.jsx index 05b4353..d78316e 100644 --- a/src/components/Header/components/UserStep/Body.jsx +++ b/src/components/Header/components/UserStep/Body.jsx @@ -1,9 +1,11 @@ import React, { useCallback, useRef, useState } from 'react'; +import { UrlPratTypes } from '@/models/UrlPartTypes'; import slice from '@/redux/modules/profiles'; import Highlight from '@/shared/components/Highlight'; import LoadingOverlay from '@/shared/components/LoadingOverlay'; import ScrollBar from '@/shared/components/ScrollBar'; import { useUIProperty } from '@/shared/hooks'; +import { useRedirectTo } from '@/shared/hooks/useRedirectTo'; import { Avatar, ListItem as ListItemOrigin, ListItemAvatar, ListSubheader, @@ -64,6 +66,7 @@ const TopHeader = ( const Body = () => { const dispatch = useDispatch(); + const redirectTo = useRedirectTo(UrlPratTypes.profile); const inputRef = useRef(); const [search, setSearch] = useState(''); const [neverChange, setNeverChange] = useState(true); @@ -80,11 +83,10 @@ const Body = () => { const onClick = useCallback( (user) => () => { - dispatch(slice.actions.setSelected(user)); setBodyOpen(false); - dispatch(slice.actions.fetchProfile(user.login)); + redirectTo(user.login); }, - [setBodyOpen, dispatch], + [redirectTo, setBodyOpen], ); useDebounce( diff --git a/src/components/StageController/index.js b/src/components/StageController/index.js index 55a2d81..7e6edf6 100644 --- a/src/components/StageController/index.js +++ b/src/components/StageController/index.js @@ -1,38 +1,22 @@ -import { useEffect, useRef } from 'react'; -import branchesSlice from '@/redux/modules/branches'; -import commitsSlice from '@/redux/modules/commits'; +import { useEffect } from 'react'; +import { useStageBranches } from '@/components/StageController/useStageBranches'; +import { useStageCommits } from '@/components/StageController/useStageCommits'; +import { useStageProfiles } from '@/components/StageController/useStageProfiles'; +import { useStageRepositories } from '@/components/StageController/useStageRepositories'; import emojisSlice from '@/redux/modules/emojis'; -import profilesSlice from '@/redux/modules/profiles'; -import repositoriesSlice from '@/redux/modules/repositories'; -import { useUIProperty } from '@/shared/hooks'; -import { useDispatch, useSelector } from 'react-redux'; +import { useRouteMatches } from '@/shared/hooks/useRouteMatches'; +import { useDispatch } from 'react-redux'; -export default () => { +const StageController = () => { const dispatch = useDispatch(); - const { selected: profile } = useSelector(profilesSlice.selectors.getState); - const { selected: repository } = useSelector(repositoriesSlice.selectors.getState); - const { items: branches, selected: branch } = useSelector(branchesSlice.selectors.getState); - const { isFetching } = useSelector(commitsSlice.selectors.getState); - const lastFetching = useRef(isFetching); - const [isAnalysing, setIsAnalysing] = useUIProperty('isAnalysing'); - const [commits] = useUIProperty('commitsCount', 0); - const owner = profile?.login; - const amount = profile?.public_repos; - const repo = repository?.name; - const defaultBranch = repository?.default_branch; - - // fetch list of top users - useEffect( - () => { - dispatch(profilesSlice.actions.fetchTop(null, 'global')); - - return () => { - dispatch(profilesSlice.actions.cancel('global')); - }; - }, - [dispatch], - ); + const { + service, + profile, + repository, + branch, + commits, + } = useRouteMatches(); // fetch list of emojis useEffect( @@ -43,96 +27,15 @@ export default () => { dispatch(emojisSlice.actions.cancel()); }; }, - [dispatch], - ); - - // fetch all repositories of a owner - useEffect( - () => { - dispatch(repositoriesSlice.actions.clear()); - dispatch(repositoriesSlice.actions.fetch({ owner, amount })); - - return () => { - dispatch(repositoriesSlice.actions.cancel()); - }; - }, - [dispatch, owner, amount], + [service, dispatch], ); - // fetch all branches of a repository - useEffect( - () => { - dispatch(branchesSlice.actions.clear()); - if (!repo) { - return undefined; - } - - dispatch(branchesSlice.actions.fetch({ - owner, - repo, - })); - - return () => { - dispatch(branchesSlice.actions.cancel()); - }; - }, - [dispatch, owner, repo], - ); - - // select a default branch of a repository - useEffect( - () => { - setIsAnalysing(false); - if (!branches.length || branch || !defaultBranch) { - return; - } - - const item = branches.find(({ name }) => name === defaultBranch); - - if (item) { - dispatch(branchesSlice.actions.setSelected(item)); - } - }, - [branch, branches, defaultBranch, dispatch, setIsAnalysing], - ); - - // fetch commits - useEffect( - () => { - if (!isAnalysing) { - return undefined; - } - - dispatch(commitsSlice.actions.clear()); - if (!branch?.name || !owner || !repo || !commits) { - return undefined; - } - - dispatch(commitsSlice.actions.fetch({ - owner, - repo, - branch: branch.name, - amount: commits, - })); - - return () => { - if (lastFetching.current) { - dispatch(commitsSlice.actions.cancel()); - } - }; - }, - [isAnalysing, branch, owner, repo, commits, dispatch], - ); - - useEffect( - () => { - if (lastFetching.current && !isFetching) { - setIsAnalysing(false); - } - lastFetching.current = isFetching; - }, - [isFetching, setIsAnalysing], - ); + useStageProfiles(service, profile); + useStageRepositories(repository); + useStageBranches(branch); + useStageCommits(commits); return null; }; + +export default StageController; diff --git a/src/components/StageController/useSetSelected.js b/src/components/StageController/useSetSelected.js new file mode 100644 index 0000000..18c232b --- /dev/null +++ b/src/components/StageController/useSetSelected.js @@ -0,0 +1,39 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; + +const defaultCondition = (name, selected) => name === selected; + +/** + * @param name - is needed to find + * @param selected - already selected + * @param {Array} items + * @param {Function} action + * @param {Function} [skipCondition] + * @param {Function} [preAction] + */ +export const useSetSelected = ({ name, selected, items, action, skipCondition, preAction }) => { + const dispatch = useDispatch(); + + const skip = skipCondition || defaultCondition; + + useEffect( + () => { + if (preAction) { + preAction(); + } + + if (!items?.length || skip(name, selected)) { + return; + } + + const found = items.find((item) => item.name === name); + + if (!found) { + return; + } + + dispatch(action(found)); + }, + [name, selected, items, dispatch, action, preAction, skip], + ); +}; diff --git a/src/components/StageController/useStageBranches.js b/src/components/StageController/useStageBranches.js new file mode 100644 index 0000000..b79b40f --- /dev/null +++ b/src/components/StageController/useStageBranches.js @@ -0,0 +1,54 @@ +import { useEffect } from 'react'; +import { UrlPratTypes } from '@/models/UrlPartTypes'; +import branchesSlice from '@/redux/modules/branches'; +import profilesSlice from '@/redux/modules/profiles'; +import repositoriesSlice from '@/redux/modules/repositories'; +import { useRedirectTo } from '@/shared/hooks/useRedirectTo'; +import { useDispatch, useSelector } from 'react-redux'; +import { useSetSelected } from './useSetSelected'; + +/** + * @param name - name of branch + */ +export const useStageBranches = (name) => { + const dispatch = useDispatch(); + const redirectTo = useRedirectTo(UrlPratTypes.branch); + + const { selected: repository } = useSelector(repositoriesSlice.selectors.getState); + const { selected: profile } = useSelector(profilesSlice.selectors.getState); + const { selected, items } = useSelector(branchesSlice.selectors.getState); + + const { login: owner } = profile || {}; + const { name: repo, default_branch: defaultBranch } = repository || {}; + const { name: branch } = selected || {}; + + useEffect( + () => { + dispatch(branchesSlice.actions.clear()); + if (!repo) { + return undefined; + } + + dispatch(branchesSlice.actions.fetch({ + owner, + repo, + })); + + return () => { + dispatch(branchesSlice.actions.cancel()); + }; + }, + [dispatch, owner, repo], + ); + + useSetSelected({ + name: name === 'default' ? defaultBranch : name, + selected: branch, + items, + action: branchesSlice.actions.setSelected, + }); + + if (!(name || branch) && defaultBranch) { + redirectTo(defaultBranch); + } +}; diff --git a/src/components/StageController/useStageCommits.js b/src/components/StageController/useStageCommits.js new file mode 100644 index 0000000..01f4f00 --- /dev/null +++ b/src/components/StageController/useStageCommits.js @@ -0,0 +1,46 @@ +import { useEffect } from 'react'; +import branchesSlice from '@/redux/modules/branches'; +import commitsSlice from '@/redux/modules/commits'; +import profilesSlice from '@/redux/modules/profiles'; +import repositoriesSlice from '@/redux/modules/repositories'; +import { useUIProperty } from '@/shared/hooks'; +import { useDispatch, useSelector } from 'react-redux'; + +export const useStageCommits = (amount) => { + const dispatch = useDispatch(); + const [, setStoredValue] = useUIProperty('commitsCount'); + const [refreshKey] = useUIProperty('refreshKey'); + + const { selected: repository } = useSelector(repositoriesSlice.selectors.getState); + const { selected: profile } = useSelector(profilesSlice.selectors.getState); + const { selected } = useSelector(branchesSlice.selectors.getState); + + const { login: owner } = profile || {}; + const { name: repo } = repository || {}; + const { name: branch } = selected || {}; + + useEffect( + () => { + dispatch(commitsSlice.actions.clear()); + const fixedAmount = +amount; + + if (!branch || !owner || !repo || !fixedAmount || fixedAmount < 1) { + return undefined; + } + + setStoredValue(fixedAmount); + + dispatch(commitsSlice.actions.fetch({ + owner, + repo, + branch, + amount: fixedAmount, + })); + + return () => { + dispatch(commitsSlice.actions.cancel()); + }; + }, + [branch, owner, repo, dispatch, amount, setStoredValue, refreshKey], + ); +}; diff --git a/src/components/StageController/useStageProfiles.js b/src/components/StageController/useStageProfiles.js new file mode 100644 index 0000000..fa08d29 --- /dev/null +++ b/src/components/StageController/useStageProfiles.js @@ -0,0 +1,43 @@ +import { useEffect } from 'react'; +import profilesSlice from '@/redux/modules/profiles'; +import { useDispatch, useSelector } from 'react-redux'; + +export const useStageProfiles = (service, profile) => { + const dispatch = useDispatch(); + const { selected } = useSelector(profilesSlice.selectors.getState); + const { login } = selected || {}; + + // fetch list of top users + useEffect( + () => { + dispatch(profilesSlice.actions.fetchTop(null, 'global')); + + return () => { + dispatch(profilesSlice.actions.cancel('global')); + }; + }, + [dispatch, service], + ); + + useEffect( + () => { + dispatch(profilesSlice.actions.clear()); + }, + [dispatch, service], + ); + + useEffect( + () => { + if (!profile || login === profile) { + return; + } + + dispatch(profilesSlice.actions.fetchProfile(profile)); + + return () => { + dispatch(profilesSlice.actions.cancel()); + }; + }, + [profile, login, dispatch], + ); +}; diff --git a/src/components/StageController/useStageRepositories.js b/src/components/StageController/useStageRepositories.js new file mode 100644 index 0000000..e5bcce8 --- /dev/null +++ b/src/components/StageController/useStageRepositories.js @@ -0,0 +1,39 @@ +import { useEffect } from 'react'; +import { useSetSelected } from '@/components/StageController/useSetSelected'; +import profilesSlice from '@/redux/modules/profiles'; +import repositoriesSlice from '@/redux/modules/repositories'; +import { useDispatch, useSelector } from 'react-redux'; + +/** + * @param {String} name - name of repository + */ +export const useStageRepositories = (name) => { + const dispatch = useDispatch(); + const { selected, items } = useSelector(repositoriesSlice.selectors.getState); + const { selected: profile } = useSelector(profilesSlice.selectors.getState); + + const { login: owner, public_repos: amount = 0 } = profile || {}; + const { name: repository } = selected || {}; + + useEffect( + () => { + dispatch(repositoriesSlice.actions.clear()); + dispatch(repositoriesSlice.actions.fetch({ + owner, + amount, + })); + + return () => { + dispatch(repositoriesSlice.actions.cancel()); + }; + }, + [owner, amount, dispatch], + ); + + useSetSelected({ + name, + selected: repository, + items, + action: repositoriesSlice.actions.setSelected, + }); +}; diff --git a/src/models/UrlPartTypes.js b/src/models/UrlPartTypes.js new file mode 100644 index 0000000..fccccbc --- /dev/null +++ b/src/models/UrlPartTypes.js @@ -0,0 +1,7 @@ +export const UrlPratTypes = { + service: 'service', + profile: 'profile', + repository: 'repository', + branch: 'branch', + commits: 'commits', +}; diff --git a/src/redux/modules/branches.js b/src/redux/modules/branches.js index 2def72d..f99cd09 100644 --- a/src/redux/modules/branches.js +++ b/src/redux/modules/branches.js @@ -67,12 +67,12 @@ export default createSlice({ yield put(actions.stopFetching()); } catch (error) { - if (yield cancelled()) { - yield put(actions.stopFetching); - return; - } yield put(actions.fail(error)); } finally { + if (yield cancelled()) { + yield put(actions.stopFetching()); + } + yield delay(500); yield put(slice.actions.toggle(false)); } diff --git a/src/redux/modules/commits.js b/src/redux/modules/commits.js index 72ab7db..9c04e4d 100644 --- a/src/redux/modules/commits.js +++ b/src/redux/modules/commits.js @@ -67,12 +67,12 @@ export default createSlice({ yield put(actions.stopFetching()); } catch (error) { - if (yield cancelled()) { - yield put(actions.stopFetching); - return; - } yield put(actions.fail(error)); } finally { + if (yield cancelled()) { + yield put(actions.stopFetching()); + } + yield delay(500); yield put(slice.actions.toggle(false)); } diff --git a/src/redux/modules/profiles.js b/src/redux/modules/profiles.js index e49a592..e7f2142 100644 --- a/src/redux/modules/profiles.js +++ b/src/redux/modules/profiles.js @@ -50,11 +50,11 @@ export default createSlice({ const { data } = yield call(getProfile, payload); yield put(actions.fetchProfileSuccess(data)); } catch (error) { + yield put(actions.fail(error)); + } finally { if (yield cancelled()) { - yield put(actions.stopFetching); - return; + yield put(actions.stopFetching()); } - yield put(actions.fail(error)); } }, }, @@ -65,11 +65,11 @@ export default createSlice({ const { data } = yield call(searchAccount, payload); yield put(actions.searchSuccess(data)); } catch (error) { + yield put(actions.fail(error)); + } finally { if (yield cancelled()) { - yield put(actions.stopFetching); - return; + yield put(actions.stopFetching()); } - yield put(actions.fail(error)); } }, }, @@ -80,11 +80,11 @@ export default createSlice({ const { data } = yield call(searchAccount, 'followers:>1000'); yield put(actions.fetchTopSuccess(data)); } catch (error) { + yield put(actions.fail(error)); + } finally { if (yield cancelled()) { - yield put(actions.stopFetching); - return; + yield put(actions.stopFetching()); } - yield put(actions.fail(error)); } }, }, diff --git a/src/redux/modules/progress.js b/src/redux/modules/progress.js index 0c46144..2f0f908 100644 --- a/src/redux/modules/progress.js +++ b/src/redux/modules/progress.js @@ -1,4 +1,4 @@ -import { createSlice } from '@/redux/utils'; +import { createSlice, startFetching, stopFetching } from '@/redux/utils'; const initialState = { max: 100, @@ -13,10 +13,20 @@ export default createSlice({ initialState, reducers: { change: (state, { payload }) => { - return { + const { show, ...rest } = payload; + + const newState = { ...state, - ...payload, + ...rest, }; + + if (show) { + startFetching(newState, { payload: 'show' }); + } else if (show === false) { + stopFetching(newState, { payload: 'show' }); + } + + return newState; }, incValue: (state, { payload }) => { @@ -28,7 +38,12 @@ export default createSlice({ }, toggle: (state, { payload }) => { - state.show = payload ?? !state.show; + const show = payload ?? !state.show; + if (show) { + startFetching(state, { payload: 'show' }); + } else { + stopFetching(state, { payload: 'show' }); + } }, }, }); diff --git a/src/redux/modules/repositories.js b/src/redux/modules/repositories.js index fed2b2e..b8dbc7d 100644 --- a/src/redux/modules/repositories.js +++ b/src/redux/modules/repositories.js @@ -67,12 +67,12 @@ export default createSlice({ yield put(actions.stopFetching()); } catch (error) { - if (yield cancelled()) { - yield put(actions.stopFetching); - return; - } yield put(actions.fail(error)); } finally { + if (yield cancelled()) { + yield put(actions.stopFetching()); + } + yield delay(500); yield put(slice.actions.toggle(false)); } diff --git a/src/redux/utils/index.js b/src/redux/utils/index.js index ff99751..14aadd1 100644 --- a/src/redux/utils/index.js +++ b/src/redux/utils/index.js @@ -5,9 +5,11 @@ export * from './withCancellation'; /** * incs counter of request and set isFetching to true * @param state + * @param {String} payload - name of property */ -export const startFetching = (state) => { - state.isFetching = true; +export const startFetching = (state, { payload } = {}) => { + const prop = typeof payload === 'string' ? payload : 'isFetching'; + state[prop] = true; state._requests = (state._requests ?? 0) + 1; state.error = ''; }; @@ -15,10 +17,12 @@ export const startFetching = (state) => { /** * decs counter of request and set isFetching to false if counter less than 1. * @param state + * @param {String} [payload] - name of property */ -export const stopFetching = (state) => { +export const stopFetching = (state, { payload } = {}) => { + const prop = typeof payload === 'string' ? payload : 'isFetching'; state._requests = Math.max(0, (state._requests ?? 1) - 1); - state.isFetching = !!state._requests; + state[prop] = !!state._requests; }; /** diff --git a/src/routes/index.jsx b/src/routes/index.jsx index d2105bf..8833d44 100644 --- a/src/routes/index.jsx +++ b/src/routes/index.jsx @@ -6,7 +6,7 @@ const AppRouter = () => { return ( - + } /> ); diff --git a/src/shared/hooks/useRedirectTo.js b/src/shared/hooks/useRedirectTo.js new file mode 100644 index 0000000..1a1b466 --- /dev/null +++ b/src/shared/hooks/useRedirectTo.js @@ -0,0 +1,47 @@ +import { useCallback, useMemo } from 'react'; +import { UrlPratTypes } from '@/models/UrlPartTypes'; +import { useHistory } from 'react-router-dom'; +import { useRouteMatches } from './useRouteMatches'; + +const order = [ + UrlPratTypes.service, + UrlPratTypes.profile, + UrlPratTypes.repository, + UrlPratTypes.branch, + UrlPratTypes.commits, +]; + +export const useRedirectTo = (partType) => { + const history = useHistory(); + const { + service, + profile, + repository, + branch, + } = useRouteMatches(); + + const hash = useMemo( + () => ({ + [UrlPratTypes.service]: service, + [UrlPratTypes.profile]: profile, + [UrlPratTypes.repository]: repository, + [UrlPratTypes.branch]: branch, + }), + [service, profile, repository, branch], + ); + + return useCallback( + (part) => { + const index = order.indexOf(partType); + if (index < 0) { + return; + } + + let url = order.slice(0, index).map((key) => hash[key]).join('/'); + url = url ? `/${url}` : url; + + history.push(`${url}/${part}`); + }, + [hash, history, partType], + ); +}; diff --git a/src/shared/hooks/useRouteMatches.js b/src/shared/hooks/useRouteMatches.js new file mode 100644 index 0000000..21f3c76 --- /dev/null +++ b/src/shared/hooks/useRouteMatches.js @@ -0,0 +1,23 @@ +import { useRouteMatch } from 'react-router-dom'; + +const servicePath = '/:service'; +const profilePath = `${servicePath}/:profile`; +const repositoryPath = `${profilePath}/:repository`; +const branchPath = `${repositoryPath}/:branch`; +const commitsPath = `${branchPath}/:commits`; + +export const useRouteMatches = () => { + const { params: { service } = { service: 'github' } } = useRouteMatch({ path: servicePath, exact: false }) || {}; + const { params: { profile } = {} } = useRouteMatch({ path: profilePath, exact: false }) || {}; + const { params: { repository } = {} } = useRouteMatch({ path: repositoryPath, exact: false }) || {}; + const { params: { branch } = {} } = useRouteMatch({ path: branchPath, exact: false }) || {}; + const { params: { commits } = {} } = useRouteMatch({ path: commitsPath, exact: false }) || {}; + + return { + service, + profile, + repository, + branch, + commits, + }; +};