feat: add personal projects

personal projects are projects that have no team.

they can only seen by the project members (one of which is whoever first
creates the project).
This commit is contained in:
Jordan Knott
2020-09-19 20:20:36 -05:00
parent 28a53f14ad
commit 4277b7b2a8
20 changed files with 327 additions and 178 deletions

View File

@ -137,7 +137,7 @@ const ProjectFinder = () => {
return {
id: team.id,
name: team.name,
projects: projects.filter(project => project.team.id === team.id),
projects: projects.filter(project => project.team && project.team.id === team.id),
};
});
return (

View File

@ -158,7 +158,7 @@ const Project = () => {
const [updateTaskName] = useUpdateTaskNameMutation();
const { loading, data } = useFindProjectQuery({
const { loading, data, error } = useFindProjectQuery({
variables: { projectID },
});
@ -224,6 +224,9 @@ const Project = () => {
</>
);
}
if (error) {
history.push('/projects');
}
if (data) {
labelsRef.current = data.findProject.labels;
@ -260,7 +263,7 @@ const Project = () => {
currentTab={0}
projectMembers={data.findProject.members}
projectID={projectID}
teamID={data.findProject.team.id}
teamID={data.findProject.team ? data.findProject.team.id : null}
name={data.findProject.name}
/>
<Route path={`${match.path}`} exact render={() => <Redirect to={`${match.url}/board`} />} />

View File

@ -8,8 +8,6 @@ import {
useCreateProjectMutation,
GetProjectsDocument,
GetProjectsQuery,
MeQuery,
MeDocument,
} from 'shared/generated/graphql';
import { Link } from 'react-router-dom';
@ -23,33 +21,6 @@ import updateApolloCache from 'shared/utils/cache';
import produce from 'immer';
import NOOP from 'shared/utils/noop';
const EmptyStateContent = styled.div`
display: flex;
justy-content: center;
align-items: center;
flex-direction: column;
`;
const EmptyStateTitle = styled.h3`
color: #fff;
font-size: 18px;
`;
const EmptyStatePrompt = styled.span`
color: rgba(${props => props.theme.colors.text.primary});
font-size: 16px;
margin-top: 8px;
`;
const EmptyState = styled(Empty)`
display: block;
margin: 0 auto;
`;
const CreateTeamButton = styled(Button)`
width: 100%;
`;
type CreateTeamData = { teamName: string };
type CreateTeamFormProps = {
@ -58,6 +29,10 @@ type CreateTeamFormProps = {
const CreateTeamFormContainer = styled.form``;
const CreateTeamButton = styled(Button)`
width: 100%;
`;
const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
const { register, handleSubmit } = useForm<CreateTeamData>();
const createTeam = (data: CreateTeamData) => {
@ -211,13 +186,6 @@ const ProjectsContainer = styled.div`
min-width: 288px;
`;
const ProjectGrid = styled.div`
max-width: 780px;
display: grid;
grid-template-columns: 240px 240px 240px;
gap: 20px 10px;
`;
const AddTeamButton = styled(Button)`
padding: 6px 12px;
position: absolute;
@ -225,10 +193,6 @@ const AddTeamButton = styled(Button)`
right: 12px;
`;
const CreateFirstTeam = styled(Button)`
margin-top: 8px;
`;
type ShowNewProject = {
open: boolean;
initialTeamID: null | string;
@ -269,6 +233,13 @@ const Projects = () => {
if (data && user) {
const { projects, teams, organizations } = data;
const organizationID = organizations[0].id ?? null;
const personalProjects = projects
.filter(p => p.team === null)
.sort((a, b) => {
const textA = a.name.toUpperCase();
const textB = b.name.toUpperCase();
return textA < textB ? -1 : textA > textB ? 1 : 0; // eslint-disable-line no-nested-ternary
});
const projectTeams = teams
.sort((a, b) => {
const textA = a.name.toUpperCase();
@ -280,7 +251,7 @@ const Projects = () => {
id: team.id,
name: team.name,
projects: projects
.filter(project => project.team.id === team.id)
.filter(project => project.team && project.team.id === team.id)
.sort((a, b) => {
const textA = a.name.toUpperCase();
const textB = b.name.toUpperCase();
@ -321,39 +292,35 @@ const Projects = () => {
Add Team
</AddTeamButton>
)}
{projectTeams.length === 0 && (
<EmptyStateContent>
<EmptyState width={425} height={425} />
<EmptyStateTitle>No teams exist</EmptyStateTitle>
<EmptyStatePrompt>Create a new team to get started</EmptyStatePrompt>
<CreateFirstTeam
variant="outline"
onClick={$target => {
showPopup(
$target,
<Popup
title="Create team"
tab={0}
onClose={() => {
hidePopup();
}}
>
<CreateTeamForm
onCreateTeam={teamName => {
if (organizationID) {
createTeam({ variables: { name: teamName, organizationID } });
hidePopup();
}
}}
/>
</Popup>,
);
}}
>
Create new team
</CreateFirstTeam>
</EmptyStateContent>
)}
<div>
<ProjectSectionTitleWrapper>
<ProjectSectionTitle>Personal Projects</ProjectSectionTitle>
</ProjectSectionTitleWrapper>
<ProjectList>
{personalProjects.map((project, idx) => (
<ProjectListItem key={project.id}>
<ProjectTile color={colors[idx % 5]} to={`/projects/${project.id}`}>
<ProjectTileFade />
<ProjectTileDetails>
<ProjectTileName>{project.name}</ProjectTileName>
</ProjectTileDetails>
</ProjectTile>
</ProjectListItem>
))}
<ProjectListItem>
<ProjectAddTile
onClick={() => {
setShowNewProject({ open: true, initialTeamID: 'no-team' });
}}
>
<ProjectTileFade />
<ProjectAddTileDetails>
<ProjectTileName centered>Create new project</ProjectTileName>
</ProjectAddTileDetails>
</ProjectAddTile>
</ProjectListItem>
</ProjectList>
</div>
{projectTeams.map(team => {
return (
<div key={team.id}>
@ -407,7 +374,7 @@ const Projects = () => {
initialTeamID={showNewProject.initialTeamID}
onCreateProject={(name, teamID) => {
if (user) {
createProject({ variables: { teamID, name, userID: user.id } });
createProject({ variables: { teamID, name } });
setShowNewProject({ open: false, initialTeamID: null });
}
}}

View File

@ -217,13 +217,13 @@ type NewProjectProps = {
initialTeamID: string | null;
teams: Array<Team>;
onClose: () => void;
onCreateProject: (projectName: string, teamID: string) => void;
onCreateProject: (projectName: string, teamID: string | null) => void;
};
const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose, onCreateProject }) => {
const [projectName, setProjectName] = useState('');
const [team, setTeam] = useState<null | string>(initialTeamID);
const options = teams.map(t => ({ label: t.name, value: t.id }));
const options = [{ label: 'No team', value: 'no-team' }, ...teams.map(t => ({ label: t.name, value: t.id }))];
return (
<Overlay>
<Content>
@ -271,8 +271,8 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
</ProjectInfo>
<CreateButton
onClick={() => {
if (team && projectName !== '') {
onCreateProject(projectName, team);
if (projectName !== '') {
onCreateProject(projectName, team === 'no-team' ? null : team);
}
}}
>

View File

@ -126,7 +126,7 @@ export type Project = {
id: Scalars['ID'];
createdAt: Scalars['Time'];
name: Scalars['String'];
team: Team;
team?: Maybe<Team>;
taskGroups: Array<TaskGroup>;
members: Array<Member>;
labels: Array<ProjectLabel>;
@ -643,8 +643,7 @@ export type Notification = {
};
export type NewProject = {
userID: Scalars['UUID'];
teamID: Scalars['UUID'];
teamID?: Maybe<Scalars['UUID']>;
name: Scalars['String'];
};
@ -1085,8 +1084,7 @@ export type ClearProfileAvatarMutation = (
);
export type CreateProjectMutationVariables = {
teamID: Scalars['UUID'];
userID: Scalars['UUID'];
teamID?: Maybe<Scalars['UUID']>;
name: Scalars['String'];
};
@ -1096,10 +1094,10 @@ export type CreateProjectMutation = (
& { createProject: (
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
& { team: (
& { team?: Maybe<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
) }
)> }
) }
);
@ -1194,10 +1192,10 @@ export type FindProjectQuery = (
& { findProject: (
{ __typename?: 'Project' }
& Pick<Project, 'name'>
& { team: (
& { team?: Maybe<(
{ __typename?: 'Team' }
& Pick<Team, 'id'>
), members: Array<(
)>, members: Array<(
{ __typename?: 'Member' }
& Pick<Member, 'id' | 'fullName' | 'username'>
& { role: (
@ -1361,10 +1359,10 @@ export type GetProjectsQuery = (
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
& { team: (
& { team?: Maybe<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
) }
)> }
)> }
);
@ -1837,10 +1835,10 @@ export type GetTeamQuery = (
), projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
& { team: (
& { team?: Maybe<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
) }
)> }
)>, users: Array<(
{ __typename?: 'UserAccount' }
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'username'>
@ -2365,8 +2363,8 @@ export type ClearProfileAvatarMutationHookResult = ReturnType<typeof useClearPro
export type ClearProfileAvatarMutationResult = ApolloReactCommon.MutationResult<ClearProfileAvatarMutation>;
export type ClearProfileAvatarMutationOptions = ApolloReactCommon.BaseMutationOptions<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>;
export const CreateProjectDocument = gql`
mutation createProject($teamID: UUID!, $userID: UUID!, $name: String!) {
createProject(input: {teamID: $teamID, userID: $userID, name: $name}) {
mutation createProject($teamID: UUID, $name: String!) {
createProject(input: {teamID: $teamID, name: $name}) {
id
name
team {
@ -2392,7 +2390,6 @@ export type CreateProjectMutationFn = ApolloReactCommon.MutationFunction<CreateP
* const [createProjectMutation, { data, loading, error }] = useCreateProjectMutation({
* variables: {
* teamID: // value for 'teamID'
* userID: // value for 'userID'
* name: // value for 'name'
* },
* });

View File

@ -1,5 +1,5 @@
mutation createProject($teamID: UUID!, $userID: UUID!, $name: String!) {
createProject(input: {teamID: $teamID, userID: $userID, name: $name}) {
mutation createProject($teamID: UUID, $name: String!) {
createProject(input: {teamID: $teamID, name: $name}) {
id
name
team {