From 05e94fd5bd0e810829f892e58b10c3b6422536d4 Mon Sep 17 00:00:00 2001 From: jeonyeonkyu Date: Thu, 17 Jun 2021 23:26:15 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=ED=83=ADUI=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=9D=BC=EB=94=94=EC=98=A4=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20->=20=EC=B2=B4=ED=81=AC=EB=B0=95=EC=8A=A4=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20=20?= =?UTF-8?q?#115?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fe/client/src/Pages/IssueListPage.tsx | 15 ++- fe/client/src/components/common/FilterTab.tsx | 104 +++++++++++------- fe/client/src/components/common/Label.tsx | 2 +- .../src/components/common/atoms/filterAtom.ts | 31 ++++-- .../components/common/atoms/issueListAtom.ts | 8 ++ .../src/components/createIssue/FilterItem.tsx | 69 ++++++++++++ fe/client/src/components/createIssue/Tabs.tsx | 8 +- .../src/components/issueList/FilterItem.tsx | 7 +- .../src/components/issueList/HeadContent.tsx | 1 + .../src/components/issueList/ListHeader.tsx | 17 ++- fe/client/src/utils/commonUtils.ts | 4 - fe/client/src/utils/functionalUtils.ts | 11 ++ 12 files changed, 204 insertions(+), 73 deletions(-) create mode 100644 fe/client/src/components/common/atoms/issueListAtom.ts create mode 100644 fe/client/src/components/createIssue/FilterItem.tsx create mode 100644 fe/client/src/utils/functionalUtils.ts diff --git a/fe/client/src/Pages/IssueListPage.tsx b/fe/client/src/Pages/IssueListPage.tsx index 26889f9a4..2547a7b36 100644 --- a/fe/client/src/Pages/IssueListPage.tsx +++ b/fe/client/src/Pages/IssueListPage.tsx @@ -1,18 +1,25 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useEffect } from 'react' import HeadContent from '@components/issueList/HeadContent'; import { ListWrapper } from '@components/common/baseStyle/baseStyle'; import ListHeader from '@components/issueList/ListHeader'; import ListItem from '@components/issueList/ListItem'; +import { issueListItemAtom } from '@components/common/atoms/issueListAtom'; +import { filterAtom, FilterBooleanType } from '@/components/common/atoms/filterAtom'; +import { IssueListItemType } from '@components/common/types/APIType'; import API from '@/utils/API'; import useFetch, { AsyncState } from '@/utils/hook/useFetch'; -import { IssueListItemType } from '@components/common/types/APIType'; -import { filterAtom, FilterBooleanType } from '@/components/common/atoms/filterAtom'; import { useRecoilState } from '@/utils/myRecoil/useRecoilState'; const IssueListPage = () => { const [issueItems] = useFetch(API.get.issues); const { data, loading, error }: AsyncState = issueItems; const [, setFilterModalState] = useRecoilState(filterAtom); + const [, setFilteredList] = useRecoilState(issueListItemAtom) + + useEffect(() => { + if (!data) return; + setFilteredList(data); + }, [data]); const handleClickShowFilterModal = useCallback((title: string) => () => { setFilterModalState((filterModalState: FilterBooleanType) => ({ ...filterModalState, [title]: true })); @@ -23,7 +30,7 @@ const IssueListPage = () => { { - data && <> + data && <> {data.map((issueItem: IssueListItemType) => { return diff --git a/fe/client/src/components/common/FilterTab.tsx b/fe/client/src/components/common/FilterTab.tsx index 90f31b915..863a42984 100644 --- a/fe/client/src/components/common/FilterTab.tsx +++ b/fe/client/src/components/common/FilterTab.tsx @@ -1,11 +1,16 @@ import React, { useCallback, useEffect } from 'react' import styled from 'styled-components'; import { useRecoilState } from '@/utils/myRecoil/useRecoilState'; -import { filterAtom, filterDefaultCheckerAtom, FilterStringType } from '@components/common/atoms/filterAtom'; +import { + filterAtom, filterCheckboxListAtom, + FilterCheckboxListType, + filterRadioButtonListAtom, FilterRadioButtonListType +} from '@components/common/atoms/filterAtom'; type FilterTabType = { header: string; filterList: Array; + inputType: string; } const filterHeaderNames: { [key: string]: string } = { @@ -16,9 +21,10 @@ const filterHeaderNames: { [key: string]: string } = { writer: '작성자' } -const FilterTab = ({ header, filterList }: FilterTabType) => { +const FilterTab = ({ header, filterList, inputType }: FilterTabType) => { const [filterModalState, setFilterModalState] = useRecoilState(filterAtom); - const [defaultCheckerState, setDefaultCheckerState] = useRecoilState(filterDefaultCheckerAtom); + const switchAtom = inputType === 'radio' ? filterRadioButtonListAtom : filterCheckboxListAtom; + const [defaultCheckerState, setDefaultCheckerState] = useRecoilState(switchAtom); const handleClickHideFilterModal = useCallback((event: MouseEvent) => { const targetList = (event.target as HTMLElement); @@ -33,15 +39,29 @@ const FilterTab = ({ header, filterList }: FilterTabType) => { }) }, []); - const handleChangeDefaultChecker = useCallback((listItem: string) => () => { - setDefaultCheckerState((state: FilterStringType) => ({ ...state, [header]: listItem })); + const handleChangeDefaultChecker = useCallback((listItem) => () => { + const { name, ...info } = listItem; + if (inputType === 'radio') { + setDefaultCheckerState((state: FilterRadioButtonListType) => ({ ...state, [header]: { name, info } })); + } else { + setDefaultCheckerState((state: FilterCheckboxListType) => { + const presentIndex = state[header].findIndex((item: any) => item.name === name); + if (presentIndex !== -1) { + return { + ...state, + [header]: [...state[header].slice(0, presentIndex), ...state[header].slice(presentIndex + 1)] + } + } + return { ...state, [header]: [{ name, info }, ...state[header]] } + }); + } }, []); useEffect(() => { if (Object.values(filterModalState).every(v => !v)) return; document.addEventListener('click', handleClickHideFilterModal); return () => { - document.removeEventListener('click', handleClickHideFilterModal) + document.removeEventListener('click', handleClickHideFilterModal); }; }, [filterModalState]); @@ -60,13 +80,17 @@ const FilterTab = ({ header, filterList }: FilterTabType) => { isShow={filterModalState[header]} {...{ header }} > {filterHeaderNames[header]} 필터 - {filterList.map((listItem, idx) => { + {filterList.map((listItem: any, idx) => { + return (
- - {listItem} + checked={inputType === 'radio' ? + defaultCheckerState[header].name === listItem.name : + defaultCheckerState[header] + .findIndex((item: any) => item.name === listItem.name) !== -1} /> + {listItem.name}
) })} @@ -75,47 +99,47 @@ const FilterTab = ({ header, filterList }: FilterTabType) => { } const FilterTabWrapper = styled.div<{ isShow: boolean, header: string }>` - position: absolute; - width: 200px; - top:${({ header }) => header === 'issue' ? '35px' : '30px'}; - left: ${({ header }) => header === 'issue' && 0}; - right: ${({ header }) => header !== 'issue' && '-8px'}; - z-index: 1; - border:1px solid #d9dbe9; - background: #FEFEFE; - border-radius: 11px; - box-shadow:0px 1px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); - visibility:${({ isShow }) => isShow ? 'visible' : 'hidden'}; - > div{ - padding: .5rem; - } - div + div{ - border-top:1px solid #d9dbe9; - } - &:hover{ - cursor:default; - } + position: absolute; + width: 200px; + top:${({ header }) => header === 'issue' ? '35px' : '30px'}; + left: ${({ header }) => header === 'issue' && 0}; + right: ${({ header }) => header !== 'issue' && '-8px'}; + z-index: 1; + border:1px solid #d9dbe9; + background: #FEFEFE; + border-radius: 11px; + box-shadow:0px 1px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); + visibility:${({ isShow }) => isShow ? 'visible' : 'hidden'}; + > div{ + padding: .5rem; + } + div + div{ + border-top:1px solid #d9dbe9; + } + &:hover{ + cursor:default; + } ` const FilterTabHeader = styled.div` - background:#F7F7FC; - border-radius: 11px 11px 0 0; + background:#F7F7FC; + border-radius: 11px 11px 0 0; `; const FilterLabel = styled.label` - display:flex; - justify-content: space-between; - flex-direction: row-reverse; + display:flex; + justify-content: space-between; + flex-direction: row-reverse; & > span{ - color: #A3A1B0; - font-size: 12px; + color: #A3A1B0; + font-size: 12px; } & > :checked + span{ - color: #000; + color: #000; } - &:hover{ - cursor:pointer; + &:hover{ + cursor:pointer; } `; diff --git a/fe/client/src/components/common/Label.tsx b/fe/client/src/components/common/Label.tsx index b4bc5ea98..e5b1b031d 100644 --- a/fe/client/src/components/common/Label.tsx +++ b/fe/client/src/components/common/Label.tsx @@ -1,7 +1,7 @@ import React from 'react' import styled from 'styled-components'; import { getHexToRGB, getTextColor } from '@/utils/serviceUtils'; -import { pipe } from '@/utils/commonUtils'; +import { pipe } from '@/utils/functionalUtils'; type LabelType = { name: string; diff --git a/fe/client/src/components/common/atoms/filterAtom.ts b/fe/client/src/components/common/atoms/filterAtom.ts index 059809b6b..b07ecb4a0 100644 --- a/fe/client/src/components/common/atoms/filterAtom.ts +++ b/fe/client/src/components/common/atoms/filterAtom.ts @@ -20,22 +20,29 @@ export const filterAtom = atom({ } }) -export type FilterStringType = { - issue: string; - manager: string; - label: string; - milestone: string; - writer: string; +export type FilterRadioButtonListType = { + [key: string]: { name: string, info: any } } -export const filterDefaultCheckerAtom = atom({ +export const filterRadioButtonListAtom = atom({ key: 'filterDefaultCheckerAtom', default: { - issue: '', - manager: '', - label: '', - milestone: '', - writer:'' + issue: { name: '', info: {} }, + manager: { name: '', info: {} }, + label: { name: '', info: {} }, + milestone: { name: '', info: {} }, + writer: { name: '', info: {} } } }) +export type FilterCheckboxListType = { + manager: string[]; + label: string[]; + milestone: string[]; + [key: string]: string[]; +} + +export const filterCheckboxListAtom = atom({ + key: 'filterCheckboxListAtom', + default: { manager: [], label: [], milestone: [] } +}); \ No newline at end of file diff --git a/fe/client/src/components/common/atoms/issueListAtom.ts b/fe/client/src/components/common/atoms/issueListAtom.ts new file mode 100644 index 000000000..b5f439221 --- /dev/null +++ b/fe/client/src/components/common/atoms/issueListAtom.ts @@ -0,0 +1,8 @@ +import atom from '@/utils/myRecoil/atom'; +import { IssueListItemType } from '@components/common/types/APIType'; + +export const issueListItemAtom = atom({ + key: 'issueListItemAtom', + default: [] +}); + diff --git a/fe/client/src/components/createIssue/FilterItem.tsx b/fe/client/src/components/createIssue/FilterItem.tsx new file mode 100644 index 000000000..aa51b0b7f --- /dev/null +++ b/fe/client/src/components/createIssue/FilterItem.tsx @@ -0,0 +1,69 @@ +import React, { useState, useEffect } from 'react' +import styled from 'styled-components'; +import FilterTab from '@components/common/FilterTab'; +import ProgressBar from '@components/common/ProgressBar'; +import Label from '@components/common/Label'; +import { filterCheckboxListAtom } from '@components/common/atoms/filterAtom'; +import { useRecoilState } from '@/utils/myRecoil/useRecoilState'; +import useFetch, { AsyncState } from '@/utils/hook/useFetch'; +import API from '@/utils/API'; + +const filterNames: { [key: string]: { apiName: string; name: string } } = { + manager: { apiName: 'users', name: '담당자' }, + label: { apiName: 'labels', name: '레이블' }, + milestone: { apiName: 'milestones', name: '마일스톤' }, +} + +const FilterItem = ({ header }: { header: string }) => { + const { apiName } = filterNames[header]; + const [issueList] = useFetch(API.get[apiName]); + const { data }: AsyncState = issueList; + const [checkedItems] = useRecoilState(filterCheckboxListAtom); + + return ( + <> + + + Oni + + + + + + + + 마스터즈 코스 + + + {data && } + + ) +} + +const CheckedItemWrapper = styled.div` + display: flex; + margin: 0 32px 16px 32px; + color: #6E7191; + place-items: center; +`; + +const ImageTag = styled.img` + width: 44px; + height: 44px; + margin-right: 8px; + border-radius: 50%; +`; + +const ProgressBarWrapper = styled.div` + margin: 0 32px 16px 32px; + > span { + display:inline-block; + margin-top: 8px; + } +`; + +export default React.memo(FilterItem); diff --git a/fe/client/src/components/createIssue/Tabs.tsx b/fe/client/src/components/createIssue/Tabs.tsx index 808bf40c4..0f38214a4 100644 --- a/fe/client/src/components/createIssue/Tabs.tsx +++ b/fe/client/src/components/createIssue/Tabs.tsx @@ -1,9 +1,9 @@ import React, { useCallback } from 'react' import styled from 'styled-components'; -import FilterTab from '@components/common/FilterTab'; import { filterAtom, FilterBooleanType } from '@components/common/atoms/filterAtom'; import { useRecoilState } from '@/utils/myRecoil/useRecoilState'; import PlusIcon from '@/Icons/Plus.svg'; +import FilterItem from './FilterItem'; const tabItems = [ { name: '담당자', title: 'manager' }, @@ -11,10 +11,8 @@ const tabItems = [ { name: '마일스톤', title: 'milestone' } ]; -const mockup = ['비모', '비모2', '비모3']; const Tabs = () => { const [, setFilterModalState] = useRecoilState(filterAtom); - const handleClickShowFilterModal = useCallback((title: string) => () => { setFilterModalState((filterModalState: FilterBooleanType) => ({ ...filterModalState, [title]: true })); }, []); @@ -28,9 +26,7 @@ const Tabs = () => { {name} - + ) })} diff --git a/fe/client/src/components/issueList/FilterItem.tsx b/fe/client/src/components/issueList/FilterItem.tsx index d9cb3d8b4..198b6aa73 100644 --- a/fe/client/src/components/issueList/FilterItem.tsx +++ b/fe/client/src/components/issueList/FilterItem.tsx @@ -19,8 +19,8 @@ type FilterItemType = { const FilterItem = ({ title, handleClickShowFilterModal }: FilterItemType) => { const { apiName } = filterNames[title]; - const [users] = useFetch(API.get[apiName]); - const { data }: AsyncState = users; + const [filterList] = useFetch(API.get[apiName]); + const { data }: AsyncState = filterList; return ( @@ -30,8 +30,9 @@ const FilterItem = ({ title, handleClickShowFilterModal }: FilterItemType) => { {data && name)} /> + filterList={data} /> } ) diff --git a/fe/client/src/components/issueList/HeadContent.tsx b/fe/client/src/components/issueList/HeadContent.tsx index 5128d1e4c..6dc23ee87 100644 --- a/fe/client/src/components/issueList/HeadContent.tsx +++ b/fe/client/src/components/issueList/HeadContent.tsx @@ -30,6 +30,7 @@ const HeadContent = ({ handleClickShowFilterModal }: HeadContentType) => { style={{ transform: 'translateY(3px)' }} /> diff --git a/fe/client/src/components/issueList/ListHeader.tsx b/fe/client/src/components/issueList/ListHeader.tsx index 9983d696d..ddda18a98 100644 --- a/fe/client/src/components/issueList/ListHeader.tsx +++ b/fe/client/src/components/issueList/ListHeader.tsx @@ -4,6 +4,7 @@ import IconButton from '@components/common/IconButton'; import { IssueListItemType } from '@components/common/types/APIType'; import { issueCheckedItemAtom, issueCheckedAllItemAtom } from '@components/common/atoms/checkBoxAtom'; import { useRecoilState } from '@/utils/myRecoil/useRecoilState'; +import { pipe, getLength, _ } from '@/utils/functionalUtils'; type ListHeaderType = { issueItems: Array; @@ -20,7 +21,17 @@ const ListHeader = ({ issueItems, handleClickShowFilterModal }: ListHeaderType) : setCheckedIssueItems(new Set()); setAllIssueChecked(!isAllIssueChecked); }; - + + const openIssueCount = pipe( + _.filter(({ closed }: { closed: boolean }) => !closed), + getLength + )(issueItems); + + const closeIssueCount = pipe( + _.filter(({ closed }: { closed: boolean }) => closed), + getLength + )(issueItems); + return (
@@ -28,13 +39,13 @@ const ListHeader = ({ issueItems, handleClickShowFilterModal }: ListHeaderType) - 열린 이슈 + 열린 이슈 ({openIssueCount}) - 닫힌 이슈 + 닫힌 이슈 ({closeIssueCount})
diff --git a/fe/client/src/utils/commonUtils.ts b/fe/client/src/utils/commonUtils.ts index 93592aa27..b546f5672 100644 --- a/fe/client/src/utils/commonUtils.ts +++ b/fe/client/src/utils/commonUtils.ts @@ -1,5 +1 @@ -export const pipe = (...fns: Function[]) => (value: string) => { - return fns.reduce((acc, cur) => cur(acc), value); -} - export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); \ No newline at end of file diff --git a/fe/client/src/utils/functionalUtils.ts b/fe/client/src/utils/functionalUtils.ts new file mode 100644 index 000000000..ab3bdc217 --- /dev/null +++ b/fe/client/src/utils/functionalUtils.ts @@ -0,0 +1,11 @@ +export const pipe = (...fns: Function[]) => (value: any) => { + return fns.reduce((acc, cur) => cur(acc), value); +} + +export const _ = { + map: (callback: any) => (array: Array) => array.map(callback), + filter: (callback: any) => (array: Array) => array.filter(callback), + find: (callback: any) => (array: Array) => array.find(callback), +} + +export const getLength = (array: Array) => array.length; \ No newline at end of file