From a7c1ca328f9ba5eca6262cc6d4d279588641f902 Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Tue, 5 Jan 2021 16:46:49 -0600 Subject: [PATCH] feat: add search and minify to project finder --- frontend/src/App/ProjectFinder.tsx | 236 ++++++++++++++++++ frontend/src/App/TopNavbar.tsx | 164 +----------- .../src/shared/components/PopupMenu/Styles.ts | 6 + 3 files changed, 243 insertions(+), 163 deletions(-) create mode 100644 frontend/src/App/ProjectFinder.tsx diff --git a/frontend/src/App/ProjectFinder.tsx b/frontend/src/App/ProjectFinder.tsx new file mode 100644 index 0000000..060c39c --- /dev/null +++ b/frontend/src/App/ProjectFinder.tsx @@ -0,0 +1,236 @@ +import React, { useState } from 'react'; +import styled from 'styled-components/macro'; +import { useGetProjectsQuery } from 'shared/generated/graphql'; +import { Link } from 'react-router-dom'; +import LoadingSpinner from 'shared/components/LoadingSpinner'; +import theme from './ThemeStyles'; +import ControlledInput from 'shared/components/ControlledInput'; +import { CaretDown, CaretRight } from 'shared/icons'; +import useStickyState from 'shared/hooks/useStickyState'; + +const colors = [theme.colors.primary, theme.colors.secondary]; + +const TeamContainer = styled.div` + display: flex; + flex-direction: column; + margin: 0 4px; +`; + +const TeamTitle = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const TeamTitleText = styled.span` + font-size: 14px; + font-weight: 700; +`; +const TeamProjects = styled.div` + display: flex; + flex-direction: column; + margin-top: 8px; + margin-bottom: 4px; +`; + +const TeamProjectLink = styled(Link)` + display: flex; + font-weight: 700; + height: 36px; + overflow: hidden; + padding: 0; + position: relative; + text-decoration: none; + user-select: none; +`; + +const TeamProjectBackground = styled.div<{ idx: number }>` + background-image: url(null); + background-color: ${props => props.theme.colors.multiColors[props.idx % 5]}; + + background-size: cover; + background-position: 50%; + position: absolute; + width: 100%; + height: 36px; + opacity: 1; + border-radius: 3px; + &:before { + background: ${props => props.theme.colors.bg.secondary}; + bottom: 0; + content: ''; + left: 0; + opacity: 0.88; + position: absolute; + right: 0; + top: 0; + } +`; +const Empty = styled.div` + display: flex; + align-items: center; + justify-content: center; +`; + +const TeamProjectAvatar = styled.div<{ idx: number }>` + background-image: url(null); + background-color: ${props => props.theme.colors.multiColors[props.idx % 5]}; + + display: inline-block; + flex: 0 0 auto; + background-size: cover; + border-radius: 3px 0 0 3px; + height: 36px; + width: 36px; + position: relative; + opacity: 0.7; +`; + +const TeamProjectContent = styled.div` + display: flex; + position: relative; + flex: 1; + width: 100%; + padding: 9px 0 9px 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const TeamProjectTitle = styled.div` + font-weight: 700; + display: block; + padding-right: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const TeamProjectContainer = styled.div` + box-sizing: border-box; + border-radius: 3px; + position: relative; + margin: 0 4px 4px 0; + min-width: 0; + &:hover ${TeamProjectTitle} { + color: ${props => props.theme.colors.text.secondary}; + } + &:hover ${TeamProjectAvatar} { + opacity: 1; + } + &:hover ${TeamProjectBackground}:before { + opacity: 0.78; + } +`; +const Search = styled(ControlledInput)` + margin: 0 4px 4px 4px; + & input { + width: 100%; + } +`; + +const Minify = styled.div` + height: 28px; + min-height: 28px; + min-width: 28px; + width: 28px; + border-radius: 6px; + user-select: none; + + margin-right: 4px; + + align-items: center; + box-sizing: border-box; + display: inline-flex; + justify-content: center; + transition-duration: 0.2s; + transition-property: background, border, box-shadow, fill; + cursor: pointer; + svg { + fill: ${props => props.theme.colors.text.primary}; + transition-duration: 0.2s; + transition-property: background, border, box-shadow, fill; + } + + &:hover svg { + fill: ${props => props.theme.colors.text.secondary}; + } +`; + +const ProjectFinder = () => { + const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'cache-and-network' }); + const [search, setSearch] = useState(''); + const [minified, setMinified] = useStickyState>([], 'project_finder_minified'); + if (data) { + const { teams } = data; + const projects = data.projects.filter(p => { + if (search.trim() === '') return true; + return p.name.toLowerCase().startsWith(search.trim().toLowerCase()); + }); + const personalProjects = projects.filter(p => p.team === null); + const projectTeams = [ + { id: 'personal', name: 'Personal', projects: personalProjects.sort((a, b) => a.name.localeCompare(b.name)) }, + ...teams.map(team => { + return { + id: team.id, + name: team.name, + projects: projects + .filter(project => project.team && project.team.id === team.id) + .sort((a, b) => a.name.localeCompare(b.name)), + }; + }), + ]; + return ( + <> + setSearch(e.currentTarget.value)} + placeholder="Find projects by name..." + /> + {projectTeams.map(team => { + const isMinified = minified.find(m => m === team.id); + if (team.projects.length === 0) return null; + return ( + + + {team.name} + {isMinified ? ( + setMinified(prev => prev.filter(m => m !== team.id))}> + + + ) : ( + setMinified(prev => [...prev, team.id])}> + + + )} + + {!isMinified && ( + + {team.projects.map((project, idx) => ( + + + + + + {project.name} + + + + ))} + + )} + + ); + })} + + ); + } + return ( + + + + ); +}; + +export default ProjectFinder; diff --git a/frontend/src/App/TopNavbar.tsx b/frontend/src/App/TopNavbar.tsx index e887a27..e1d8e55 100644 --- a/frontend/src/App/TopNavbar.tsx +++ b/frontend/src/App/TopNavbar.tsx @@ -1,6 +1,5 @@ import React from 'react'; import TopNavbar, { MenuItem } from 'shared/components/TopNavbar'; -import styled from 'styled-components/macro'; import { ProfileMenu } from 'shared/components/DropdownMenu'; import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings'; import { useHistory } from 'react-router'; @@ -9,177 +8,16 @@ import { RoleCode, useTopNavbarQuery, useDeleteProjectMutation, - useGetProjectsQuery, GetProjectsDocument, } from 'shared/generated/graphql'; import { usePopup, Popup } from 'shared/components/PopupMenu'; -import { History } from 'history'; import produce from 'immer'; -import { Link } from 'react-router-dom'; import MiniProfile from 'shared/components/MiniProfile'; import cache from 'App/cache'; -import NOOP from 'shared/utils/noop'; import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup'; import theme from './ThemeStyles'; +import ProjectFinder from './ProjectFinder'; -const TeamContainer = styled.div` - display: flex; - flex-direction: column; - margin: 0 8px; -`; - -const TeamTitle = styled.h3` - font-size: 14px; - font-weight: 700; -`; - -const TeamProjects = styled.div` - display: flex; - flex-direction: column; - margin-top: 8px; - margin-bottom: 4px; -`; - -const TeamProjectLink = styled(Link)` - display: flex; - font-weight: 700; - height: 36px; - overflow: hidden; - padding: 0; - position: relative; - text-decoration: none; - user-select: none; -`; - -const TeamProjectBackground = styled.div<{ color: string }>` - background-image: url(null); - background-color: ${props => props.color}; - - background-size: cover; - background-position: 50%; - position: absolute; - width: 100%; - height: 36px; - opacity: 1; - border-radius: 3px; - &:before { - background: ${props => props.theme.colors.bg.secondary}; - bottom: 0; - content: ''; - left: 0; - opacity: 0.88; - position: absolute; - right: 0; - top: 0; - } -`; - -const TeamProjectAvatar = styled.div<{ color: string }>` - background-image: url(null); - background-color: ${props => props.color}; - - display: inline-block; - flex: 0 0 auto; - background-size: cover; - border-radius: 3px 0 0 3px; - height: 36px; - width: 36px; - position: relative; - opacity: 0.7; -`; - -const TeamProjectContent = styled.div` - display: flex; - position: relative; - flex: 1; - width: 100%; - padding: 9px 0 9px 10px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const TeamProjectTitle = styled.div` - font-weight: 700; - display: block; - padding-right: 12px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const TeamProjectContainer = styled.div` - box-sizing: border-box; - border-radius: 3px; - position: relative; - margin: 0 4px 4px 0; - min-width: 0; - &:hover ${TeamProjectTitle} { - color: ${props => props.theme.colors.text.secondary}; - } - &:hover ${TeamProjectAvatar} { - opacity: 1; - } - &:hover ${TeamProjectBackground}:before { - opacity: 0.78; - } -`; - -const colors = [theme.colors.primary, theme.colors.secondary]; - -const ProjectFinder = () => { - const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'cache-and-network' }); - if (data) { - const { projects, teams } = data; - const personalProjects = data.projects.filter(p => p.team === null); - const projectTeams = teams.map(team => { - return { - id: team.id, - name: team.name, - projects: projects.filter(project => project.team && project.team.id === team.id), - }; - }); - return ( - <> - - Personal - - {personalProjects.map((project, idx) => ( - - - - - - {project.name} - - - - ))} - - - {projectTeams.map(team => ( - - {team.name} - - {team.projects.map((project, idx) => ( - - - - - - {project.name} - - - - ))} - - - ))} - - ); - } - return loading; -}; type ProjectPopupProps = { history: any; name: string; diff --git a/frontend/src/shared/components/PopupMenu/Styles.ts b/frontend/src/shared/components/PopupMenu/Styles.ts index e156f11..4d5114e 100644 --- a/frontend/src/shared/components/PopupMenu/Styles.ts +++ b/frontend/src/shared/components/PopupMenu/Styles.ts @@ -87,6 +87,12 @@ export const HeaderTitle = styled.span` export const Content = styled.div` max-height: 632px; + overflow-y: auto; + overflow-x: hidden; + &::-webkit-scrollbar-track-piece { + background: ${props => props.theme.colors.bg.primary}; + border-radius: 20px; + } `; export const LabelSearch = styled(ControlledInput)`