From 90dab3026dd914fe3641625ee24844a2a4c2c06a Mon Sep 17 00:00:00 2001 From: Artem Zubkov Date: Wed, 30 Dec 2020 09:51:11 +0700 Subject: [PATCH] #68 Added Branch step --- .../Header/components/BranchStep/Body.jsx | 185 ++++++++++++++++++ .../Header/components/BranchStep/Header.jsx | 45 +++++ .../components/BranchStep/Secondary.jsx | 31 +++ .../Header/components/RepositoryStep/Body.jsx | 53 +++-- .../components/RepositoryStep/Header.jsx | 47 +++-- .../components/RepositoryStep/Secondary.jsx | 50 ++--- .../Header/components/UserStep/Body.jsx | 22 +-- .../Header/components/UserStep/Header.jsx | 76 +++---- .../components/shared/HeaderContainer.jsx | 3 +- .../components/shared/InfoContainer.jsx | 12 ++ .../Header/components/shared/Marker.jsx | 15 ++ .../Header/components/shared/Properties.jsx | 9 + .../Header/components/shared/Property.jsx | 22 +++ .../components/shared/PropertyValue.jsx | 7 + .../Header/components/shared/Title.jsx | 12 ++ src/components/StageController/index.js | 59 +++++- src/models/StageTypes.js | 1 + src/redux/modules/branches.js | 88 +++++++++ src/redux/modules/index.js | 10 +- 19 files changed, 603 insertions(+), 144 deletions(-) create mode 100644 src/components/Header/components/BranchStep/Body.jsx create mode 100644 src/components/Header/components/BranchStep/Header.jsx create mode 100644 src/components/Header/components/BranchStep/Secondary.jsx create mode 100644 src/components/Header/components/shared/InfoContainer.jsx create mode 100644 src/components/Header/components/shared/Marker.jsx create mode 100644 src/components/Header/components/shared/Properties.jsx create mode 100644 src/components/Header/components/shared/Property.jsx create mode 100644 src/components/Header/components/shared/PropertyValue.jsx create mode 100644 src/components/Header/components/shared/Title.jsx create mode 100644 src/redux/modules/branches.js diff --git a/src/components/Header/components/BranchStep/Body.jsx b/src/components/Header/components/BranchStep/Body.jsx new file mode 100644 index 0000000..a171185 --- /dev/null +++ b/src/components/Header/components/BranchStep/Body.jsx @@ -0,0 +1,185 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +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 { 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'; +import debounce from 'lodash.debounce'; +import SourceBranchIcon from 'mdi-react/SourceBranchIcon'; +import { useDispatch, useSelector } from 'react-redux'; +import { useDebounce } from 'react-use'; +import { FixedSizeList } from 'react-window'; +import styled from 'styled-components'; +import Marker from '../shared/Marker'; +import Secondary from './Secondary'; + +const Container = styled.div` + min-height: 100px; + display: flex; + flex-direction: column; + padding: 10px; + box-sizing: border-box; + width: 100%; +`; + +const ListItems = styled(FixedSizeList)` + height: auto !important; + max-height: 300px; + + ${ScrollBarMixin} +`; + +const ListItem = styled(ListItemOrigin)` + cursor: pointer; + transition: background 0.3s; + &:hover { + background: rgba(255, 255, 255, 0.1); + } + &:active { + background: rgba(255, 255, 255, 0.2); + } +`; + +const NotData = styled(({ className }) => ( +
+
Branches not found
+
+))` + display: flex; + justify-content: center; + align-items: center; +`; + +const Primary = styled.span` + display: flex; + align-items: center; +`; + +const bySearch = (search) => (item) => { + return String(item?.name).toLowerCase().includes(search.toLowerCase()); +}; + +const Body = () => { + const dispatch = useDispatch(); + const inputRef = useRef(); + const [search, setSearch] = useState(''); + const { isFetching, items } = useSelector(slice.selectors.getState); + const [bodyOpen, setBodyOpen] = useUIProperty('bodyOpen'); + const [filtered, setFiltered] = useState(items); + const { selected: repository } = useSelector(repositoriesSlice.selectors.getState); + const { default_branch } = repository || {}; + + const changeSearch = useMemo( + () => debounce( + (value) => setSearch(value), + 300, + ), + [], + ); + + const onChange = useCallback( + (event) => { + changeSearch(event.target.value); + }, + [changeSearch], + ); + + const onClick = useCallback( + (item) => () => { + dispatch(slice.actions.setSelected(item)); + setBodyOpen(false); + }, + [setBodyOpen, dispatch], + ); + + const ListHeader = useMemo( + () => ( + + Branches: {filtered.length || 0} of {items.length || 0} + + ), + [filtered.length, items.length], + ); + + const Item = useCallback( + ({ index, style }) => { + const item = filtered[index]; + const isDefault = default_branch === item.name; + + return ( + + + + + + + + {isDefault && default} + + + )} + secondary={} + /> + + ); + }, + [filtered, default_branch, onClick, search], + ); + + useEffect( + () => { + setFiltered(search ? items.filter(bySearch(search)) : items); + }, + [items, search, dispatch], + ); + + useDebounce( + () => { + if (inputRef.current && bodyOpen) { + inputRef.current.querySelector('input').focus(); + } + }, + 100, + [bodyOpen], + ); + + return ( + + + + + {!filtered.length && } + + {Item} + + + + + ); +}; + +export default Body; diff --git a/src/components/Header/components/BranchStep/Header.jsx b/src/components/Header/components/BranchStep/Header.jsx new file mode 100644 index 0000000..7997501 --- /dev/null +++ b/src/components/Header/components/BranchStep/Header.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import slice from '@/redux/modules/branches'; +import HistoryIcon from 'mdi-react/HistoryIcon'; +import SourceBranchIcon from 'mdi-react/SourceBranchIcon'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; +import Container from '../shared/HeaderContainer'; +import InfoContainer from '../shared/InfoContainer'; +import PropertiesOrigin from '../shared/Properties'; +import Property from '../shared/Property'; +import PropertyValue from '../shared/PropertyValue'; +import Title from '../shared/Title'; + +const Properties = styled(PropertiesOrigin)` + font-size: 1em; +`; + +const Header = (props) => { + const { selected } = useSelector(slice.selectors.getState); + const { name, commits } = selected || {}; + + return ( + + + + {!selected &&
Choose a branch
} + {selected && ( + + + {name} + + + + + {commits} + + + + )} +
+
+ ); +}; + +export default Header; diff --git a/src/components/Header/components/BranchStep/Secondary.jsx b/src/components/Header/components/BranchStep/Secondary.jsx new file mode 100644 index 0000000..f8b1aec --- /dev/null +++ b/src/components/Header/components/BranchStep/Secondary.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import HistoryIcon from 'mdi-react/HistoryIcon'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import Properties from '../shared/Properties'; +import Property from '../shared/Property'; +import PropertyValue from '../shared/PropertyValue'; + +const SecondaryContainer = styled.span` + display: flex; + flex-direction: column; +`; + +const Secondary = ({ item }) => ( + + + + + + {item.commits} + + + + +); + +Secondary.propTypes = { + item: PropTypes.shape().isRequired, +}; + +export default Secondary; diff --git a/src/components/Header/components/RepositoryStep/Body.jsx b/src/components/Header/components/RepositoryStep/Body.jsx index 0697544..d37f372 100644 --- a/src/components/Header/components/RepositoryStep/Body.jsx +++ b/src/components/Header/components/RepositoryStep/Body.jsx @@ -1,22 +1,25 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 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 Highlight from '@/shared/components/Highlight'; +import LoadingOverlay from '@/shared/components/LoadingOverlay'; +import { ScrollBarMixin } from '@/shared/components/ScrollBar'; +import { useUIProperty } from '@/shared/hooks'; 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"; +} from '@material-ui/core'; +import List from '@material-ui/core/List'; +import ListItemText from '@material-ui/core/ListItemText'; import debounce from 'lodash.debounce'; -import SourceRepositoryIcon from "mdi-react/SourceRepositoryIcon"; -import { useDispatch, useSelector } from "react-redux"; -import { useDebounce } from "react-use"; +import BookIcon from 'mdi-react/BookIcon'; +import BookLockIcon from 'mdi-react/BookLockIcon'; +import SourceRepositoryIcon from 'mdi-react/SourceRepositoryIcon'; +import { useDispatch, useSelector } from 'react-redux'; +import { useDebounce } from 'react-use'; import { FixedSizeList } from 'react-window'; -import styled from "styled-components"; -import Secondary from "./Secondary"; +import styled from 'styled-components'; +import Marker from '../shared/Marker'; +import Secondary from './Secondary'; const Container = styled.div` min-height: 100px; @@ -28,7 +31,8 @@ const Container = styled.div` `; const ListItems = styled(FixedSizeList)` - height: 300px !important; + height: auto !important; + max-height: 300px; ${ScrollBarMixin} `; @@ -54,8 +58,13 @@ const NotData = styled(({ className }) => ( align-items: center; `; +const Primary = styled.span` + display: flex; + align-items: center; +`; + const bySearch = (search) => (item) => { - return item?.name?.includes(search); + return String(item?.name).toLowerCase().includes(search.toLowerCase()); }; const Body = () => { @@ -83,7 +92,7 @@ const Body = () => { const onClick = useCallback( (item) => () => { - dispatch(slice.actions.setRepository(item)); + dispatch(slice.actions.setSelected(item)); setBodyOpen(false); }, [setBodyOpen, dispatch], @@ -101,6 +110,7 @@ const Body = () => { const Item = useCallback( ({ index, style }) => { const item = filtered[index]; + const title = (item.private ? 'Private' : item.fork ? 'Fork' : 'Public'); return ( { key={item.name} onClick={onClick(item)} style={style} + title={`${title} | ${item.name}`} > - + {item.private ? : ( + item.fork ? : + )} } + primary={( + + {item.private && private} + {item.fork && fork} + + + )} secondary={} /> diff --git a/src/components/Header/components/RepositoryStep/Header.jsx b/src/components/Header/components/RepositoryStep/Header.jsx index 0898649..a57e1f0 100644 --- a/src/components/Header/components/RepositoryStep/Header.jsx +++ b/src/components/Header/components/RepositoryStep/Header.jsx @@ -1,34 +1,43 @@ import React from 'react'; import slice from '@/redux/modules/repositories'; -import SourceRepositoryIcon from "mdi-react/SourceRepositoryIcon"; -import { useSelector } from "react-redux"; -import styled from "styled-components"; +import Link from '@material-ui/core/Link'; +import BookIcon from 'mdi-react/BookIcon'; +import BookLockIcon from 'mdi-react/BookLockIcon'; +import LinkIcon from 'mdi-react/LinkIcon'; +import SourceRepositoryIcon from 'mdi-react/SourceRepositoryIcon'; +import { useSelector } from 'react-redux'; import Container from '../shared/HeaderContainer'; +import InfoContainer from '../shared/InfoContainer'; +import Title from '../shared/Title'; -const InfoContainer = styled.div` - display: flex; - flex-direction: column; - margin-left: 5px; -`; - -const Title = styled.div` - font-weight: bold; - display: flex; - flex-wrap: nowrap; - white-space: nowrap; -`; +const onClick = (event) => event.stopPropagation(); const Header = (props) => { const { selected } = useSelector(slice.selectors.getState); - const { name } = selected || {}; + const { name, private: locked, fork, html_url } = selected || {}; return ( - + {locked ? : ( + fork + ? + : + )} - {!selected &&
Choice a repository
} + {!selected &&
Choose a repository
} {selected && ( - {name} + + <Link + target="_blank" + onClick={onClick} + href={html_url} + title="On github" + style={{ verticalAlign: 'middle', marginRight: '3px' }} + > + <LinkIcon size={16} /> + </Link> + {name} + )}
diff --git a/src/components/Header/components/RepositoryStep/Secondary.jsx b/src/components/Header/components/RepositoryStep/Secondary.jsx index 330c019..cf32839 100644 --- a/src/components/Header/components/RepositoryStep/Secondary.jsx +++ b/src/components/Header/components/RepositoryStep/Secondary.jsx @@ -1,13 +1,16 @@ -import React from "react"; -import GithubEmoji from "@/shared/components/GithubEmoje"; -import capitalize from "@material-ui/core/utils/capitalize"; -import AlertCircleOutlineIcon from "mdi-react/AlertCircleOutlineIcon"; -import CodeTagsIcon from "mdi-react/CodeTagsIcon"; -import EyeOutlineIcon from "mdi-react/EyeOutlineIcon"; -import SourceForkIcon from "mdi-react/SourceForkIcon"; -import StarIcon from "mdi-react/StarIcon"; +import React from 'react'; +import GithubEmoji from '@/shared/components/GithubEmoje'; +import capitalize from '@material-ui/core/utils/capitalize'; +import AlertCircleOutlineIcon from 'mdi-react/AlertCircleOutlineIcon'; +import CodeTagsIcon from 'mdi-react/CodeTagsIcon'; +import EyeOutlineIcon from 'mdi-react/EyeOutlineIcon'; +import SourceForkIcon from 'mdi-react/SourceForkIcon'; +import StarIcon from 'mdi-react/StarIcon'; import PropTypes from 'prop-types'; -import styled from "styled-components"; +import styled from 'styled-components'; +import Properties from '../shared/Properties'; +import Property from '../shared/Property'; +import PropertyValue from '../shared/PropertyValue'; const SecondaryContainer = styled.span` display: flex; @@ -20,35 +23,6 @@ const Description = styled.span` white-space: nowrap; `; -const Properties = styled.span` - display: flex; - align-items: center; - font-size: 0.8em; -`; - -const Property = styled.span` - display: flex; - align-items: center; - margin-left: 5px; - padding-right: 5px; - border-right: 1px solid rgba(255, 255, 255, 0.2); - - &:first-child { - margin-left: 0; - } - - &:last-child { - border-right: 0; - padding-right: 0; - } - - color: #fff; -`; - -const PropertyValue = styled.span` - margin-left: 5px; -`; - const Secondary = ({ item }) => ( {item.description && ( diff --git a/src/components/Header/components/UserStep/Body.jsx b/src/components/Header/components/UserStep/Body.jsx index 29cf83a..05b4353 100644 --- a/src/components/Header/components/UserStep/Body.jsx +++ b/src/components/Header/components/UserStep/Body.jsx @@ -1,19 +1,19 @@ import React, { useCallback, useRef, useState } from 'react'; 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 Highlight from '@/shared/components/Highlight'; +import LoadingOverlay from '@/shared/components/LoadingOverlay'; +import ScrollBar from '@/shared/components/ScrollBar'; +import { useUIProperty } from '@/shared/hooks'; 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"; -import { useDispatch, useSelector } from "react-redux"; -import { useDebounce } from "react-use"; -import styled from "styled-components"; +} from '@material-ui/core'; +import List from '@material-ui/core/List'; +import ListItemText from '@material-ui/core/ListItemText'; +import { useDispatch, useSelector } from 'react-redux'; +import { useDebounce } from 'react-use'; +import styled from 'styled-components'; const Container = styled.div` min-height: 100px; @@ -80,7 +80,7 @@ const Body = () => { const onClick = useCallback( (user) => () => { - dispatch(slice.actions.setProfile(user)); + dispatch(slice.actions.setSelected(user)); setBodyOpen(false); dispatch(slice.actions.fetchProfile(user.login)); }, diff --git a/src/components/Header/components/UserStep/Header.jsx b/src/components/Header/components/UserStep/Header.jsx index a6ce2b8..e21f219 100644 --- a/src/components/Header/components/UserStep/Header.jsx +++ b/src/components/Header/components/UserStep/Header.jsx @@ -1,26 +1,18 @@ import React from 'react'; import slice from '@/redux/modules/profiles'; -import { Avatar } from "@material-ui/core"; -import LinkOrigin from "@material-ui/core/Link"; -import GithubIcon from "mdi-react/GithubIcon"; -import LinkVariantIcon from "mdi-react/LinkVariantIcon"; -import SourceRepositoriesIcon from "mdi-react/SourceRepositoriesIcon"; -import { useSelector } from "react-redux"; -import styled from "styled-components"; +import { Avatar } from '@material-ui/core'; +import LinkOrigin from '@material-ui/core/Link'; +import BookMultipleIcon from 'mdi-react/BookMultipleIcon'; +import GithubIcon from 'mdi-react/GithubIcon'; +import LinkVariantIcon from 'mdi-react/LinkVariantIcon'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; import HeaderContainer from '../shared/HeaderContainer'; - -const InfoContainer = styled.div` - display: flex; - flex-direction: column; - margin-left: 5px; -`; - -const Title = styled.div` - font-weight: bold; - display: flex; - flex-wrap: nowrap; - white-space: nowrap; -`; +import InfoContainer from '../shared/InfoContainer'; +import PropertiesOrigin from '../shared/Properties'; +import Property from '../shared/Property'; +import PropertyValue from '../shared/PropertyValue'; +import Title from '../shared/Title'; const Link = styled(LinkOrigin)` display: flex; @@ -28,29 +20,11 @@ const Link = styled(LinkOrigin)` flex-wrap: nowrap; `; -const Properties = styled.div` - display: flex; - align-items: center; +const Properties = styled(PropertiesOrigin)` + font-size: 1em; `; -const Property = styled.div` - display: flex; - align-items: center; - padding-right: 5px; - margin-left: 5px; - border-right: 1px solid; - &:first-child { - margin-left: 0; - } - &:last-child { - border-right: 0; - padding-right: 0; - } -`; - -const PropertyValue = styled.div` - margin-left: 2px; -`; +const onClick = (event) => event.stopPropagation(); const Header = (props) => { const { selected } = useSelector(slice.selectors.getState); @@ -66,25 +40,37 @@ const Header = (props) => { {!selected &&
Find a user
} {selected && ( - {name || login} + {name || login} - {login} + + {login} + - + {public_repos} {blog && ( - blog + + site + )} diff --git a/src/components/Header/components/shared/HeaderContainer.jsx b/src/components/Header/components/shared/HeaderContainer.jsx index dddb211..2899fcf 100644 --- a/src/components/Header/components/shared/HeaderContainer.jsx +++ b/src/components/Header/components/shared/HeaderContainer.jsx @@ -1,4 +1,4 @@ -import styled from "styled-components"; +import styled from 'styled-components'; const HeaderContainer = styled.button` display: flex; @@ -12,6 +12,7 @@ const HeaderContainer = styled.button` border-radius: 0; color: inherit; cursor: pointer; + overflow: hidden; transition: background 0.3s, opacity 0.5s; outline: 0; diff --git a/src/components/Header/components/shared/InfoContainer.jsx b/src/components/Header/components/shared/InfoContainer.jsx new file mode 100644 index 0000000..3da82fb --- /dev/null +++ b/src/components/Header/components/shared/InfoContainer.jsx @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +const InfoContainer = styled.div` + display: flex; + flex-direction: column; + margin-left: 5px; + overflow: hidden; + flex: 1 1 0; + text-align: left; +`; + +export default InfoContainer; diff --git a/src/components/Header/components/shared/Marker.jsx b/src/components/Header/components/shared/Marker.jsx new file mode 100644 index 0000000..d75e293 --- /dev/null +++ b/src/components/Header/components/shared/Marker.jsx @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +const Marker = styled.span` + color: rgba(255, 255, 255, 0.5); + padding: 3px 5px; + line-height: 1em; + border: 1px solid; + text-transform: uppercase; + background: rgba(0, 0, 0, 0.1); + margin-right: 5px; + font-size: 0.6em; + border-radius: 5px; +`; + +export default Marker; diff --git a/src/components/Header/components/shared/Properties.jsx b/src/components/Header/components/shared/Properties.jsx new file mode 100644 index 0000000..57323ba --- /dev/null +++ b/src/components/Header/components/shared/Properties.jsx @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +const Properties = styled.span` + display: flex; + align-items: center; + font-size: 0.8em; +`; + +export default Properties; diff --git a/src/components/Header/components/shared/Property.jsx b/src/components/Header/components/shared/Property.jsx new file mode 100644 index 0000000..9b0ee35 --- /dev/null +++ b/src/components/Header/components/shared/Property.jsx @@ -0,0 +1,22 @@ +import styled from 'styled-components'; + +const Property = styled.span` + display: flex; + align-items: center; + margin-left: 5px; + padding-right: 5px; + border-right: 1px solid rgba(255, 255, 255, 0.2); + + &:first-child { + margin-left: 0; + } + + &:last-child { + border-right: 0; + padding-right: 0; + } + + color: #fff; +`; + +export default Property; diff --git a/src/components/Header/components/shared/PropertyValue.jsx b/src/components/Header/components/shared/PropertyValue.jsx new file mode 100644 index 0000000..f3ce24c --- /dev/null +++ b/src/components/Header/components/shared/PropertyValue.jsx @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +const PropertyValue = styled.span` + margin-left: 2px; +`; + +export default PropertyValue; diff --git a/src/components/Header/components/shared/Title.jsx b/src/components/Header/components/shared/Title.jsx new file mode 100644 index 0000000..7ae1ccb --- /dev/null +++ b/src/components/Header/components/shared/Title.jsx @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +const Title = styled.div` + font-weight: bold; + flex-wrap: nowrap; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 100%; +`; + +export default Title; diff --git a/src/components/StageController/index.js b/src/components/StageController/index.js index 929a1e0..6c94ba4 100644 --- a/src/components/StageController/index.js +++ b/src/components/StageController/index.js @@ -1,12 +1,20 @@ -import { useEffect } from "react"; -import emojisSlice from "@/redux/modules/emojis"; -import profilesSlice from "@/redux/modules/profiles"; -import repositoriesSlice from "@/redux/modules/repositories"; -import { useDispatch, useSelector } from "react-redux"; +import { useEffect } from 'react'; +import branchesSlice from '@/redux/modules/branches'; +import emojisSlice from '@/redux/modules/emojis'; +import profilesSlice from '@/redux/modules/profiles'; +import repositoriesSlice from '@/redux/modules/repositories'; +import { useDispatch, useSelector } from 'react-redux'; export default () => { 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 owner = profile?.login; + const amount = profile?.public_repos; + const repo = repository?.name; + const defaultBranch = repository?.default_branch; useEffect( () => { @@ -22,17 +30,50 @@ export default () => { [dispatch], ); - const login = profile?.login; - const amount = profile?.public_repos; useEffect( () => { - dispatch(repositoriesSlice.actions.fetchRepositories({ login, amount })); + dispatch(repositoriesSlice.actions.clear()); + dispatch(repositoriesSlice.actions.fetchRepositories({ owner, amount })); return () => { dispatch(repositoriesSlice.actions.cancel()); }; }, - [dispatch, login, amount], + [dispatch, owner, amount], + ); + + useEffect( + () => { + dispatch(branchesSlice.actions.clear()); + if (!repo) { + return undefined; + } + + dispatch(branchesSlice.actions.fetch({ + owner, + repo, + })); + + return () => { + dispatch(branchesSlice.actions.cancel()); + }; + }, + [dispatch, owner, repo], + ); + + useEffect( + () => { + if (!branches.length || branch || !defaultBranch) { + return; + } + + const item = branches.find(({ name }) => name === defaultBranch); + + if (item) { + dispatch(branchesSlice.actions.setSelected(item)); + } + }, + [branch, branches, defaultBranch, dispatch], ); return null; diff --git a/src/models/StageTypes.js b/src/models/StageTypes.js index cb055d6..91df26b 100644 --- a/src/models/StageTypes.js +++ b/src/models/StageTypes.js @@ -1,5 +1,6 @@ export const StageTypes = { user: 'user', repository: 'repository', + branch: 'branch', show: 'show', }; diff --git a/src/redux/modules/branches.js b/src/redux/modules/branches.js new file mode 100644 index 0000000..ceb58e7 --- /dev/null +++ b/src/redux/modules/branches.js @@ -0,0 +1,88 @@ +import { getBranches } from '@/redux/api/github'; +import slice from '@/redux/modules/progress'; +import { createSlice, startFetching, stopFetching } from '@/redux/utils'; +import { call, cancelled, delay, put } from 'redux-saga/effects'; + +const initialState = { + selected: null, + items: [], + error: null, +}; + +export default createSlice({ + name: 'branches', + initialState, + reducers: { + fetch: startFetching, + fetchSuccess: (state, { payload: { data, append } }) => { + const fixed = Array.isArray(data) ? data : []; + state.items = append ? [ + ...state.items, + ...fixed, + ] : fixed; + }, + + setSelected: (state, { payload }) => { + state.selected = payload; + }, + + stopFetching, + + fail: (state, { payload }) => { + stopFetching(state); + state.error = payload; + }, + }, + + sagas: (actions) => ({ + [actions.fetch]: { + * saga({ payload: { owner, repo } }) { + try { + if (!owner || !repo) { + yield put(actions.stopFetching()); + return; + } + + yield put(slice.actions.change({ + max: 100, + value: 0, + valueBuffer: 0, + show: true, + })); + + let next = true; + let page = 0; + + while (next) { + const { data, pageInfo } = yield call(getBranches, { + owner, + repo, + perPage: 100, + page: page, + }); + + yield put(actions.fetchSuccess({ data, append: page > 0 })); + + page = pageInfo.nextPage; + next = pageInfo.hasNextPage; + + yield put(slice.actions.change({ max: pageInfo.total || 100 })); + yield put(slice.actions.incValue(data.length)); + yield put(slice.actions.incValueBuffer(data.length + 100)); + } + + yield put(actions.stopFetching()); + } catch (error) { + if (yield cancelled()) { + yield put(actions.stopFetching); + return; + } + yield put(actions.fail(error)); + } finally { + yield delay(500); + yield put(slice.actions.toggle(false)); + } + }, + }, + }), +}); diff --git a/src/redux/modules/index.js b/src/redux/modules/index.js index 020ee5e..8b85dc3 100644 --- a/src/redux/modules/index.js +++ b/src/redux/modules/index.js @@ -1,8 +1,9 @@ import { all } from 'redux-saga/effects'; -import emojis from "./emojis"; -import profiles from "./profiles"; -import progress from "./progress"; -import repositories from "./repositories"; +import branches from './branches'; +import emojis from './emojis'; +import profiles from './profiles'; +import progress from './progress'; +import repositories from './repositories'; import ui from './ui'; // Put modules that have their reducers nested in other (root) reducers here @@ -10,6 +11,7 @@ const nestedSlices = []; // Put modules whose reducers you want in the root tree in this array. const rootSlices = [ + branches, emojis, profiles, ui,