From 7e78ee36b49aeccd9d130e2f3378ad0e32621c34 Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Mon, 20 Apr 2020 18:04:27 -0500 Subject: [PATCH] feature: UI changes --- api/graph/generated.go | 740 +++++++++++++++++- api/graph/models_gen.go | 12 + api/graph/schema.graphqls | 25 +- api/graph/schema.resolvers.go | 66 +- .../0012-add-project-label-table.up.sql | 7 + ...p.sql => 0015_add-task-label-table.up.sql} | 2 +- ..._color-column-to-user-account-table.up.sql | 1 + api/pg/models.go | 31 +- api/pg/pg.go | 5 + api/pg/project_label.sql.go | 92 +++ api/pg/querier.go | 4 + api/pg/task_assigned.sql.go | 21 + api/pg/task_label.sql.go | 18 +- api/pg/user_accounts.sql.go | 12 +- api/query/project_label.sql | 9 + api/query/task_assigned.sql | 3 + api/query/task_label.sql | 2 +- web/src/App/TopNavbar.tsx | 16 +- web/src/Projects/Project/Details/index.tsx | 25 +- .../Projects/Project/KanbanBoard/Styles.ts | 3 +- web/src/Projects/Project/index.tsx | 30 +- web/src/citadel.d.ts | 1 + web/src/shared/components/Card/Styles.ts | 38 +- web/src/shared/components/Card/index.tsx | 21 +- .../DropdownMenu/DropdownMenu.stories.tsx | 14 +- .../shared/components/DropdownMenu/index.tsx | 11 +- .../DueDateManager/DueDateManager.stories.tsx | 4 +- web/src/shared/components/Lists/index.tsx | 1 + .../shared/components/MiniProfile/Styles.ts | 50 ++ .../shared/components/MiniProfile/index.tsx | 26 + web/src/shared/components/Modal/Styles.ts | 3 +- web/src/shared/components/Navbar/Styles.ts | 52 +- web/src/shared/components/Navbar/index.tsx | 4 +- .../PopupMenu/PopupMenu.stories.tsx | 51 +- .../shared/components/TaskDetails/Styles.ts | 6 + .../TaskDetails/TaskDetails.stories.tsx | 8 +- .../shared/components/TaskDetails/index.tsx | 46 +- web/src/shared/components/TopNavbar/Styles.ts | 62 +- .../TopNavbar/TopNavbar.stories.tsx | 12 +- web/src/shared/components/TopNavbar/index.tsx | 29 +- web/src/shared/generated/graphql.tsx | 120 ++- web/src/shared/graphql/findProject.graphqls | 11 + web/src/shared/graphql/findTask.graphqls | 1 + web/src/shared/graphql/me.graphqls | 1 + web/src/shared/graphql/unassignTask.graphqls | 10 + 45 files changed, 1569 insertions(+), 137 deletions(-) create mode 100644 api/migrations/0012-add-project-label-table.up.sql rename api/migrations/{0012-add-task-label-table.up.sql => 0015_add-task-label-table.up.sql} (69%) create mode 100644 api/migrations/0016_add-profile_bg_color-column-to-user-account-table.up.sql create mode 100644 api/pg/project_label.sql.go create mode 100644 api/query/project_label.sql create mode 100644 web/src/shared/components/MiniProfile/Styles.ts create mode 100644 web/src/shared/components/MiniProfile/index.tsx create mode 100644 web/src/shared/graphql/unassignTask.graphqls diff --git a/api/graph/generated.go b/api/graph/generated.go index b7f8689..f4e8755 100644 --- a/api/graph/generated.go +++ b/api/graph/generated.go @@ -39,6 +39,7 @@ type Config struct { type ResolverRoot interface { Mutation() MutationResolver Project() ProjectResolver + ProjectLabel() ProjectLabelResolver Query() QueryResolver Task() TaskResolver TaskGroup() TaskGroupResolver @@ -64,6 +65,7 @@ type ComplexityRoot struct { AddTaskLabel func(childComplexity int, input *AddTaskLabelInput) int AssignTask func(childComplexity int, input *AssignTaskInput) int CreateProject func(childComplexity int, input NewProject) int + CreateProjectLabel func(childComplexity int, input NewProjectLabel) int CreateRefreshToken func(childComplexity int, input NewRefreshToken) int CreateTask func(childComplexity int, input NewTask) int CreateTaskGroup func(childComplexity int, input NewTaskGroup) int @@ -73,6 +75,7 @@ type ComplexityRoot struct { DeleteTaskGroup func(childComplexity int, input DeleteTaskGroupInput) int LogoutUser func(childComplexity int, input LogoutUser) int RemoveTaskLabel func(childComplexity int, input *RemoveTaskLabelInput) int + UnassignTask func(childComplexity int, input *UnassignTaskInput) int UpdateTaskDescription func(childComplexity int, input UpdateTaskDescriptionInput) int UpdateTaskGroupLocation func(childComplexity int, input NewTaskGroupLocation) int UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int @@ -80,12 +83,14 @@ type ComplexityRoot struct { } ProfileIcon struct { + BgColor func(childComplexity int) int Initials func(childComplexity int) int URL func(childComplexity int) int } Project struct { CreatedAt func(childComplexity int) int + Labels func(childComplexity int) int Members func(childComplexity int) int Name func(childComplexity int) int Owner func(childComplexity int) int @@ -94,6 +99,13 @@ type ComplexityRoot struct { Team func(childComplexity int) int } + ProjectLabel struct { + ColorHex func(childComplexity int) int + CreatedDate func(childComplexity int) int + Name func(childComplexity int) int + ProjectLabelID func(childComplexity int) int + } + ProjectMember struct { FirstName func(childComplexity int) int LastName func(childComplexity int) int @@ -139,9 +151,11 @@ type ComplexityRoot struct { } TaskLabel struct { - ColorHex func(childComplexity int) int - LabelColorID func(childComplexity int) int - TaskLabelID func(childComplexity int) int + AssignedDate func(childComplexity int) int + ColorHex func(childComplexity int) int + Name func(childComplexity int) int + ProjectLabelID func(childComplexity int) int + TaskLabelID func(childComplexity int) int } Team struct { @@ -166,6 +180,7 @@ type MutationResolver interface { CreateUserAccount(ctx context.Context, input NewUserAccount) (*pg.UserAccount, error) CreateTeam(ctx context.Context, input NewTeam) (*pg.Team, error) CreateProject(ctx context.Context, input NewProject) (*pg.Project, error) + CreateProjectLabel(ctx context.Context, input NewProjectLabel) (*pg.ProjectLabel, error) CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*pg.TaskGroup, error) UpdateTaskGroupLocation(ctx context.Context, input NewTaskGroupLocation) (*pg.TaskGroup, error) DeleteTaskGroup(ctx context.Context, input DeleteTaskGroupInput) (*DeleteTaskGroupPayload, error) @@ -177,6 +192,7 @@ type MutationResolver interface { UpdateTaskName(ctx context.Context, input UpdateTaskName) (*pg.Task, error) DeleteTask(ctx context.Context, input DeleteTaskInput) (*DeleteTaskPayload, error) AssignTask(ctx context.Context, input *AssignTaskInput) (*pg.Task, error) + UnassignTask(ctx context.Context, input *UnassignTaskInput) (*pg.Task, error) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) } type ProjectResolver interface { @@ -184,6 +200,11 @@ type ProjectResolver interface { Owner(ctx context.Context, obj *pg.Project) (*ProjectMember, error) TaskGroups(ctx context.Context, obj *pg.Project) ([]pg.TaskGroup, error) Members(ctx context.Context, obj *pg.Project) ([]ProjectMember, error) + Labels(ctx context.Context, obj *pg.Project) ([]pg.ProjectLabel, error) +} +type ProjectLabelResolver interface { + ColorHex(ctx context.Context, obj *pg.ProjectLabel) (string, error) + Name(ctx context.Context, obj *pg.ProjectLabel) (*string, error) } type QueryResolver interface { Users(ctx context.Context) ([]pg.UserAccount, error) @@ -208,6 +229,7 @@ type TaskGroupResolver interface { } type TaskLabelResolver interface { ColorHex(ctx context.Context, obj *pg.TaskLabel) (string, error) + Name(ctx context.Context, obj *pg.TaskLabel) (*string, error) } type UserAccountResolver interface { ProfileIcon(ctx context.Context, obj *pg.UserAccount) (*ProfileIcon, error) @@ -292,6 +314,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CreateProject(childComplexity, args["input"].(NewProject)), true + case "Mutation.createProjectLabel": + if e.complexity.Mutation.CreateProjectLabel == nil { + break + } + + args, err := ec.field_Mutation_createProjectLabel_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateProjectLabel(childComplexity, args["input"].(NewProjectLabel)), true + case "Mutation.createRefreshToken": if e.complexity.Mutation.CreateRefreshToken == nil { break @@ -400,6 +434,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.RemoveTaskLabel(childComplexity, args["input"].(*RemoveTaskLabelInput)), true + case "Mutation.unassignTask": + if e.complexity.Mutation.UnassignTask == nil { + break + } + + args, err := ec.field_Mutation_unassignTask_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UnassignTask(childComplexity, args["input"].(*UnassignTaskInput)), true + case "Mutation.updateTaskDescription": if e.complexity.Mutation.UpdateTaskDescription == nil { break @@ -448,6 +494,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.UpdateTaskName(childComplexity, args["input"].(UpdateTaskName)), true + case "ProfileIcon.bgColor": + if e.complexity.ProfileIcon.BgColor == nil { + break + } + + return e.complexity.ProfileIcon.BgColor(childComplexity), true + case "ProfileIcon.initials": if e.complexity.ProfileIcon.Initials == nil { break @@ -469,6 +522,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Project.CreatedAt(childComplexity), true + case "Project.labels": + if e.complexity.Project.Labels == nil { + break + } + + return e.complexity.Project.Labels(childComplexity), true + case "Project.members": if e.complexity.Project.Members == nil { break @@ -511,6 +571,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Project.Team(childComplexity), true + case "ProjectLabel.colorHex": + if e.complexity.ProjectLabel.ColorHex == nil { + break + } + + return e.complexity.ProjectLabel.ColorHex(childComplexity), true + + case "ProjectLabel.createdDate": + if e.complexity.ProjectLabel.CreatedDate == nil { + break + } + + return e.complexity.ProjectLabel.CreatedDate(childComplexity), true + + case "ProjectLabel.name": + if e.complexity.ProjectLabel.Name == nil { + break + } + + return e.complexity.ProjectLabel.Name(childComplexity), true + + case "ProjectLabel.projectLabelID": + if e.complexity.ProjectLabel.ProjectLabelID == nil { + break + } + + return e.complexity.ProjectLabel.ProjectLabelID(childComplexity), true + case "ProjectMember.firstName": if e.complexity.ProjectMember.FirstName == nil { break @@ -734,6 +822,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.TaskGroup.Tasks(childComplexity), true + case "TaskLabel.assignedDate": + if e.complexity.TaskLabel.AssignedDate == nil { + break + } + + return e.complexity.TaskLabel.AssignedDate(childComplexity), true + case "TaskLabel.colorHex": if e.complexity.TaskLabel.ColorHex == nil { break @@ -741,12 +836,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.TaskLabel.ColorHex(childComplexity), true - case "TaskLabel.labelColorID": - if e.complexity.TaskLabel.LabelColorID == nil { + case "TaskLabel.name": + if e.complexity.TaskLabel.Name == nil { break } - return e.complexity.TaskLabel.LabelColorID(childComplexity), true + return e.complexity.TaskLabel.Name(childComplexity), true + + case "TaskLabel.projectLabelID": + if e.complexity.TaskLabel.ProjectLabelID == nil { + break + } + + return e.complexity.TaskLabel.ProjectLabelID(childComplexity), true case "TaskLabel.taskLabelID": if e.complexity.TaskLabel.TaskLabelID == nil { @@ -892,15 +994,25 @@ var sources = []*ast.Source{ &ast.Source{Name: "graph/schema.graphqls", Input: `scalar Time scalar UUID +type ProjectLabel { + projectLabelID: ID! + createdDate: Time! + colorHex: String! + name: String +} + type TaskLabel { taskLabelID: ID! - labelColorID: UUID! + projectLabelID: UUID! + assignedDate: Time! colorHex: String! + name: String } type ProfileIcon { url: String initials: String + bgColor: String } type ProjectMember { @@ -941,6 +1053,7 @@ type Project { owner: ProjectMember! taskGroups: [TaskGroup!]! members: [ProjectMember!]! + labels: [ProjectLabel!]! } type TaskGroup { @@ -1065,6 +1178,10 @@ input AssignTaskInput { userID: UUID! } +input UnassignTaskInput { + taskID: UUID! + userID: UUID! +} input UpdateTaskDescriptionInput { taskID: UUID! description: String! @@ -1080,6 +1197,12 @@ input RemoveTaskLabelInput { taskLabelID: UUID! } +input NewProjectLabel { + projectID: UUID! + labelColorID: UUID! + name: String +} + type Mutation { createRefreshToken(input: NewRefreshToken!): RefreshToken! @@ -1088,6 +1211,7 @@ type Mutation { createTeam(input: NewTeam!): Team! createProject(input: NewProject!): Project! + createProjectLabel(input: NewProjectLabel!): ProjectLabel! createTaskGroup(input: NewTaskGroup!): TaskGroup! updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup! @@ -1102,6 +1226,7 @@ type Mutation { updateTaskName(input: UpdateTaskName!): Task! deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! assignTask(input: AssignTaskInput): Task! + unassignTask(input: UnassignTaskInput): Task! logoutUser(input: LogoutUser!): Boolean! } @@ -1141,6 +1266,20 @@ func (ec *executionContext) field_Mutation_assignTask_args(ctx context.Context, return args, nil } +func (ec *executionContext) field_Mutation_createProjectLabel_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 NewProjectLabel + if tmp, ok := rawArgs["input"]; ok { + arg0, err = ec.unmarshalNNewProjectLabel2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐNewProjectLabel(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_createProject_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1281,6 +1420,20 @@ func (ec *executionContext) field_Mutation_removeTaskLabel_args(ctx context.Cont return args, nil } +func (ec *executionContext) field_Mutation_unassignTask_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 *UnassignTaskInput + if tmp, ok := rawArgs["input"]; ok { + arg0, err = ec.unmarshalOUnassignTaskInput2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUnassignTaskInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_updateTaskDescription_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1743,6 +1896,47 @@ func (ec *executionContext) _Mutation_createProject(ctx context.Context, field g return ec.marshalNProject2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐProject(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_createProjectLabel(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_createProjectLabel_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateProjectLabel(rctx, args["input"].(NewProjectLabel)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*pg.ProjectLabel) + fc.Result = res + return ec.marshalNProjectLabel2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐProjectLabel(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_createTaskGroup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2194,6 +2388,47 @@ func (ec *executionContext) _Mutation_assignTask(ctx context.Context, field grap return ec.marshalNTask2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTask(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_unassignTask(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_unassignTask_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UnassignTask(rctx, args["input"].(*UnassignTaskInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*pg.Task) + fc.Result = res + return ec.marshalNTask2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTask(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_logoutUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2297,6 +2532,37 @@ func (ec *executionContext) _ProfileIcon_initials(ctx context.Context, field gra return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } +func (ec *executionContext) _ProfileIcon_bgColor(ctx context.Context, field graphql.CollectedField, obj *ProfileIcon) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ProfileIcon", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.BgColor, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _Project_projectID(ctx context.Context, field graphql.CollectedField, obj *pg.Project) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2535,6 +2801,173 @@ func (ec *executionContext) _Project_members(ctx context.Context, field graphql. return ec.marshalNProjectMember2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐProjectMemberᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _Project_labels(ctx context.Context, field graphql.CollectedField, obj *pg.Project) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Project", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Project().Labels(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]pg.ProjectLabel) + fc.Result = res + return ec.marshalNProjectLabel2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐProjectLabelᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) _ProjectLabel_projectLabelID(ctx context.Context, field graphql.CollectedField, obj *pg.ProjectLabel) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ProjectLabel", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ProjectLabelID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) _ProjectLabel_createdDate(ctx context.Context, field graphql.CollectedField, obj *pg.ProjectLabel) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ProjectLabel", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.CreatedDate, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(time.Time) + fc.Result = res + return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) _ProjectLabel_colorHex(ctx context.Context, field graphql.CollectedField, obj *pg.ProjectLabel) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ProjectLabel", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.ProjectLabel().ColorHex(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _ProjectLabel_name(ctx context.Context, field graphql.CollectedField, obj *pg.ProjectLabel) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ProjectLabel", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.ProjectLabel().Name(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _ProjectMember_userID(ctx context.Context, field graphql.CollectedField, obj *ProjectMember) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3649,7 +4082,7 @@ func (ec *executionContext) _TaskLabel_taskLabelID(ctx context.Context, field gr return ec.marshalNID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) } -func (ec *executionContext) _TaskLabel_labelColorID(ctx context.Context, field graphql.CollectedField, obj *pg.TaskLabel) (ret graphql.Marshaler) { +func (ec *executionContext) _TaskLabel_projectLabelID(ctx context.Context, field graphql.CollectedField, obj *pg.TaskLabel) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -3666,7 +4099,7 @@ func (ec *executionContext) _TaskLabel_labelColorID(ctx context.Context, field g ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.LabelColorID, nil + return obj.ProjectLabelID, nil }) if err != nil { ec.Error(ctx, err) @@ -3683,6 +4116,40 @@ func (ec *executionContext) _TaskLabel_labelColorID(ctx context.Context, field g return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) } +func (ec *executionContext) _TaskLabel_assignedDate(ctx context.Context, field graphql.CollectedField, obj *pg.TaskLabel) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TaskLabel", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.AssignedDate, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(time.Time) + fc.Result = res + return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res) +} + func (ec *executionContext) _TaskLabel_colorHex(ctx context.Context, field graphql.CollectedField, obj *pg.TaskLabel) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3717,6 +4184,37 @@ func (ec *executionContext) _TaskLabel_colorHex(ctx context.Context, field graph return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _TaskLabel_name(ctx context.Context, field graphql.CollectedField, obj *pg.TaskLabel) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "TaskLabel", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.TaskLabel().Name(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _Team_teamID(ctx context.Context, field graphql.CollectedField, obj *pg.Team) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5298,6 +5796,36 @@ func (ec *executionContext) unmarshalInputNewProject(ctx context.Context, obj in return it, nil } +func (ec *executionContext) unmarshalInputNewProjectLabel(ctx context.Context, obj interface{}) (NewProjectLabel, error) { + var it NewProjectLabel + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "projectID": + var err error + it.ProjectID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + case "labelColorID": + var err error + it.LabelColorID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + case "name": + var err error + it.Name, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputNewRefreshToken(ctx context.Context, obj interface{}) (NewRefreshToken, error) { var it NewRefreshToken var asMap = obj.(map[string]interface{}) @@ -5538,6 +6066,30 @@ func (ec *executionContext) unmarshalInputRemoveTaskLabelInput(ctx context.Conte return it, nil } +func (ec *executionContext) unmarshalInputUnassignTaskInput(ctx context.Context, obj interface{}) (UnassignTaskInput, error) { + var it UnassignTaskInput + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "taskID": + var err error + it.TaskID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + case "userID": + var err error + it.UserID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputUpdateTaskDescriptionInput(ctx context.Context, obj interface{}) (UpdateTaskDescriptionInput, error) { var it UpdateTaskDescriptionInput var asMap = obj.(map[string]interface{}) @@ -5693,6 +6245,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "createProjectLabel": + out.Values[i] = ec._Mutation_createProjectLabel(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "createTaskGroup": out.Values[i] = ec._Mutation_createTaskGroup(ctx, field) if out.Values[i] == graphql.Null { @@ -5748,6 +6305,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "unassignTask": + out.Values[i] = ec._Mutation_unassignTask(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "logoutUser": out.Values[i] = ec._Mutation_logoutUser(ctx, field) if out.Values[i] == graphql.Null { @@ -5779,6 +6341,8 @@ func (ec *executionContext) _ProfileIcon(ctx context.Context, sel ast.SelectionS out.Values[i] = ec._ProfileIcon_url(ctx, field, obj) case "initials": out.Values[i] = ec._ProfileIcon_initials(ctx, field, obj) + case "bgColor": + out.Values[i] = ec._ProfileIcon_bgColor(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -5872,6 +6436,77 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, } return res }) + case "labels": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Project_labels(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + +var projectLabelImplementors = []string{"ProjectLabel"} + +func (ec *executionContext) _ProjectLabel(ctx context.Context, sel ast.SelectionSet, obj *pg.ProjectLabel) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, projectLabelImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ProjectLabel") + case "projectLabelID": + out.Values[i] = ec._ProjectLabel_projectLabelID(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + case "createdDate": + out.Values[i] = ec._ProjectLabel_createdDate(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + case "colorHex": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ProjectLabel_colorHex(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) + case "name": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ProjectLabel_name(ctx, field, obj) + return res + }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -6276,8 +6911,13 @@ func (ec *executionContext) _TaskLabel(ctx context.Context, sel ast.SelectionSet if out.Values[i] == graphql.Null { atomic.AddUint32(&invalids, 1) } - case "labelColorID": - out.Values[i] = ec._TaskLabel_labelColorID(ctx, field, obj) + case "projectLabelID": + out.Values[i] = ec._TaskLabel_projectLabelID(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + case "assignedDate": + out.Values[i] = ec._TaskLabel_assignedDate(ctx, field, obj) if out.Values[i] == graphql.Null { atomic.AddUint32(&invalids, 1) } @@ -6295,6 +6935,17 @@ func (ec *executionContext) _TaskLabel(ctx context.Context, sel ast.SelectionSet } return res }) + case "name": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._TaskLabel_name(ctx, field, obj) + return res + }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -6766,6 +7417,10 @@ func (ec *executionContext) unmarshalNNewProject2githubᚗcomᚋjordanknottᚋpr return ec.unmarshalInputNewProject(ctx, v) } +func (ec *executionContext) unmarshalNNewProjectLabel2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐNewProjectLabel(ctx context.Context, v interface{}) (NewProjectLabel, error) { + return ec.unmarshalInputNewProjectLabel(ctx, v) +} + func (ec *executionContext) unmarshalNNewRefreshToken2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐNewRefreshToken(ctx context.Context, v interface{}) (NewRefreshToken, error) { return ec.unmarshalInputNewRefreshToken(ctx, v) } @@ -6859,6 +7514,57 @@ func (ec *executionContext) marshalNProject2ᚖgithubᚗcomᚋjordanknottᚋproj return ec._Project(ctx, sel, v) } +func (ec *executionContext) marshalNProjectLabel2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐProjectLabel(ctx context.Context, sel ast.SelectionSet, v pg.ProjectLabel) graphql.Marshaler { + return ec._ProjectLabel(ctx, sel, &v) +} + +func (ec *executionContext) marshalNProjectLabel2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐProjectLabelᚄ(ctx context.Context, sel ast.SelectionSet, v []pg.ProjectLabel) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNProjectLabel2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐProjectLabel(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) marshalNProjectLabel2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐProjectLabel(ctx context.Context, sel ast.SelectionSet, v *pg.ProjectLabel) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._ProjectLabel(ctx, sel, v) +} + func (ec *executionContext) marshalNProjectMember2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐProjectMember(ctx context.Context, sel ast.SelectionSet, v ProjectMember) graphql.Marshaler { return ec._ProjectMember(ctx, sel, &v) } @@ -7502,6 +8208,18 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as return ec.marshalOString2string(ctx, sel, *v) } +func (ec *executionContext) unmarshalOUnassignTaskInput2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUnassignTaskInput(ctx context.Context, v interface{}) (UnassignTaskInput, error) { + return ec.unmarshalInputUnassignTaskInput(ctx, v) +} + +func (ec *executionContext) unmarshalOUnassignTaskInput2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUnassignTaskInput(ctx context.Context, v interface{}) (*UnassignTaskInput, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalOUnassignTaskInput2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUnassignTaskInput(ctx, v) + return &res, err +} + func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/api/graph/models_gen.go b/api/graph/models_gen.go index bf136ff..9db2f97 100644 --- a/api/graph/models_gen.go +++ b/api/graph/models_gen.go @@ -57,6 +57,12 @@ type NewProject struct { Name string `json:"name"` } +type NewProjectLabel struct { + ProjectID uuid.UUID `json:"projectID"` + LabelColorID uuid.UUID `json:"labelColorID"` + Name *string `json:"name"` +} + type NewRefreshToken struct { UserID string `json:"userId"` } @@ -100,6 +106,7 @@ type NewUserAccount struct { type ProfileIcon struct { URL *string `json:"url"` Initials *string `json:"initials"` + BgColor *string `json:"bgColor"` } type ProjectMember struct { @@ -118,6 +125,11 @@ type RemoveTaskLabelInput struct { TaskLabelID uuid.UUID `json:"taskLabelID"` } +type UnassignTaskInput struct { + TaskID uuid.UUID `json:"taskID"` + UserID uuid.UUID `json:"userID"` +} + type UpdateTaskDescriptionInput struct { TaskID uuid.UUID `json:"taskID"` Description string `json:"description"` diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index 349f297..2131599 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -1,15 +1,25 @@ scalar Time scalar UUID +type ProjectLabel { + projectLabelID: ID! + createdDate: Time! + colorHex: String! + name: String +} + type TaskLabel { taskLabelID: ID! - labelColorID: UUID! + projectLabelID: UUID! + assignedDate: Time! colorHex: String! + name: String } type ProfileIcon { url: String initials: String + bgColor: String } type ProjectMember { @@ -50,6 +60,7 @@ type Project { owner: ProjectMember! taskGroups: [TaskGroup!]! members: [ProjectMember!]! + labels: [ProjectLabel!]! } type TaskGroup { @@ -174,6 +185,10 @@ input AssignTaskInput { userID: UUID! } +input UnassignTaskInput { + taskID: UUID! + userID: UUID! +} input UpdateTaskDescriptionInput { taskID: UUID! description: String! @@ -189,6 +204,12 @@ input RemoveTaskLabelInput { taskLabelID: UUID! } +input NewProjectLabel { + projectID: UUID! + labelColorID: UUID! + name: String +} + type Mutation { createRefreshToken(input: NewRefreshToken!): RefreshToken! @@ -197,6 +218,7 @@ type Mutation { createTeam(input: NewTeam!): Team! createProject(input: NewProject!): Project! + createProjectLabel(input: NewProjectLabel!): ProjectLabel! createTaskGroup(input: NewTaskGroup!): TaskGroup! updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup! @@ -211,6 +233,7 @@ type Mutation { updateTaskName(input: UpdateTaskName!): Task! deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! assignTask(input: AssignTaskInput): Task! + unassignTask(input: UnassignTaskInput): Task! logoutUser(input: LogoutUser!): Boolean! } diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 02940b7..3f73773 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -45,6 +45,10 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) return &project, err } +func (r *mutationResolver) CreateProjectLabel(ctx context.Context, input NewProjectLabel) (*pg.ProjectLabel, error) { + panic(fmt.Errorf("not implemented")) +} + func (r *mutationResolver) CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*pg.TaskGroup, error) { createdAt := time.Now().UTC() projectID, err := uuid.Parse(input.ProjectID) @@ -164,6 +168,18 @@ func (r *mutationResolver) AssignTask(ctx context.Context, input *AssignTaskInpu return &task, err } +func (r *mutationResolver) UnassignTask(ctx context.Context, input *UnassignTaskInput) (*pg.Task, error) { + task, err := r.Repository.GetTaskByID(ctx, input.TaskID) + if err != nil { + return &pg.Task{}, err + } + _, err = r.Repository.DeleteTaskAssignedByID(ctx, pg.DeleteTaskAssignedByIDParams{input.TaskID, input.UserID}) + if err != nil { + return &pg.Task{}, err + } + return &task, nil +} + func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) { userID, err := uuid.Parse(input.UserID) if err != nil { @@ -185,7 +201,7 @@ func (r *projectResolver) Owner(ctx context.Context, obj *pg.Project) (*ProjectM return &ProjectMember{}, err } initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0]) - profileIcon := &ProfileIcon{nil, &initials} + profileIcon := &ProfileIcon{nil, &initials, &user.ProfileBgColor} return &ProjectMember{obj.Owner, user.FirstName, user.LastName, profileIcon}, nil } @@ -200,11 +216,28 @@ func (r *projectResolver) Members(ctx context.Context, obj *pg.Project) ([]Proje return members, err } initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0]) - profileIcon := &ProfileIcon{nil, &initials} + profileIcon := &ProfileIcon{nil, &initials, &user.ProfileBgColor} members = append(members, ProjectMember{obj.Owner, user.FirstName, user.LastName, profileIcon}) return members, nil } +func (r *projectResolver) Labels(ctx context.Context, obj *pg.Project) ([]pg.ProjectLabel, error) { + labels, err := r.Repository.GetProjectLabelsForProject(ctx, obj.ProjectID) + return labels, err +} + +func (r *projectLabelResolver) ColorHex(ctx context.Context, obj *pg.ProjectLabel) (string, error) { + labelColor, err := r.Repository.GetLabelColorByID(ctx, obj.LabelColorID) + if err != nil { + return "", err + } + return labelColor.ColorHex, nil +} + +func (r *projectLabelResolver) Name(ctx context.Context, obj *pg.ProjectLabel) (*string, error) { + panic(fmt.Errorf("not implemented")) +} + func (r *queryResolver) Users(ctx context.Context) ([]pg.UserAccount, error) { return r.Repository.GetAllUserAccounts(ctx) } @@ -306,7 +339,7 @@ func (r *taskResolver) Assigned(ctx context.Context, obj *pg.Task) ([]ProjectMem return taskMembers, err } initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0]) - profileIcon := &ProfileIcon{nil, &initials} + profileIcon := &ProfileIcon{nil, &initials, &user.ProfileBgColor} taskMembers = append(taskMembers, ProjectMember{taskMemberLink.UserID, user.FirstName, user.LastName, profileIcon}) } return taskMembers, nil @@ -326,16 +359,32 @@ func (r *taskGroupResolver) Tasks(ctx context.Context, obj *pg.TaskGroup) ([]pg. } func (r *taskLabelResolver) ColorHex(ctx context.Context, obj *pg.TaskLabel) (string, error) { - labelColor, err := r.Repository.GetLabelColorByID(ctx, obj.LabelColorID) + projectLabel, err := r.Repository.GetProjectLabelByID(ctx, obj.ProjectLabelID) + if err != nil { + return "", err + } + labelColor, err := r.Repository.GetLabelColorByID(ctx, projectLabel.LabelColorID) if err != nil { return "", err } return labelColor.ColorHex, nil } +func (r *taskLabelResolver) Name(ctx context.Context, obj *pg.TaskLabel) (*string, error) { + projectLabel, err := r.Repository.GetProjectLabelByID(ctx, obj.ProjectLabelID) + if err != nil { + return nil, err + } + name := projectLabel.Name + if !name.Valid { + return nil, err + } + return &name.String, err +} + 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} + profileIcon := &ProfileIcon{nil, &initials, &obj.ProfileBgColor} return profileIcon, nil } @@ -345,6 +394,9 @@ func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } // Project returns ProjectResolver implementation. func (r *Resolver) Project() ProjectResolver { return &projectResolver{r} } +// ProjectLabel returns ProjectLabelResolver implementation. +func (r *Resolver) ProjectLabel() ProjectLabelResolver { return &projectLabelResolver{r} } + // Query returns QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } @@ -362,6 +414,7 @@ func (r *Resolver) UserAccount() UserAccountResolver { return &userAccountResolv type mutationResolver struct{ *Resolver } type projectResolver struct{ *Resolver } +type projectLabelResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type taskResolver struct{ *Resolver } type taskGroupResolver struct{ *Resolver } @@ -374,6 +427,9 @@ type userAccountResolver struct{ *Resolver } // - 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 *taskLabelResolver) ProjectLabelID(ctx context.Context, obj *pg.TaskLabel) (uuid.UUID, error) { + panic(fmt.Errorf("not implemented")) +} func (r *userAccountResolver) DisplayName(ctx context.Context, obj *pg.UserAccount) (string, error) { return obj.FirstName + " " + obj.LastName, nil } diff --git a/api/migrations/0012-add-project-label-table.up.sql b/api/migrations/0012-add-project-label-table.up.sql new file mode 100644 index 0000000..e06cd25 --- /dev/null +++ b/api/migrations/0012-add-project-label-table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE project_label ( + project_label_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + project_id uuid NOT NULL REFERENCES project(project_id), + label_color_id uuid NOT NULL REFERENCES label_color(label_color_id), + created_date timestamptz NOT NULL, + name text +); diff --git a/api/migrations/0012-add-task-label-table.up.sql b/api/migrations/0015_add-task-label-table.up.sql similarity index 69% rename from api/migrations/0012-add-task-label-table.up.sql rename to api/migrations/0015_add-task-label-table.up.sql index bddcc8e..c8cdedb 100644 --- a/api/migrations/0012-add-task-label-table.up.sql +++ b/api/migrations/0015_add-task-label-table.up.sql @@ -1,6 +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), + project_label_id uuid NOT NULL REFERENCES project_label(project_label_id), assigned_date timestamptz NOT NULL ); diff --git a/api/migrations/0016_add-profile_bg_color-column-to-user-account-table.up.sql b/api/migrations/0016_add-profile_bg_color-column-to-user-account-table.up.sql new file mode 100644 index 0000000..22f2a39 --- /dev/null +++ b/api/migrations/0016_add-profile_bg_color-column-to-user-account-table.up.sql @@ -0,0 +1 @@ +ALTER TABLE user_account ADD COLUMN profile_bg_color text NOT NULL DEFAULT '#7367F0'; diff --git a/api/pg/models.go b/api/pg/models.go index 2145255..8081bbd 100644 --- a/api/pg/models.go +++ b/api/pg/models.go @@ -29,6 +29,14 @@ type Project struct { Owner uuid.UUID `json:"owner"` } +type ProjectLabel struct { + ProjectLabelID uuid.UUID `json:"project_label_id"` + ProjectID uuid.UUID `json:"project_id"` + LabelColorID uuid.UUID `json:"label_color_id"` + CreatedDate time.Time `json:"created_date"` + Name sql.NullString `json:"name"` +} + type RefreshToken struct { TokenID uuid.UUID `json:"token_id"` UserID uuid.UUID `json:"user_id"` @@ -62,10 +70,10 @@ type TaskGroup struct { } 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"` + TaskLabelID uuid.UUID `json:"task_label_id"` + TaskID uuid.UUID `json:"task_id"` + ProjectLabelID uuid.UUID `json:"project_label_id"` + AssignedDate time.Time `json:"assigned_date"` } type Team struct { @@ -76,11 +84,12 @@ type Team struct { } type UserAccount struct { - UserID uuid.UUID `json:"user_id"` - CreatedAt time.Time `json:"created_at"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` - Username string `json:"username"` - PasswordHash string `json:"password_hash"` + UserID uuid.UUID `json:"user_id"` + CreatedAt time.Time `json:"created_at"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + Username string `json:"username"` + PasswordHash string `json:"password_hash"` + ProfileBgColor string `json:"profile_bg_color"` } diff --git a/api/pg/pg.go b/api/pg/pg.go index 19b3bef..e5c108e 100644 --- a/api/pg/pg.go +++ b/api/pg/pg.go @@ -22,6 +22,10 @@ type Repository interface { GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) + CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error) + GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error) + GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error) + CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error) DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error @@ -55,6 +59,7 @@ type Repository interface { CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error) GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error) + DeleteTaskAssignedByID(ctx context.Context, arg DeleteTaskAssignedByIDParams) (TaskAssigned, error) } type repoSvc struct { diff --git a/api/pg/project_label.sql.go b/api/pg/project_label.sql.go new file mode 100644 index 0000000..39a5758 --- /dev/null +++ b/api/pg/project_label.sql.go @@ -0,0 +1,92 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: project_label.sql + +package pg + +import ( + "context" + "database/sql" + "time" + + "github.com/google/uuid" +) + +const createProjectLabel = `-- name: CreateProjectLabel :one +INSERT INTO project_label (project_id, label_color_id, created_date, name) + VALUES ($1, $2, $3, $4) RETURNING project_label_id, project_id, label_color_id, created_date, name +` + +type CreateProjectLabelParams struct { + ProjectID uuid.UUID `json:"project_id"` + LabelColorID uuid.UUID `json:"label_color_id"` + CreatedDate time.Time `json:"created_date"` + Name sql.NullString `json:"name"` +} + +func (q *Queries) CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error) { + row := q.db.QueryRowContext(ctx, createProjectLabel, + arg.ProjectID, + arg.LabelColorID, + arg.CreatedDate, + arg.Name, + ) + var i ProjectLabel + err := row.Scan( + &i.ProjectLabelID, + &i.ProjectID, + &i.LabelColorID, + &i.CreatedDate, + &i.Name, + ) + return i, err +} + +const getProjectLabelByID = `-- name: GetProjectLabelByID :one +SELECT project_label_id, project_id, label_color_id, created_date, name FROM project_label WHERE project_label_id = $1 +` + +func (q *Queries) GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error) { + row := q.db.QueryRowContext(ctx, getProjectLabelByID, projectLabelID) + var i ProjectLabel + err := row.Scan( + &i.ProjectLabelID, + &i.ProjectID, + &i.LabelColorID, + &i.CreatedDate, + &i.Name, + ) + return i, err +} + +const getProjectLabelsForProject = `-- name: GetProjectLabelsForProject :many +SELECT project_label_id, project_id, label_color_id, created_date, name FROM project_label WHERE project_id = $1 +` + +func (q *Queries) GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error) { + rows, err := q.db.QueryContext(ctx, getProjectLabelsForProject, projectID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ProjectLabel + for rows.Next() { + var i ProjectLabel + if err := rows.Scan( + &i.ProjectLabelID, + &i.ProjectID, + &i.LabelColorID, + &i.CreatedDate, + &i.Name, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/api/pg/querier.go b/api/pg/querier.go index 114709b..4265666 100644 --- a/api/pg/querier.go +++ b/api/pg/querier.go @@ -11,6 +11,7 @@ import ( type Querier interface { CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) + CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error) CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error) @@ -21,6 +22,7 @@ type Querier interface { DeleteExpiredTokens(ctx context.Context) error DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error DeleteRefreshTokenByUserID(ctx context.Context, userID uuid.UUID) error + DeleteTaskAssignedByID(ctx context.Context, arg DeleteTaskAssignedByIDParams) (TaskAssigned, error) DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error DeleteTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (int64, error) DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) (int64, error) @@ -35,6 +37,8 @@ type Querier interface { 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) + GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error) + GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, 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) diff --git a/api/pg/task_assigned.sql.go b/api/pg/task_assigned.sql.go index e642f54..73f0df9 100644 --- a/api/pg/task_assigned.sql.go +++ b/api/pg/task_assigned.sql.go @@ -33,6 +33,27 @@ func (q *Queries) CreateTaskAssigned(ctx context.Context, arg CreateTaskAssigned return i, err } +const deleteTaskAssignedByID = `-- name: DeleteTaskAssignedByID :one +DELETE FROM task_assigned WHERE task_id = $1 AND user_id = $2 RETURNING task_assigned_id, task_id, user_id, assigned_date +` + +type DeleteTaskAssignedByIDParams struct { + TaskID uuid.UUID `json:"task_id"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) DeleteTaskAssignedByID(ctx context.Context, arg DeleteTaskAssignedByIDParams) (TaskAssigned, error) { + row := q.db.QueryRowContext(ctx, deleteTaskAssignedByID, arg.TaskID, arg.UserID) + 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 ` diff --git a/api/pg/task_label.sql.go b/api/pg/task_label.sql.go index 6c47ba4..2c07303 100644 --- a/api/pg/task_label.sql.go +++ b/api/pg/task_label.sql.go @@ -11,30 +11,30 @@ import ( ) 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 +INSERT INTO task_label (task_id, project_label_id, assigned_date) + VALUES ($1, $2, $3) RETURNING task_label_id, task_id, project_label_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"` + TaskID uuid.UUID `json:"task_id"` + ProjectLabelID uuid.UUID `json:"project_label_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) + row := q.db.QueryRowContext(ctx, createTaskLabelForTask, arg.TaskID, arg.ProjectLabelID, arg.AssignedDate) var i TaskLabel err := row.Scan( &i.TaskLabelID, &i.TaskID, - &i.LabelColorID, + &i.ProjectLabelID, &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 +SELECT task_label_id, task_id, project_label_id, assigned_date FROM task_label WHERE task_id = $1 ` func (q *Queries) GetTaskLabelsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskLabel, error) { @@ -49,7 +49,7 @@ func (q *Queries) GetTaskLabelsForTaskID(ctx context.Context, taskID uuid.UUID) if err := rows.Scan( &i.TaskLabelID, &i.TaskID, - &i.LabelColorID, + &i.ProjectLabelID, &i.AssignedDate, ); err != nil { return nil, err diff --git a/api/pg/user_accounts.sql.go b/api/pg/user_accounts.sql.go index 52f78a1..1815f64 100644 --- a/api/pg/user_accounts.sql.go +++ b/api/pg/user_accounts.sql.go @@ -13,7 +13,7 @@ import ( const createUserAccount = `-- name: CreateUserAccount :one INSERT INTO user_account(first_name, last_name, email, username, created_at, password_hash) VALUES ($1, $2, $3, $4, $5, $6) -RETURNING user_id, created_at, first_name, last_name, email, username, password_hash +RETURNING user_id, created_at, first_name, last_name, email, username, password_hash, profile_bg_color ` type CreateUserAccountParams struct { @@ -43,12 +43,13 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa &i.Email, &i.Username, &i.PasswordHash, + &i.ProfileBgColor, ) return i, err } const getAllUserAccounts = `-- name: GetAllUserAccounts :many -SELECT user_id, created_at, first_name, last_name, email, username, password_hash FROM user_account +SELECT user_id, created_at, first_name, last_name, email, username, password_hash, profile_bg_color FROM user_account ` func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) { @@ -68,6 +69,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) &i.Email, &i.Username, &i.PasswordHash, + &i.ProfileBgColor, ); err != nil { return nil, err } @@ -83,7 +85,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) } const getUserAccountByID = `-- name: GetUserAccountByID :one -SELECT user_id, created_at, first_name, last_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, profile_bg_color FROM user_account WHERE user_id = $1 ` func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) { @@ -97,12 +99,13 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use &i.Email, &i.Username, &i.PasswordHash, + &i.ProfileBgColor, ) return i, err } const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one -SELECT user_id, created_at, first_name, last_name, email, username, password_hash FROM user_account WHERE username = $1 +SELECT user_id, created_at, first_name, last_name, email, username, password_hash, profile_bg_color FROM user_account WHERE username = $1 ` func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) { @@ -116,6 +119,7 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) &i.Email, &i.Username, &i.PasswordHash, + &i.ProfileBgColor, ) return i, err } diff --git a/api/query/project_label.sql b/api/query/project_label.sql new file mode 100644 index 0000000..110a593 --- /dev/null +++ b/api/query/project_label.sql @@ -0,0 +1,9 @@ +-- name: CreateProjectLabel :one +INSERT INTO project_label (project_id, label_color_id, created_date, name) + VALUES ($1, $2, $3, $4) RETURNING *; + +-- name: GetProjectLabelsForProject :many +SELECT * FROM project_label WHERE project_id = $1; + +-- name: GetProjectLabelByID :one +SELECT * FROM project_label WHERE project_label_id = $1; diff --git a/api/query/task_assigned.sql b/api/query/task_assigned.sql index 57cd033..039e6de 100644 --- a/api/query/task_assigned.sql +++ b/api/query/task_assigned.sql @@ -4,3 +4,6 @@ INSERT INTO task_assigned (task_id, user_id, assigned_date) -- name: GetAssignedMembersForTask :many SELECT * FROM task_assigned WHERE task_id = $1; + +-- name: DeleteTaskAssignedByID :one +DELETE FROM task_assigned WHERE task_id = $1 AND user_id = $2 RETURNING *; diff --git a/api/query/task_label.sql b/api/query/task_label.sql index 58ab624..b014927 100644 --- a/api/query/task_label.sql +++ b/api/query/task_label.sql @@ -1,5 +1,5 @@ -- name: CreateTaskLabelForTask :one -INSERT INTO task_label (task_id, label_color_id, assigned_date) +INSERT INTO task_label (task_id, project_label_id, assigned_date) VALUES ($1, $2, $3) RETURNING *; -- name: GetTaskLabelsForTaskID :many diff --git a/web/src/App/TopNavbar.tsx b/web/src/App/TopNavbar.tsx index fc1f776..4919d2f 100644 --- a/web/src/App/TopNavbar.tsx +++ b/web/src/App/TopNavbar.tsx @@ -41,13 +41,27 @@ const GlobalTopNavbar: React.FC = () => { return ( <> console.log('beep')} onProfileClick={onProfileClick} /> - {menu.isOpen && } + {menu.isOpen && ( + { + setMenu({ + top: 0, + left: 0, + isOpen: false, + }); + }} + onLogout={onLogout} + left={menu.left} + top={menu.top} + /> + )} ); }; diff --git a/web/src/Projects/Project/Details/index.tsx b/web/src/Projects/Project/Details/index.tsx index f1b03d7..270ed8a 100644 --- a/web/src/Projects/Project/Details/index.tsx +++ b/web/src/Projects/Project/Details/index.tsx @@ -4,7 +4,7 @@ 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 { useFindTaskQuery, useAssignTaskMutation, useUnassignTaskMutation } from 'shared/generated/graphql'; import UserIDContext from 'App/context'; type DetailsProps = { @@ -15,6 +15,7 @@ type DetailsProps = { onDeleteTask: (task: Task) => void; onOpenAddLabelPopup: (task: Task, bounds: ElementBounds) => void; availableMembers: Array; + refreshCache: () => void; }; const initialMemberPopupState = { taskID: '', isOpen: false, top: 0, left: 0 }; @@ -27,13 +28,25 @@ const Details: React.FC = ({ onDeleteTask, onOpenAddLabelPopup, availableMembers, + refreshCache, }) => { const { userID } = useContext(UserIDContext); const history = useHistory(); const match = useRouteMatch(); const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState); - const { loading, data } = useFindTaskQuery({ variables: { taskID } }); - const [assignTask] = useAssignTaskMutation(); + const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } }); + const [assignTask] = useAssignTaskMutation({ + onCompleted: () => { + refetch(); + refreshCache(); + }, + }); + const [unassignTask] = useUnassignTaskMutation({ + onCompleted: () => { + refetch(); + refreshCache(); + }, + }); if (loading) { return
loading
; } @@ -47,6 +60,7 @@ const Details: React.FC = ({ profileIcon: { url: null, initials: assigned.profileIcon.initials ?? null, + bgColor: assigned.profileIcon.bgColor ?? null, }, }; }); @@ -93,10 +107,13 @@ const Details: React.FC = ({ > { + console.log(`is active ${member.userID} - ${isActive}`); if (isActive) { assignTask({ variables: { taskID: data.findTask.taskID, userID: userID ?? '' } }); + } else { + unassignTask({ variables: { taskID: data.findTask.taskID, userID: userID ?? '' } }); } console.log(member, isActive); }} diff --git a/web/src/Projects/Project/KanbanBoard/Styles.ts b/web/src/Projects/Project/KanbanBoard/Styles.ts index 301d2bf..941a636 100644 --- a/web/src/Projects/Project/KanbanBoard/Styles.ts +++ b/web/src/Projects/Project/KanbanBoard/Styles.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; export const Board = styled.div` - margin-left: 36px; + margin-top: 12px; + margin-left: 8px; `; diff --git a/web/src/Projects/Project/index.tsx b/web/src/Projects/Project/index.tsx index 16ebaee..479eb60 100644 --- a/web/src/Projects/Project/index.tsx +++ b/web/src/Projects/Project/index.tsx @@ -109,9 +109,10 @@ const Project = () => { ); }, }); - const { loading, data } = useFindProjectQuery({ + const { loading, data, refetch } = useFindProjectQuery({ variables: { projectId }, onCompleted: newData => { + console.log('beep!'); const newListsData: BoardState = { tasks: {}, columns: {} }; newData.findProject.taskGroups.forEach(taskGroup => { newListsData.columns[taskGroup.taskGroupID] = { @@ -121,15 +122,27 @@ const Project = () => { tasks: [], }; taskGroup.tasks.forEach(task => { + const taskMembers = task.assigned.map(assigned => { + return { + userID: assigned.userID, + displayName: `${assigned.firstName} ${assigned.lastName}`, + profileIcon: { + url: null, + initials: assigned.profileIcon.initials ?? '', + bgColor: assigned.profileIcon.bgColor ?? '#7367F0', + }, + }; + }); newListsData.tasks[task.taskID] = { taskID: task.taskID, taskGroup: { taskGroupID: taskGroup.taskGroupID, }, name: task.name, - position: task.position, labels: [], + position: task.position, description: task.description ?? undefined, + members: taskMembers, }; }); }); @@ -196,15 +209,16 @@ const Project = () => { const availableMembers = data.findProject.members.map(member => { return { displayName: `${member.firstName} ${member.lastName}`, - profileIcon: { url: null, initials: member.profileIcon.initials ?? null }, + profileIcon: { + url: null, + initials: member.profileIcon.initials ?? null, + bgColor: member.profileIcon.bgColor ?? null, + }, userID: member.userID, }; }); return ( <> - - {data.findProject.name} - { path={`${match.path}/c/:taskID`} render={(routeProps: RouteComponentProps) => (
{ + console.log('beep 2!'); + refetch(); + }} availableMembers={availableMembers} projectURL={match.url} taskID={routeProps.match.params.taskID} diff --git a/web/src/citadel.d.ts b/web/src/citadel.d.ts index 5a833f2..c467e98 100644 --- a/web/src/citadel.d.ts +++ b/web/src/citadel.d.ts @@ -37,6 +37,7 @@ type InnerTaskGroup = { type ProfileIcon = { url: string | null; initials: string | null; + bgColor: string | null; }; type TaskUser = { diff --git a/web/src/shared/components/Card/Styles.ts b/web/src/shared/components/Card/Styles.ts index 00cbfc2..350f389 100644 --- a/web/src/shared/components/Card/Styles.ts +++ b/web/src/shared/components/Card/Styles.ts @@ -98,9 +98,6 @@ export const ListCardOperation = styled.span` display: flex; align-content: center; justify-content: center; - background-color: ${props => mixin.darken('#262c49', 0.15)}; - background-clip: padding-box; - background-origin: padding-box; border-radius: 3px; opacity: 0.8; padding: 6px; @@ -108,6 +105,10 @@ export const ListCardOperation = styled.span` right: 2px; top: 2px; z-index: 10; + + &:hover { + background-color: ${props => mixin.darken('#262c49', 0.45)}; + } `; export const CardTitle = styled.span` @@ -120,3 +121,34 @@ export const CardTitle = styled.span` word-wrap: break-word; color: #c2c6dc; `; + +export const CardMembers = styled.div` + float: right; + margin: 0 -2px 0 0; +`; + +export const CardMember = styled.div<{ bgColor: string }>` + height: 28px; + width: 28px; + float: right; + margin: 0 0 4px 4px; + + background-color: ${props => props.bgColor}; + color: #fff; + border-radius: 25em; + cursor: pointer; + display: block; + overflow: visible; + position: relative; + text-decoration: none; + z-index: 0; +`; + +export const CardMemberInitials = styled.div` + height: 28px; + width: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; +`; diff --git a/web/src/shared/components/Card/index.tsx b/web/src/shared/components/Card/index.tsx index 6c79cfe..4411ff0 100644 --- a/web/src/shared/components/Card/index.tsx +++ b/web/src/shared/components/Card/index.tsx @@ -18,6 +18,9 @@ import { ListCardLabel, ListCardOperation, CardTitle, + CardMembers, + CardMember, + CardMemberInitials, } from './Styles'; type DueDate = { @@ -42,6 +45,7 @@ type Props = { watched?: boolean; labels?: Label[]; wrapperProps?: any; + members?: Array | null; }; const Card = React.forwardRef( @@ -58,6 +62,7 @@ const Card = React.forwardRef( description, checklists, watched, + members, }: Props, $cardRef: any, ) => { @@ -95,9 +100,11 @@ const Card = React.forwardRef( {...wrapperProps} > - - - + {isActive && ( + + + + )} {labels && @@ -132,6 +139,14 @@ const Card = React.forwardRef( )} + + {members && + members.map(member => ( + + {member.profileIcon.initials} + + ))} + diff --git a/web/src/shared/components/DropdownMenu/DropdownMenu.stories.tsx b/web/src/shared/components/DropdownMenu/DropdownMenu.stories.tsx index 49f6d41..f3e5218 100644 --- a/web/src/shared/components/DropdownMenu/DropdownMenu.stories.tsx +++ b/web/src/shared/components/DropdownMenu/DropdownMenu.stories.tsx @@ -1,8 +1,7 @@ import React, { createRef, useState } from 'react'; import styled from 'styled-components'; - -import DropdownMenu from '.'; import { action } from '@storybook/addon-actions'; +import DropdownMenu from '.'; export default { component: DropdownMenu, @@ -50,7 +49,16 @@ export const Default = () => { Click me - {menu.isOpen && } + {menu.isOpen && ( + { + setMenu({ top: 0, left: 0, isOpen: false }); + }} + onLogout={action('on logout')} + left={menu.left} + top={menu.top} + /> + )} ); }; diff --git a/web/src/shared/components/DropdownMenu/index.tsx b/web/src/shared/components/DropdownMenu/index.tsx index a8d4ea8..0fadfec 100644 --- a/web/src/shared/components/DropdownMenu/index.tsx +++ b/web/src/shared/components/DropdownMenu/index.tsx @@ -1,5 +1,5 @@ -import React from 'react'; - +import React, { useRef } from 'react'; +import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import { Exit, User } from 'shared/icons'; import { Separator, Container, WrapperDiamond, Wrapper, ActionsList, ActionItem, ActionTitle } from './Styles'; @@ -7,11 +7,14 @@ type DropdownMenuProps = { left: number; top: number; onLogout: () => void; + onCloseDropdown: () => void; }; -const DropdownMenu: React.FC = ({ left, top, onLogout }) => { +const DropdownMenu: React.FC = ({ left, top, onLogout, onCloseDropdown }) => { + const $containerRef = useRef(null); + useOnOutsideClick($containerRef, true, onCloseDropdown, null); return ( - + diff --git a/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx b/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx index dd31a7b..ea0fcec 100644 --- a/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx +++ b/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx @@ -23,7 +23,9 @@ export const Default = () => { position: 1, labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }], description: 'hello!', - members: [{ userID: '1', profileIcon: { url: null, initials: null }, displayName: 'Jordan Knott' }], + members: [ + { userID: '1', profileIcon: { url: null, initials: null, bgColor: null }, displayName: 'Jordan Knott' }, + ], }} onCancel={action('cancel')} onDueDateChange={action('due date change')} diff --git a/web/src/shared/components/Lists/index.tsx b/web/src/shared/components/Lists/index.tsx index 22e8f32..1bcd813 100644 --- a/web/src/shared/components/Lists/index.tsx +++ b/web/src/shared/components/Lists/index.tsx @@ -159,6 +159,7 @@ const Lists: React.FC = ({ description="" title={task.name} labels={task.labels} + members={task.members} onClick={() => onCardClick(task)} onContextMenu={onQuickEditorOpen} /> diff --git a/web/src/shared/components/MiniProfile/Styles.ts b/web/src/shared/components/MiniProfile/Styles.ts new file mode 100644 index 0000000..8b00495 --- /dev/null +++ b/web/src/shared/components/MiniProfile/Styles.ts @@ -0,0 +1,50 @@ +import styled from 'styled-components'; + +export const Profile = styled.div` + margin: 8px 0; + min-height: 56px; + position: relative; +`; + +export const ProfileIcon = styled.div<{ bgColor: string }>` + float: left; + margin: 2px; + background-color: ${props => props.bgColor}; + border-radius: 25em; + display: block; + height: 50px; + overflow: hidden; + position: relative; + width: 50px; + z-index: 1; +`; + +export const ProfileInfo = styled.div` + margin: 0 0 0 64px; + word-wrap: break-word; +`; + +export const InfoTitle = styled.h3` + margin: 0 40px 0 0; + font-size: 16px; + font-weight: 600; + line-height: 20px; + color: #172b4d; +`; + +export const InfoUsername = styled.p` + color: #5e6c84; + font-size: 14px; + line-height: 20px; +`; + +export const InfoBio = styled.p` + font-size: 14px; + line-height: 20px; + color: #5e6c84; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin: 0; + padding: 0; +`; diff --git a/web/src/shared/components/MiniProfile/index.tsx b/web/src/shared/components/MiniProfile/index.tsx new file mode 100644 index 0000000..36b8431 --- /dev/null +++ b/web/src/shared/components/MiniProfile/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import { Profile, ProfileIcon, ProfileInfo, InfoTitle, InfoUsername, InfoBio } from './Styles'; + +type MiniProfileProps = { + displayName: string; + username: string; + bio: string; + profileIcon: ProfileIcon; +}; +const MiniProfile: React.FC = ({ displayName, username, bio, profileIcon }) => { + return ( + <> + + {profileIcon.initials} + + {displayName} + {username} + {bio} + + + + ); +}; + +export default MiniProfile; diff --git a/web/src/shared/components/Modal/Styles.ts b/web/src/shared/components/Modal/Styles.ts index c037d7b..728c516 100644 --- a/web/src/shared/components/Modal/Styles.ts +++ b/web/src/shared/components/Modal/Styles.ts @@ -17,13 +17,12 @@ export const ClickableOverlay = styled.div` background: rgba(0, 0, 0, 0.4); display: flex; justify-content: center; - align-items: center; - padding: 50px; `; export const StyledModal = styled.div<{ width: number }>` display: inline-block; position: relative; + margin: 48px 0 80px; width: 100%; background: #262c49; max-width: ${props => props.width}px; diff --git a/web/src/shared/components/Navbar/Styles.ts b/web/src/shared/components/Navbar/Styles.ts index 786c34d..ff4e4e2 100644 --- a/web/src/shared/components/Navbar/Styles.ts +++ b/web/src/shared/components/Navbar/Styles.ts @@ -1,32 +1,14 @@ import styled, { css } from 'styled-components'; -export const LogoWrapper = styled.div` - margin: 20px 0px 20px; - - position: relative; - width: 100%; - height: 42px; - line-height: 42px; - padding-left: 64px; - color: rgb(222, 235, 255); - cursor: pointer; - user-select: none; - transition: color 0.1s ease 0s; -`; - -export const Logo = styled.div` - position: absolute; - left: 19px; -`; +export const Logo = styled.div``; export const LogoTitle = styled.div` - position: relative; - right: 12px; + position: absolute; visibility: hidden; opacity: 0; font-size: 24px; font-weight: 600; - transition: right 0.1s ease 0s, visibility, opacity, transform 0.25s ease; + transition: visibility, opacity, transform 0.25s ease; color: #7367f0; `; export const ActionContainer = styled.div` @@ -54,6 +36,10 @@ export const IconWrapper = styled.div` export const ActionButtonContainer = styled.div` padding: 0 12px; position: relative; + + & > a:first-child > div { + padding-top: 48px; + } `; export const ActionButtonWrapper = styled.div<{ active?: boolean }>` @@ -65,7 +51,7 @@ export const ActionButtonWrapper = styled.div<{ active?: boolean }>` `} border-radius: 6px; cursor: pointer; - padding: 10px 15px; + padding: 24px 15px; display: flex; align-items: center; &:hover ${ActionButtonTitle} { @@ -76,6 +62,20 @@ export const ActionButtonWrapper = styled.div<{ active?: boolean }>` } `; +export const LogoWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + position: relative; + width: 100%; + height: 80px; + color: rgb(222, 235, 255); + cursor: pointer; + transition: color 0.1s ease 0s, border 0.1s ease 0s; + border-bottom: 1px solid rgba(65, 69, 97, 0.65); +`; + export const Container = styled.aside` z-index: 100; position: fixed; @@ -87,13 +87,15 @@ export const Container = styled.aside` transform: translateZ(0px); background: #10163a; transition: all 0.1s ease 0s; + border-right: 1px solid rgba(65, 69, 97, 0.65); &:hover { width: 260px; box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 50px 0px; + border-right: 1px solid rgba(65, 69, 97, 0); } &:hover ${LogoTitle} { - right: 0px; + bottom: -12px; visibility: visible; opacity: 1; } @@ -102,4 +104,8 @@ export const Container = styled.aside` visibility: visible; opacity: 1; } + + &:hover ${LogoWrapper} { + border-bottom: 1px solid rgba(65, 69, 97, 0); + } `; diff --git a/web/src/shared/components/Navbar/index.tsx b/web/src/shared/components/Navbar/index.tsx index 429c90d..e171c0d 100644 --- a/web/src/shared/components/Navbar/index.tsx +++ b/web/src/shared/components/Navbar/index.tsx @@ -35,9 +35,7 @@ export const ButtonContainer: React.FC = ({ children }) => ( export const PrimaryLogo = () => { return ( - - - + Citadel ); diff --git a/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx b/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx index 801ca06..c205b05 100644 --- a/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx +++ b/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx @@ -6,6 +6,7 @@ import LabelEditor from 'shared/components/PopupMenu/LabelEditor'; import ListActions from 'shared/components/ListActions'; import MemberManager from 'shared/components/MemberManager'; import DueDateManager from 'shared/components/DueDateManager'; +import MiniProfile from 'shared/components/MiniProfile'; import PopupMenu from '.'; import NormalizeStyles from 'App/NormalizeStyles'; @@ -115,7 +116,7 @@ export const MemberManagerPopup = () => { setPopupData(initalState)} left={popupData.left}> { position: 1, labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }], description: 'hello!', - members: [{ userID: '1', profileIcon: { url: null, initials: null }, displayName: 'Jordan Knott' }], + members: [ + { userID: '1', profileIcon: { bgColor: null, url: null, initials: null }, displayName: 'Jordan Knott' }, + ], }} onCancel={action('cancel')} onDueDateChange={action('due date change')} @@ -189,3 +192,47 @@ export const DueDateManagerPopup = () => { ); }; + +export const MiniProfilePopup = () => { + const $buttonRef = useRef(null); + const [popupData, setPopupData] = useState(initalState); + return ( + <> + + + {popupData.isOpen && ( + setPopupData(initalState)} left={popupData.left}> + + + )} + { + if ($buttonRef && $buttonRef.current) { + const pos = $buttonRef.current.getBoundingClientRect(); + setPopupData({ + isOpen: true, + left: pos.left, + top: pos.top + pos.height + 10, + }); + } + }} + > + Open + + + ); +}; diff --git a/web/src/shared/components/TaskDetails/Styles.ts b/web/src/shared/components/TaskDetails/Styles.ts index e5ceeeb..72ae829 100644 --- a/web/src/shared/components/TaskDetails/Styles.ts +++ b/web/src/shared/components/TaskDetails/Styles.ts @@ -281,3 +281,9 @@ export const NoDueDateLabel = styled.span` font-size: 14px; cursor: pointer; `; + +export const UnassignedLabel = styled.div` + color: rgb(137, 147, 164); + font-size: 14px; + cursor: pointer; +`; diff --git a/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx index a860aaa..2dc84a8 100644 --- a/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx +++ b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx @@ -35,7 +35,13 @@ export const Default = () => { position: 1, labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }], description, - members: [{ userID: '1', profileIcon: { url: null, initials: null }, displayName: 'Jordan Knott' }], + members: [ + { + userID: '1', + profileIcon: { bgColor: null, url: null, initials: null }, + displayName: 'Jordan Knott', + }, + ], }} onTaskNameChange={action('task name change')} onTaskDescriptionChange={(_task, desc) => setDescription(desc)} diff --git a/web/src/shared/components/TaskDetails/index.tsx b/web/src/shared/components/TaskDetails/index.tsx index 1bcd962..bc07334 100644 --- a/web/src/shared/components/TaskDetails/index.tsx +++ b/web/src/shared/components/TaskDetails/index.tsx @@ -4,6 +4,7 @@ import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import { NoDueDateLabel, + UnassignedLabel, TaskDetailsAddMember, TaskGroupLabel, TaskGroupLabelName, @@ -126,11 +127,16 @@ const TaskDetails: React.FC = ({ onTaskNameChange(task, taskName); } }; + const $unassignedRef = useRef(null); const $addMemberRef = useRef(null); + const onUnassignedClick = () => { + const bounds = convertDivElementRefToBounds($unassignedRef); + if (bounds) { + onOpenAddMemberPopup(task, bounds); + } + }; const onAddMember = () => { - console.log('beep!'); const bounds = convertDivElementRefToBounds($addMemberRef); - console.log(bounds); if (bounds) { onOpenAddMemberPopup(task, bounds); } @@ -191,20 +197,28 @@ const TaskDetails: React.FC = ({ Assignees - {task.members && - task.members.map(member => { - console.log(member); - return ( - - {member.profileIcon.initials ?? ''} - - ); - })} - - - - - + {task.members && task.members.length === 0 ? ( + + Unassigned + + ) : ( + <> + {task.members && + task.members.map(member => { + console.log(member); + return ( + + {member.profileIcon.initials ?? ''} + + ); + })} + + + + + + + )} Labels diff --git a/web/src/shared/components/TopNavbar/Styles.ts b/web/src/shared/components/TopNavbar/Styles.ts index d7af5ad..29628e1 100644 --- a/web/src/shared/components/TopNavbar/Styles.ts +++ b/web/src/shared/components/TopNavbar/Styles.ts @@ -1,19 +1,18 @@ import styled from 'styled-components'; export const NavbarWrapper = styled.div` - height: 103px; - padding: 1.3rem 2.2rem 2.2rem; width: 100%; `; export const NavbarHeader = styled.header` - border-radius: 0.5rem; - padding: 0.8rem 1rem; + height: 80px; + padding: 0 1.75rem; display: flex; align-items: center; justify-content: space-between; background: rgb(16, 22, 58); box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05); + border-bottom: 1px solid rgba(65, 69, 97, 0.65); `; export const Breadcrumbs = styled.div` color: rgb(94, 108, 132); @@ -26,7 +25,14 @@ export const BreadcrumpSeparator = styled.span` margin: 0px 10px; `; -export const ProjectActions = styled.div``; +export const ProjectActions = styled.div` + align-items: flex-start; + display: flex; + flex: 1; + flex-direction: column; + min-width: 1px; +`; + export const GlobalActions = styled.div` display: flex; align-items: center; @@ -55,7 +61,7 @@ export const ProfileNameSecondary = styled.small` color: #c2c6dc; `; -export const ProfileIcon = styled.div` +export const ProfileIcon = styled.div<{ bgColor: string }>` margin-left: 10px; width: 40px; height: 40px; @@ -65,6 +71,48 @@ export const ProfileIcon = styled.div` justify-content: center; color: #fff; font-weight: 700; - background: rgb(115, 103, 240); + background: ${props => props.bgColor}; cursor: pointer; `; + +export const ProjectMeta = styled.div` + align-items: center; + display: flex; + max-width: 100%; + min-height: 51px; +`; + +export const ProjectTabs = styled.div` + align-items: flex-end; + align-self: stretch; + display: flex; + flex: 1 0 auto; + justify-content: flex-start; + max-width: 100%; +`; + +export const ProjectTab = styled.span` + font-size: 80%; + color: #c2c6dc; + font-size: 15px; + cursor: default; + display: flex; + line-height: normal; + min-width: 1px; + transition-duration: 0.2s; + transition-property: box-shadow, color; + white-space: nowrap; + flex: 0 1 auto; + + padding-bottom: 12px; + + box-shadow: inset 0 -2px #d85dd8; + color: #d85dd8; +`; + +export const ProjectName = styled.h1` + color: #c2c6dc; + margin-top: 9px; + font-weight: 600; + font-size: 20px; +`; diff --git a/web/src/shared/components/TopNavbar/TopNavbar.stories.tsx b/web/src/shared/components/TopNavbar/TopNavbar.stories.tsx index 64fa318..dfdcdfd 100644 --- a/web/src/shared/components/TopNavbar/TopNavbar.stories.tsx +++ b/web/src/shared/components/TopNavbar/TopNavbar.stories.tsx @@ -38,13 +38,23 @@ export const Default = () => { - {menu.isOpen && } + {menu.isOpen && ( + { + setMenu({ left: 0, top: 0, isOpen: false }); + }} + onLogout={action('on logout')} + left={menu.left} + top={menu.top} + /> + )} ); }; diff --git a/web/src/shared/components/TopNavbar/index.tsx b/web/src/shared/components/TopNavbar/index.tsx index 0181e7c..ad87287 100644 --- a/web/src/shared/components/TopNavbar/index.tsx +++ b/web/src/shared/components/TopNavbar/index.tsx @@ -5,6 +5,10 @@ import { NotificationContainer, GlobalActions, ProjectActions, + ProjectMeta, + ProjectName, + ProjectTabs, + ProjectTab, NavbarWrapper, NavbarHeader, Breadcrumbs, @@ -19,11 +23,19 @@ import { type NavBarProps = { onProfileClick: (bottom: number, right: number) => void; onNotificationClick: () => void; + bgColor: string; firstName: string; lastName: string; initials: string; }; -const NavBar: React.FC = ({ onProfileClick, onNotificationClick, firstName, lastName, initials }) => { +const NavBar: React.FC = ({ + onProfileClick, + onNotificationClick, + firstName, + lastName, + initials, + bgColor, +}) => { const $profileRef: any = useRef(null); const handleProfileClick = () => { console.log('click'); @@ -34,13 +46,12 @@ const NavBar: React.FC = ({ onProfileClick, onNotificationClick, fi - - Projects - / - project name - / - Board - + + Production Team + + + Board + @@ -53,7 +64,7 @@ const NavBar: React.FC = ({ onProfileClick, onNotificationClick, fi Manager - + {initials} diff --git a/web/src/shared/generated/graphql.tsx b/web/src/shared/generated/graphql.tsx index ee6f512..716eab1 100644 --- a/web/src/shared/generated/graphql.tsx +++ b/web/src/shared/generated/graphql.tsx @@ -15,17 +15,28 @@ export type Scalars = { +export type ProjectLabel = { + __typename?: 'ProjectLabel'; + projectLabelID: Scalars['ID']; + createdDate: Scalars['Time']; + colorHex: Scalars['String']; + name?: Maybe; +}; + export type TaskLabel = { __typename?: 'TaskLabel'; taskLabelID: Scalars['ID']; - labelColorID: Scalars['UUID']; + projectLabelID: Scalars['UUID']; + assignedDate: Scalars['Time']; colorHex: Scalars['String']; + name?: Maybe; }; export type ProfileIcon = { __typename?: 'ProfileIcon'; url?: Maybe; initials?: Maybe; + bgColor?: Maybe; }; export type ProjectMember = { @@ -71,6 +82,7 @@ export type Project = { owner: ProjectMember; taskGroups: Array; members: Array; + labels: Array; }; export type TaskGroup = { @@ -222,6 +234,11 @@ export type AssignTaskInput = { userID: Scalars['UUID']; }; +export type UnassignTaskInput = { + taskID: Scalars['UUID']; + userID: Scalars['UUID']; +}; + export type UpdateTaskDescriptionInput = { taskID: Scalars['UUID']; description: Scalars['String']; @@ -237,12 +254,19 @@ export type RemoveTaskLabelInput = { taskLabelID: Scalars['UUID']; }; +export type NewProjectLabel = { + projectID: Scalars['UUID']; + labelColorID: Scalars['UUID']; + name?: Maybe; +}; + export type Mutation = { __typename?: 'Mutation'; createRefreshToken: RefreshToken; createUserAccount: UserAccount; createTeam: Team; createProject: Project; + createProjectLabel: ProjectLabel; createTaskGroup: TaskGroup; updateTaskGroupLocation: TaskGroup; deleteTaskGroup: DeleteTaskGroupPayload; @@ -254,6 +278,7 @@ export type Mutation = { updateTaskName: Task; deleteTask: DeleteTaskPayload; assignTask: Task; + unassignTask: Task; logoutUser: Scalars['Boolean']; }; @@ -278,6 +303,11 @@ export type MutationCreateProjectArgs = { }; +export type MutationCreateProjectLabelArgs = { + input: NewProjectLabel; +}; + + export type MutationCreateTaskGroupArgs = { input: NewTaskGroup; }; @@ -333,6 +363,11 @@ export type MutationAssignTaskArgs = { }; +export type MutationUnassignTaskArgs = { + input?: Maybe; +}; + + export type MutationLogoutUserArgs = { input: LogoutUser; }; @@ -438,7 +473,7 @@ export type FindProjectQuery = ( & Pick & { profileIcon: ( { __typename?: 'ProfileIcon' } - & Pick + & Pick ) } )>, taskGroups: Array<( { __typename?: 'TaskGroup' } @@ -446,6 +481,14 @@ export type FindProjectQuery = ( & { tasks: Array<( { __typename?: 'Task' } & Pick + & { assigned: Array<( + { __typename?: 'ProjectMember' } + & Pick + & { profileIcon: ( + { __typename?: 'ProfileIcon' } + & Pick + ) } + )> } )> } )> } ) } @@ -469,7 +512,7 @@ export type FindTaskQuery = ( & Pick & { profileIcon: ( { __typename?: 'ProfileIcon' } - & Pick + & Pick ) } )> } ) } @@ -500,11 +543,29 @@ export type MeQuery = ( & Pick & { profileIcon: ( { __typename?: 'ProfileIcon' } - & Pick + & Pick ) } ) } ); +export type UnassignTaskMutationVariables = { + taskID: Scalars['UUID']; + userID: Scalars['UUID']; +}; + + +export type UnassignTaskMutation = ( + { __typename?: 'Mutation' } + & { unassignTask: ( + { __typename?: 'Task' } + & Pick + & { assigned: Array<( + { __typename?: 'ProjectMember' } + & Pick + )> } + ) } +); + export type UpdateTaskDescriptionMutationVariables = { taskID: Scalars['UUID']; description: Scalars['String']; @@ -759,6 +820,7 @@ export const FindProjectDocument = gql` profileIcon { url initials + bgColor } } taskGroups { @@ -770,6 +832,16 @@ export const FindProjectDocument = gql` name position description + assigned { + userID + firstName + lastName + profileIcon { + url + initials + bgColor + } + } } } } @@ -818,6 +890,7 @@ export const FindTaskDocument = gql` profileIcon { url initials + bgColor } } } @@ -893,6 +966,7 @@ export const MeDocument = gql` lastName profileIcon { initials + bgColor } } } @@ -922,6 +996,44 @@ export function useMeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptio export type MeQueryHookResult = ReturnType; export type MeLazyQueryHookResult = ReturnType; export type MeQueryResult = ApolloReactCommon.QueryResult; +export const UnassignTaskDocument = gql` + mutation unassignTask($taskID: UUID!, $userID: UUID!) { + unassignTask(input: {taskID: $taskID, userID: $userID}) { + assigned { + userID + firstName + lastName + } + taskID + } +} + `; +export type UnassignTaskMutationFn = ApolloReactCommon.MutationFunction; + +/** + * __useUnassignTaskMutation__ + * + * To run a mutation, you first call `useUnassignTaskMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUnassignTaskMutation` 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 [unassignTaskMutation, { data, loading, error }] = useUnassignTaskMutation({ + * variables: { + * taskID: // value for 'taskID' + * userID: // value for 'userID' + * }, + * }); + */ +export function useUnassignTaskMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(UnassignTaskDocument, baseOptions); + } +export type UnassignTaskMutationHookResult = ReturnType; +export type UnassignTaskMutationResult = ApolloReactCommon.MutationResult; +export type UnassignTaskMutationOptions = ApolloReactCommon.BaseMutationOptions; export const UpdateTaskDescriptionDocument = gql` mutation updateTaskDescription($taskID: UUID!, $description: String!) { updateTaskDescription(input: {taskID: $taskID, description: $description}) { diff --git a/web/src/shared/graphql/findProject.graphqls b/web/src/shared/graphql/findProject.graphqls index 221b7d5..9348daa 100644 --- a/web/src/shared/graphql/findProject.graphqls +++ b/web/src/shared/graphql/findProject.graphqls @@ -8,6 +8,7 @@ query findProject($projectId: String!) { profileIcon { url initials + bgColor } } taskGroups { @@ -19,6 +20,16 @@ query findProject($projectId: String!) { name position description + assigned { + userID + firstName + lastName + profileIcon { + url + initials + bgColor + } + } } } } diff --git a/web/src/shared/graphql/findTask.graphqls b/web/src/shared/graphql/findTask.graphqls index 7091b19..fdc5404 100644 --- a/web/src/shared/graphql/findTask.graphqls +++ b/web/src/shared/graphql/findTask.graphqls @@ -14,6 +14,7 @@ query findTask($taskID: UUID!) { profileIcon { url initials + bgColor } } } diff --git a/web/src/shared/graphql/me.graphqls b/web/src/shared/graphql/me.graphqls index 7f182c6..b9feeca 100644 --- a/web/src/shared/graphql/me.graphqls +++ b/web/src/shared/graphql/me.graphqls @@ -4,6 +4,7 @@ query me { lastName profileIcon { initials + bgColor } } } diff --git a/web/src/shared/graphql/unassignTask.graphqls b/web/src/shared/graphql/unassignTask.graphqls new file mode 100644 index 0000000..5c7669f --- /dev/null +++ b/web/src/shared/graphql/unassignTask.graphqls @@ -0,0 +1,10 @@ +mutation unassignTask($taskID: UUID!, $userID: UUID!) { + unassignTask(input: {taskID: $taskID, userID: $userID}) { + assigned { + userID + firstName + lastName + } + taskID + } +}