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 package graph
import ( import (
"context"
"net/http" "net/http"
"os" "os"
"time" "time"
@ -10,6 +11,7 @@ import (
"github.com/99designs/gqlgen/graphql/handler/lru" "github.com/99designs/gqlgen/graphql/handler/lru"
"github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground" "github.com/99designs/gqlgen/graphql/playground"
"github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/pg" "github.com/jordanknott/project-citadel/api/pg"
) )
@ -45,3 +47,7 @@ func NewHandler(repo pg.Repository) http.Handler {
func NewPlaygroundHandler(endpoint string) http.Handler { func NewPlaygroundHandler(endpoint string) http.Handler {
return playground.Handler("GraphQL Playground", endpoint) 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" "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 { type DeleteTaskGroupInput struct {
TaskGroupID uuid.UUID `json:"taskGroupID"` TaskGroupID uuid.UUID `json:"taskGroupID"`
} }
@ -29,6 +39,10 @@ type FindProject struct {
ProjectID string `json:"projectId"` ProjectID string `json:"projectId"`
} }
type FindTask struct {
TaskID uuid.UUID `json:"taskID"`
}
type FindUser struct { type FindUser struct {
UserID string `json:"userId"` UserID string `json:"userId"`
} }
@ -37,12 +51,9 @@ type LogoutUser struct {
UserID string `json:"userID"` UserID string `json:"userID"`
} }
type NewOrganization struct {
Name string `json:"name"`
}
type NewProject struct { type NewProject struct {
TeamID string `json:"teamID"` UserID uuid.UUID `json:"userID"`
TeamID uuid.UUID `json:"teamID"`
Name string `json:"name"` Name string `json:"name"`
} }
@ -81,14 +92,37 @@ type NewTeam struct {
type NewUserAccount struct { type NewUserAccount struct {
Username string `json:"username"` Username string `json:"username"`
Email string `json:"email"` Email string `json:"email"`
DisplayName string `json:"displayName"` FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Password string `json:"password"` 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 { type ProjectsFilter struct {
TeamID *string `json:"teamID"` 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 { type UpdateTaskName struct {
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
Name string `json:"name"` Name string `json:"name"`

View File

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

View File

@ -6,6 +6,7 @@ package graph
import ( import (
"context" "context"
"database/sql" "database/sql"
"fmt"
"time" "time"
"github.com/google/uuid" "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) { func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*pg.UserAccount, error) {
createdAt := time.Now().UTC() 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 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) { func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*pg.Team, error) {
organizationID, err := uuid.Parse(input.OrganizationID) organizationID, err := uuid.Parse(input.OrganizationID)
if err != nil { 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) { func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) (*pg.Project, error) {
createdAt := time.Now().UTC() createdAt := time.Now().UTC()
teamID, err := uuid.Parse(input.TeamID) project, err := r.Repository.CreateProject(ctx, pg.CreateProjectParams{input.UserID, input.TeamID, createdAt, input.Name})
if err != nil {
return &pg.Project{}, err
}
project, err := r.Repository.CreateProject(ctx, pg.CreateProjectParams{teamID, createdAt, input.Name})
return &project, err return &project, err
} }
@ -89,6 +80,20 @@ func (r *mutationResolver) DeleteTaskGroup(ctx context.Context, input DeleteTask
return &DeleteTaskGroupPayload{true, int(deletedTasks + deletedTaskGroups), &taskGroup}, nil 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) { func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*pg.Task, error) {
taskGroupID, err := uuid.Parse(input.TaskGroupID) taskGroupID, err := uuid.Parse(input.TaskGroupID)
createdAt := time.Now().UTC() createdAt := time.Now().UTC()
@ -100,6 +105,11 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*pg.T
return &task, err 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) { func (r *mutationResolver) UpdateTaskLocation(ctx context.Context, input NewTaskLocation) (*pg.Task, error) {
taskID, err := uuid.Parse(input.TaskID) taskID, err := uuid.Parse(input.TaskID)
if err != nil { if err != nil {
@ -139,6 +149,21 @@ func (r *mutationResolver) DeleteTask(ctx context.Context, input DeleteTaskInput
return &DeleteTaskPayload{taskID.String()}, nil 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) { func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) {
userID, err := uuid.Parse(input.UserID) userID, err := uuid.Parse(input.UserID)
if err != nil { if err != nil {
@ -149,21 +174,35 @@ func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bo
return true, err return true, err
} }
func (r *organizationResolver) Teams(ctx context.Context, obj *pg.Organization) ([]pg.Team, error) { func (r *projectResolver) Team(ctx context.Context, obj *pg.Project) (*pg.Team, error) {
teams, err := r.Repository.GetTeamsForOrganization(ctx, obj.OrganizationID) team, err := r.Repository.GetTeamByID(ctx, obj.TeamID)
return teams, err return &team, err
} }
func (r *projectResolver) TeamID(ctx context.Context, obj *pg.Project) (string, error) { func (r *projectResolver) Owner(ctx context.Context, obj *pg.Project) (*ProjectMember, error) {
return obj.TeamID.String(), nil 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) { func (r *projectResolver) TaskGroups(ctx context.Context, obj *pg.Project) ([]pg.TaskGroup, error) {
return r.Repository.GetTaskGroupsForProject(ctx, obj.ProjectID) return r.Repository.GetTaskGroupsForProject(ctx, obj.ProjectID)
} }
func (r *queryResolver) Organizations(ctx context.Context) ([]pg.Organization, error) { func (r *projectResolver) Members(ctx context.Context, obj *pg.Project) ([]ProjectMember, error) {
return r.Repository.GetAllOrganizations(ctx) 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) { 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 return &project, err
} }
func (r *queryResolver) Teams(ctx context.Context) ([]pg.Team, error) { func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*pg.Task, error) {
return r.Repository.GetAllTeams(ctx) task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
return &task, err
} }
func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]pg.Project, error) { 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) 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) { func (r *taskResolver) TaskGroup(ctx context.Context, obj *pg.Task) (*pg.TaskGroup, error) {
taskGroup, err := r.Repository.GetTaskGroupByID(ctx, obj.TaskGroupID) taskGroup, err := r.Repository.GetTaskGroupByID(ctx, obj.TaskGroupID)
return &taskGroup, err 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) { func (r *taskGroupResolver) ProjectID(ctx context.Context, obj *pg.TaskGroup) (string, error) {
return obj.ProjectID.String(), nil return obj.ProjectID.String(), nil
} }
@ -237,16 +325,23 @@ func (r *taskGroupResolver) Tasks(ctx context.Context, obj *pg.TaskGroup) ([]pg.
return tasks, err return tasks, err
} }
func (r *teamResolver) Projects(ctx context.Context, obj *pg.Team) ([]pg.Project, error) { func (r *taskLabelResolver) ColorHex(ctx context.Context, obj *pg.TaskLabel) (string, error) {
return r.Repository.GetAllProjectsForTeam(ctx, obj.TeamID) 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. // Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Organization returns OrganizationResolver implementation.
func (r *Resolver) Organization() OrganizationResolver { return &organizationResolver{r} }
// Project returns ProjectResolver implementation. // Project returns ProjectResolver implementation.
func (r *Resolver) Project() ProjectResolver { return &projectResolver{r} } func (r *Resolver) Project() ProjectResolver { return &projectResolver{r} }
@ -259,13 +354,26 @@ func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
// TaskGroup returns TaskGroupResolver implementation. // TaskGroup returns TaskGroupResolver implementation.
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} } func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
// Team returns TeamResolver implementation. // TaskLabel returns TaskLabelResolver implementation.
func (r *Resolver) Team() TeamResolver { return &teamResolver{r} } 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 mutationResolver struct{ *Resolver }
type organizationResolver struct{ *Resolver }
type projectResolver struct{ *Resolver } type projectResolver struct{ *Resolver }
type queryResolver struct{ *Resolver } type queryResolver struct{ *Resolver }
type taskResolver struct{ *Resolver } type taskResolver struct{ *Resolver }
type taskGroupResolver 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 ( CREATE TABLE user_account (
user_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at timestamptz NOT NULL, 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, email text NOT NULL UNIQUE,
username text NOT NULL UNIQUE, username text NOT NULL UNIQUE,
password_hash text NOT NULL 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 package pg
import ( import (
"database/sql"
"time" "time"
"github.com/google/uuid" "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 { type Organization struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@ -19,6 +26,7 @@ type Project struct {
TeamID uuid.UUID `json:"team_id"` 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"`
Owner uuid.UUID `json:"owner"`
} }
type RefreshToken struct { type RefreshToken struct {
@ -34,6 +42,15 @@ type Task struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
Name string `json:"name"` Name string `json:"name"`
Position float64 `json:"position"` 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 { type TaskGroup struct {
@ -44,6 +61,13 @@ type TaskGroup struct {
Position float64 `json:"position"` 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 { type Team struct {
TeamID uuid.UUID `json:"team_id"` TeamID uuid.UUID `json:"team_id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@ -54,7 +78,8 @@ type Team struct {
type UserAccount struct { type UserAccount struct {
UserID uuid.UUID `json:"user_id"` UserID uuid.UUID `json:"user_id"`
CreatedAt time.Time `json:"created_at"` 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"` Email string `json:"email"`
Username string `json:"username"` Username string `json:"username"`
PasswordHash string `json:"password_hash"` PasswordHash string `json:"password_hash"`

View File

@ -41,11 +41,20 @@ type Repository interface {
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error) GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, error)
GetAllTasks(ctx context.Context) ([]Task, error) GetAllTasks(ctx context.Context) ([]Task, error)
GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error)
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error) UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, 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 { type repoSvc struct {

View File

@ -11,29 +11,36 @@ import (
) )
const createProject = `-- name: CreateProject :one 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 { type CreateProjectParams struct {
Owner uuid.UUID `json:"owner"`
TeamID uuid.UUID `json:"team_id"` 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) 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 var i Project
err := row.Scan( err := row.Scan(
&i.ProjectID, &i.ProjectID,
&i.TeamID, &i.TeamID,
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.Owner,
) )
return i, err return i, err
} }
const getAllProjects = `-- name: GetAllProjects :many 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) { 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.TeamID,
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.Owner,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -65,7 +73,7 @@ func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) {
} }
const getAllProjectsForTeam = `-- name: GetAllProjectsForTeam :many 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) { 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.TeamID,
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.Owner,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -97,7 +106,7 @@ func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) (
} }
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, owner FROM project WHERE project_id = $1
` `
func (q *Queries) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error) { 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.TeamID,
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.Owner,
) )
return i, err return i, err
} }

View File

@ -13,7 +13,9 @@ type Querier interface {
CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error)
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, 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) CreateTaskGroup(ctx context.Context, arg CreateTaskGroupParams) (TaskGroup, 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)
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
@ -30,15 +32,20 @@ type Querier interface {
GetAllTasks(ctx context.Context) ([]Task, error) GetAllTasks(ctx context.Context) ([]Task, error)
GetAllTeams(ctx context.Context) ([]Team, error) GetAllTeams(ctx context.Context) ([]Team, error)
GetAllUserAccounts(ctx context.Context) ([]UserAccount, 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) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, 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) GetTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (TaskGroup, error)
GetTaskGroupsForProject(ctx context.Context, projectID 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) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error)
GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error) GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error)
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error) GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
GetUserAccountByUsername(ctx context.Context, username string) (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) UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error) UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)

View File

@ -5,6 +5,7 @@ package pg
import ( import (
"context" "context"
"database/sql"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@ -12,7 +13,7 @@ import (
const createTask = `-- name: CreateTask :one const createTask = `-- name: CreateTask :one
INSERT INTO task (task_group_id, created_at, name, position) 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 { type CreateTaskParams struct {
@ -36,6 +37,8 @@ func (q *Queries) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, e
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.Position, &i.Position,
&i.Description,
&i.DueDate,
) )
return i, err return i, err
} }
@ -62,7 +65,7 @@ func (q *Queries) DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid
} }
const getAllTasks = `-- name: GetAllTasks :many 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) { 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.CreatedAt,
&i.Name, &i.Name,
&i.Position, &i.Position,
&i.Description,
&i.DueDate,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -94,8 +99,27 @@ func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
return items, nil 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 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) { 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.CreatedAt,
&i.Name, &i.Name,
&i.Position, &i.Position,
&i.Description,
&i.DueDate,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -127,8 +153,32 @@ func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.U
return items, nil 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 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 { type UpdateTaskLocationParams struct {
@ -146,12 +196,14 @@ func (q *Queries) UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocation
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.Position, &i.Position,
&i.Description,
&i.DueDate,
) )
return i, err return i, err
} }
const updateTaskName = `-- name: UpdateTaskName :one 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 { type UpdateTaskNameParams struct {
@ -168,6 +220,8 @@ func (q *Queries) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams)
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.Position, &i.Position,
&i.Description,
&i.DueDate,
) )
return i, err 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 const createUserAccount = `-- name: CreateUserAccount :one
INSERT INTO user_account(display_name, email, username, created_at, password_hash) INSERT INTO user_account(first_name, last_name, email, username, created_at, password_hash)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING user_id, created_at, display_name, email, username, password_hash RETURNING user_id, created_at, first_name, last_name, email, username, password_hash
` `
type CreateUserAccountParams struct { type CreateUserAccountParams struct {
DisplayName string `json:"display_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"` Email string `json:"email"`
Username string `json:"username"` Username string `json:"username"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@ -26,7 +27,8 @@ type CreateUserAccountParams struct {
func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error) { func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error) {
row := q.db.QueryRowContext(ctx, createUserAccount, row := q.db.QueryRowContext(ctx, createUserAccount,
arg.DisplayName, arg.FirstName,
arg.LastName,
arg.Email, arg.Email,
arg.Username, arg.Username,
arg.CreatedAt, arg.CreatedAt,
@ -36,7 +38,8 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
err := row.Scan( err := row.Scan(
&i.UserID, &i.UserID,
&i.CreatedAt, &i.CreatedAt,
&i.DisplayName, &i.FirstName,
&i.LastName,
&i.Email, &i.Email,
&i.Username, &i.Username,
&i.PasswordHash, &i.PasswordHash,
@ -45,7 +48,7 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
} }
const getAllUserAccounts = `-- name: GetAllUserAccounts :many 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) { 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( if err := rows.Scan(
&i.UserID, &i.UserID,
&i.CreatedAt, &i.CreatedAt,
&i.DisplayName, &i.FirstName,
&i.LastName,
&i.Email, &i.Email,
&i.Username, &i.Username,
&i.PasswordHash, &i.PasswordHash,
@ -79,7 +83,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
} }
const getUserAccountByID = `-- name: GetUserAccountByID :one 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) { 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( err := row.Scan(
&i.UserID, &i.UserID,
&i.CreatedAt, &i.CreatedAt,
&i.DisplayName, &i.FirstName,
&i.LastName,
&i.Email, &i.Email,
&i.Username, &i.Username,
&i.PasswordHash, &i.PasswordHash,
@ -97,7 +102,7 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use
} }
const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one 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) { 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( err := row.Scan(
&i.UserID, &i.UserID,
&i.CreatedAt, &i.CreatedAt,
&i.DisplayName, &i.FirstName,
&i.LastName,
&i.Email, &i.Email,
&i.Username, &i.Username,
&i.PasswordHash, &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; SELECT * FROM project WHERE project_id = $1;
-- name: CreateProject :one -- 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) INSERT INTO task (task_group_id, created_at, name, position)
VALUES($1, $2, $3, $4) RETURNING *; 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 -- name: GetTasksForTaskGroupID :many
SELECT * FROM task WHERE task_group_id = $1; 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; SELECT * FROM user_account WHERE username = $1;
-- name: CreateUserAccount :one -- name: CreateUserAccount :one
INSERT INTO user_account(display_name, email, username, created_at, password_hash) INSERT INTO user_account(first_name, last_name, email, username, created_at, password_hash)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *; RETURNING *;

View File

@ -42,7 +42,7 @@ func (h *CitadelHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Requ
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
accessTokenString, err := NewAccessToken("1") accessTokenString, err := NewAccessToken(token.UserID.String())
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) 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}) 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) { func (h *CitadelHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
var requestData LoginRequestData var requestData LoginRequestData
err := json.NewDecoder(r.Body).Decode(&requestData) err := json.NewDecoder(r.Body).Decode(&requestData)
@ -85,12 +104,11 @@ func (h *CitadelHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
userID := uuid.MustParse("0183d9ab-d0ed-4c9b-a3df-77a0cdd93dca")
refreshCreatedAt := time.Now().UTC() refreshCreatedAt := time.Now().UTC()
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1) 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 { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
@ -109,5 +127,6 @@ func (rs authResource) Routes(citadelHandler CitadelHandler) chi.Router {
r := chi.NewRouter() r := chi.NewRouter()
r.Post("/login", citadelHandler.LoginHandler) r.Post("/login", citadelHandler.LoginHandler)
r.Post("/refresh_token", citadelHandler.RefreshTokenHandler) r.Post("/refresh_token", citadelHandler.RefreshTokenHandler)
r.Post("/logout", citadelHandler.LogoutHandler)
return r return r
} }

View File

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -40,7 +41,13 @@ func AuthenticationMiddleware(next http.Handler) http.Handler {
return 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)) next.ServeHTTP(w, r.WithContext(ctx))
}) })

View File

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

View File

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

View File

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

View File

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

View File

@ -12,14 +12,12 @@ type RoutesProps = {
}; };
const Routes = ({ history }: RoutesProps) => ( const Routes = ({ history }: RoutesProps) => (
<Router history={history}>
<Switch> <Switch>
<Route exact path="/login" component={Login} />
<Route exact path="/" component={Dashboard} /> <Route exact path="/" component={Dashboard} />
<Route exact path="/projects" component={Projects} /> <Route exact path="/projects" component={Projects} />
<Route path="/projects/:projectId" component={Project} /> <Route path="/projects/:projectId" component={Project} />
<Route exact path="/login" component={Login} />
</Switch> </Switch>
</Router>
); );
export default Routes; 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 TopNavbar from 'shared/components/TopNavbar';
import DropdownMenu from 'shared/components/DropdownMenu'; 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 GlobalTopNavbar: React.FC = () => {
const { loading, data } = useMeQuery();
const history = useHistory();
const { userID, setUserID } = useContext(UserIDContext);
const [menu, setMenu] = useState({ const [menu, setMenu] = useState({
top: 0, top: 0,
left: 0, left: 0,
@ -15,10 +21,33 @@ const GlobalTopNavbar: React.FC = () => {
top: bottom, 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 ( return (
<> <>
<TopNavbar onNotificationClick={() => console.log('beep')} onProfileClick={onProfileClick} /> <TopNavbar
{menu.isOpen && <DropdownMenu left={menu.left} top={menu.top} />} 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 React, { useState, useEffect } from 'react';
import jwtDecode from 'jwt-decode';
import { createBrowserHistory } from 'history'; import { createBrowserHistory } from 'history';
import { setAccessToken } from 'shared/utils/accessToken'; import { setAccessToken } from 'shared/utils/accessToken';
import Navbar from 'shared/components/Navbar';
import GlobalTopNavbar from 'App/TopNavbar'; import GlobalTopNavbar from 'App/TopNavbar';
import styled from 'styled-components'; import styled from 'styled-components';
import NormalizeStyles from './NormalizeStyles'; import NormalizeStyles from './NormalizeStyles';
import BaseStyles from './BaseStyles'; import BaseStyles from './BaseStyles';
import Routes from './Routes'; import Routes from './Routes';
import { UserIDContext } from './context';
import Navbar from './Navbar';
import { Router } from 'react-router';
const history = createBrowserHistory(); const history = createBrowserHistory();
const MainContent = styled.div` const MainContent = styled.div`
padding: 0 0 50px 80px; padding: 0 0 50px 80px;
background: #262c49; background: #262c49;
height: 100%;
`; `;
const App = () => { const App = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [userID, setUserID] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
fetch('http://localhost:3333/auth/refresh_token', { fetch('http://localhost:3333/auth/refresh_token', {
@ -29,28 +34,33 @@ const App = () => {
} else { } else {
const response: RefreshTokenResponse = await x.json(); const response: RefreshTokenResponse = await x.json();
const { accessToken } = response; const { accessToken } = response;
const claims: JWTToken = jwtDecode(accessToken);
setUserID(claims.userId);
setAccessToken(accessToken); setAccessToken(accessToken);
} }
// }
setLoading(false); setLoading(false);
}); });
}, []); }, []);
if (loading) {
return ( return (
<>
<UserIDContext.Provider value={{ userID, setUserID }}>
<NormalizeStyles />
<BaseStyles />
<Router history={history}>
{loading ? (
<div>loading</div>
) : (
<> <>
<Navbar /> <Navbar />
<MainContent> <MainContent>
<GlobalTopNavbar /> <GlobalTopNavbar />
<Routes history={history} />
</MainContent> </MainContent>
</> </>
); )}
} </Router>
return ( </UserIDContext.Provider>
<>
<NormalizeStyles />
<BaseStyles />
<Routes history={history} />
</> </>
); );
}; };

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 { useForm } from 'react-hook-form';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
@ -6,10 +6,13 @@ import { setAccessToken } from 'shared/utils/accessToken';
import Login from 'shared/components/Login'; import Login from 'shared/components/Login';
import { Container, LoginWrapper } from './Styles'; import { Container, LoginWrapper } from './Styles';
import UserIDContext from 'App/context';
import JwtDecode from 'jwt-decode';
const Auth = () => { const Auth = () => {
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0); const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
const history = useHistory(); const history = useHistory();
const { setUserID } = useContext(UserIDContext);
const login = ( const login = (
data: LoginFormData, data: LoginFormData,
setComplete: (val: boolean) => void, setComplete: (val: boolean) => void,
@ -31,8 +34,11 @@ const Auth = () => {
} else { } else {
const response = await x.json(); const response = await x.json();
const { accessToken } = response; const { accessToken } = response;
setAccessToken(accessToken); const claims: JWTToken = JwtDecode(accessToken);
setUserID(claims.userId);
setComplete(true); setComplete(true);
setAccessToken(accessToken);
history.push('/'); 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, useUpdateTaskGroupLocationMutation,
useCreateTaskGroupMutation, useCreateTaskGroupMutation,
useDeleteTaskGroupMutation, useDeleteTaskGroupMutation,
useUpdateTaskDescriptionMutation,
useAssignTaskMutation,
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
import Navbar from 'App/Navbar';
import TopNavbar from 'App/TopNavbar';
import QuickCardEditor from 'shared/components/QuickCardEditor'; import QuickCardEditor from 'shared/components/QuickCardEditor';
import PopupMenu from 'shared/components/PopupMenu'; import PopupMenu from 'shared/components/PopupMenu';
import ListActions from 'shared/components/ListActions'; 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 MemberManager from 'shared/components/MemberManager';
import { LabelsPopup } from 'shared/components/PopupMenu/PopupMenu.stories'; import { LabelsPopup } from 'shared/components/PopupMenu/PopupMenu.stories';
import KanbanBoard from 'Projects/Project/KanbanBoard'; import KanbanBoard from 'Projects/Project/KanbanBoard';
import Details from './Details';
type TaskRouteProps = { type TaskRouteProps = {
taskID: string; taskID: string;
@ -35,17 +34,6 @@ interface QuickCardEditorState {
task?: Task; 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` const TitleWrapper = styled.div`
margin-left: 38px; margin-left: 38px;
margin-bottom: 15px; margin-bottom: 15px;
@ -64,7 +52,6 @@ interface ProjectParams {
const initialState: BoardState = { tasks: {}, columns: {} }; const initialState: BoardState = { tasks: {}, columns: {} };
const initialPopupState = { left: 0, top: 0, isOpen: false, taskGroupID: '' }; const initialPopupState = { left: 0, top: 0, isOpen: false, taskGroupID: '' };
const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top: 0, left: 0 }; 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 initialLabelsPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const initialTaskDetailsState = { isOpen: false, taskID: '' }; const initialTaskDetailsState = { isOpen: false, taskID: '' };
@ -73,9 +60,9 @@ const Project = () => {
const match = useRouteMatch(); const match = useRouteMatch();
const history = useHistory(); const history = useHistory();
const [updateTaskDescription] = useUpdateTaskDescriptionMutation();
const [listsData, setListsData] = useState(initialState); const [listsData, setListsData] = useState(initialState);
const [popupData, setPopupData] = useState(initialPopupState); const [popupData, setPopupData] = useState(initialPopupState);
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
const [taskDetails, setTaskDetails] = useState(initialTaskDetailsState); const [taskDetails, setTaskDetails] = useState(initialTaskDetailsState);
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState); const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
const [updateTaskLocation] = useUpdateTaskLocationMutation(); const [updateTaskLocation] = useUpdateTaskLocationMutation();
@ -142,6 +129,7 @@ const Project = () => {
name: task.name, name: task.name,
position: task.position, position: task.position,
labels: [], labels: [],
description: task.description ?? undefined,
}; };
}); });
}); });
@ -199,17 +187,24 @@ const Project = () => {
createTaskGroup({ variables: { projectID: projectId, name: listName, position } }); createTaskGroup({ variables: { projectID: projectId, name: listName, position } });
}; };
const [assignTask] = useAssignTaskMutation();
if (loading) { if (loading) {
return <Wrapper>Loading</Wrapper>; return <Title>Error Loading</Title>;
} }
if (data) { 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 ( return (
<> <>
<Navbar />
<MainContent>
<TopNavbar />
<TitleWrapper> <TitleWrapper>
<Title>{data.findProject.name}</Title> <Title>{data.findProject.name}</Title>
</TitleWrapper>
<KanbanBoard <KanbanBoard
listsData={listsData} listsData={listsData}
onCardDrop={onCardDrop} onCardDrop={onCardDrop}
@ -221,8 +216,6 @@ const Project = () => {
setPopupData({ isOpen, top, left, taskGroupID }); setPopupData({ isOpen, top, left, taskGroupID });
}} }}
/> />
</TitleWrapper>
</MainContent>
{popupData.isOpen && ( {popupData.isOpen && (
<PopupMenu <PopupMenu
title="List Actions" title="List Actions"
@ -259,50 +252,28 @@ const Project = () => {
<Route <Route
path={`${match.path}/c/:taskID`} path={`${match.path}/c/:taskID`}
render={(routeProps: RouteComponentProps<TaskRouteProps>) => ( render={(routeProps: RouteComponentProps<TaskRouteProps>) => (
<Modal <Details
width={1040} availableMembers={availableMembers}
onClose={() => { projectURL={match.url}
history.push(match.url); taskID={routeProps.match.params.taskID}
}}
renderContent={() => {
const task = listsData.tasks[routeProps.match.params.taskID];
if (!task) {
return <div>loading</div>;
}
return (
<TaskDetails
task={task}
onTaskNameChange={(updatedTask, newName) => { onTaskNameChange={(updatedTask, newName) => {
updateTaskName({ variables: { taskID: updatedTask.taskID, name: newName } }); updateTaskName({ variables: { taskID: updatedTask.taskID, name: newName } });
}} }}
onTaskDescriptionChange={(updatedTask, newDescription) => { onTaskDescriptionChange={(updatedTask, newDescription) => {
console.log(updatedTask, newDescription); updateTaskDescription({ variables: { taskID: updatedTask.taskID, description: newDescription } });
}} }}
onDeleteTask={deletedTask => { onDeleteTask={deletedTask => {
setTaskDetails(initialTaskDetailsState); setTaskDetails(initialTaskDetailsState);
deleteTask({ variables: { taskID: deletedTask.taskID } }); 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) => {}} onOpenAddLabelPopup={(task, bounds) => {}}
/> />
);
}}
/>
)} )}
/> />
</> </>
); );
} }
return <Wrapper>Error</Wrapper>; return <div>Error</div>;
}; };
export default Project; export default Project;

View File

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

View File

@ -2,6 +2,7 @@ import React, { createRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import DropdownMenu from '.'; import DropdownMenu from '.';
import { action } from '@storybook/addon-actions';
export default { export default {
component: DropdownMenu, component: DropdownMenu,
@ -49,7 +50,7 @@ export const Default = () => {
Click me Click me
</Button> </Button>
</Container> </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 = { type DropdownMenuProps = {
left: number; left: number;
top: number; top: number;
onLogout: () => void;
}; };
const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top }) => { const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top, onLogout }) => {
return ( return (
<Container left={left} top={top}> <Container left={left} top={top}>
<Wrapper> <Wrapper>
@ -18,7 +19,7 @@ const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top }) => {
</ActionItem> </ActionItem>
<Separator /> <Separator />
<ActionsList> <ActionsList>
<ActionItem> <ActionItem onClick={onLogout}>
<Exit size={16} color="#c2c6dc" /> <Exit size={16} color="#c2c6dc" />
<ActionTitle>Logout</ActionTitle> <ActionTitle>Logout</ActionTitle>
</ActionItem> </ActionItem>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,8 +37,14 @@ export const Default = () => {
<> <>
<NormalizeStyles /> <NormalizeStyles />
<BaseStyles /> <BaseStyles />
<TopNavbar onNotificationClick={action('notifications click')} onProfileClick={onClick} /> <TopNavbar
{menu.isOpen && <DropdownMenu left={menu.left} top={menu.top} />} 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 = { type NavBarProps = {
onProfileClick: (bottom: number, right: number) => void; onProfileClick: (bottom: number, right: number) => void;
onNotificationClick: () => 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 $profileRef: any = useRef(null);
const handleProfileClick = () => { const handleProfileClick = () => {
console.log('click'); console.log('click');
@ -45,11 +48,13 @@ const NavBar: React.FC<NavBarProps> = ({ onProfileClick, onNotificationClick })
</NotificationContainer> </NotificationContainer>
<ProfileContainer> <ProfileContainer>
<ProfileNameWrapper> <ProfileNameWrapper>
<ProfileNamePrimary>Jordan Knott</ProfileNamePrimary> <ProfileNamePrimary>
{firstName} {lastName}
</ProfileNamePrimary>
<ProfileNameSecondary>Manager</ProfileNameSecondary> <ProfileNameSecondary>Manager</ProfileNameSecondary>
</ProfileNameWrapper> </ProfileNameWrapper>
<ProfileIcon ref={$profileRef} onClick={handleProfileClick}> <ProfileIcon ref={$profileRef} onClick={handleProfileClick}>
JK {initials}
</ProfileIcon> </ProfileIcon>
</ProfileContainer> </ProfileContainer>
</GlobalActions> </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 = { export type RefreshToken = {
__typename?: 'RefreshToken'; __typename?: 'RefreshToken';
tokenId: Scalars['ID']; tokenId: Scalars['ID'];
@ -28,16 +49,10 @@ export type UserAccount = {
userID: Scalars['ID']; userID: Scalars['ID'];
email: Scalars['String']; email: Scalars['String'];
createdAt: Scalars['Time']; createdAt: Scalars['Time'];
displayName: Scalars['String']; firstName: Scalars['String'];
lastName: Scalars['String'];
username: Scalars['String']; username: Scalars['String'];
}; profileIcon: ProfileIcon;
export type Organization = {
__typename?: 'Organization';
organizationID: Scalars['ID'];
createdAt: Scalars['Time'];
name: Scalars['String'];
teams: Array<Team>;
}; };
export type Team = { export type Team = {
@ -45,16 +60,17 @@ export type Team = {
teamID: Scalars['ID']; teamID: Scalars['ID'];
createdAt: Scalars['Time']; createdAt: Scalars['Time'];
name: Scalars['String']; name: Scalars['String'];
projects: Array<Project>;
}; };
export type Project = { export type Project = {
__typename?: 'Project'; __typename?: 'Project';
projectID: Scalars['ID']; projectID: Scalars['ID'];
teamID: Scalars['String'];
createdAt: Scalars['Time']; createdAt: Scalars['Time'];
name: Scalars['String']; name: Scalars['String'];
team: Team;
owner: ProjectMember;
taskGroups: Array<TaskGroup>; taskGroups: Array<TaskGroup>;
members: Array<ProjectMember>;
}; };
export type TaskGroup = { export type TaskGroup = {
@ -74,6 +90,9 @@ export type Task = {
createdAt: Scalars['Time']; createdAt: Scalars['Time'];
name: Scalars['String']; name: Scalars['String'];
position: Scalars['Float']; position: Scalars['Float'];
description?: Maybe<Scalars['String']>;
assigned: Array<ProjectMember>;
labels: Array<TaskLabel>;
}; };
export type ProjectsFilter = { export type ProjectsFilter = {
@ -88,15 +107,19 @@ export type FindProject = {
projectId: Scalars['String']; projectId: Scalars['String'];
}; };
export type FindTask = {
taskID: Scalars['UUID'];
};
export type Query = { export type Query = {
__typename?: 'Query'; __typename?: 'Query';
organizations: Array<Organization>;
users: Array<UserAccount>; users: Array<UserAccount>;
findUser: UserAccount; findUser: UserAccount;
findProject: Project; findProject: Project;
teams: Array<Team>; findTask: Task;
projects: Array<Project>; projects: Array<Project>;
taskGroups: Array<TaskGroup>; taskGroups: Array<TaskGroup>;
me: UserAccount;
}; };
@ -110,6 +133,11 @@ export type QueryFindProjectArgs = {
}; };
export type QueryFindTaskArgs = {
input: FindTask;
};
export type QueryProjectsArgs = { export type QueryProjectsArgs = {
input?: Maybe<ProjectsFilter>; input?: Maybe<ProjectsFilter>;
}; };
@ -121,7 +149,8 @@ export type NewRefreshToken = {
export type NewUserAccount = { export type NewUserAccount = {
username: Scalars['String']; username: Scalars['String'];
email: Scalars['String']; email: Scalars['String'];
displayName: Scalars['String']; firstName: Scalars['String'];
lastName: Scalars['String'];
password: Scalars['String']; password: Scalars['String'];
}; };
@ -131,7 +160,8 @@ export type NewTeam = {
}; };
export type NewProject = { export type NewProject = {
teamID: Scalars['String']; userID: Scalars['UUID'];
teamID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
}; };
@ -141,10 +171,6 @@ export type NewTaskGroup = {
position: Scalars['Float']; position: Scalars['Float'];
}; };
export type NewOrganization = {
name: Scalars['String'];
};
export type LogoutUser = { export type LogoutUser = {
userID: Scalars['String']; userID: Scalars['String'];
}; };
@ -191,20 +217,43 @@ export type DeleteTaskGroupPayload = {
taskGroup: TaskGroup; 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 = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
createRefreshToken: RefreshToken; createRefreshToken: RefreshToken;
createUserAccount: UserAccount; createUserAccount: UserAccount;
createOrganization: Organization;
createTeam: Team; createTeam: Team;
createProject: Project; createProject: Project;
createTaskGroup: TaskGroup; createTaskGroup: TaskGroup;
updateTaskGroupLocation: TaskGroup; updateTaskGroupLocation: TaskGroup;
deleteTaskGroup: DeleteTaskGroupPayload; deleteTaskGroup: DeleteTaskGroupPayload;
addTaskLabel: Task;
removeTaskLabel: Task;
createTask: Task; createTask: Task;
updateTaskDescription: Task;
updateTaskLocation: Task; updateTaskLocation: Task;
updateTaskName: Task; updateTaskName: Task;
deleteTask: DeleteTaskPayload; deleteTask: DeleteTaskPayload;
assignTask: Task;
logoutUser: Scalars['Boolean']; logoutUser: Scalars['Boolean'];
}; };
@ -219,11 +268,6 @@ export type MutationCreateUserAccountArgs = {
}; };
export type MutationCreateOrganizationArgs = {
input: NewOrganization;
};
export type MutationCreateTeamArgs = { export type MutationCreateTeamArgs = {
input: NewTeam; input: NewTeam;
}; };
@ -249,11 +293,26 @@ export type MutationDeleteTaskGroupArgs = {
}; };
export type MutationAddTaskLabelArgs = {
input?: Maybe<AddTaskLabelInput>;
};
export type MutationRemoveTaskLabelArgs = {
input?: Maybe<RemoveTaskLabelInput>;
};
export type MutationCreateTaskArgs = { export type MutationCreateTaskArgs = {
input: NewTask; input: NewTask;
}; };
export type MutationUpdateTaskDescriptionArgs = {
input: UpdateTaskDescriptionInput;
};
export type MutationUpdateTaskLocationArgs = { export type MutationUpdateTaskLocationArgs = {
input: NewTaskLocation; input: NewTaskLocation;
}; };
@ -269,10 +328,33 @@ export type MutationDeleteTaskArgs = {
}; };
export type MutationAssignTaskArgs = {
input?: Maybe<AssignTaskInput>;
};
export type MutationLogoutUserArgs = { export type MutationLogoutUserArgs = {
input: LogoutUser; 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 = { export type CreateTaskMutationVariables = {
taskGroupID: Scalars['String']; taskGroupID: Scalars['String'];
name: Scalars['String']; name: Scalars['String'];
@ -351,36 +433,92 @@ export type FindProjectQuery = (
& { findProject: ( & { findProject: (
{ __typename?: 'Project' } { __typename?: 'Project' }
& Pick<Project, 'name'> & 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' } { __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID' | 'name' | 'position'> & Pick<TaskGroup, 'taskGroupID' | 'name' | 'position'>
& { tasks: Array<( & { tasks: Array<(
{ __typename?: 'Task' } { __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 GetProjectsQueryVariables = {};
export type GetProjectsQuery = ( export type GetProjectsQuery = (
{ __typename?: 'Query' } { __typename?: 'Query' }
& { organizations: Array<(
{ __typename?: 'Organization' }
& Pick<Organization, 'name'>
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'name'>
& { projects: Array<( & { projects: Array<(
{ __typename?: 'Project' } { __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 = { export type UpdateTaskGroupLocationMutationVariables = {
taskGroupID: Scalars['UUID']; taskGroupID: Scalars['UUID'];
position: Scalars['Float']; 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` export const CreateTaskDocument = gql`
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) { mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) { createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) {
@ -576,6 +752,15 @@ export const FindProjectDocument = gql`
query findProject($projectId: String!) { query findProject($projectId: String!) {
findProject(input: {projectId: $projectId}) { findProject(input: {projectId: $projectId}) {
name name
members {
userID
firstName
lastName
profileIcon {
url
initials
}
}
taskGroups { taskGroups {
taskGroupID taskGroupID
name name
@ -584,6 +769,7 @@ export const FindProjectDocument = gql`
taskID taskID
name name
position position
description
} }
} }
} }
@ -615,16 +801,62 @@ export function useFindProjectLazyQuery(baseOptions?: ApolloReactHooks.LazyQuery
export type FindProjectQueryHookResult = ReturnType<typeof useFindProjectQuery>; export type FindProjectQueryHookResult = ReturnType<typeof useFindProjectQuery>;
export type FindProjectLazyQueryHookResult = ReturnType<typeof useFindProjectLazyQuery>; export type FindProjectLazyQueryHookResult = ReturnType<typeof useFindProjectLazyQuery>;
export type FindProjectQueryResult = ApolloReactCommon.QueryResult<FindProjectQuery, FindProjectQueryVariables>; 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` export const GetProjectsDocument = gql`
query getProjects { query getProjects {
organizations {
name
teams {
name
projects { projects {
name
projectID projectID
} name
team {
teamID
name
} }
} }
} }
@ -654,6 +886,75 @@ export function useGetProjectsLazyQuery(baseOptions?: ApolloReactHooks.LazyQuery
export type GetProjectsQueryHookResult = ReturnType<typeof useGetProjectsQuery>; export type GetProjectsQueryHookResult = ReturnType<typeof useGetProjectsQuery>;
export type GetProjectsLazyQueryHookResult = ReturnType<typeof useGetProjectsLazyQuery>; export type GetProjectsLazyQueryHookResult = ReturnType<typeof useGetProjectsLazyQuery>;
export type GetProjectsQueryResult = ApolloReactCommon.QueryResult<GetProjectsQuery, GetProjectsQueryVariables>; 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` export const UpdateTaskGroupLocationDocument = gql`
mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) { mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) {
updateTaskGroupLocation(input: {taskGroupID: $taskGroupID, position: $position}) { 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!) { query findProject($projectId: String!) {
findProject(input: { projectId: $projectId }) { findProject(input: { projectId: $projectId }) {
name name
members {
userID
firstName
lastName
profileIcon {
url
initials
}
}
taskGroups { taskGroups {
taskGroupID taskGroupID
name name
@ -9,6 +18,7 @@ query findProject($projectId: String!) {
taskID taskID
name name
position 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 { query getProjects {
organizations {
name
teams {
name
projects { projects {
name
projectID 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" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== 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": "@types/lodash@^4.14.149":
version "4.14.149" version "4.14.149"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" 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" jwa "^1.4.1"
safe-buffer "^5.0.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: killable@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"