feature: add ability to assign tasks

This commit is contained in:
Jordan Knott 2020-04-19 22:02:55 -05:00
parent beaa215bc2
commit c38024e692
60 changed files with 2871 additions and 790 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
package graph
import (
"context"
"net/http"
"os"
"time"
@ -10,6 +11,7 @@ import (
"github.com/99designs/gqlgen/graphql/handler/lru"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/pg"
)
@ -45,3 +47,7 @@ func NewHandler(repo pg.Repository) http.Handler {
func NewPlaygroundHandler(endpoint string) http.Handler {
return playground.Handler("GraphQL Playground", endpoint)
}
func GetUserID(ctx context.Context) (uuid.UUID, bool) {
userID, ok := ctx.Value("userID").(uuid.UUID)
return userID, ok
}

View File

@ -7,6 +7,16 @@ import (
"github.com/jordanknott/project-citadel/api/pg"
)
type AddTaskLabelInput struct {
TaskID uuid.UUID `json:"taskID"`
LabelColorID uuid.UUID `json:"labelColorID"`
}
type AssignTaskInput struct {
TaskID uuid.UUID `json:"taskID"`
UserID uuid.UUID `json:"userID"`
}
type DeleteTaskGroupInput struct {
TaskGroupID uuid.UUID `json:"taskGroupID"`
}
@ -29,6 +39,10 @@ type FindProject struct {
ProjectID string `json:"projectId"`
}
type FindTask struct {
TaskID uuid.UUID `json:"taskID"`
}
type FindUser struct {
UserID string `json:"userId"`
}
@ -37,12 +51,9 @@ type LogoutUser struct {
UserID string `json:"userID"`
}
type NewOrganization struct {
Name string `json:"name"`
}
type NewProject struct {
TeamID string `json:"teamID"`
UserID uuid.UUID `json:"userID"`
TeamID uuid.UUID `json:"teamID"`
Name string `json:"name"`
}
@ -81,14 +92,37 @@ type NewTeam struct {
type NewUserAccount struct {
Username string `json:"username"`
Email string `json:"email"`
DisplayName string `json:"displayName"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Password string `json:"password"`
}
type ProfileIcon struct {
URL *string `json:"url"`
Initials *string `json:"initials"`
}
type ProjectMember struct {
UserID uuid.UUID `json:"userID"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
ProfileIcon *ProfileIcon `json:"profileIcon"`
}
type ProjectsFilter struct {
TeamID *string `json:"teamID"`
}
type RemoveTaskLabelInput struct {
TaskID uuid.UUID `json:"taskID"`
TaskLabelID uuid.UUID `json:"taskLabelID"`
}
type UpdateTaskDescriptionInput struct {
TaskID uuid.UUID `json:"taskID"`
Description string `json:"description"`
}
type UpdateTaskName struct {
TaskID string `json:"taskID"`
Name string `json:"name"`

View File

@ -1,7 +1,24 @@
scalar Time
scalar UUID
type TaskLabel {
taskLabelID: ID!
labelColorID: UUID!
colorHex: String!
}
type ProfileIcon {
url: String
initials: String
}
type ProjectMember {
userID: ID!
firstName: String!
lastName: String!
profileIcon: ProfileIcon!
}
type RefreshToken {
tokenId: ID!
userId: UUID!
@ -13,30 +30,26 @@ type UserAccount {
userID: ID!
email: String!
createdAt: Time!
displayName: String!
firstName: String!
lastName: String!
username: String!
}
type Organization {
organizationID: ID!
createdAt: Time!
name: String!
teams: [Team!]!
profileIcon: ProfileIcon!
}
type Team {
teamID: ID!
createdAt: Time!
name: String!
projects: [Project!]!
}
type Project {
projectID: ID!
teamID: String!
createdAt: Time!
name: String!
team: Team!
owner: ProjectMember!
taskGroups: [TaskGroup!]!
members: [ProjectMember!]!
}
type TaskGroup {
@ -54,6 +67,9 @@ type Task {
createdAt: Time!
name: String!
position: Float!
description: String
assigned: [ProjectMember!]!
labels: [TaskLabel!]!
}
input ProjectsFilter {
@ -68,14 +84,18 @@ input FindProject {
projectId: String!
}
input FindTask {
taskID: UUID!
}
type Query {
organizations: [Organization!]!
users: [UserAccount!]!
findUser(input: FindUser!): UserAccount!
findProject(input: FindProject!): Project!
teams: [Team!]!
findTask(input: FindTask!): Task!
projects(input: ProjectsFilter): [Project!]!
taskGroups: [TaskGroup!]!
me: UserAccount!
}
input NewRefreshToken {
@ -85,7 +105,8 @@ input NewRefreshToken {
input NewUserAccount {
username: String!
email: String!
displayName: String!
firstName: String!
lastName: String!
password: String!
}
@ -95,7 +116,8 @@ input NewTeam {
}
input NewProject {
teamID: String!
userID: UUID!
teamID: UUID!
name: String!
}
@ -105,10 +127,6 @@ input NewTaskGroup {
position: Float!
}
input NewOrganization {
name: String!
}
input LogoutUser {
userID: String!
}
@ -151,13 +169,31 @@ type DeleteTaskGroupPayload {
taskGroup: TaskGroup!
}
input AssignTaskInput {
taskID: UUID!
userID: UUID!
}
input UpdateTaskDescriptionInput {
taskID: UUID!
description: String!
}
input AddTaskLabelInput {
taskID: UUID!
labelColorID: UUID!
}
input RemoveTaskLabelInput {
taskID: UUID!
taskLabelID: UUID!
}
type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken!
createUserAccount(input: NewUserAccount!): UserAccount!
createOrganization(input: NewOrganization!): Organization!
createTeam(input: NewTeam!): Team!
createProject(input: NewProject!): Project!
@ -166,10 +202,15 @@ type Mutation {
updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup!
deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload!
addTaskLabel(input: AddTaskLabelInput): Task!
removeTaskLabel(input: RemoveTaskLabelInput): Task!
createTask(input: NewTask!): Task!
updateTaskDescription(input: UpdateTaskDescriptionInput!): Task!
updateTaskLocation(input: NewTaskLocation!): Task!
updateTaskName(input: UpdateTaskName!): Task!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
assignTask(input: AssignTaskInput): Task!
logoutUser(input: LogoutUser!): Boolean!
}

View File

@ -6,6 +6,7 @@ package graph
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
@ -24,16 +25,10 @@ func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefr
func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*pg.UserAccount, error) {
createdAt := time.Now().UTC()
userAccount, err := r.Repository.CreateUserAccount(ctx, pg.CreateUserAccountParams{input.Username, input.Email, input.DisplayName, createdAt, input.Password})
userAccount, err := r.Repository.CreateUserAccount(ctx, pg.CreateUserAccountParams{input.FirstName, input.LastName, input.Email, input.Username, createdAt, input.Password})
return &userAccount, err
}
func (r *mutationResolver) CreateOrganization(ctx context.Context, input NewOrganization) (*pg.Organization, error) {
createdAt := time.Now().UTC()
organization, err := r.Repository.CreateOrganization(ctx, pg.CreateOrganizationParams{createdAt, input.Name})
return &organization, err
}
func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*pg.Team, error) {
organizationID, err := uuid.Parse(input.OrganizationID)
if err != nil {
@ -46,11 +41,7 @@ func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*pg.T
func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) (*pg.Project, error) {
createdAt := time.Now().UTC()
teamID, err := uuid.Parse(input.TeamID)
if err != nil {
return &pg.Project{}, err
}
project, err := r.Repository.CreateProject(ctx, pg.CreateProjectParams{teamID, createdAt, input.Name})
project, err := r.Repository.CreateProject(ctx, pg.CreateProjectParams{input.UserID, input.TeamID, createdAt, input.Name})
return &project, err
}
@ -89,6 +80,20 @@ func (r *mutationResolver) DeleteTaskGroup(ctx context.Context, input DeleteTask
return &DeleteTaskGroupPayload{true, int(deletedTasks + deletedTaskGroups), &taskGroup}, nil
}
func (r *mutationResolver) AddTaskLabel(ctx context.Context, input *AddTaskLabelInput) (*pg.Task, error) {
assignedDate := time.Now().UTC()
_, err := r.Repository.CreateTaskLabelForTask(ctx, pg.CreateTaskLabelForTaskParams{input.TaskID, input.LabelColorID, assignedDate})
if err != nil {
return &pg.Task{}, err
}
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
return &task, nil
}
func (r *mutationResolver) RemoveTaskLabel(ctx context.Context, input *RemoveTaskLabelInput) (*pg.Task, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*pg.Task, error) {
taskGroupID, err := uuid.Parse(input.TaskGroupID)
createdAt := time.Now().UTC()
@ -100,6 +105,11 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*pg.T
return &task, err
}
func (r *mutationResolver) UpdateTaskDescription(ctx context.Context, input UpdateTaskDescriptionInput) (*pg.Task, error) {
task, err := r.Repository.UpdateTaskDescription(ctx, pg.UpdateTaskDescriptionParams{input.TaskID, sql.NullString{String: input.Description, Valid: true}})
return &task, err
}
func (r *mutationResolver) UpdateTaskLocation(ctx context.Context, input NewTaskLocation) (*pg.Task, error) {
taskID, err := uuid.Parse(input.TaskID)
if err != nil {
@ -139,6 +149,21 @@ func (r *mutationResolver) DeleteTask(ctx context.Context, input DeleteTaskInput
return &DeleteTaskPayload{taskID.String()}, nil
}
func (r *mutationResolver) AssignTask(ctx context.Context, input *AssignTaskInput) (*pg.Task, error) {
assignedDate := time.Now().UTC()
assignedTask, err := r.Repository.CreateTaskAssigned(ctx, pg.CreateTaskAssignedParams{input.TaskID, input.UserID, assignedDate})
log.WithFields(log.Fields{
"userID": assignedTask.UserID,
"taskID": assignedTask.TaskID,
"assignedTaskID": assignedTask.TaskAssignedID,
}).Info("assigned task")
if err != nil {
return &pg.Task{}, err
}
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
return &task, err
}
func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) {
userID, err := uuid.Parse(input.UserID)
if err != nil {
@ -149,21 +174,35 @@ func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bo
return true, err
}
func (r *organizationResolver) Teams(ctx context.Context, obj *pg.Organization) ([]pg.Team, error) {
teams, err := r.Repository.GetTeamsForOrganization(ctx, obj.OrganizationID)
return teams, err
func (r *projectResolver) Team(ctx context.Context, obj *pg.Project) (*pg.Team, error) {
team, err := r.Repository.GetTeamByID(ctx, obj.TeamID)
return &team, err
}
func (r *projectResolver) TeamID(ctx context.Context, obj *pg.Project) (string, error) {
return obj.TeamID.String(), nil
func (r *projectResolver) Owner(ctx context.Context, obj *pg.Project) (*ProjectMember, error) {
user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
if err != nil {
return &ProjectMember{}, err
}
initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
return &ProjectMember{obj.Owner, user.FirstName, user.LastName, profileIcon}, nil
}
func (r *projectResolver) TaskGroups(ctx context.Context, obj *pg.Project) ([]pg.TaskGroup, error) {
return r.Repository.GetTaskGroupsForProject(ctx, obj.ProjectID)
}
func (r *queryResolver) Organizations(ctx context.Context) ([]pg.Organization, error) {
return r.Repository.GetAllOrganizations(ctx)
func (r *projectResolver) Members(ctx context.Context, obj *pg.Project) ([]ProjectMember, error) {
user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
members := []ProjectMember{}
if err != nil {
return members, err
}
initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
members = append(members, ProjectMember{obj.Owner, user.FirstName, user.LastName, profileIcon})
return members, nil
}
func (r *queryResolver) Users(ctx context.Context) ([]pg.UserAccount, error) {
@ -204,8 +243,9 @@ func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*pg
return &project, err
}
func (r *queryResolver) Teams(ctx context.Context) ([]pg.Team, error) {
return r.Repository.GetAllTeams(ctx)
func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*pg.Task, error) {
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
return &task, err
}
func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]pg.Project, error) {
@ -223,11 +263,59 @@ func (r *queryResolver) TaskGroups(ctx context.Context) ([]pg.TaskGroup, error)
return r.Repository.GetAllTaskGroups(ctx)
}
func (r *queryResolver) Me(ctx context.Context) (*pg.UserAccount, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &pg.UserAccount{}, fmt.Errorf("internal server error")
}
log.WithFields(log.Fields{
"userID": userID,
}).Info("getting user account")
user, err := r.Repository.GetUserAccountByID(ctx, userID)
if err != nil {
return &pg.UserAccount{}, err
}
return &user, err
}
func (r *taskResolver) TaskGroup(ctx context.Context, obj *pg.Task) (*pg.TaskGroup, error) {
taskGroup, err := r.Repository.GetTaskGroupByID(ctx, obj.TaskGroupID)
return &taskGroup, err
}
func (r *taskResolver) Description(ctx context.Context, obj *pg.Task) (*string, error) {
task, err := r.Repository.GetTaskByID(ctx, obj.TaskID)
if err != nil {
return nil, err
}
if !task.Description.Valid {
return nil, nil
}
return &task.Description.String, nil
}
func (r *taskResolver) Assigned(ctx context.Context, obj *pg.Task) ([]ProjectMember, error) {
taskMemberLinks, err := r.Repository.GetAssignedMembersForTask(ctx, obj.TaskID)
taskMembers := []ProjectMember{}
if err != nil {
return taskMembers, err
}
for _, taskMemberLink := range taskMemberLinks {
user, err := r.Repository.GetUserAccountByID(ctx, taskMemberLink.UserID)
if err != nil {
return taskMembers, err
}
initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
taskMembers = append(taskMembers, ProjectMember{taskMemberLink.UserID, user.FirstName, user.LastName, profileIcon})
}
return taskMembers, nil
}
func (r *taskResolver) Labels(ctx context.Context, obj *pg.Task) ([]pg.TaskLabel, error) {
return r.Repository.GetTaskLabelsForTaskID(ctx, obj.TaskID)
}
func (r *taskGroupResolver) ProjectID(ctx context.Context, obj *pg.TaskGroup) (string, error) {
return obj.ProjectID.String(), nil
}
@ -237,16 +325,23 @@ func (r *taskGroupResolver) Tasks(ctx context.Context, obj *pg.TaskGroup) ([]pg.
return tasks, err
}
func (r *teamResolver) Projects(ctx context.Context, obj *pg.Team) ([]pg.Project, error) {
return r.Repository.GetAllProjectsForTeam(ctx, obj.TeamID)
func (r *taskLabelResolver) ColorHex(ctx context.Context, obj *pg.TaskLabel) (string, error) {
labelColor, err := r.Repository.GetLabelColorByID(ctx, obj.LabelColorID)
if err != nil {
return "", err
}
return labelColor.ColorHex, nil
}
func (r *userAccountResolver) ProfileIcon(ctx context.Context, obj *pg.UserAccount) (*ProfileIcon, error) {
initials := string([]rune(obj.FirstName)[0]) + string([]rune(obj.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
return profileIcon, nil
}
// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Organization returns OrganizationResolver implementation.
func (r *Resolver) Organization() OrganizationResolver { return &organizationResolver{r} }
// Project returns ProjectResolver implementation.
func (r *Resolver) Project() ProjectResolver { return &projectResolver{r} }
@ -259,13 +354,26 @@ func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
// TaskGroup returns TaskGroupResolver implementation.
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
// Team returns TeamResolver implementation.
func (r *Resolver) Team() TeamResolver { return &teamResolver{r} }
// TaskLabel returns TaskLabelResolver implementation.
func (r *Resolver) TaskLabel() TaskLabelResolver { return &taskLabelResolver{r} }
// UserAccount returns UserAccountResolver implementation.
func (r *Resolver) UserAccount() UserAccountResolver { return &userAccountResolver{r} }
type mutationResolver struct{ *Resolver }
type organizationResolver struct{ *Resolver }
type projectResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }
type taskGroupResolver struct{ *Resolver }
type teamResolver struct{ *Resolver }
type taskLabelResolver struct{ *Resolver }
type userAccountResolver struct{ *Resolver }
// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
// it when you're done.
// - You have helper methods in this file. Move them out to keep these resolver files clean.
func (r *userAccountResolver) DisplayName(ctx context.Context, obj *pg.UserAccount) (string, error) {
return obj.FirstName + " " + obj.LastName, nil
}

View File

@ -1,7 +1,8 @@
CREATE TABLE user_account (
user_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at timestamptz NOT NULL,
display_name text NOT NULL,
first_name text NOT NULL,
last_name text NOT NULL,
email text NOT NULL UNIQUE,
username text NOT NULL UNIQUE,
password_hash text NOT NULL

View File

@ -0,0 +1,6 @@
CREATE TABLE task_assigned (
task_assigned_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
task_id uuid NOT NULL REFERENCES task(task_id),
user_id uuid NOT NULL REFERENCES user_account(user_id),
assigned_date timestamptz NOT NULL
);

View File

@ -0,0 +1 @@
ALTER TABLE task ADD COLUMN description text;

View File

@ -0,0 +1,5 @@
CREATE TABLE label_color (
label_color_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
color_hex TEXT NOT NULL,
position FLOAT NOT NULL
);

View File

@ -0,0 +1,6 @@
CREATE TABLE task_label (
task_label_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
task_id uuid NOT NULL REFERENCES task(task_id),
label_color_id uuid NOT NULL REFERENCES label_color(label_color_id),
assigned_date timestamptz NOT NULL
);

View File

@ -0,0 +1 @@
ALTER TABLE task ADD COLUMN due_date timestamptz;

View File

@ -0,0 +1 @@
ALTER TABLE project ADD COLUMN owner uuid NOT NULL;

21
api/pg/label_color.sql.go Normal file
View File

@ -0,0 +1,21 @@
// Code generated by sqlc. DO NOT EDIT.
// source: label_color.sql
package pg
import (
"context"
"github.com/google/uuid"
)
const getLabelColorByID = `-- name: GetLabelColorByID :one
SELECT label_color_id, color_hex, position FROM label_color WHERE label_color_id = $1
`
func (q *Queries) GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error) {
row := q.db.QueryRowContext(ctx, getLabelColorByID, labelColorID)
var i LabelColor
err := row.Scan(&i.LabelColorID, &i.ColorHex, &i.Position)
return i, err
}

View File

@ -3,11 +3,18 @@
package pg
import (
"database/sql"
"time"
"github.com/google/uuid"
)
type LabelColor struct {
LabelColorID uuid.UUID `json:"label_color_id"`
ColorHex string `json:"color_hex"`
Position float64 `json:"position"`
}
type Organization struct {
OrganizationID uuid.UUID `json:"organization_id"`
CreatedAt time.Time `json:"created_at"`
@ -19,6 +26,7 @@ type Project struct {
TeamID uuid.UUID `json:"team_id"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Owner uuid.UUID `json:"owner"`
}
type RefreshToken struct {
@ -34,6 +42,15 @@ type Task struct {
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Position float64 `json:"position"`
Description sql.NullString `json:"description"`
DueDate sql.NullTime `json:"due_date"`
}
type TaskAssigned struct {
TaskAssignedID uuid.UUID `json:"task_assigned_id"`
TaskID uuid.UUID `json:"task_id"`
UserID uuid.UUID `json:"user_id"`
AssignedDate time.Time `json:"assigned_date"`
}
type TaskGroup struct {
@ -44,6 +61,13 @@ type TaskGroup struct {
Position float64 `json:"position"`
}
type TaskLabel struct {
TaskLabelID uuid.UUID `json:"task_label_id"`
TaskID uuid.UUID `json:"task_id"`
LabelColorID uuid.UUID `json:"label_color_id"`
AssignedDate time.Time `json:"assigned_date"`
}
type Team struct {
TeamID uuid.UUID `json:"team_id"`
CreatedAt time.Time `json:"created_at"`
@ -54,7 +78,8 @@ type Team struct {
type UserAccount struct {
UserID uuid.UUID `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
DisplayName string `json:"display_name"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Username string `json:"username"`
PasswordHash string `json:"password_hash"`

View File

@ -41,11 +41,20 @@ type Repository interface {
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, error)
GetAllTasks(ctx context.Context) ([]Task, error)
GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error)
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescriptionParams) (Task, error)
CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error)
GetTaskLabelsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskLabel, error)
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error)
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
}
type repoSvc struct {

View File

@ -11,29 +11,36 @@ import (
)
const createProject = `-- name: CreateProject :one
INSERT INTO project(team_id, created_at, name) VALUES ($1, $2, $3) RETURNING project_id, team_id, created_at, name
INSERT INTO project(owner, team_id, created_at, name) VALUES ($1, $2, $3, $4) RETURNING project_id, team_id, created_at, name, owner
`
type CreateProjectParams struct {
Owner uuid.UUID `json:"owner"`
TeamID uuid.UUID `json:"team_id"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
}
func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) {
row := q.db.QueryRowContext(ctx, createProject, arg.TeamID, arg.CreatedAt, arg.Name)
row := q.db.QueryRowContext(ctx, createProject,
arg.Owner,
arg.TeamID,
arg.CreatedAt,
arg.Name,
)
var i Project
err := row.Scan(
&i.ProjectID,
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.Owner,
)
return i, err
}
const getAllProjects = `-- name: GetAllProjects :many
SELECT project_id, team_id, created_at, name FROM project
SELECT project_id, team_id, created_at, name, owner FROM project
`
func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) {
@ -50,6 +57,7 @@ func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) {
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.Owner,
); err != nil {
return nil, err
}
@ -65,7 +73,7 @@ func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) {
}
const getAllProjectsForTeam = `-- name: GetAllProjectsForTeam :many
SELECT project_id, team_id, created_at, name FROM project WHERE team_id = $1
SELECT project_id, team_id, created_at, name, owner FROM project WHERE team_id = $1
`
func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error) {
@ -82,6 +90,7 @@ func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) (
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.Owner,
); err != nil {
return nil, err
}
@ -97,7 +106,7 @@ func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) (
}
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, owner FROM project WHERE project_id = $1
`
func (q *Queries) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error) {
@ -108,6 +117,7 @@ func (q *Queries) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Proj
&i.TeamID,
&i.CreatedAt,
&i.Name,
&i.Owner,
)
return i, err
}

View File

@ -13,7 +13,9 @@ type Querier interface {
CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error)
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error)
CreateTaskGroup(ctx context.Context, arg CreateTaskGroupParams) (TaskGroup, error)
CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error)
CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error)
CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error)
DeleteExpiredTokens(ctx context.Context) error
@ -30,15 +32,20 @@ type Querier interface {
GetAllTasks(ctx context.Context) ([]Task, error)
GetAllTeams(ctx context.Context) ([]Team, error)
GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, error)
GetTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (TaskGroup, error)
GetTaskGroupsForProject(ctx context.Context, projectID uuid.UUID) ([]TaskGroup, error)
GetTaskLabelsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskLabel, error)
GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error)
GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error)
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescriptionParams) (Task, error)
UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)

View File

@ -5,6 +5,7 @@ package pg
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
@ -12,7 +13,7 @@ import (
const createTask = `-- name: CreateTask :one
INSERT INTO task (task_group_id, created_at, name, position)
VALUES($1, $2, $3, $4) RETURNING task_id, task_group_id, created_at, name, position
VALUES($1, $2, $3, $4) RETURNING task_id, task_group_id, created_at, name, position, description, due_date
`
type CreateTaskParams struct {
@ -36,6 +37,8 @@ func (q *Queries) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, e
&i.CreatedAt,
&i.Name,
&i.Position,
&i.Description,
&i.DueDate,
)
return i, err
}
@ -62,7 +65,7 @@ func (q *Queries) DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid
}
const getAllTasks = `-- name: GetAllTasks :many
SELECT task_id, task_group_id, created_at, name, position FROM task
SELECT task_id, task_group_id, created_at, name, position, description, due_date FROM task
`
func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
@ -80,6 +83,8 @@ func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
&i.CreatedAt,
&i.Name,
&i.Position,
&i.Description,
&i.DueDate,
); err != nil {
return nil, err
}
@ -94,8 +99,27 @@ func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
return items, nil
}
const getTaskByID = `-- name: GetTaskByID :one
SELECT task_id, task_group_id, created_at, name, position, description, due_date FROM task WHERE task_id = $1
`
func (q *Queries) GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, error) {
row := q.db.QueryRowContext(ctx, getTaskByID, taskID)
var i Task
err := row.Scan(
&i.TaskID,
&i.TaskGroupID,
&i.CreatedAt,
&i.Name,
&i.Position,
&i.Description,
&i.DueDate,
)
return i, err
}
const getTasksForTaskGroupID = `-- name: GetTasksForTaskGroupID :many
SELECT task_id, task_group_id, created_at, name, position FROM task WHERE task_group_id = $1
SELECT task_id, task_group_id, created_at, name, position, description, due_date FROM task WHERE task_group_id = $1
`
func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error) {
@ -113,6 +137,8 @@ func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.U
&i.CreatedAt,
&i.Name,
&i.Position,
&i.Description,
&i.DueDate,
); err != nil {
return nil, err
}
@ -127,8 +153,32 @@ func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.U
return items, nil
}
const updateTaskDescription = `-- name: UpdateTaskDescription :one
UPDATE task SET description = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date
`
type UpdateTaskDescriptionParams struct {
TaskID uuid.UUID `json:"task_id"`
Description sql.NullString `json:"description"`
}
func (q *Queries) UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescriptionParams) (Task, error) {
row := q.db.QueryRowContext(ctx, updateTaskDescription, arg.TaskID, arg.Description)
var i Task
err := row.Scan(
&i.TaskID,
&i.TaskGroupID,
&i.CreatedAt,
&i.Name,
&i.Position,
&i.Description,
&i.DueDate,
)
return i, err
}
const updateTaskLocation = `-- name: UpdateTaskLocation :one
UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position
UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date
`
type UpdateTaskLocationParams struct {
@ -146,12 +196,14 @@ func (q *Queries) UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocation
&i.CreatedAt,
&i.Name,
&i.Position,
&i.Description,
&i.DueDate,
)
return i, err
}
const updateTaskName = `-- name: UpdateTaskName :one
UPDATE task SET name = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position
UPDATE task SET name = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date
`
type UpdateTaskNameParams struct {
@ -168,6 +220,8 @@ func (q *Queries) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams)
&i.CreatedAt,
&i.Name,
&i.Position,
&i.Description,
&i.DueDate,
)
return i, err
}

View File

@ -0,0 +1,66 @@
// Code generated by sqlc. DO NOT EDIT.
// source: task_assigned.sql
package pg
import (
"context"
"time"
"github.com/google/uuid"
)
const createTaskAssigned = `-- name: CreateTaskAssigned :one
INSERT INTO task_assigned (task_id, user_id, assigned_date)
VALUES($1, $2, $3) RETURNING task_assigned_id, task_id, user_id, assigned_date
`
type CreateTaskAssignedParams struct {
TaskID uuid.UUID `json:"task_id"`
UserID uuid.UUID `json:"user_id"`
AssignedDate time.Time `json:"assigned_date"`
}
func (q *Queries) CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error) {
row := q.db.QueryRowContext(ctx, createTaskAssigned, arg.TaskID, arg.UserID, arg.AssignedDate)
var i TaskAssigned
err := row.Scan(
&i.TaskAssignedID,
&i.TaskID,
&i.UserID,
&i.AssignedDate,
)
return i, err
}
const getAssignedMembersForTask = `-- name: GetAssignedMembersForTask :many
SELECT task_assigned_id, task_id, user_id, assigned_date FROM task_assigned WHERE task_id = $1
`
func (q *Queries) GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error) {
rows, err := q.db.QueryContext(ctx, getAssignedMembersForTask, taskID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TaskAssigned
for rows.Next() {
var i TaskAssigned
if err := rows.Scan(
&i.TaskAssignedID,
&i.TaskID,
&i.UserID,
&i.AssignedDate,
); 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
}

66
api/pg/task_label.sql.go Normal file
View File

@ -0,0 +1,66 @@
// Code generated by sqlc. DO NOT EDIT.
// source: task_label.sql
package pg
import (
"context"
"time"
"github.com/google/uuid"
)
const createTaskLabelForTask = `-- name: CreateTaskLabelForTask :one
INSERT INTO task_label (task_id, label_color_id, assigned_date)
VALUES ($1, $2, $3) RETURNING task_label_id, task_id, label_color_id, assigned_date
`
type CreateTaskLabelForTaskParams struct {
TaskID uuid.UUID `json:"task_id"`
LabelColorID uuid.UUID `json:"label_color_id"`
AssignedDate time.Time `json:"assigned_date"`
}
func (q *Queries) CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error) {
row := q.db.QueryRowContext(ctx, createTaskLabelForTask, arg.TaskID, arg.LabelColorID, arg.AssignedDate)
var i TaskLabel
err := row.Scan(
&i.TaskLabelID,
&i.TaskID,
&i.LabelColorID,
&i.AssignedDate,
)
return i, err
}
const getTaskLabelsForTaskID = `-- name: GetTaskLabelsForTaskID :many
SELECT task_label_id, task_id, label_color_id, assigned_date FROM task_label WHERE task_id = $1
`
func (q *Queries) GetTaskLabelsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskLabel, error) {
rows, err := q.db.QueryContext(ctx, getTaskLabelsForTaskID, taskID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TaskLabel
for rows.Next() {
var i TaskLabel
if err := rows.Scan(
&i.TaskLabelID,
&i.TaskID,
&i.LabelColorID,
&i.AssignedDate,
); 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
}

View File

@ -11,13 +11,14 @@ import (
)
const createUserAccount = `-- name: CreateUserAccount :one
INSERT INTO user_account(display_name, email, username, created_at, password_hash)
VALUES ($1, $2, $3, $4, $5)
RETURNING user_id, created_at, display_name, email, username, password_hash
INSERT INTO user_account(first_name, last_name, email, username, created_at, password_hash)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING user_id, created_at, first_name, last_name, email, username, password_hash
`
type CreateUserAccountParams struct {
DisplayName string `json:"display_name"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Username string `json:"username"`
CreatedAt time.Time `json:"created_at"`
@ -26,7 +27,8 @@ type CreateUserAccountParams struct {
func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error) {
row := q.db.QueryRowContext(ctx, createUserAccount,
arg.DisplayName,
arg.FirstName,
arg.LastName,
arg.Email,
arg.Username,
arg.CreatedAt,
@ -36,7 +38,8 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.DisplayName,
&i.FirstName,
&i.LastName,
&i.Email,
&i.Username,
&i.PasswordHash,
@ -45,7 +48,7 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
}
const getAllUserAccounts = `-- name: GetAllUserAccounts :many
SELECT user_id, created_at, display_name, email, username, password_hash FROM user_account
SELECT user_id, created_at, first_name, last_name, email, username, password_hash FROM user_account
`
func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
@ -60,7 +63,8 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
if err := rows.Scan(
&i.UserID,
&i.CreatedAt,
&i.DisplayName,
&i.FirstName,
&i.LastName,
&i.Email,
&i.Username,
&i.PasswordHash,
@ -79,7 +83,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
}
const getUserAccountByID = `-- name: GetUserAccountByID :one
SELECT user_id, created_at, display_name, email, username, password_hash FROM user_account WHERE user_id = $1
SELECT user_id, created_at, first_name, last_name, email, username, password_hash FROM user_account WHERE user_id = $1
`
func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
@ -88,7 +92,8 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.DisplayName,
&i.FirstName,
&i.LastName,
&i.Email,
&i.Username,
&i.PasswordHash,
@ -97,7 +102,7 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use
}
const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one
SELECT user_id, created_at, display_name, email, username, password_hash FROM user_account WHERE username = $1
SELECT user_id, created_at, first_name, last_name, email, username, password_hash FROM user_account WHERE username = $1
`
func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
@ -106,7 +111,8 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string)
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.DisplayName,
&i.FirstName,
&i.LastName,
&i.Email,
&i.Username,
&i.PasswordHash,

View File

@ -0,0 +1,2 @@
-- name: GetLabelColorByID :one
SELECT * FROM label_color WHERE label_color_id = $1;

View File

@ -8,4 +8,4 @@ SELECT * FROM project WHERE team_id = $1;
SELECT * FROM project WHERE project_id = $1;
-- name: CreateProject :one
INSERT INTO project(team_id, created_at, name) VALUES ($1, $2, $3) RETURNING *;
INSERT INTO project(owner, team_id, created_at, name) VALUES ($1, $2, $3, $4) RETURNING *;

View File

@ -2,6 +2,12 @@
INSERT INTO task (task_group_id, created_at, name, position)
VALUES($1, $2, $3, $4) RETURNING *;
-- name: UpdateTaskDescription :one
UPDATE task SET description = $2 WHERE task_id = $1 RETURNING *;
-- name: GetTaskByID :one
SELECT * FROM task WHERE task_id = $1;
-- name: GetTasksForTaskGroupID :many
SELECT * FROM task WHERE task_group_id = $1;

View File

@ -0,0 +1,6 @@
-- name: CreateTaskAssigned :one
INSERT INTO task_assigned (task_id, user_id, assigned_date)
VALUES($1, $2, $3) RETURNING *;
-- name: GetAssignedMembersForTask :many
SELECT * FROM task_assigned WHERE task_id = $1;

6
api/query/task_label.sql Normal file
View File

@ -0,0 +1,6 @@
-- name: CreateTaskLabelForTask :one
INSERT INTO task_label (task_id, label_color_id, assigned_date)
VALUES ($1, $2, $3) RETURNING *;
-- name: GetTaskLabelsForTaskID :many
SELECT * FROM task_label WHERE task_id = $1;

View File

@ -8,6 +8,6 @@ SELECT * FROM user_account;
SELECT * FROM user_account WHERE username = $1;
-- name: CreateUserAccount :one
INSERT INTO user_account(display_name, email, username, created_at, password_hash)
VALUES ($1, $2, $3, $4, $5)
INSERT INTO user_account(first_name, last_name, email, username, created_at, password_hash)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *;

View File

@ -42,7 +42,7 @@ func (h *CitadelHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Requ
w.WriteHeader(http.StatusInternalServerError)
}
accessTokenString, err := NewAccessToken("1")
accessTokenString, err := NewAccessToken(token.UserID.String())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
@ -57,6 +57,25 @@ func (h *CitadelHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Requ
json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString})
}
func (h *CitadelHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("refreshToken")
if err != nil {
if err == http.ErrNoCookie {
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
refreshTokenID := uuid.MustParse(c.Value)
err = h.repo.DeleteRefreshTokenByID(r.Context(), refreshTokenID)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(LogoutResponseData{Status: "success"})
}
func (h *CitadelHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
var requestData LoginRequestData
err := json.NewDecoder(r.Body).Decode(&requestData)
@ -85,12 +104,11 @@ func (h *CitadelHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
return
}
userID := uuid.MustParse("0183d9ab-d0ed-4c9b-a3df-77a0cdd93dca")
refreshCreatedAt := time.Now().UTC()
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), pg.CreateRefreshTokenParams{userID, refreshCreatedAt, refreshExpiresAt})
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), pg.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
accessTokenString, err := NewAccessToken("1")
accessTokenString, err := NewAccessToken(user.UserID.String())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
@ -109,5 +127,6 @@ func (rs authResource) Routes(citadelHandler CitadelHandler) chi.Router {
r := chi.NewRouter()
r.Post("/login", citadelHandler.LoginHandler)
r.Post("/refresh_token", citadelHandler.RefreshTokenHandler)
r.Post("/logout", citadelHandler.LogoutHandler)
return r
}

View File

@ -5,6 +5,7 @@ import (
"net/http"
"strings"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
@ -40,7 +41,13 @@ func AuthenticationMiddleware(next http.Handler) http.Handler {
return
}
ctx := context.WithValue(r.Context(), "accessClaims", accessClaims)
userID, err := uuid.Parse(accessClaims.UserID)
if err != nil {
log.Error(err)
w.WriteHeader(http.StatusBadRequest)
return
}
ctx := context.WithValue(r.Context(), "userID", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})

View File

@ -23,6 +23,10 @@ type LoginResponseData struct {
AccessToken string `json:"accessToken"`
}
type LogoutResponseData struct {
Status string `json:"status"`
}
type RefreshTokenResponseData struct {
AccessToken string `json:"accessToken"`
}

View File

@ -20,6 +20,7 @@
"@testing-library/user-event": "^7.1.2",
"@types/color": "^3.0.1",
"@types/jest": "^24.0.0",
"@types/jwt-decode": "^2.2.1",
"@types/lodash": "^4.14.149",
"@types/node": "^12.0.0",
"@types/react": "^16.9.21",
@ -41,6 +42,7 @@
"graphql-tag": "^2.10.3",
"history": "^4.10.1",
"immer": "^6.0.3",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"prop-types": "^15.7.2",

View File

@ -7,6 +7,7 @@ export default createGlobalStyle`
height: 100%;
min-height: 100%;
min-width: 768px;
background: #262c49;
}
body {

View File

@ -1,9 +1,14 @@
import React from 'react';
import React, { useContext } from 'react';
import { Home, Stack } from 'shared/icons';
import Navbar, { ActionButton, ButtonContainer, PrimaryLogo } from 'shared/components/Navbar';
import { Link } from 'react-router-dom';
import UserIDContext from './context';
const GlobalNavbar = () => {
const { userID } = useContext(UserIDContext);
if (!userID) {
return null;
}
return (
<Navbar>
<PrimaryLogo />

View File

@ -12,14 +12,12 @@ type RoutesProps = {
};
const Routes = ({ history }: RoutesProps) => (
<Router history={history}>
<Switch>
<Route exact path="/login" component={Login} />
<Route exact path="/" component={Dashboard} />
<Route exact path="/projects" component={Projects} />
<Route path="/projects/:projectId" component={Project} />
<Route exact path="/login" component={Login} />
</Switch>
</Router>
);
export default Routes;

View File

@ -1,8 +1,14 @@
import React, { useState } from 'react';
import React, { useState, useContext } from 'react';
import TopNavbar from 'shared/components/TopNavbar';
import DropdownMenu from 'shared/components/DropdownMenu';
import { useHistory } from 'react-router';
import UserIDContext from 'App/context';
import { useMeQuery } from 'shared/generated/graphql';
const GlobalTopNavbar: React.FC = () => {
const { loading, data } = useMeQuery();
const history = useHistory();
const { userID, setUserID } = useContext(UserIDContext);
const [menu, setMenu] = useState({
top: 0,
left: 0,
@ -15,10 +21,33 @@ const GlobalTopNavbar: React.FC = () => {
top: bottom,
});
};
const onLogout = () => {
fetch('http://localhost:3333/auth/logout', {
method: 'POST',
credentials: 'include',
}).then(async x => {
const { status } = x;
if (status === 200) {
history.replace('/login');
setUserID(null);
}
});
};
if (!userID) {
return null;
}
console.log(data);
return (
<>
<TopNavbar onNotificationClick={() => console.log('beep')} onProfileClick={onProfileClick} />
{menu.isOpen && <DropdownMenu left={menu.left} top={menu.top} />}
<TopNavbar
firstName={data ? data.me.firstName : ''}
lastName={data ? data.me.lastName : ''}
initials={!data ? '' : data.me.profileIcon.initials ?? ''}
onNotificationClick={() => console.log('beep')}
onProfileClick={onProfileClick}
/>
{menu.isOpen && <DropdownMenu onLogout={onLogout} left={menu.left} top={menu.top} />}
</>
);
};

9
web/src/App/context.ts Normal file
View File

@ -0,0 +1,9 @@
import React from 'react';
type UserIDContextState = {
userID: string | null;
setUserID: (userID: string | null) => void;
};
export const UserIDContext = React.createContext<UserIDContextState>({ userID: null, setUserID: _userID => null });
export default UserIDContext;

View File

@ -1,22 +1,27 @@
import React, { useState, useEffect } from 'react';
import jwtDecode from 'jwt-decode';
import { createBrowserHistory } from 'history';
import { setAccessToken } from 'shared/utils/accessToken';
import Navbar from 'shared/components/Navbar';
import GlobalTopNavbar from 'App/TopNavbar';
import styled from 'styled-components';
import NormalizeStyles from './NormalizeStyles';
import BaseStyles from './BaseStyles';
import Routes from './Routes';
import { UserIDContext } from './context';
import Navbar from './Navbar';
import { Router } from 'react-router';
const history = createBrowserHistory();
const MainContent = styled.div`
padding: 0 0 50px 80px;
background: #262c49;
height: 100%;
`;
const App = () => {
const [loading, setLoading] = useState(true);
const [userID, setUserID] = useState<string | null>(null);
useEffect(() => {
fetch('http://localhost:3333/auth/refresh_token', {
@ -29,28 +34,33 @@ const App = () => {
} else {
const response: RefreshTokenResponse = await x.json();
const { accessToken } = response;
const claims: JWTToken = jwtDecode(accessToken);
setUserID(claims.userId);
setAccessToken(accessToken);
}
// }
setLoading(false);
});
}, []);
if (loading) {
return (
<>
<UserIDContext.Provider value={{ userID, setUserID }}>
<NormalizeStyles />
<BaseStyles />
<Router history={history}>
{loading ? (
<div>loading</div>
) : (
<>
<Navbar />
<MainContent>
<GlobalTopNavbar />
<Routes history={history} />
</MainContent>
</>
);
}
return (
<>
<NormalizeStyles />
<BaseStyles />
<Routes history={history} />
)}
</Router>
</UserIDContext.Provider>
</>
);
};

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useContext } from 'react';
import { useForm } from 'react-hook-form';
import { useHistory } from 'react-router';
@ -6,10 +6,13 @@ import { setAccessToken } from 'shared/utils/accessToken';
import Login from 'shared/components/Login';
import { Container, LoginWrapper } from './Styles';
import UserIDContext from 'App/context';
import JwtDecode from 'jwt-decode';
const Auth = () => {
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
const history = useHistory();
const { setUserID } = useContext(UserIDContext);
const login = (
data: LoginFormData,
setComplete: (val: boolean) => void,
@ -31,8 +34,11 @@ const Auth = () => {
} else {
const response = await x.json();
const { accessToken } = response;
setAccessToken(accessToken);
const claims: JWTToken = JwtDecode(accessToken);
setUserID(claims.userId);
setComplete(true);
setAccessToken(accessToken);
history.push('/');
}
});

View File

@ -0,0 +1,110 @@
import React, { useState, useContext } from 'react';
import Modal from 'shared/components/Modal';
import TaskDetails from 'shared/components/TaskDetails';
import PopupMenu from 'shared/components/PopupMenu';
import MemberManager from 'shared/components/MemberManager';
import { useRouteMatch, useHistory } from 'react-router';
import { useFindTaskQuery, useAssignTaskMutation } from 'shared/generated/graphql';
import UserIDContext from 'App/context';
type DetailsProps = {
taskID: string;
projectURL: string;
onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
onOpenAddLabelPopup: (task: Task, bounds: ElementBounds) => void;
availableMembers: Array<TaskUser>;
};
const initialMemberPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const Details: React.FC<DetailsProps> = ({
projectURL,
taskID,
onTaskNameChange,
onTaskDescriptionChange,
onDeleteTask,
onOpenAddLabelPopup,
availableMembers,
}) => {
const { userID } = useContext(UserIDContext);
const history = useHistory();
const match = useRouteMatch();
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
const { loading, data } = useFindTaskQuery({ variables: { taskID } });
const [assignTask] = useAssignTaskMutation();
if (loading) {
return <div>loading</div>;
}
if (!data) {
return <div>loading</div>;
}
const taskMembers = data.findTask.assigned.map(assigned => {
return {
userID: assigned.userID,
displayName: `${assigned.firstName} ${assigned.lastName}`,
profileIcon: {
url: null,
initials: assigned.profileIcon.initials ?? null,
},
};
});
return (
<>
<Modal
width={1040}
onClose={() => {
history.push(projectURL);
}}
renderContent={() => {
return (
<TaskDetails
task={{
...data.findTask,
members: taskMembers,
description: data.findTask.description ?? '',
labels: [],
}}
onTaskNameChange={onTaskNameChange}
onTaskDescriptionChange={onTaskDescriptionChange}
onDeleteTask={onDeleteTask}
onCloseModal={() => history.push(projectURL)}
onOpenAddMemberPopup={(task, bounds) => {
console.log(task, bounds);
setMemberPopupData({
isOpen: true,
taskID: task.taskID,
top: bounds.position.top + bounds.size.height + 10,
left: bounds.position.left,
});
}}
onOpenAddLabelPopup={onOpenAddLabelPopup}
/>
);
}}
/>
{memberPopupData.isOpen && (
<PopupMenu
title="Members"
top={memberPopupData.top}
onClose={() => setMemberPopupData(initialMemberPopupState)}
left={memberPopupData.left}
>
<MemberManager
availableMembers={availableMembers}
activeMembers={[]}
onMemberChange={(member, isActive) => {
if (isActive) {
assignTask({ variables: { taskID: data.findTask.taskID, userID: userID ?? '' } });
}
console.log(member, isActive);
}}
/>
</PopupMenu>
)}
</>
);
};
export default Details;

View File

@ -11,18 +11,17 @@ import {
useUpdateTaskGroupLocationMutation,
useCreateTaskGroupMutation,
useDeleteTaskGroupMutation,
useUpdateTaskDescriptionMutation,
useAssignTaskMutation,
} from 'shared/generated/graphql';
import Navbar from 'App/Navbar';
import TopNavbar from 'App/TopNavbar';
import QuickCardEditor from 'shared/components/QuickCardEditor';
import PopupMenu from 'shared/components/PopupMenu';
import ListActions from 'shared/components/ListActions';
import Modal from 'shared/components/Modal';
import TaskDetails from 'shared/components/TaskDetails';
import MemberManager from 'shared/components/MemberManager';
import { LabelsPopup } from 'shared/components/PopupMenu/PopupMenu.stories';
import KanbanBoard from 'Projects/Project/KanbanBoard';
import Details from './Details';
type TaskRouteProps = {
taskID: string;
@ -35,17 +34,6 @@ interface QuickCardEditorState {
task?: Task;
}
const MainContent = styled.div`
padding: 0 0 50px 80px;
height: 100%;
background: #262c49;
`;
const Wrapper = styled.div`
font-size: 16px;
background-color: red;
`;
const TitleWrapper = styled.div`
margin-left: 38px;
margin-bottom: 15px;
@ -64,7 +52,6 @@ interface ProjectParams {
const initialState: BoardState = { tasks: {}, columns: {} };
const initialPopupState = { left: 0, top: 0, isOpen: false, taskGroupID: '' };
const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top: 0, left: 0 };
const initialMemberPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const initialLabelsPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const initialTaskDetailsState = { isOpen: false, taskID: '' };
@ -73,9 +60,9 @@ const Project = () => {
const match = useRouteMatch();
const history = useHistory();
const [updateTaskDescription] = useUpdateTaskDescriptionMutation();
const [listsData, setListsData] = useState(initialState);
const [popupData, setPopupData] = useState(initialPopupState);
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
const [taskDetails, setTaskDetails] = useState(initialTaskDetailsState);
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
const [updateTaskLocation] = useUpdateTaskLocationMutation();
@ -142,6 +129,7 @@ const Project = () => {
name: task.name,
position: task.position,
labels: [],
description: task.description ?? undefined,
};
});
});
@ -199,17 +187,24 @@ const Project = () => {
createTaskGroup({ variables: { projectID: projectId, name: listName, position } });
};
const [assignTask] = useAssignTaskMutation();
if (loading) {
return <Wrapper>Loading</Wrapper>;
return <Title>Error Loading</Title>;
}
if (data) {
const availableMembers = data.findProject.members.map(member => {
return {
displayName: `${member.firstName} ${member.lastName}`,
profileIcon: { url: null, initials: member.profileIcon.initials ?? null },
userID: member.userID,
};
});
return (
<>
<Navbar />
<MainContent>
<TopNavbar />
<TitleWrapper>
<Title>{data.findProject.name}</Title>
</TitleWrapper>
<KanbanBoard
listsData={listsData}
onCardDrop={onCardDrop}
@ -221,8 +216,6 @@ const Project = () => {
setPopupData({ isOpen, top, left, taskGroupID });
}}
/>
</TitleWrapper>
</MainContent>
{popupData.isOpen && (
<PopupMenu
title="List Actions"
@ -259,50 +252,28 @@ const Project = () => {
<Route
path={`${match.path}/c/:taskID`}
render={(routeProps: RouteComponentProps<TaskRouteProps>) => (
<Modal
width={1040}
onClose={() => {
history.push(match.url);
}}
renderContent={() => {
const task = listsData.tasks[routeProps.match.params.taskID];
if (!task) {
return <div>loading</div>;
}
return (
<TaskDetails
task={task}
<Details
availableMembers={availableMembers}
projectURL={match.url}
taskID={routeProps.match.params.taskID}
onTaskNameChange={(updatedTask, newName) => {
updateTaskName({ variables: { taskID: updatedTask.taskID, name: newName } });
}}
onTaskDescriptionChange={(updatedTask, newDescription) => {
console.log(updatedTask, newDescription);
updateTaskDescription({ variables: { taskID: updatedTask.taskID, description: newDescription } });
}}
onDeleteTask={deletedTask => {
setTaskDetails(initialTaskDetailsState);
deleteTask({ variables: { taskID: deletedTask.taskID } });
}}
onCloseModal={() => history.push(match.url)}
onOpenAddMemberPopup={(task, bounds) => {
console.log(task, bounds);
setMemberPopupData({
isOpen: true,
taskID: task.taskID,
top: bounds.position.top + bounds.size.height + 10,
left: bounds.position.left,
});
}}
onOpenAddLabelPopup={(task, bounds) => {}}
/>
);
}}
/>
)}
/>
</>
);
}
return <Wrapper>Error</Wrapper>;
return <div>Error</div>;
};
export default Project;

View File

@ -2,7 +2,6 @@ import React, { useState } from 'react';
import styled from 'styled-components/macro';
import { useGetProjectsQuery } from 'shared/generated/graphql';
import TopNavbar from 'App/TopNavbar';
import ProjectGridItem from 'shared/components/ProjectGridItem';
import { Link } from 'react-router-dom';
import Navbar from 'App/Navbar';
@ -15,59 +14,42 @@ const MainContent = styled.div`
const ProjectGrid = styled.div`
width: 60%;
max-width: 780px;
margin: 25px auto;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
`;
const Wrapper = styled.div`
font-size: 16px;
background-color: red;
const ProjectLink = styled(Link)`
flex: 1 0 33%;
margin-bottom: 20px;
`;
const Projects = () => {
const { loading, data } = useGetProjectsQuery();
console.log(loading, data);
if (loading) {
return (
<>
<Navbar />
<MainContent>
<TopNavbar />
</MainContent>
<span>loading</span>
</>
);
}
if (data) {
const { teams } = data.organizations[0];
const projects: Project[] = [];
teams.forEach(team =>
team.projects.forEach(project => {
projects.push({
taskGroups: [],
projectID: project.projectID,
teamTitle: team.name,
name: project.name,
color: '#aa62e3',
});
}),
);
const { projects } = data;
return (
<>
<Navbar />
<MainContent>
<TopNavbar />
<ProjectGrid>
{projects.map(project => (
<Link to={`/projects/${project.projectID}`}>
<ProjectGridItem project={project} />
</Link>
<ProjectLink key={project.projectID} to={`/projects/${project.projectID}`}>
<ProjectGridItem project={{ ...project, teamTitle: project.team.name, taskGroups: [] }} />
</ProjectLink>
))}
</ProjectGrid>
</MainContent>
</>
);
}
return <Wrapper>Error</Wrapper>;
return <div>Error!</div>;
};
export default Projects;

11
web/src/citadel.d.ts vendored
View File

@ -1,3 +1,8 @@
interface JWTToken {
userId: string;
iat: string;
exp: string;
}
interface ColumnState {
[key: string]: TaskGroup;
}
@ -29,9 +34,15 @@ type InnerTaskGroup = {
position?: number;
};
type ProfileIcon = {
url: string | null;
initials: string | null;
};
type TaskUser = {
userID: string;
displayName: string;
profileIcon: ProfileIcon;
};
type Task = {

View File

@ -2,6 +2,7 @@ import React, { createRef, useState } from 'react';
import styled from 'styled-components';
import DropdownMenu from '.';
import { action } from '@storybook/addon-actions';
export default {
component: DropdownMenu,
@ -49,7 +50,7 @@ export const Default = () => {
Click me
</Button>
</Container>
{menu.isOpen && <DropdownMenu left={menu.left} top={menu.top} />}
{menu.isOpen && <DropdownMenu onLogout={action('on logout')} left={menu.left} top={menu.top} />}
</>
);
};

View File

@ -6,9 +6,10 @@ import { Separator, Container, WrapperDiamond, Wrapper, ActionsList, ActionItem,
type DropdownMenuProps = {
left: number;
top: number;
onLogout: () => void;
};
const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top }) => {
const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top, onLogout }) => {
return (
<Container left={left} top={top}>
<Wrapper>
@ -18,7 +19,7 @@ const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top }) => {
</ActionItem>
<Separator />
<ActionsList>
<ActionItem>
<ActionItem onClick={onLogout}>
<Exit size={16} color="#c2c6dc" />
<ActionTitle>Logout</ActionTitle>
</ActionItem>

View File

@ -23,7 +23,7 @@ export const Default = () => {
position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
description: 'hello!',
members: [{ userID: '1', displayName: 'Jordan Knott' }],
members: [{ userID: '1', profileIcon: { url: null, initials: null }, displayName: 'Jordan Knott' }],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}

View File

@ -55,7 +55,7 @@ const Login = ({ onSubmit }: LoginProps) => {
<FormLabel htmlFor="password">
Password
<FormTextInput
type="text"
type="password"
id="password"
name="password"
ref={register({ required: 'Password is required' })}

View File

@ -114,19 +114,15 @@ export const MemberManagerPopup = () => {
{popupData.isOpen && (
<PopupMenu title="Members" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<MemberManager
availableMembers={[{ userID: '1', displayName: 'Jordan Knott' }]}
availableMembers={[
{ userID: '1', displayName: 'Jordan Knott', profileIcon: { url: null, initials: null } },
]}
activeMembers={[]}
onMemberChange={action('member change')}
/>
</PopupMenu>
)}
<span
style={{
width: '60px',
textAlign: 'center',
margin: '25px auto',
cursor: 'pointer',
}}
ref={$buttonRef}
onClick={() => {
if ($buttonRef && $buttonRef.current) {
@ -162,7 +158,7 @@ export const DueDateManagerPopup = () => {
position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
description: 'hello!',
members: [{ userID: '1', displayName: 'Jordan Knott' }],
members: [{ userID: '1', profileIcon: { url: null, initials: null }, displayName: 'Jordan Knott' }],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}

View File

@ -4,27 +4,22 @@ import { mixin } from 'shared/utils/styles';
export const ProjectContent = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;
export const ProjectTitle = styled.span`
font-size: 18px;
font-weight: 700;
transition: transform 0.25s ease;
text-align: center;
`;
export const TeamTitle = styled.span`
margin-top: 5px;
font-size: 14px;
font-weight: normal;
text-align: center;
color: #c2c6dc;
`;
export const ProjectWrapper = styled.div<{ color: string }>`
display: flex;
align-items: center;
padding: 15px 25px;
border-radius: 20px;
${mixin.boxShadowCard}
@ -32,11 +27,10 @@ export const ProjectWrapper = styled.div<{ color: string }>`
color: #fff;
cursor: pointer;
margin: 0 10px;
width: 120px;
height: 120px;
align-items: center;
justify-content: center;
width: 240px;
height: 100px;
transition: transform 0.25s ease;
align-items: center;
&:hover {
transform: translateY(-5px);

View File

@ -35,7 +35,7 @@ export const Default = () => {
position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
description,
members: [{ userID: '1', displayName: 'Jordan Knott' }],
members: [{ userID: '1', profileIcon: { url: null, initials: null }, displayName: 'Jordan Knott' }],
}}
onTaskNameChange={action('task name change')}
onTaskDescriptionChange={(_task, desc) => setDescription(desc)}

View File

@ -97,9 +97,9 @@ type TaskDetailsProps = {
onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
onCloseModal: () => void;
onOpenAddMemberPopup: (task: Task, bounds: ElementBounds) => void;
onOpenAddLabelPopup: (task: Task, bounds: ElementBounds) => void;
onCloseModal: () => void;
};
const TaskDetails: React.FC<TaskDetailsProps> = ({
@ -112,6 +112,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onOpenAddLabelPopup,
}) => {
const [editorOpen, setEditorOpen] = useState(false);
const [description, setDescription] = useState(task.description ?? '');
const [taskName, setTaskName] = useState(task.name);
const handleClick = () => {
setEditorOpen(!editorOpen);
@ -141,6 +142,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onOpenAddLabelPopup(task, bounds);
}
};
console.log(task);
return (
<>
<TaskActions>
@ -172,9 +174,10 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<TaskDetailsLabel>Description</TaskDetailsLabel>
{editorOpen ? (
<DetailsEditor
description={task.description ?? ''}
description={description}
onTaskDescriptionChange={newDescription => {
setEditorOpen(false);
setDescription(newDescription);
onTaskDescriptionChange(task, newDescription);
}}
onCancel={() => {
@ -182,7 +185,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
}}
/>
) : (
<TaskContent description={task.description ?? ''} onEditContent={handleClick} />
<TaskContent description={description} onEditContent={handleClick} />
)}
</TaskDetailsContent>
<TaskDetailsSidebar>
@ -190,10 +193,10 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<TaskDetailAssignees>
{task.members &&
task.members.map(member => {
const initials = 'JK';
console.log(member);
return (
<TaskDetailAssignee key={member.userID}>
<ProfileIcon>{initials}</ProfileIcon>
<ProfileIcon>{member.profileIcon.initials ?? ''}</ProfileIcon>
</TaskDetailAssignee>
);
})}

View File

@ -37,8 +37,14 @@ export const Default = () => {
<>
<NormalizeStyles />
<BaseStyles />
<TopNavbar onNotificationClick={action('notifications click')} onProfileClick={onClick} />
{menu.isOpen && <DropdownMenu left={menu.left} top={menu.top} />}
<TopNavbar
firstName="Jordan"
lastName="Knott"
initials="JK"
onNotificationClick={action('notifications click')}
onProfileClick={onClick}
/>
{menu.isOpen && <DropdownMenu onLogout={action('on logout')} left={menu.left} top={menu.top} />}
</>
);
};

View File

@ -19,8 +19,11 @@ import {
type NavBarProps = {
onProfileClick: (bottom: number, right: number) => void;
onNotificationClick: () => void;
firstName: string;
lastName: string;
initials: string;
};
const NavBar: React.FC<NavBarProps> = ({ onProfileClick, onNotificationClick }) => {
const NavBar: React.FC<NavBarProps> = ({ onProfileClick, onNotificationClick, firstName, lastName, initials }) => {
const $profileRef: any = useRef(null);
const handleProfileClick = () => {
console.log('click');
@ -45,11 +48,13 @@ const NavBar: React.FC<NavBarProps> = ({ onProfileClick, onNotificationClick })
</NotificationContainer>
<ProfileContainer>
<ProfileNameWrapper>
<ProfileNamePrimary>Jordan Knott</ProfileNamePrimary>
<ProfileNamePrimary>
{firstName} {lastName}
</ProfileNamePrimary>
<ProfileNameSecondary>Manager</ProfileNameSecondary>
</ProfileNameWrapper>
<ProfileIcon ref={$profileRef} onClick={handleProfileClick}>
JK
{initials}
</ProfileIcon>
</ProfileContainer>
</GlobalActions>

View File

@ -15,6 +15,27 @@ export type Scalars = {
export type TaskLabel = {
__typename?: 'TaskLabel';
taskLabelID: Scalars['ID'];
labelColorID: Scalars['UUID'];
colorHex: Scalars['String'];
};
export type ProfileIcon = {
__typename?: 'ProfileIcon';
url?: Maybe<Scalars['String']>;
initials?: Maybe<Scalars['String']>;
};
export type ProjectMember = {
__typename?: 'ProjectMember';
userID: Scalars['ID'];
firstName: Scalars['String'];
lastName: Scalars['String'];
profileIcon: ProfileIcon;
};
export type RefreshToken = {
__typename?: 'RefreshToken';
tokenId: Scalars['ID'];
@ -28,16 +49,10 @@ export type UserAccount = {
userID: Scalars['ID'];
email: Scalars['String'];
createdAt: Scalars['Time'];
displayName: Scalars['String'];
firstName: Scalars['String'];
lastName: Scalars['String'];
username: Scalars['String'];
};
export type Organization = {
__typename?: 'Organization';
organizationID: Scalars['ID'];
createdAt: Scalars['Time'];
name: Scalars['String'];
teams: Array<Team>;
profileIcon: ProfileIcon;
};
export type Team = {
@ -45,16 +60,17 @@ export type Team = {
teamID: Scalars['ID'];
createdAt: Scalars['Time'];
name: Scalars['String'];
projects: Array<Project>;
};
export type Project = {
__typename?: 'Project';
projectID: Scalars['ID'];
teamID: Scalars['String'];
createdAt: Scalars['Time'];
name: Scalars['String'];
team: Team;
owner: ProjectMember;
taskGroups: Array<TaskGroup>;
members: Array<ProjectMember>;
};
export type TaskGroup = {
@ -74,6 +90,9 @@ export type Task = {
createdAt: Scalars['Time'];
name: Scalars['String'];
position: Scalars['Float'];
description?: Maybe<Scalars['String']>;
assigned: Array<ProjectMember>;
labels: Array<TaskLabel>;
};
export type ProjectsFilter = {
@ -88,15 +107,19 @@ export type FindProject = {
projectId: Scalars['String'];
};
export type FindTask = {
taskID: Scalars['UUID'];
};
export type Query = {
__typename?: 'Query';
organizations: Array<Organization>;
users: Array<UserAccount>;
findUser: UserAccount;
findProject: Project;
teams: Array<Team>;
findTask: Task;
projects: Array<Project>;
taskGroups: Array<TaskGroup>;
me: UserAccount;
};
@ -110,6 +133,11 @@ export type QueryFindProjectArgs = {
};
export type QueryFindTaskArgs = {
input: FindTask;
};
export type QueryProjectsArgs = {
input?: Maybe<ProjectsFilter>;
};
@ -121,7 +149,8 @@ export type NewRefreshToken = {
export type NewUserAccount = {
username: Scalars['String'];
email: Scalars['String'];
displayName: Scalars['String'];
firstName: Scalars['String'];
lastName: Scalars['String'];
password: Scalars['String'];
};
@ -131,7 +160,8 @@ export type NewTeam = {
};
export type NewProject = {
teamID: Scalars['String'];
userID: Scalars['UUID'];
teamID: Scalars['UUID'];
name: Scalars['String'];
};
@ -141,10 +171,6 @@ export type NewTaskGroup = {
position: Scalars['Float'];
};
export type NewOrganization = {
name: Scalars['String'];
};
export type LogoutUser = {
userID: Scalars['String'];
};
@ -191,20 +217,43 @@ export type DeleteTaskGroupPayload = {
taskGroup: TaskGroup;
};
export type AssignTaskInput = {
taskID: Scalars['UUID'];
userID: Scalars['UUID'];
};
export type UpdateTaskDescriptionInput = {
taskID: Scalars['UUID'];
description: Scalars['String'];
};
export type AddTaskLabelInput = {
taskID: Scalars['UUID'];
labelColorID: Scalars['UUID'];
};
export type RemoveTaskLabelInput = {
taskID: Scalars['UUID'];
taskLabelID: Scalars['UUID'];
};
export type Mutation = {
__typename?: 'Mutation';
createRefreshToken: RefreshToken;
createUserAccount: UserAccount;
createOrganization: Organization;
createTeam: Team;
createProject: Project;
createTaskGroup: TaskGroup;
updateTaskGroupLocation: TaskGroup;
deleteTaskGroup: DeleteTaskGroupPayload;
addTaskLabel: Task;
removeTaskLabel: Task;
createTask: Task;
updateTaskDescription: Task;
updateTaskLocation: Task;
updateTaskName: Task;
deleteTask: DeleteTaskPayload;
assignTask: Task;
logoutUser: Scalars['Boolean'];
};
@ -219,11 +268,6 @@ export type MutationCreateUserAccountArgs = {
};
export type MutationCreateOrganizationArgs = {
input: NewOrganization;
};
export type MutationCreateTeamArgs = {
input: NewTeam;
};
@ -249,11 +293,26 @@ export type MutationDeleteTaskGroupArgs = {
};
export type MutationAddTaskLabelArgs = {
input?: Maybe<AddTaskLabelInput>;
};
export type MutationRemoveTaskLabelArgs = {
input?: Maybe<RemoveTaskLabelInput>;
};
export type MutationCreateTaskArgs = {
input: NewTask;
};
export type MutationUpdateTaskDescriptionArgs = {
input: UpdateTaskDescriptionInput;
};
export type MutationUpdateTaskLocationArgs = {
input: NewTaskLocation;
};
@ -269,10 +328,33 @@ export type MutationDeleteTaskArgs = {
};
export type MutationAssignTaskArgs = {
input?: Maybe<AssignTaskInput>;
};
export type MutationLogoutUserArgs = {
input: LogoutUser;
};
export type AssignTaskMutationVariables = {
taskID: Scalars['UUID'];
userID: Scalars['UUID'];
};
export type AssignTaskMutation = (
{ __typename?: 'Mutation' }
& { assignTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID'>
& { assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
)> }
) }
);
export type CreateTaskMutationVariables = {
taskGroupID: Scalars['String'];
name: Scalars['String'];
@ -351,36 +433,92 @@ export type FindProjectQuery = (
& { findProject: (
{ __typename?: 'Project' }
& Pick<Project, 'name'>
& { taskGroups: Array<(
& { members: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials'>
) }
)>, taskGroups: Array<(
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID' | 'name' | 'position'>
& { tasks: Array<(
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'position'>
& Pick<Task, 'taskID' | 'name' | 'position' | 'description'>
)> }
)> }
) }
);
export type FindTaskQueryVariables = {
taskID: Scalars['UUID'];
};
export type FindTaskQuery = (
{ __typename?: 'Query' }
& { findTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'description' | 'position'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID'>
), assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials'>
) }
)> }
) }
);
export type GetProjectsQueryVariables = {};
export type GetProjectsQuery = (
{ __typename?: 'Query' }
& { organizations: Array<(
{ __typename?: 'Organization' }
& Pick<Organization, 'name'>
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'name'>
& { projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'name' | 'projectID'>
)> }
)> }
& Pick<Project, 'projectID' | 'name'>
& { team: (
{ __typename?: 'Team' }
& Pick<Team, 'teamID' | 'name'>
) }
)> }
);
export type MeQueryVariables = {};
export type MeQuery = (
{ __typename?: 'Query' }
& { me: (
{ __typename?: 'UserAccount' }
& Pick<UserAccount, 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'initials'>
) }
) }
);
export type UpdateTaskDescriptionMutationVariables = {
taskID: Scalars['UUID'];
description: Scalars['String'];
};
export type UpdateTaskDescriptionMutation = (
{ __typename?: 'Mutation' }
& { updateTaskDescription: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID'>
) }
);
export type UpdateTaskGroupLocationMutationVariables = {
taskGroupID: Scalars['UUID'];
position: Scalars['Float'];
@ -425,6 +563,44 @@ export type UpdateTaskNameMutation = (
);
export const AssignTaskDocument = gql`
mutation assignTask($taskID: UUID!, $userID: UUID!) {
assignTask(input: {taskID: $taskID, userID: $userID}) {
assigned {
userID
firstName
lastName
}
taskID
}
}
`;
export type AssignTaskMutationFn = ApolloReactCommon.MutationFunction<AssignTaskMutation, AssignTaskMutationVariables>;
/**
* __useAssignTaskMutation__
*
* To run a mutation, you first call `useAssignTaskMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useAssignTaskMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [assignTaskMutation, { data, loading, error }] = useAssignTaskMutation({
* variables: {
* taskID: // value for 'taskID'
* userID: // value for 'userID'
* },
* });
*/
export function useAssignTaskMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<AssignTaskMutation, AssignTaskMutationVariables>) {
return ApolloReactHooks.useMutation<AssignTaskMutation, AssignTaskMutationVariables>(AssignTaskDocument, baseOptions);
}
export type AssignTaskMutationHookResult = ReturnType<typeof useAssignTaskMutation>;
export type AssignTaskMutationResult = ApolloReactCommon.MutationResult<AssignTaskMutation>;
export type AssignTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<AssignTaskMutation, AssignTaskMutationVariables>;
export const CreateTaskDocument = gql`
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) {
@ -576,6 +752,15 @@ export const FindProjectDocument = gql`
query findProject($projectId: String!) {
findProject(input: {projectId: $projectId}) {
name
members {
userID
firstName
lastName
profileIcon {
url
initials
}
}
taskGroups {
taskGroupID
name
@ -584,6 +769,7 @@ export const FindProjectDocument = gql`
taskID
name
position
description
}
}
}
@ -615,16 +801,62 @@ export function useFindProjectLazyQuery(baseOptions?: ApolloReactHooks.LazyQuery
export type FindProjectQueryHookResult = ReturnType<typeof useFindProjectQuery>;
export type FindProjectLazyQueryHookResult = ReturnType<typeof useFindProjectLazyQuery>;
export type FindProjectQueryResult = ApolloReactCommon.QueryResult<FindProjectQuery, FindProjectQueryVariables>;
export const FindTaskDocument = gql`
query findTask($taskID: UUID!) {
findTask(input: {taskID: $taskID}) {
taskID
name
description
position
taskGroup {
taskGroupID
}
assigned {
userID
firstName
lastName
profileIcon {
url
initials
}
}
}
}
`;
/**
* __useFindTaskQuery__
*
* To run a query within a React component, call `useFindTaskQuery` and pass it any options that fit your needs.
* When your component renders, `useFindTaskQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindTaskQuery({
* variables: {
* taskID: // value for 'taskID'
* },
* });
*/
export function useFindTaskQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<FindTaskQuery, FindTaskQueryVariables>) {
return ApolloReactHooks.useQuery<FindTaskQuery, FindTaskQueryVariables>(FindTaskDocument, baseOptions);
}
export function useFindTaskLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<FindTaskQuery, FindTaskQueryVariables>) {
return ApolloReactHooks.useLazyQuery<FindTaskQuery, FindTaskQueryVariables>(FindTaskDocument, baseOptions);
}
export type FindTaskQueryHookResult = ReturnType<typeof useFindTaskQuery>;
export type FindTaskLazyQueryHookResult = ReturnType<typeof useFindTaskLazyQuery>;
export type FindTaskQueryResult = ApolloReactCommon.QueryResult<FindTaskQuery, FindTaskQueryVariables>;
export const GetProjectsDocument = gql`
query getProjects {
organizations {
name
teams {
name
projects {
name
projectID
}
name
team {
teamID
name
}
}
}
@ -654,6 +886,75 @@ export function useGetProjectsLazyQuery(baseOptions?: ApolloReactHooks.LazyQuery
export type GetProjectsQueryHookResult = ReturnType<typeof useGetProjectsQuery>;
export type GetProjectsLazyQueryHookResult = ReturnType<typeof useGetProjectsLazyQuery>;
export type GetProjectsQueryResult = ApolloReactCommon.QueryResult<GetProjectsQuery, GetProjectsQueryVariables>;
export const MeDocument = gql`
query me {
me {
firstName
lastName
profileIcon {
initials
}
}
}
`;
/**
* __useMeQuery__
*
* To run a query within a React component, call `useMeQuery` and pass it any options that fit your needs.
* When your component renders, `useMeQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useMeQuery({
* variables: {
* },
* });
*/
export function useMeQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<MeQuery, MeQueryVariables>) {
return ApolloReactHooks.useQuery<MeQuery, MeQueryVariables>(MeDocument, baseOptions);
}
export function useMeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<MeQuery, MeQueryVariables>) {
return ApolloReactHooks.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, baseOptions);
}
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
export type MeQueryResult = ApolloReactCommon.QueryResult<MeQuery, MeQueryVariables>;
export const UpdateTaskDescriptionDocument = gql`
mutation updateTaskDescription($taskID: UUID!, $description: String!) {
updateTaskDescription(input: {taskID: $taskID, description: $description}) {
taskID
}
}
`;
export type UpdateTaskDescriptionMutationFn = ApolloReactCommon.MutationFunction<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>;
/**
* __useUpdateTaskDescriptionMutation__
*
* To run a mutation, you first call `useUpdateTaskDescriptionMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateTaskDescriptionMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateTaskDescriptionMutation, { data, loading, error }] = useUpdateTaskDescriptionMutation({
* variables: {
* taskID: // value for 'taskID'
* description: // value for 'description'
* },
* });
*/
export function useUpdateTaskDescriptionMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>(UpdateTaskDescriptionDocument, baseOptions);
}
export type UpdateTaskDescriptionMutationHookResult = ReturnType<typeof useUpdateTaskDescriptionMutation>;
export type UpdateTaskDescriptionMutationResult = ApolloReactCommon.MutationResult<UpdateTaskDescriptionMutation>;
export type UpdateTaskDescriptionMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>;
export const UpdateTaskGroupLocationDocument = gql`
mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) {
updateTaskGroupLocation(input: {taskGroupID: $taskGroupID, position: $position}) {

View File

@ -0,0 +1,10 @@
mutation assignTask($taskID: UUID!, $userID: UUID!) {
assignTask(input: {taskID: $taskID, userID: $userID}) {
assigned {
userID
firstName
lastName
}
taskID
}
}

View File

@ -1,6 +1,15 @@
query findProject($projectId: String!) {
findProject(input: { projectId: $projectId }) {
name
members {
userID
firstName
lastName
profileIcon {
url
initials
}
}
taskGroups {
taskGroupID
name
@ -9,6 +18,7 @@ query findProject($projectId: String!) {
taskID
name
position
description
}
}
}

View File

@ -0,0 +1,20 @@
query findTask($taskID: UUID!) {
findTask(input: {taskID: $taskID}) {
taskID
name
description
position
taskGroup {
taskGroupID
}
assigned {
userID
firstName
lastName
profileIcon {
url
initials
}
}
}
}

View File

@ -1,12 +1,10 @@
query getProjects {
organizations {
name
teams {
name
projects {
name
projectID
}
name
team {
teamID
name
}
}
}

View File

@ -0,0 +1,9 @@
query me {
me {
firstName
lastName
profileIcon {
initials
}
}
}

View File

@ -0,0 +1,5 @@
mutation updateTaskDescription($taskID: UUID!, $description: String!) {
updateTaskDescription(input: {taskID: $taskID, description: $description}) {
taskID
}
}

View File

@ -3104,6 +3104,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
"@types/jwt-decode@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@types/jwt-decode/-/jwt-decode-2.2.1.tgz#afdf5c527fcfccbd4009b5fd02d1e18241f2d2f2"
integrity sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A==
"@types/lodash@^4.14.149":
version "4.14.149"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440"
@ -10202,6 +10207,11 @@ jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
jwt-decode@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
integrity sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=
killable@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"