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:
parent
28a53f14ad
commit
4277b7b2a8
@ -137,7 +137,7 @@ const ProjectFinder = () => {
|
|||||||
return {
|
return {
|
||||||
id: team.id,
|
id: team.id,
|
||||||
name: team.name,
|
name: team.name,
|
||||||
projects: projects.filter(project => project.team.id === team.id),
|
projects: projects.filter(project => project.team && project.team.id === team.id),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
|
@ -158,7 +158,7 @@ const Project = () => {
|
|||||||
|
|
||||||
const [updateTaskName] = useUpdateTaskNameMutation();
|
const [updateTaskName] = useUpdateTaskNameMutation();
|
||||||
|
|
||||||
const { loading, data } = useFindProjectQuery({
|
const { loading, data, error } = useFindProjectQuery({
|
||||||
variables: { projectID },
|
variables: { projectID },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -224,6 +224,9 @@ const Project = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (error) {
|
||||||
|
history.push('/projects');
|
||||||
|
}
|
||||||
if (data) {
|
if (data) {
|
||||||
labelsRef.current = data.findProject.labels;
|
labelsRef.current = data.findProject.labels;
|
||||||
|
|
||||||
@ -260,7 +263,7 @@ const Project = () => {
|
|||||||
currentTab={0}
|
currentTab={0}
|
||||||
projectMembers={data.findProject.members}
|
projectMembers={data.findProject.members}
|
||||||
projectID={projectID}
|
projectID={projectID}
|
||||||
teamID={data.findProject.team.id}
|
teamID={data.findProject.team ? data.findProject.team.id : null}
|
||||||
name={data.findProject.name}
|
name={data.findProject.name}
|
||||||
/>
|
/>
|
||||||
<Route path={`${match.path}`} exact render={() => <Redirect to={`${match.url}/board`} />} />
|
<Route path={`${match.path}`} exact render={() => <Redirect to={`${match.url}/board`} />} />
|
||||||
|
@ -8,8 +8,6 @@ import {
|
|||||||
useCreateProjectMutation,
|
useCreateProjectMutation,
|
||||||
GetProjectsDocument,
|
GetProjectsDocument,
|
||||||
GetProjectsQuery,
|
GetProjectsQuery,
|
||||||
MeQuery,
|
|
||||||
MeDocument,
|
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -23,33 +21,6 @@ import updateApolloCache from 'shared/utils/cache';
|
|||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import NOOP from 'shared/utils/noop';
|
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 CreateTeamData = { teamName: string };
|
||||||
|
|
||||||
type CreateTeamFormProps = {
|
type CreateTeamFormProps = {
|
||||||
@ -58,6 +29,10 @@ type CreateTeamFormProps = {
|
|||||||
|
|
||||||
const CreateTeamFormContainer = styled.form``;
|
const CreateTeamFormContainer = styled.form``;
|
||||||
|
|
||||||
|
const CreateTeamButton = styled(Button)`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
|
const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
|
||||||
const { register, handleSubmit } = useForm<CreateTeamData>();
|
const { register, handleSubmit } = useForm<CreateTeamData>();
|
||||||
const createTeam = (data: CreateTeamData) => {
|
const createTeam = (data: CreateTeamData) => {
|
||||||
@ -211,13 +186,6 @@ const ProjectsContainer = styled.div`
|
|||||||
min-width: 288px;
|
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)`
|
const AddTeamButton = styled(Button)`
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -225,10 +193,6 @@ const AddTeamButton = styled(Button)`
|
|||||||
right: 12px;
|
right: 12px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CreateFirstTeam = styled(Button)`
|
|
||||||
margin-top: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type ShowNewProject = {
|
type ShowNewProject = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
initialTeamID: null | string;
|
initialTeamID: null | string;
|
||||||
@ -269,6 +233,13 @@ const Projects = () => {
|
|||||||
if (data && user) {
|
if (data && user) {
|
||||||
const { projects, teams, organizations } = data;
|
const { projects, teams, organizations } = data;
|
||||||
const organizationID = organizations[0].id ?? null;
|
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
|
const projectTeams = teams
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const textA = a.name.toUpperCase();
|
const textA = a.name.toUpperCase();
|
||||||
@ -280,7 +251,7 @@ const Projects = () => {
|
|||||||
id: team.id,
|
id: team.id,
|
||||||
name: team.name,
|
name: team.name,
|
||||||
projects: projects
|
projects: projects
|
||||||
.filter(project => project.team.id === team.id)
|
.filter(project => project.team && project.team.id === team.id)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const textA = a.name.toUpperCase();
|
const textA = a.name.toUpperCase();
|
||||||
const textB = b.name.toUpperCase();
|
const textB = b.name.toUpperCase();
|
||||||
@ -321,39 +292,35 @@ const Projects = () => {
|
|||||||
Add Team
|
Add Team
|
||||||
</AddTeamButton>
|
</AddTeamButton>
|
||||||
)}
|
)}
|
||||||
{projectTeams.length === 0 && (
|
<div>
|
||||||
<EmptyStateContent>
|
<ProjectSectionTitleWrapper>
|
||||||
<EmptyState width={425} height={425} />
|
<ProjectSectionTitle>Personal Projects</ProjectSectionTitle>
|
||||||
<EmptyStateTitle>No teams exist</EmptyStateTitle>
|
</ProjectSectionTitleWrapper>
|
||||||
<EmptyStatePrompt>Create a new team to get started</EmptyStatePrompt>
|
<ProjectList>
|
||||||
<CreateFirstTeam
|
{personalProjects.map((project, idx) => (
|
||||||
variant="outline"
|
<ProjectListItem key={project.id}>
|
||||||
onClick={$target => {
|
<ProjectTile color={colors[idx % 5]} to={`/projects/${project.id}`}>
|
||||||
showPopup(
|
<ProjectTileFade />
|
||||||
$target,
|
<ProjectTileDetails>
|
||||||
<Popup
|
<ProjectTileName>{project.name}</ProjectTileName>
|
||||||
title="Create team"
|
</ProjectTileDetails>
|
||||||
tab={0}
|
</ProjectTile>
|
||||||
onClose={() => {
|
</ProjectListItem>
|
||||||
hidePopup();
|
))}
|
||||||
|
<ProjectListItem>
|
||||||
|
<ProjectAddTile
|
||||||
|
onClick={() => {
|
||||||
|
setShowNewProject({ open: true, initialTeamID: 'no-team' });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CreateTeamForm
|
<ProjectTileFade />
|
||||||
onCreateTeam={teamName => {
|
<ProjectAddTileDetails>
|
||||||
if (organizationID) {
|
<ProjectTileName centered>Create new project</ProjectTileName>
|
||||||
createTeam({ variables: { name: teamName, organizationID } });
|
</ProjectAddTileDetails>
|
||||||
hidePopup();
|
</ProjectAddTile>
|
||||||
}
|
</ProjectListItem>
|
||||||
}}
|
</ProjectList>
|
||||||
/>
|
</div>
|
||||||
</Popup>,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create new team
|
|
||||||
</CreateFirstTeam>
|
|
||||||
</EmptyStateContent>
|
|
||||||
)}
|
|
||||||
{projectTeams.map(team => {
|
{projectTeams.map(team => {
|
||||||
return (
|
return (
|
||||||
<div key={team.id}>
|
<div key={team.id}>
|
||||||
@ -407,7 +374,7 @@ const Projects = () => {
|
|||||||
initialTeamID={showNewProject.initialTeamID}
|
initialTeamID={showNewProject.initialTeamID}
|
||||||
onCreateProject={(name, teamID) => {
|
onCreateProject={(name, teamID) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
createProject({ variables: { teamID, name, userID: user.id } });
|
createProject({ variables: { teamID, name } });
|
||||||
setShowNewProject({ open: false, initialTeamID: null });
|
setShowNewProject({ open: false, initialTeamID: null });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -217,13 +217,13 @@ type NewProjectProps = {
|
|||||||
initialTeamID: string | null;
|
initialTeamID: string | null;
|
||||||
teams: Array<Team>;
|
teams: Array<Team>;
|
||||||
onClose: () => void;
|
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 NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose, onCreateProject }) => {
|
||||||
const [projectName, setProjectName] = useState('');
|
const [projectName, setProjectName] = useState('');
|
||||||
const [team, setTeam] = useState<null | string>(initialTeamID);
|
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 (
|
return (
|
||||||
<Overlay>
|
<Overlay>
|
||||||
<Content>
|
<Content>
|
||||||
@ -271,8 +271,8 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
|
|||||||
</ProjectInfo>
|
</ProjectInfo>
|
||||||
<CreateButton
|
<CreateButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (team && projectName !== '') {
|
if (projectName !== '') {
|
||||||
onCreateProject(projectName, team);
|
onCreateProject(projectName, team === 'no-team' ? null : team);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -126,7 +126,7 @@ export type Project = {
|
|||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
createdAt: Scalars['Time'];
|
createdAt: Scalars['Time'];
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
team: Team;
|
team?: Maybe<Team>;
|
||||||
taskGroups: Array<TaskGroup>;
|
taskGroups: Array<TaskGroup>;
|
||||||
members: Array<Member>;
|
members: Array<Member>;
|
||||||
labels: Array<ProjectLabel>;
|
labels: Array<ProjectLabel>;
|
||||||
@ -643,8 +643,7 @@ export type Notification = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type NewProject = {
|
export type NewProject = {
|
||||||
userID: Scalars['UUID'];
|
teamID?: Maybe<Scalars['UUID']>;
|
||||||
teamID: Scalars['UUID'];
|
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1085,8 +1084,7 @@ export type ClearProfileAvatarMutation = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export type CreateProjectMutationVariables = {
|
export type CreateProjectMutationVariables = {
|
||||||
teamID: Scalars['UUID'];
|
teamID?: Maybe<Scalars['UUID']>;
|
||||||
userID: Scalars['UUID'];
|
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1096,10 +1094,10 @@ export type CreateProjectMutation = (
|
|||||||
& { createProject: (
|
& { createProject: (
|
||||||
{ __typename?: 'Project' }
|
{ __typename?: 'Project' }
|
||||||
& Pick<Project, 'id' | 'name'>
|
& Pick<Project, 'id' | 'name'>
|
||||||
& { team: (
|
& { team?: Maybe<(
|
||||||
{ __typename?: 'Team' }
|
{ __typename?: 'Team' }
|
||||||
& Pick<Team, 'id' | 'name'>
|
& Pick<Team, 'id' | 'name'>
|
||||||
) }
|
)> }
|
||||||
) }
|
) }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1194,10 +1192,10 @@ export type FindProjectQuery = (
|
|||||||
& { findProject: (
|
& { findProject: (
|
||||||
{ __typename?: 'Project' }
|
{ __typename?: 'Project' }
|
||||||
& Pick<Project, 'name'>
|
& Pick<Project, 'name'>
|
||||||
& { team: (
|
& { team?: Maybe<(
|
||||||
{ __typename?: 'Team' }
|
{ __typename?: 'Team' }
|
||||||
& Pick<Team, 'id'>
|
& Pick<Team, 'id'>
|
||||||
), members: Array<(
|
)>, members: Array<(
|
||||||
{ __typename?: 'Member' }
|
{ __typename?: 'Member' }
|
||||||
& Pick<Member, 'id' | 'fullName' | 'username'>
|
& Pick<Member, 'id' | 'fullName' | 'username'>
|
||||||
& { role: (
|
& { role: (
|
||||||
@ -1361,10 +1359,10 @@ export type GetProjectsQuery = (
|
|||||||
)>, projects: Array<(
|
)>, projects: Array<(
|
||||||
{ __typename?: 'Project' }
|
{ __typename?: 'Project' }
|
||||||
& Pick<Project, 'id' | 'name'>
|
& Pick<Project, 'id' | 'name'>
|
||||||
& { team: (
|
& { team?: Maybe<(
|
||||||
{ __typename?: 'Team' }
|
{ __typename?: 'Team' }
|
||||||
& Pick<Team, 'id' | 'name'>
|
& Pick<Team, 'id' | 'name'>
|
||||||
) }
|
)> }
|
||||||
)> }
|
)> }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1837,10 +1835,10 @@ export type GetTeamQuery = (
|
|||||||
), projects: Array<(
|
), projects: Array<(
|
||||||
{ __typename?: 'Project' }
|
{ __typename?: 'Project' }
|
||||||
& Pick<Project, 'id' | 'name'>
|
& Pick<Project, 'id' | 'name'>
|
||||||
& { team: (
|
& { team?: Maybe<(
|
||||||
{ __typename?: 'Team' }
|
{ __typename?: 'Team' }
|
||||||
& Pick<Team, 'id' | 'name'>
|
& Pick<Team, 'id' | 'name'>
|
||||||
) }
|
)> }
|
||||||
)>, users: Array<(
|
)>, users: Array<(
|
||||||
{ __typename?: 'UserAccount' }
|
{ __typename?: 'UserAccount' }
|
||||||
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'username'>
|
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'username'>
|
||||||
@ -2365,8 +2363,8 @@ export type ClearProfileAvatarMutationHookResult = ReturnType<typeof useClearPro
|
|||||||
export type ClearProfileAvatarMutationResult = ApolloReactCommon.MutationResult<ClearProfileAvatarMutation>;
|
export type ClearProfileAvatarMutationResult = ApolloReactCommon.MutationResult<ClearProfileAvatarMutation>;
|
||||||
export type ClearProfileAvatarMutationOptions = ApolloReactCommon.BaseMutationOptions<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>;
|
export type ClearProfileAvatarMutationOptions = ApolloReactCommon.BaseMutationOptions<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>;
|
||||||
export const CreateProjectDocument = gql`
|
export const CreateProjectDocument = gql`
|
||||||
mutation createProject($teamID: UUID!, $userID: UUID!, $name: String!) {
|
mutation createProject($teamID: UUID, $name: String!) {
|
||||||
createProject(input: {teamID: $teamID, userID: $userID, name: $name}) {
|
createProject(input: {teamID: $teamID, name: $name}) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
team {
|
team {
|
||||||
@ -2392,7 +2390,6 @@ export type CreateProjectMutationFn = ApolloReactCommon.MutationFunction<CreateP
|
|||||||
* const [createProjectMutation, { data, loading, error }] = useCreateProjectMutation({
|
* const [createProjectMutation, { data, loading, error }] = useCreateProjectMutation({
|
||||||
* variables: {
|
* variables: {
|
||||||
* teamID: // value for 'teamID'
|
* teamID: // value for 'teamID'
|
||||||
* userID: // value for 'userID'
|
|
||||||
* name: // value for 'name'
|
* name: // value for 'name'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
mutation createProject($teamID: UUID!, $userID: UUID!, $name: String!) {
|
mutation createProject($teamID: UUID, $name: String!) {
|
||||||
createProject(input: {teamID: $teamID, userID: $userID, name: $name}) {
|
createProject(input: {teamID: $teamID, name: $name}) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
team {
|
team {
|
||||||
|
@ -38,6 +38,12 @@ type Organization struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PersonalProject struct {
|
||||||
|
PersonalProjectID uuid.UUID `json:"personal_project_id"`
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
type Project struct {
|
type Project struct {
|
||||||
ProjectID uuid.UUID `json:"project_id"`
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
TeamID uuid.UUID `json:"team_id"`
|
TeamID uuid.UUID `json:"team_id"`
|
||||||
|
@ -10,18 +10,17 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const createProject = `-- name: CreateProject :one
|
const createPersonalProject = `-- name: CreatePersonalProject :one
|
||||||
INSERT INTO project(team_id, created_at, name) VALUES ($1, $2, $3) RETURNING project_id, team_id, created_at, name
|
INSERT INTO project(team_id, created_at, name) VALUES (null, $1, $2) RETURNING project_id, team_id, created_at, name
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateProjectParams struct {
|
type CreatePersonalProjectParams struct {
|
||||||
TeamID uuid.UUID `json:"team_id"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) {
|
func (q *Queries) CreatePersonalProject(ctx context.Context, arg CreatePersonalProjectParams) (Project, error) {
|
||||||
row := q.db.QueryRowContext(ctx, createProject, arg.TeamID, arg.CreatedAt, arg.Name)
|
row := q.db.QueryRowContext(ctx, createPersonalProject, arg.CreatedAt, arg.Name)
|
||||||
var i Project
|
var i Project
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ProjectID,
|
&i.ProjectID,
|
||||||
@ -32,6 +31,22 @@ func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (P
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createPersonalProjectLink = `-- name: CreatePersonalProjectLink :one
|
||||||
|
INSERT INTO personal_project (project_id, user_id) VALUES ($1, $2) RETURNING personal_project_id, project_id, user_id
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreatePersonalProjectLinkParams struct {
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreatePersonalProjectLink(ctx context.Context, arg CreatePersonalProjectLinkParams) (PersonalProject, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createPersonalProjectLink, arg.ProjectID, arg.UserID)
|
||||||
|
var i PersonalProject
|
||||||
|
err := row.Scan(&i.PersonalProjectID, &i.ProjectID, &i.UserID)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const createProjectMember = `-- name: CreateProjectMember :one
|
const createProjectMember = `-- name: CreateProjectMember :one
|
||||||
INSERT INTO project_member (project_id, user_id, role_code, added_at) VALUES ($1, $2, $3, $4)
|
INSERT INTO project_member (project_id, user_id, role_code, added_at) VALUES ($1, $2, $3, $4)
|
||||||
RETURNING project_member_id, project_id, user_id, added_at, role_code
|
RETURNING project_member_id, project_id, user_id, added_at, role_code
|
||||||
@ -62,6 +77,28 @@ func (q *Queries) CreateProjectMember(ctx context.Context, arg CreateProjectMemb
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createTeamProject = `-- name: CreateTeamProject :one
|
||||||
|
INSERT INTO project(team_id, created_at, name) VALUES ($1, $2, $3) RETURNING project_id, team_id, created_at, name
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateTeamProjectParams struct {
|
||||||
|
TeamID uuid.UUID `json:"team_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateTeamProject(ctx context.Context, arg CreateTeamProjectParams) (Project, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createTeamProject, arg.TeamID, arg.CreatedAt, arg.Name)
|
||||||
|
var i Project
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ProjectID,
|
||||||
|
&i.TeamID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.Name,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const deleteProjectByID = `-- name: DeleteProjectByID :exec
|
const deleteProjectByID = `-- name: DeleteProjectByID :exec
|
||||||
DELETE FROM project WHERE project_id = $1
|
DELETE FROM project WHERE project_id = $1
|
||||||
`
|
`
|
||||||
@ -209,6 +246,40 @@ func (q *Queries) GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPersonalProjectsForUserID = `-- name: GetPersonalProjectsForUserID :many
|
||||||
|
SELECT project.project_id, project.team_id, project.created_at, project.name FROM project
|
||||||
|
LEFT JOIN personal_project ON personal_project.project_id = project.project_id
|
||||||
|
WHERE personal_project.user_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetPersonalProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getPersonalProjectsForUserID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Project
|
||||||
|
for rows.Next() {
|
||||||
|
var i Project
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ProjectID,
|
||||||
|
&i.TeamID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.Name,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const getProjectByID = `-- name: GetProjectByID :one
|
const getProjectByID = `-- name: GetProjectByID :one
|
||||||
SELECT project_id, team_id, created_at, name FROM project WHERE project_id = $1
|
SELECT project_id, team_id, created_at, name FROM project WHERE project_id = $1
|
||||||
`
|
`
|
||||||
|
@ -13,7 +13,8 @@ type Querier interface {
|
|||||||
CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error)
|
CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error)
|
||||||
CreateNotificationObject(ctx context.Context, arg CreateNotificationObjectParams) (NotificationObject, error)
|
CreateNotificationObject(ctx context.Context, arg CreateNotificationObjectParams) (NotificationObject, error)
|
||||||
CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error)
|
CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error)
|
||||||
CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error)
|
CreatePersonalProject(ctx context.Context, arg CreatePersonalProjectParams) (Project, error)
|
||||||
|
CreatePersonalProjectLink(ctx context.Context, arg CreatePersonalProjectLinkParams) (PersonalProject, error)
|
||||||
CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error)
|
CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error)
|
||||||
CreateProjectMember(ctx context.Context, arg CreateProjectMemberParams) (ProjectMember, error)
|
CreateProjectMember(ctx context.Context, arg CreateProjectMemberParams) (ProjectMember, error)
|
||||||
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
|
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
|
||||||
@ -27,6 +28,7 @@ type Querier interface {
|
|||||||
CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error)
|
CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error)
|
||||||
CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error)
|
CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error)
|
||||||
CreateTeamMember(ctx context.Context, arg CreateTeamMemberParams) (TeamMember, error)
|
CreateTeamMember(ctx context.Context, arg CreateTeamMemberParams) (TeamMember, error)
|
||||||
|
CreateTeamProject(ctx context.Context, arg CreateTeamProjectParams) (Project, error)
|
||||||
CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error)
|
CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error)
|
||||||
DeleteExpiredTokens(ctx context.Context) error
|
DeleteExpiredTokens(ctx context.Context) error
|
||||||
DeleteProjectByID(ctx context.Context, projectID uuid.UUID) error
|
DeleteProjectByID(ctx context.Context, projectID uuid.UUID) error
|
||||||
@ -62,6 +64,7 @@ type Querier interface {
|
|||||||
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
||||||
GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
||||||
GetNotificationForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetNotificationForNotificationIDRow, error)
|
GetNotificationForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetNotificationForNotificationIDRow, error)
|
||||||
|
GetPersonalProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error)
|
||||||
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
|
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
|
||||||
GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error)
|
GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error)
|
||||||
GetProjectIDForTaskChecklist(ctx context.Context, taskChecklistID uuid.UUID) (uuid.UUID, error)
|
GetProjectIDForTaskChecklist(ctx context.Context, taskChecklistID uuid.UUID) (uuid.UUID, error)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
-- name: GetAllProjects :many
|
-- name: GetAllTeamProjects :many
|
||||||
SELECT * FROM project;
|
SELECT * FROM project WHERE team_id IS NOT null;
|
||||||
|
|
||||||
-- name: GetAllProjectsForTeam :many
|
-- name: GetAllProjectsForTeam :many
|
||||||
SELECT * FROM project WHERE team_id = $1;
|
SELECT * FROM project WHERE team_id = $1;
|
||||||
@ -7,9 +7,12 @@ SELECT * FROM project WHERE team_id = $1;
|
|||||||
-- name: GetProjectByID :one
|
-- name: GetProjectByID :one
|
||||||
SELECT * FROM project WHERE project_id = $1;
|
SELECT * FROM project WHERE project_id = $1;
|
||||||
|
|
||||||
-- name: CreateProject :one
|
-- name: CreateTeamProject :one
|
||||||
INSERT INTO project(team_id, created_at, name) VALUES ($1, $2, $3) RETURNING *;
|
INSERT INTO project(team_id, created_at, name) VALUES ($1, $2, $3) RETURNING *;
|
||||||
|
|
||||||
|
-- name: CreatePersonalProject :one
|
||||||
|
INSERT INTO project(team_id, created_at, name) VALUES (null, $1, $2) RETURNING *;
|
||||||
|
|
||||||
-- name: UpdateProjectNameByID :one
|
-- name: UpdateProjectNameByID :one
|
||||||
UPDATE project SET name = $2 WHERE project_id = $1 RETURNING *;
|
UPDATE project SET name = $2 WHERE project_id = $1 RETURNING *;
|
||||||
|
|
||||||
@ -44,9 +47,18 @@ SELECT project_id FROM project_member WHERE user_id = $1;
|
|||||||
SELECT project.* FROM project LEFT JOIN
|
SELECT project.* FROM project LEFT JOIN
|
||||||
project_member ON project_member.project_id = project.project_id WHERE project_member.user_id = $1;
|
project_member ON project_member.project_id = project.project_id WHERE project_member.user_id = $1;
|
||||||
|
|
||||||
|
-- name: GetPersonalProjectsForUserID :many
|
||||||
|
SELECT project.* FROM project
|
||||||
|
LEFT JOIN personal_project ON personal_project.project_id = project.project_id
|
||||||
|
WHERE personal_project.user_id = $1;
|
||||||
|
|
||||||
|
|
||||||
-- name: GetUserRolesForProject :one
|
-- name: GetUserRolesForProject :one
|
||||||
SELECT p.team_id, COALESCE(tm.role_code, '') AS team_role, COALESCE(pm.role_code, '') AS project_role
|
SELECT p.team_id, COALESCE(tm.role_code, '') AS team_role, COALESCE(pm.role_code, '') AS project_role
|
||||||
FROM project AS p
|
FROM project AS p
|
||||||
LEFT JOIN project_member AS pm ON pm.project_id = p.project_id AND pm.user_id = $1
|
LEFT JOIN project_member AS pm ON pm.project_id = p.project_id AND pm.user_id = $1
|
||||||
LEFT JOIN team_member AS tm ON tm.team_id = p.team_id AND tm.user_id = $1
|
LEFT JOIN team_member AS tm ON tm.team_id = p.team_id AND tm.user_id = $1
|
||||||
WHERE p.project_id = $2;
|
WHERE p.project_id = $2;
|
||||||
|
|
||||||
|
-- name: CreatePersonalProjectLink :one
|
||||||
|
INSERT INTO personal_project (project_id, user_id) VALUES ($1, $2) RETURNING *;
|
||||||
|
@ -2571,7 +2571,7 @@ type Project {
|
|||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
name: String!
|
name: String!
|
||||||
team: Team!
|
team: Team
|
||||||
taskGroups: [TaskGroup!]!
|
taskGroups: [TaskGroup!]!
|
||||||
members: [Member!]!
|
members: [Member!]!
|
||||||
labels: [ProjectLabel!]!
|
labels: [ProjectLabel!]!
|
||||||
@ -2659,7 +2659,8 @@ type Query {
|
|||||||
organizations: [Organization!]!
|
organizations: [Organization!]!
|
||||||
users: [UserAccount!]!
|
users: [UserAccount!]!
|
||||||
findUser(input: FindUser!): UserAccount!
|
findUser(input: FindUser!): UserAccount!
|
||||||
findProject(input: FindProject!): Project!
|
findProject(input: FindProject!):
|
||||||
|
Project! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
|
||||||
findTask(input: FindTask!): Task!
|
findTask(input: FindTask!): Task!
|
||||||
projects(input: ProjectsFilter): [Project!]!
|
projects(input: ProjectsFilter): [Project!]!
|
||||||
findTeam(input: FindTeam!): Team!
|
findTeam(input: FindTeam!): Team!
|
||||||
@ -2753,8 +2754,7 @@ extend type Mutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input NewProject {
|
input NewProject {
|
||||||
userID: UUID!
|
teamID: UUID
|
||||||
teamID: UUID!
|
|
||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10193,14 +10193,11 @@ func (ec *executionContext) _Project_team(ctx context.Context, field graphql.Col
|
|||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
if !graphql.HasFieldError(ctx, fc) {
|
|
||||||
ec.Errorf(ctx, "must not be null")
|
|
||||||
}
|
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(*db.Team)
|
res := resTmp.(*db.Team)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalNTeam2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeam(ctx, field.Selections, res)
|
return ec.marshalOTeam2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeam(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Project_taskGroups(ctx context.Context, field graphql.CollectedField, obj *db.Project) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Project_taskGroups(ctx context.Context, field graphql.CollectedField, obj *db.Project) (ret graphql.Marshaler) {
|
||||||
@ -10638,8 +10635,40 @@ func (ec *executionContext) _Query_findProject(ctx context.Context, field graphq
|
|||||||
}
|
}
|
||||||
fc.Args = args
|
fc.Args = args
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
|
directive0 := func(rctx context.Context) (interface{}, error) {
|
||||||
ctx = rctx // use context from middleware stack in children
|
ctx = rctx // use context from middleware stack in children
|
||||||
return ec.resolvers.Query().FindProject(rctx, args["input"].(FindProject))
|
return ec.resolvers.Query().FindProject(rctx, args["input"].(FindProject))
|
||||||
|
}
|
||||||
|
directive1 := func(ctx context.Context) (interface{}, error) {
|
||||||
|
roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN", "MEMBER"})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ec.directives.HasRole == nil {
|
||||||
|
return nil, errors.New("directive hasRole is not implemented")
|
||||||
|
}
|
||||||
|
return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := directive1(rctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tmp == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if data, ok := tmp.(*db.Project); ok {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Project`, tmp)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ec.Error(ctx, err)
|
ec.Error(ctx, err)
|
||||||
@ -15121,15 +15150,9 @@ func (ec *executionContext) unmarshalInputNewProject(ctx context.Context, obj in
|
|||||||
|
|
||||||
for k, v := range asMap {
|
for k, v := range asMap {
|
||||||
switch k {
|
switch k {
|
||||||
case "userID":
|
|
||||||
var err error
|
|
||||||
it.UserID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
|
|
||||||
if err != nil {
|
|
||||||
return it, err
|
|
||||||
}
|
|
||||||
case "teamID":
|
case "teamID":
|
||||||
var err error
|
var err error
|
||||||
it.TeamID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
|
it.TeamID, err = ec.unmarshalOUUID2ᚖgithubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return it, err
|
return it, err
|
||||||
}
|
}
|
||||||
@ -17286,9 +17309,6 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet,
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
res = ec._Project_team(ctx, field, obj)
|
res = ec._Project_team(ctx, field, obj)
|
||||||
if res == graphql.Null {
|
|
||||||
atomic.AddUint32(&invalids, 1)
|
|
||||||
}
|
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
case "taskGroups":
|
case "taskGroups":
|
||||||
@ -20927,6 +20947,17 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as
|
|||||||
return ec.marshalOString2string(ctx, sel, *v)
|
return ec.marshalOString2string(ctx, sel, *v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalOTeam2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeam(ctx context.Context, sel ast.SelectionSet, v db.Team) graphql.Marshaler {
|
||||||
|
return ec._Team(ctx, sel, &v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalOTeam2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeam(ctx context.Context, sel ast.SelectionSet, v *db.Team) graphql.Marshaler {
|
||||||
|
if v == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
return ec._Team(ctx, sel, v)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalOTime2timeᚐTime(ctx context.Context, v interface{}) (time.Time, error) {
|
func (ec *executionContext) unmarshalOTime2timeᚐTime(ctx context.Context, v interface{}) (time.Time, error) {
|
||||||
return graphql.UnmarshalTime(v)
|
return graphql.UnmarshalTime(v)
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,12 @@ package graph
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
@ -19,6 +21,7 @@ import (
|
|||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
"github.com/jordanknott/taskcafe/internal/utils"
|
"github.com/jordanknott/taskcafe/internal/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHandler returns a new graphql endpoint handler.
|
// NewHandler returns a new graphql endpoint handler.
|
||||||
@ -66,6 +69,11 @@ func NewHandler(repo db.Repository) http.Handler {
|
|||||||
log.Error("subject field name does not exist on input type")
|
log.Error("subject field name does not exist on input type")
|
||||||
return nil, errors.New("subject field name does not exist on input type")
|
return nil, errors.New("subject field name does not exist on input type")
|
||||||
}
|
}
|
||||||
|
if fieldName == "TeamID" && subjectField.IsNil() {
|
||||||
|
// Is a personal project, no check
|
||||||
|
// TODO: add config setting to disable personal projects
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
subjectID, ok = subjectField.Interface().(uuid.UUID)
|
subjectID, ok = subjectField.Interface().(uuid.UUID)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Error("error while casting subject UUID")
|
log.Error("error while casting subject UUID")
|
||||||
@ -93,16 +101,30 @@ func NewHandler(repo db.Repository) http.Handler {
|
|||||||
}
|
}
|
||||||
projectRoles, err := GetProjectRoles(ctx, repo, subjectID)
|
projectRoles, err := GetProjectRoles(ctx, repo, subjectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, &gqlerror.Error{
|
||||||
|
Message: "not authorized",
|
||||||
|
Extensions: map[string]interface{}{
|
||||||
|
"code": "401",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
log.WithError(err).Error("error while getting project roles")
|
log.WithError(err).Error("error while getting project roles")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, validRole := range roles {
|
for _, validRole := range roles {
|
||||||
if GetRoleLevel(projectRoles.TeamRole) == validRole || GetRoleLevel(projectRoles.ProjectRole) == validRole {
|
log.WithFields(log.Fields{"validRole": validRole}).Info("checking role")
|
||||||
|
if CompareRoleLevel(projectRoles.TeamRole, validRole) || CompareRoleLevel(projectRoles.ProjectRole, validRole) {
|
||||||
log.WithFields(log.Fields{"teamRole": projectRoles.TeamRole, "projectRole": projectRoles.ProjectRole}).Info("is team or project role")
|
log.WithFields(log.Fields{"teamRole": projectRoles.TeamRole, "projectRole": projectRoles.ProjectRole}).Info("is team or project role")
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, errors.New("must be a team or project admin")
|
return nil, &gqlerror.Error{
|
||||||
|
Message: "not authorized",
|
||||||
|
Extensions: map[string]interface{}{
|
||||||
|
"code": "401",
|
||||||
|
},
|
||||||
|
}
|
||||||
} else if level == ActionLevelTeam {
|
} else if level == ActionLevelTeam {
|
||||||
userID, ok := GetUserID(ctx)
|
userID, ok := GetUserID(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -114,14 +136,24 @@ func NewHandler(repo db.Repository) http.Handler {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, validRole := range roles {
|
for _, validRole := range roles {
|
||||||
if GetRoleLevel(role.RoleCode) == validRole || GetRoleLevel(role.RoleCode) == validRole {
|
if CompareRoleLevel(role.RoleCode, validRole) || CompareRoleLevel(role.RoleCode, validRole) {
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, errors.New("must be a team admin")
|
return nil, &gqlerror.Error{
|
||||||
|
Message: "not authorized",
|
||||||
|
Extensions: map[string]interface{}{
|
||||||
|
"code": "401",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil, errors.New("invalid path")
|
return nil, &gqlerror.Error{
|
||||||
|
Message: "bad path",
|
||||||
|
Extensions: map[string]interface{}{
|
||||||
|
"code": "500",
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
srv := handler.New(NewExecutableSchema(c))
|
srv := handler.New(NewExecutableSchema(c))
|
||||||
srv.AddTransport(transport.Websocket{
|
srv.AddTransport(transport.Websocket{
|
||||||
@ -184,12 +216,12 @@ func GetProjectRoles(ctx context.Context, r db.Repository, projectID uuid.UUID)
|
|||||||
return r.GetUserRolesForProject(ctx, db.GetUserRolesForProjectParams{UserID: userID, ProjectID: projectID})
|
return r.GetUserRolesForProject(ctx, db.GetUserRolesForProjectParams{UserID: userID, ProjectID: projectID})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRoleLevel converts a role level string to a RoleLevel type
|
// CompareRoleLevel compares a string against a role level
|
||||||
func GetRoleLevel(r string) RoleLevel {
|
func CompareRoleLevel(a string, b RoleLevel) bool {
|
||||||
if r == RoleLevelAdmin.String() {
|
if strings.ToLower(a) == strings.ToLower(b.String()) {
|
||||||
return RoleLevelAdmin
|
return true
|
||||||
}
|
}
|
||||||
return RoleLevelMember
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertToRoleCode converts a role code string to a RoleCode type
|
// ConvertToRoleCode converts a role code string to a RoleCode type
|
||||||
|
@ -213,8 +213,7 @@ type MemberList struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NewProject struct {
|
type NewProject struct {
|
||||||
UserID uuid.UUID `json:"userID"`
|
TeamID *uuid.UUID `json:"teamID"`
|
||||||
TeamID uuid.UUID `json:"teamID"`
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ type Project {
|
|||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
name: String!
|
name: String!
|
||||||
team: Team!
|
team: Team
|
||||||
taskGroups: [TaskGroup!]!
|
taskGroups: [TaskGroup!]!
|
||||||
members: [Member!]!
|
members: [Member!]!
|
||||||
labels: [ProjectLabel!]!
|
labels: [ProjectLabel!]!
|
||||||
@ -185,7 +185,8 @@ type Query {
|
|||||||
organizations: [Organization!]!
|
organizations: [Organization!]!
|
||||||
users: [UserAccount!]!
|
users: [UserAccount!]!
|
||||||
findUser(input: FindUser!): UserAccount!
|
findUser(input: FindUser!): UserAccount!
|
||||||
findProject(input: FindProject!): Project!
|
findProject(input: FindProject!):
|
||||||
|
Project! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
|
||||||
findTask(input: FindTask!): Task!
|
findTask(input: FindTask!): Task!
|
||||||
projects(input: ProjectsFilter): [Project!]!
|
projects(input: ProjectsFilter): [Project!]!
|
||||||
findTeam(input: FindTeam!): Team!
|
findTeam(input: FindTeam!): Team!
|
||||||
@ -279,8 +280,7 @@ extend type Mutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input NewProject {
|
input NewProject {
|
||||||
userID: UUID!
|
teamID: UUID
|
||||||
teamID: UUID!
|
|
||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,10 +23,41 @@ func (r *labelColorResolver) ID(ctx context.Context, obj *db.LabelColor) (uuid.U
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) (*db.Project, error) {
|
func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) (*db.Project, error) {
|
||||||
|
userID, ok := GetUserID(ctx)
|
||||||
|
if !ok {
|
||||||
|
return &db.Project{}, errors.New("user id is missing")
|
||||||
|
}
|
||||||
createdAt := time.Now().UTC()
|
createdAt := time.Now().UTC()
|
||||||
log.WithFields(log.Fields{"userID": input.UserID, "name": input.Name, "teamID": input.TeamID}).Info("creating new project")
|
log.WithFields(log.Fields{"name": input.Name, "teamID": input.TeamID}).Info("creating new project")
|
||||||
project, err := r.Repository.CreateProject(ctx, db.CreateProjectParams{input.TeamID, createdAt, input.Name})
|
var project db.Project
|
||||||
return &project, err
|
var err error
|
||||||
|
if input.TeamID == nil {
|
||||||
|
project, err = r.Repository.CreatePersonalProject(ctx, db.CreatePersonalProjectParams{
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
Name: input.Name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("error while creating project")
|
||||||
|
return &db.Project{}, err
|
||||||
|
}
|
||||||
|
log.WithFields(log.Fields{"userID": userID, "projectID": project.ProjectID}).Info("creating personal project link")
|
||||||
|
} else {
|
||||||
|
project, err = r.Repository.CreateTeamProject(ctx, db.CreateTeamProjectParams{
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
Name: input.Name,
|
||||||
|
TeamID: *input.TeamID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("error while creating project")
|
||||||
|
return &db.Project{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: project.ProjectID, UserID: userID, AddedAt: createdAt, RoleCode: "admin"})
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("error while creating initial project member")
|
||||||
|
return &db.Project{}, err
|
||||||
|
}
|
||||||
|
return &project, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) DeleteProject(ctx context.Context, input DeleteProject) (*DeleteProjectPayload, error) {
|
func (r *mutationResolver) DeleteProject(ctx context.Context, input DeleteProject) (*DeleteProjectPayload, error) {
|
||||||
@ -880,6 +911,9 @@ func (r *projectResolver) ID(ctx context.Context, obj *db.Project) (uuid.UUID, e
|
|||||||
func (r *projectResolver) Team(ctx context.Context, obj *db.Project) (*db.Team, error) {
|
func (r *projectResolver) Team(ctx context.Context, obj *db.Project) (*db.Team, error) {
|
||||||
team, err := r.Repository.GetTeamByID(ctx, obj.TeamID)
|
team, err := r.Repository.GetTeamByID(ctx, obj.TeamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
log.WithFields(log.Fields{"teamID": obj.TeamID, "projectID": obj.ProjectID}).WithError(err).Error("issue while getting team for project")
|
log.WithFields(log.Fields{"teamID": obj.TeamID, "projectID": obj.ProjectID}).WithError(err).Error("issue while getting team for project")
|
||||||
return &team, err
|
return &team, err
|
||||||
}
|
}
|
||||||
@ -982,25 +1016,6 @@ func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if role == auth.RoleAdmin {
|
|
||||||
return &project, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
projectRoles, err := GetProjectRoles(ctx, r.Repository, input.ProjectID)
|
|
||||||
log.WithFields(log.Fields{"projectID": input.ProjectID, "teamRole": projectRoles.TeamRole, "projectRole": projectRoles.ProjectRole}).Info("get project roles ")
|
|
||||||
if err != nil {
|
|
||||||
return &project, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if projectRoles.TeamRole == "" && projectRoles.ProjectRole == "" {
|
|
||||||
return &db.Project{}, &gqlerror.Error{
|
|
||||||
Message: "project not accessible",
|
|
||||||
Extensions: map[string]interface{}{
|
|
||||||
"code": "11-400",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &project, nil
|
return &project, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1021,12 +1036,14 @@ func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]
|
|||||||
return r.Repository.GetAllProjectsForTeam(ctx, *input.TeamID)
|
return r.Repository.GetAllProjectsForTeam(ctx, *input.TeamID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var teams []db.Team
|
||||||
|
var err error
|
||||||
if orgRole == "admin" {
|
if orgRole == "admin" {
|
||||||
log.Info("showing all projects for admin")
|
teams, err = r.Repository.GetAllTeams(ctx)
|
||||||
return r.Repository.GetAllProjects(ctx)
|
} else {
|
||||||
|
teams, err = r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
teams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
|
|
||||||
projects := make(map[string]db.Project)
|
projects := make(map[string]db.Project)
|
||||||
for _, team := range teams {
|
for _, team := range teams {
|
||||||
log.WithFields(log.Fields{"teamID": team.TeamID}).Info("found team")
|
log.WithFields(log.Fields{"teamID": team.TeamID}).Info("found team")
|
||||||
@ -1078,12 +1095,14 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
|||||||
return []db.Team{}, errors.New("internal error")
|
return []db.Team{}, errors.New("internal error")
|
||||||
}
|
}
|
||||||
if orgRole == "admin" {
|
if orgRole == "admin" {
|
||||||
|
|
||||||
return r.Repository.GetAllTeams(ctx)
|
return r.Repository.GetAllTeams(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
teams := make(map[string]db.Team)
|
teams := make(map[string]db.Team)
|
||||||
adminTeams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
|
adminTeams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.WithError(err).Error("error while getting teams for user ID")
|
||||||
return []db.Team{}, err
|
return []db.Team{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1093,7 +1112,7 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
|||||||
|
|
||||||
visibleProjects, err := r.Repository.GetAllVisibleProjectsForUserID(ctx, userID)
|
visibleProjects, err := r.Repository.GetAllVisibleProjectsForUserID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("userID", userID).Info("error while getting visible projects")
|
log.WithField("userID", userID).WithError(err).Error("error while getting visible projects for user ID")
|
||||||
return []db.Team{}, err
|
return []db.Team{}, err
|
||||||
}
|
}
|
||||||
for _, project := range visibleProjects {
|
for _, project := range visibleProjects {
|
||||||
@ -1102,7 +1121,10 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
|||||||
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding visible project")
|
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding visible project")
|
||||||
team, err := r.Repository.GetTeamByID(ctx, project.TeamID)
|
team, err := r.Repository.GetTeamByID(ctx, project.TeamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("teamID", project.TeamID).Info("error getting team by id")
|
if err == sql.ErrNoRows {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.WithField("teamID", project.TeamID).WithError(err).Error("error getting team by id")
|
||||||
return []db.Team{}, err
|
return []db.Team{}, err
|
||||||
}
|
}
|
||||||
teams[project.TeamID.String()] = team
|
teams[project.TeamID.String()] = team
|
||||||
|
@ -97,7 +97,7 @@ type Project {
|
|||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
name: String!
|
name: String!
|
||||||
team: Team!
|
team: Team
|
||||||
taskGroups: [TaskGroup!]!
|
taskGroups: [TaskGroup!]!
|
||||||
members: [Member!]!
|
members: [Member!]!
|
||||||
labels: [ProjectLabel!]!
|
labels: [ProjectLabel!]!
|
||||||
|
@ -25,7 +25,8 @@ type Query {
|
|||||||
organizations: [Organization!]!
|
organizations: [Organization!]!
|
||||||
users: [UserAccount!]!
|
users: [UserAccount!]!
|
||||||
findUser(input: FindUser!): UserAccount!
|
findUser(input: FindUser!): UserAccount!
|
||||||
findProject(input: FindProject!): Project!
|
findProject(input: FindProject!):
|
||||||
|
Project! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
|
||||||
findTask(input: FindTask!): Task!
|
findTask(input: FindTask!): Task!
|
||||||
projects(input: ProjectsFilter): [Project!]!
|
projects(input: ProjectsFilter): [Project!]!
|
||||||
findTeam(input: FindTeam!): Team!
|
findTeam(input: FindTeam!): Team!
|
||||||
|
@ -7,8 +7,7 @@ extend type Mutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input NewProject {
|
input NewProject {
|
||||||
userID: UUID!
|
teamID: UUID
|
||||||
teamID: UUID!
|
|
||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE project ALTER COLUMN team_id DROP NOT NULL;
|
5
migrations/0055_add-personal_project_table.up.sql
Normal file
5
migrations/0055_add-personal_project_table.up.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE personal_project (
|
||||||
|
personal_project_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
project_id uuid NOT NULL REFERENCES project(project_id) ON DELETE CASCADE,
|
||||||
|
user_id uuid NOT NULL REFERENCES user_account(user_id) ON DELETE CASCADE
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user