feat: add search and minify to project finder
This commit is contained in:
parent
783e1c84c3
commit
a7c1ca328f
236
frontend/src/App/ProjectFinder.tsx
Normal file
236
frontend/src/App/ProjectFinder.tsx
Normal file
@ -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<Array<string>>([], '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 (
|
||||||
|
<>
|
||||||
|
<Search
|
||||||
|
variant="alternate"
|
||||||
|
value={search}
|
||||||
|
onChange={e => 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 (
|
||||||
|
<TeamContainer key={team.id}>
|
||||||
|
<TeamTitle>
|
||||||
|
<TeamTitleText>{team.name}</TeamTitleText>
|
||||||
|
{isMinified ? (
|
||||||
|
<Minify onClick={() => setMinified(prev => prev.filter(m => m !== team.id))}>
|
||||||
|
<CaretRight width={16} height={16} />
|
||||||
|
</Minify>
|
||||||
|
) : (
|
||||||
|
<Minify onClick={() => setMinified(prev => [...prev, team.id])}>
|
||||||
|
<CaretDown width={16} height={16} />
|
||||||
|
</Minify>
|
||||||
|
)}
|
||||||
|
</TeamTitle>
|
||||||
|
{!isMinified && (
|
||||||
|
<TeamProjects>
|
||||||
|
{team.projects.map((project, idx) => (
|
||||||
|
<TeamProjectContainer key={project.id}>
|
||||||
|
<TeamProjectLink to={`/projects/${project.id}`}>
|
||||||
|
<TeamProjectBackground idx={idx} />
|
||||||
|
<TeamProjectAvatar idx={idx} />
|
||||||
|
<TeamProjectContent>
|
||||||
|
<TeamProjectTitle>{project.name}</TeamProjectTitle>
|
||||||
|
</TeamProjectContent>
|
||||||
|
</TeamProjectLink>
|
||||||
|
</TeamProjectContainer>
|
||||||
|
))}
|
||||||
|
</TeamProjects>
|
||||||
|
)}
|
||||||
|
</TeamContainer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Empty>
|
||||||
|
<LoadingSpinner />
|
||||||
|
</Empty>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectFinder;
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
|
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
|
||||||
import styled from 'styled-components/macro';
|
|
||||||
import { ProfileMenu } from 'shared/components/DropdownMenu';
|
import { ProfileMenu } from 'shared/components/DropdownMenu';
|
||||||
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
@ -9,177 +8,16 @@ import {
|
|||||||
RoleCode,
|
RoleCode,
|
||||||
useTopNavbarQuery,
|
useTopNavbarQuery,
|
||||||
useDeleteProjectMutation,
|
useDeleteProjectMutation,
|
||||||
useGetProjectsQuery,
|
|
||||||
GetProjectsDocument,
|
GetProjectsDocument,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
import { History } from 'history';
|
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import MiniProfile from 'shared/components/MiniProfile';
|
import MiniProfile from 'shared/components/MiniProfile';
|
||||||
import cache from 'App/cache';
|
import cache from 'App/cache';
|
||||||
import NOOP from 'shared/utils/noop';
|
|
||||||
import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
|
import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
|
||||||
import theme from './ThemeStyles';
|
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 (
|
|
||||||
<>
|
|
||||||
<TeamContainer>
|
|
||||||
<TeamTitle>Personal</TeamTitle>
|
|
||||||
<TeamProjects>
|
|
||||||
{personalProjects.map((project, idx) => (
|
|
||||||
<TeamProjectContainer key={project.id}>
|
|
||||||
<TeamProjectLink to={`/projects/${project.id}`}>
|
|
||||||
<TeamProjectBackground color={colors[idx % 5]} />
|
|
||||||
<TeamProjectAvatar color={colors[idx % 5]} />
|
|
||||||
<TeamProjectContent>
|
|
||||||
<TeamProjectTitle>{project.name}</TeamProjectTitle>
|
|
||||||
</TeamProjectContent>
|
|
||||||
</TeamProjectLink>
|
|
||||||
</TeamProjectContainer>
|
|
||||||
))}
|
|
||||||
</TeamProjects>
|
|
||||||
</TeamContainer>
|
|
||||||
{projectTeams.map(team => (
|
|
||||||
<TeamContainer key={team.id}>
|
|
||||||
<TeamTitle>{team.name}</TeamTitle>
|
|
||||||
<TeamProjects>
|
|
||||||
{team.projects.map((project, idx) => (
|
|
||||||
<TeamProjectContainer key={project.id}>
|
|
||||||
<TeamProjectLink to={`/projects/${project.id}`}>
|
|
||||||
<TeamProjectBackground color={colors[idx % 5]} />
|
|
||||||
<TeamProjectAvatar color={colors[idx % 5]} />
|
|
||||||
<TeamProjectContent>
|
|
||||||
<TeamProjectTitle>{project.name}</TeamProjectTitle>
|
|
||||||
</TeamProjectContent>
|
|
||||||
</TeamProjectLink>
|
|
||||||
</TeamProjectContainer>
|
|
||||||
))}
|
|
||||||
</TeamProjects>
|
|
||||||
</TeamContainer>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <span>loading</span>;
|
|
||||||
};
|
|
||||||
type ProjectPopupProps = {
|
type ProjectPopupProps = {
|
||||||
history: any;
|
history: any;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -87,6 +87,12 @@ export const HeaderTitle = styled.span`
|
|||||||
|
|
||||||
export const Content = styled.div`
|
export const Content = styled.div`
|
||||||
max-height: 632px;
|
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)`
|
export const LabelSearch = styled(ControlledInput)`
|
||||||
|
Loading…
Reference in New Issue
Block a user