feature: add project labels

This commit is contained in:
Jordan Knott 2020-05-27 16:18:50 -05:00
parent fba4de631f
commit cbcd8c5f82
42 changed files with 1024 additions and 143 deletions

View File

@ -1,16 +1,55 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"github.com/jordanknott/project-citadel/api/router" "io/ioutil"
"time"
_ "github.com/lib/pq"
"github.com/jmoiron/sqlx"
"github.com/jordanknott/project-citadel/api/pg"
"github.com/BurntSushi/toml"
// "github.com/jordanknott/project-citadel/api/router"
// "time"
) )
type color struct {
Name string
Color string
Position int
}
type colors struct {
Color []color
}
func main() { func main() {
dur := time.Hour * 24 * 7 * 30 // dur := time.Hour * 24 * 7 * 30
token, err := router.NewAccessTokenCustomExpiration("21345076-6423-4a00-a6bd-cd9f830e2764", dur) // token, err := router.NewAccessTokenCustomExpiration("21345076-6423-4a00-a6bd-cd9f830e2764", dur)
// if err != nil {
// panic(err)
// }
// fmt.Println(token)
fmt.Println("seeding database...")
dat, err := ioutil.ReadFile("data/colors.toml")
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(token)
var labelColors colors
_, err = toml.Decode(string(dat), &labelColors)
if err != nil {
panic(err)
}
db, err := sqlx.Connect("postgres", "user=postgres password=test host=0.0.0.0 dbname=citadel sslmode=disable")
repository := pg.NewRepository(db)
for _, color := range labelColors.Color {
fmt.Printf("%v\n", color)
repository.CreateLabelColor(context.Background(), pg.CreateLabelColorParams{color.Name, color.Color, float64(color.Position)})
}
} }

49
api/data/colors.toml Normal file
View File

@ -0,0 +1,49 @@
[[color]]
name = 'green'
color = '#61bd4f'
position = 1
[[color]]
name = 'yellow'
color = '#f2d600'
position = 2
[[color]]
name = 'orange'
color = '#ff9f1a'
position = 3
[[color]]
name = 'red'
color = '#eb5a46'
position = 4
[[color]]
name = 'purple'
position = 5
color = '#c377e0'
[[color]]
name = 'blue'
position = 6
color = '#0079bf'
[[color]]
name = 'sky'
position = 7
color = '#00c2e0'
[[color]]
name = 'lime'
position = 8
color = '#51e898'
[[color]]
name = 'pink'
position = 9
color = '#ff78cb'
[[color]]
name = 'black'
position = 10
color = '#344563'

View File

@ -4,6 +4,7 @@ go 1.13
require ( require (
github.com/99designs/gqlgen v0.11.3 github.com/99designs/gqlgen v0.11.3
github.com/BurntSushi/toml v0.3.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/go-chi/chi v3.3.2+incompatible github.com/go-chi/chi v3.3.2+incompatible
github.com/go-chi/cors v1.0.0 github.com/go-chi/cors v1.0.0
@ -11,6 +12,7 @@ require (
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/jmoiron/sqlx v1.2.0 github.com/jmoiron/sqlx v1.2.0
github.com/lib/pq v1.0.0 github.com/lib/pq v1.0.0
github.com/pelletier/go-toml v1.8.0
github.com/pkg/errors v0.8.1 github.com/pkg/errors v0.8.1
github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus v1.4.2
github.com/urfave/cli v1.20.0 // indirect github.com/urfave/cli v1.20.0 // indirect

View File

@ -2,6 +2,7 @@ github.com/99designs/gqlgen v0.11.1 h1:QoSL8/AAJ2T3UOeQbdnBR32JcG4pO08+P/g5jdbFk
github.com/99designs/gqlgen v0.11.1/go.mod h1:vjFOyBZ7NwDl+GdSD4PFn7BQn5Fy7ohJwXn7Vk8zz+c= github.com/99designs/gqlgen v0.11.1/go.mod h1:vjFOyBZ7NwDl+GdSD4PFn7BQn5Fy7ohJwXn7Vk8zz+c=
github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4= github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4=
github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4= github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0= github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
@ -56,6 +57,8 @@ github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -112,5 +115,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=

View File

@ -37,6 +37,7 @@ type Config struct {
} }
type ResolverRoot interface { type ResolverRoot interface {
LabelColor() LabelColorResolver
Mutation() MutationResolver Mutation() MutationResolver
Project() ProjectResolver Project() ProjectResolver
ProjectLabel() ProjectLabelResolver ProjectLabel() ProjectLabelResolver
@ -63,6 +64,13 @@ type ComplexityRoot struct {
TaskID func(childComplexity int) int TaskID func(childComplexity int) int
} }
LabelColor struct {
ColorHex func(childComplexity int) int
ID func(childComplexity int) int
Name func(childComplexity int) int
Position func(childComplexity int) int
}
Mutation struct { Mutation struct {
AddTaskLabel func(childComplexity int, input *AddTaskLabelInput) int AddTaskLabel func(childComplexity int, input *AddTaskLabelInput) int
AssignTask func(childComplexity int, input *AssignTaskInput) int AssignTask func(childComplexity int, input *AssignTaskInput) int
@ -102,9 +110,9 @@ type ComplexityRoot struct {
} }
ProjectLabel struct { ProjectLabel struct {
ColorHex func(childComplexity int) int
CreatedDate func(childComplexity int) int CreatedDate func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
LabelColor func(childComplexity int) int
Name func(childComplexity int) int Name func(childComplexity int) int
} }
@ -119,6 +127,7 @@ type ComplexityRoot struct {
FindProject func(childComplexity int, input FindProject) int FindProject func(childComplexity int, input FindProject) int
FindTask func(childComplexity int, input FindTask) int FindTask func(childComplexity int, input FindTask) int
FindUser func(childComplexity int, input FindUser) int FindUser func(childComplexity int, input FindUser) int
LabelColors func(childComplexity int) int
Me func(childComplexity int) int Me func(childComplexity int) int
Projects func(childComplexity int, input *ProjectsFilter) int Projects func(childComplexity int, input *ProjectsFilter) int
TaskGroups func(childComplexity int) int TaskGroups func(childComplexity int) int
@ -177,6 +186,9 @@ type ComplexityRoot struct {
} }
} }
type LabelColorResolver interface {
ID(ctx context.Context, obj *pg.LabelColor) (uuid.UUID, error)
}
type MutationResolver interface { type MutationResolver interface {
CreateRefreshToken(ctx context.Context, input NewRefreshToken) (*pg.RefreshToken, error) CreateRefreshToken(ctx context.Context, input NewRefreshToken) (*pg.RefreshToken, error)
CreateUserAccount(ctx context.Context, input NewUserAccount) (*pg.UserAccount, error) CreateUserAccount(ctx context.Context, input NewUserAccount) (*pg.UserAccount, error)
@ -209,7 +221,7 @@ type ProjectResolver interface {
type ProjectLabelResolver interface { type ProjectLabelResolver interface {
ID(ctx context.Context, obj *pg.ProjectLabel) (uuid.UUID, error) ID(ctx context.Context, obj *pg.ProjectLabel) (uuid.UUID, error)
ColorHex(ctx context.Context, obj *pg.ProjectLabel) (string, error) LabelColor(ctx context.Context, obj *pg.ProjectLabel) (*pg.LabelColor, error)
Name(ctx context.Context, obj *pg.ProjectLabel) (*string, error) Name(ctx context.Context, obj *pg.ProjectLabel) (*string, error)
} }
type QueryResolver interface { type QueryResolver interface {
@ -218,6 +230,7 @@ type QueryResolver interface {
FindProject(ctx context.Context, input FindProject) (*pg.Project, error) FindProject(ctx context.Context, input FindProject) (*pg.Project, error)
FindTask(ctx context.Context, input FindTask) (*pg.Task, error) FindTask(ctx context.Context, input FindTask) (*pg.Task, error)
Projects(ctx context.Context, input *ProjectsFilter) ([]pg.Project, error) Projects(ctx context.Context, input *ProjectsFilter) ([]pg.Project, error)
LabelColors(ctx context.Context) ([]pg.LabelColor, error)
TaskGroups(ctx context.Context) ([]pg.TaskGroup, error) TaskGroups(ctx context.Context) ([]pg.TaskGroup, error)
Me(ctx context.Context) (*pg.UserAccount, error) Me(ctx context.Context) (*pg.UserAccount, error)
} }
@ -296,6 +309,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.DeleteTaskPayload.TaskID(childComplexity), true return e.complexity.DeleteTaskPayload.TaskID(childComplexity), true
case "LabelColor.colorHex":
if e.complexity.LabelColor.ColorHex == nil {
break
}
return e.complexity.LabelColor.ColorHex(childComplexity), true
case "LabelColor.id":
if e.complexity.LabelColor.ID == nil {
break
}
return e.complexity.LabelColor.ID(childComplexity), true
case "LabelColor.name":
if e.complexity.LabelColor.Name == nil {
break
}
return e.complexity.LabelColor.Name(childComplexity), true
case "LabelColor.position":
if e.complexity.LabelColor.Position == nil {
break
}
return e.complexity.LabelColor.Position(childComplexity), true
case "Mutation.addTaskLabel": case "Mutation.addTaskLabel":
if e.complexity.Mutation.AddTaskLabel == nil { if e.complexity.Mutation.AddTaskLabel == nil {
break break
@ -589,13 +630,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Project.Team(childComplexity), true 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": case "ProjectLabel.createdDate":
if e.complexity.ProjectLabel.CreatedDate == nil { if e.complexity.ProjectLabel.CreatedDate == nil {
break break
@ -610,6 +644,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ProjectLabel.ID(childComplexity), true return e.complexity.ProjectLabel.ID(childComplexity), true
case "ProjectLabel.labelColor":
if e.complexity.ProjectLabel.LabelColor == nil {
break
}
return e.complexity.ProjectLabel.LabelColor(childComplexity), true
case "ProjectLabel.name": case "ProjectLabel.name":
if e.complexity.ProjectLabel.Name == nil { if e.complexity.ProjectLabel.Name == nil {
break break
@ -681,6 +722,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.FindUser(childComplexity, args["input"].(FindUser)), true return e.complexity.Query.FindUser(childComplexity, args["input"].(FindUser)), true
case "Query.labelColors":
if e.complexity.Query.LabelColors == nil {
break
}
return e.complexity.Query.LabelColors(childComplexity), true
case "Query.me": case "Query.me":
if e.complexity.Query.Me == nil { if e.complexity.Query.Me == nil {
break break
@ -1015,10 +1063,17 @@ scalar UUID
type ProjectLabel { type ProjectLabel {
id: ID! id: ID!
createdDate: Time! createdDate: Time!
colorHex: String! labelColor: LabelColor!
name: String name: String
} }
type LabelColor {
id: ID!
name: String!
position: Float!
colorHex: String!
}
type TaskLabel { type TaskLabel {
id: ID! id: ID!
projectLabelID: UUID! projectLabelID: UUID!
@ -1116,6 +1171,7 @@ type Query {
findProject(input: FindProject!): Project! findProject(input: FindProject!): Project!
findTask(input: FindTask!): Task! findTask(input: FindTask!): Task!
projects(input: ProjectsFilter): [Project!]! projects(input: ProjectsFilter): [Project!]!
labelColors: [LabelColor!]!
taskGroups: [TaskGroup!]! taskGroups: [TaskGroup!]!
me: UserAccount! me: UserAccount!
} }
@ -1750,6 +1806,142 @@ func (ec *executionContext) _DeleteTaskPayload_taskID(ctx context.Context, field
return ec.marshalNString2string(ctx, field.Selections, res) return ec.marshalNString2string(ctx, field.Selections, res)
} }
func (ec *executionContext) _LabelColor_id(ctx context.Context, field graphql.CollectedField, obj *pg.LabelColor) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "LabelColor",
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.LabelColor().ID(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.(uuid.UUID)
fc.Result = res
return ec.marshalNID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res)
}
func (ec *executionContext) _LabelColor_name(ctx context.Context, field graphql.CollectedField, obj *pg.LabelColor) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "LabelColor",
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.Name, 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.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _LabelColor_position(ctx context.Context, field graphql.CollectedField, obj *pg.LabelColor) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "LabelColor",
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.Position, 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.(float64)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
}
func (ec *executionContext) _LabelColor_colorHex(ctx context.Context, field graphql.CollectedField, obj *pg.LabelColor) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "LabelColor",
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.ColorHex, 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.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_createRefreshToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation_createRefreshToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -2921,7 +3113,7 @@ func (ec *executionContext) _ProjectLabel_createdDate(ctx context.Context, field
return ec.marshalNTime2timeᚐTime(ctx, field.Selections, 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) { func (ec *executionContext) _ProjectLabel_labelColor(ctx context.Context, field graphql.CollectedField, obj *pg.ProjectLabel) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r)) ec.Error(ctx, ec.Recover(ctx, r))
@ -2938,7 +3130,7 @@ func (ec *executionContext) _ProjectLabel_colorHex(ctx context.Context, field gr
ctx = graphql.WithFieldContext(ctx, fc) ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.ProjectLabel().ColorHex(rctx, obj) return ec.resolvers.ProjectLabel().LabelColor(rctx, obj)
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -2950,9 +3142,9 @@ func (ec *executionContext) _ProjectLabel_colorHex(ctx context.Context, field gr
} }
return graphql.Null return graphql.Null
} }
res := resTmp.(string) res := resTmp.(*pg.LabelColor)
fc.Result = res fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res) return ec.marshalNLabelColor2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐLabelColor(ctx, field.Selections, res)
} }
func (ec *executionContext) _ProjectLabel_name(ctx context.Context, field graphql.CollectedField, obj *pg.ProjectLabel) (ret graphql.Marshaler) { func (ec *executionContext) _ProjectLabel_name(ctx context.Context, field graphql.CollectedField, obj *pg.ProjectLabel) (ret graphql.Marshaler) {
@ -3320,6 +3512,40 @@ func (ec *executionContext) _Query_projects(ctx context.Context, field graphql.C
return ec.marshalNProject2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐProjectᚄ(ctx, field.Selections, res) return ec.marshalNProject2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐProjectᚄ(ctx, field.Selections, res)
} }
func (ec *executionContext) _Query_labelColors(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: "Query",
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.Query().LabelColors(rctx)
})
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.LabelColor)
fc.Result = res
return ec.marshalNLabelColor2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐLabelColorᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _Query_taskGroups(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Query_taskGroups(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -6228,6 +6454,57 @@ func (ec *executionContext) _DeleteTaskPayload(ctx context.Context, sel ast.Sele
return out return out
} }
var labelColorImplementors = []string{"LabelColor"}
func (ec *executionContext) _LabelColor(ctx context.Context, sel ast.SelectionSet, obj *pg.LabelColor) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, labelColorImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("LabelColor")
case "id":
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._LabelColor_id(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "name":
out.Values[i] = ec._LabelColor_name(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
case "position":
out.Values[i] = ec._LabelColor_position(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
case "colorHex":
out.Values[i] = ec._LabelColor_colorHex(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var mutationImplementors = []string{"Mutation"} var mutationImplementors = []string{"Mutation"}
func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@ -6518,7 +6795,7 @@ func (ec *executionContext) _ProjectLabel(ctx context.Context, sel ast.Selection
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1) atomic.AddUint32(&invalids, 1)
} }
case "colorHex": case "labelColor":
field := field field := field
out.Concurrently(i, func() (res graphql.Marshaler) { out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() { defer func() {
@ -6526,7 +6803,7 @@ func (ec *executionContext) _ProjectLabel(ctx context.Context, sel ast.Selection
ec.Error(ctx, ec.Recover(ctx, r)) ec.Error(ctx, ec.Recover(ctx, r))
} }
}() }()
res = ec._ProjectLabel_colorHex(ctx, field, obj) res = ec._ProjectLabel_labelColor(ctx, field, obj)
if res == graphql.Null { if res == graphql.Null {
atomic.AddUint32(&invalids, 1) atomic.AddUint32(&invalids, 1)
} }
@ -6681,6 +6958,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
} }
return res return res
}) })
case "labelColors":
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._Query_labelColors(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "taskGroups": case "taskGroups":
field := field field := field
out.Concurrently(i, func() (res graphql.Marshaler) { out.Concurrently(i, func() (res graphql.Marshaler) {
@ -7499,6 +7790,57 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti
return res return res
} }
func (ec *executionContext) marshalNLabelColor2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐLabelColor(ctx context.Context, sel ast.SelectionSet, v pg.LabelColor) graphql.Marshaler {
return ec._LabelColor(ctx, sel, &v)
}
func (ec *executionContext) marshalNLabelColor2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐLabelColorᚄ(ctx context.Context, sel ast.SelectionSet, v []pg.LabelColor) 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.marshalNLabelColor2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐLabelColor(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
return ret
}
func (ec *executionContext) marshalNLabelColor2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐLabelColor(ctx context.Context, sel ast.SelectionSet, v *pg.LabelColor) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._LabelColor(ctx, sel, v)
}
func (ec *executionContext) unmarshalNLogoutUser2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐLogoutUser(ctx context.Context, v interface{}) (LogoutUser, error) { func (ec *executionContext) unmarshalNLogoutUser2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐLogoutUser(ctx context.Context, v interface{}) (LogoutUser, error) {
return ec.unmarshalInputLogoutUser(ctx, v) return ec.unmarshalInputLogoutUser(ctx, v)
} }

View File

@ -4,10 +4,17 @@ scalar UUID
type ProjectLabel { type ProjectLabel {
id: ID! id: ID!
createdDate: Time! createdDate: Time!
colorHex: String! labelColor: LabelColor!
name: String name: String
} }
type LabelColor {
id: ID!
name: String!
position: Float!
colorHex: String!
}
type TaskLabel { type TaskLabel {
id: ID! id: ID!
projectLabelID: UUID! projectLabelID: UUID!
@ -105,6 +112,7 @@ type Query {
findProject(input: FindProject!): Project! findProject(input: FindProject!): Project!
findTask(input: FindTask!): Task! findTask(input: FindTask!): Task!
projects(input: ProjectsFilter): [Project!]! projects(input: ProjectsFilter): [Project!]!
labelColors: [LabelColor!]!
taskGroups: [TaskGroup!]! taskGroups: [TaskGroup!]!
me: UserAccount! me: UserAccount!
} }

View File

@ -15,6 +15,10 @@ import (
"github.com/vektah/gqlparser/v2/gqlerror" "github.com/vektah/gqlparser/v2/gqlerror"
) )
func (r *labelColorResolver) ID(ctx context.Context, obj *pg.LabelColor) (uuid.UUID, error) {
return obj.LabelColorID, nil
}
func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefreshToken) (*pg.RefreshToken, error) { func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefreshToken) (*pg.RefreshToken, error) {
userID := uuid.MustParse("0183d9ab-d0ed-4c9b-a3df-77a0cdd93dca") userID := uuid.MustParse("0183d9ab-d0ed-4c9b-a3df-77a0cdd93dca")
refreshCreatedAt := time.Now().UTC() refreshCreatedAt := time.Now().UTC()
@ -46,7 +50,23 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject)
} }
func (r *mutationResolver) CreateProjectLabel(ctx context.Context, input NewProjectLabel) (*pg.ProjectLabel, error) { func (r *mutationResolver) CreateProjectLabel(ctx context.Context, input NewProjectLabel) (*pg.ProjectLabel, error) {
panic(fmt.Errorf("not implemented")) createdAt := time.Now().UTC()
var name sql.NullString
if input.Name != nil {
name = sql.NullString{
*input.Name,
true,
}
} else {
name = sql.NullString{
"",
false,
}
}
projectLabel, err := r.Repository.CreateProjectLabel(ctx, pg.CreateProjectLabelParams{input.ProjectID,
input.LabelColorID, createdAt, name})
return &projectLabel, err
} }
func (r *mutationResolver) CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*pg.TaskGroup, error) { func (r *mutationResolver) CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*pg.TaskGroup, error) {
@ -234,12 +254,12 @@ func (r *projectLabelResolver) ID(ctx context.Context, obj *pg.ProjectLabel) (uu
return obj.ProjectLabelID, nil return obj.ProjectLabelID, nil
} }
func (r *projectLabelResolver) ColorHex(ctx context.Context, obj *pg.ProjectLabel) (string, error) { func (r *projectLabelResolver) LabelColor(ctx context.Context, obj *pg.ProjectLabel) (*pg.LabelColor, error) {
labelColor, err := r.Repository.GetLabelColorByID(ctx, obj.LabelColorID) labelColor, err := r.Repository.GetLabelColorByID(ctx, obj.LabelColorID)
if err != nil { if err != nil {
return "", err return &pg.LabelColor{}, err
} }
return labelColor.ColorHex, nil return &labelColor, nil
} }
func (r *projectLabelResolver) Name(ctx context.Context, obj *pg.ProjectLabel) (*string, error) { func (r *projectLabelResolver) Name(ctx context.Context, obj *pg.ProjectLabel) (*string, error) {
@ -304,6 +324,10 @@ func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]
return r.Repository.GetAllProjects(ctx) return r.Repository.GetAllProjects(ctx)
} }
func (r *queryResolver) LabelColors(ctx context.Context) ([]pg.LabelColor, error) {
return r.Repository.GetLabelColors(ctx)
}
func (r *queryResolver) TaskGroups(ctx context.Context) ([]pg.TaskGroup, error) { func (r *queryResolver) TaskGroups(ctx context.Context) ([]pg.TaskGroup, error) {
return r.Repository.GetAllTaskGroups(ctx) return r.Repository.GetAllTaskGroups(ctx)
} }
@ -424,6 +448,9 @@ func (r *userAccountResolver) ProfileIcon(ctx context.Context, obj *pg.UserAccou
return profileIcon, nil return profileIcon, nil
} }
// LabelColor returns LabelColorResolver implementation.
func (r *Resolver) LabelColor() LabelColorResolver { return &labelColorResolver{r} }
// Mutation returns MutationResolver implementation. // Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
@ -454,6 +481,7 @@ func (r *Resolver) Team() TeamResolver { return &teamResolver{r} }
// UserAccount returns UserAccountResolver implementation. // UserAccount returns UserAccountResolver implementation.
func (r *Resolver) UserAccount() UserAccountResolver { return &userAccountResolver{r} } func (r *Resolver) UserAccount() UserAccountResolver { return &userAccountResolver{r} }
type labelColorResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver } type mutationResolver struct{ *Resolver }
type projectResolver struct{ *Resolver } type projectResolver struct{ *Resolver }
type projectLabelResolver struct{ *Resolver } type projectLabelResolver struct{ *Resolver }
@ -464,3 +492,17 @@ type taskGroupResolver struct{ *Resolver }
type taskLabelResolver struct{ *Resolver } type taskLabelResolver struct{ *Resolver }
type teamResolver struct{ *Resolver } type teamResolver struct{ *Resolver }
type userAccountResolver struct{ *Resolver } type userAccountResolver struct{ *Resolver }
// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
// it when you're done.
// - You have helper methods in this file. Move them out to keep these resolver files clean.
func (r *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
}

View File

@ -0,0 +1 @@
ALTER TABLE label_color ADD COLUMN name TEXT NOT NULL DEFAULT 'needs name';

View File

@ -9,13 +9,73 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
const createLabelColor = `-- name: CreateLabelColor :one
INSERT INTO label_color (name, color_hex, position) VALUES ($1, $2, $3)
RETURNING label_color_id, color_hex, position, name
`
type CreateLabelColorParams struct {
Name string `json:"name"`
ColorHex string `json:"color_hex"`
Position float64 `json:"position"`
}
func (q *Queries) CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error) {
row := q.db.QueryRowContext(ctx, createLabelColor, arg.Name, arg.ColorHex, arg.Position)
var i LabelColor
err := row.Scan(
&i.LabelColorID,
&i.ColorHex,
&i.Position,
&i.Name,
)
return i, err
}
const getLabelColorByID = `-- name: GetLabelColorByID :one const getLabelColorByID = `-- name: GetLabelColorByID :one
SELECT label_color_id, color_hex, position FROM label_color WHERE label_color_id = $1 SELECT label_color_id, color_hex, position, name FROM label_color WHERE label_color_id = $1
` `
func (q *Queries) GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error) { func (q *Queries) GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error) {
row := q.db.QueryRowContext(ctx, getLabelColorByID, labelColorID) row := q.db.QueryRowContext(ctx, getLabelColorByID, labelColorID)
var i LabelColor var i LabelColor
err := row.Scan(&i.LabelColorID, &i.ColorHex, &i.Position) err := row.Scan(
&i.LabelColorID,
&i.ColorHex,
&i.Position,
&i.Name,
)
return i, err return i, err
} }
const getLabelColors = `-- name: GetLabelColors :many
SELECT label_color_id, color_hex, position, name FROM label_color
`
func (q *Queries) GetLabelColors(ctx context.Context) ([]LabelColor, error) {
rows, err := q.db.QueryContext(ctx, getLabelColors)
if err != nil {
return nil, err
}
defer rows.Close()
var items []LabelColor
for rows.Next() {
var i LabelColor
if err := rows.Scan(
&i.LabelColorID,
&i.ColorHex,
&i.Position,
&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
}

View File

@ -13,6 +13,7 @@ type LabelColor struct {
LabelColorID uuid.UUID `json:"label_color_id"` LabelColorID uuid.UUID `json:"label_color_id"`
ColorHex string `json:"color_hex"` ColorHex string `json:"color_hex"`
Position float64 `json:"position"` Position float64 `json:"position"`
Name string `json:"name"`
} }
type Organization struct { type Organization struct {

View File

@ -26,6 +26,9 @@ type Repository interface {
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error) GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error) GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
GetLabelColors(ctx context.Context) ([]LabelColor, error)
CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error)
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error) GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error

View File

@ -9,6 +9,7 @@ import (
) )
type Querier interface { type Querier interface {
CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error)
CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error) CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error)
CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error)
CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error) CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error)
@ -36,6 +37,7 @@ type Querier interface {
GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error) GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error) GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
GetLabelColors(ctx context.Context) ([]LabelColor, error)
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error) GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error) GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)

View File

@ -1,2 +1,9 @@
-- name: GetLabelColorByID :one -- name: GetLabelColorByID :one
SELECT * FROM label_color WHERE label_color_id = $1; SELECT * FROM label_color WHERE label_color_id = $1;
-- name: GetLabelColors :many
SELECT * FROM label_color;
-- name: CreateLabelColor :one
INSERT INTO label_color (name, color_hex, position) VALUES ($1, $2, $3)
RETURNING *;

View File

@ -7,8 +7,9 @@ import { useMeQuery } from 'shared/generated/graphql';
type GlobalTopNavbarProps = { type GlobalTopNavbarProps = {
name: string; name: string;
projectMembers?: null | Array<TaskUser>;
}; };
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ name }) => { const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ name, projectMembers }) => {
const { loading, data } = useMeQuery(); const { loading, data } = useMeQuery();
const history = useHistory(); const history = useHistory();
const { userID, setUserID } = useContext(UserIDContext); const { userID, setUserID } = useContext(UserIDContext);
@ -50,6 +51,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ name }) => {
lastName={data ? data.me.lastName : ''} lastName={data ? data.me.lastName : ''}
initials={!data ? '' : data.me.profileIcon.initials ?? ''} initials={!data ? '' : data.me.profileIcon.initials ?? ''}
onNotificationClick={() => console.log('beep')} onNotificationClick={() => console.log('beep')}
projectMembers={projectMembers}
onProfileClick={onProfileClick} onProfileClick={onProfileClick}
/> />
{menu.isOpen && ( {menu.isOpen && (

View File

@ -12,6 +12,7 @@ type KanbanBoardProps = {
onCardCreate: (taskGroupID: string, name: string) => void; onCardCreate: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: (e: ContextMenuEvent) => void; onQuickEditorOpen: (e: ContextMenuEvent) => void;
onCreateList: (listName: string) => void; onCreateList: (listName: string) => void;
onCardMemberClick: OnCardMemberClick;
}; };
const KanbanBoard: React.FC<KanbanBoardProps> = ({ const KanbanBoard: React.FC<KanbanBoardProps> = ({
@ -22,6 +23,7 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
onCardDrop, onCardDrop,
onListDrop, onListDrop,
onCreateList, onCreateList,
onCardMemberClick,
}) => { }) => {
const match = useRouteMatch(); const match = useRouteMatch();
const history = useHistory(); const history = useHistory();
@ -40,6 +42,7 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
onListDrop={onListDrop} onListDrop={onListDrop}
{...listsData} {...listsData}
onCreateList={onCreateList} onCreateList={onCreateList}
onCardMemberClick={onCardMemberClick}
/> />
</Board> </Board>
); );

View File

@ -18,8 +18,10 @@ import {
useAssignTaskMutation, useAssignTaskMutation,
DeleteTaskDocument, DeleteTaskDocument,
FindProjectDocument, FindProjectDocument,
useCreateProjectLabelMutation,
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
import TaskAssignee from 'shared/components/TaskAssignee';
import QuickCardEditor from 'shared/components/QuickCardEditor'; import QuickCardEditor from 'shared/components/QuickCardEditor';
import ListActions from 'shared/components/ListActions'; import ListActions from 'shared/components/ListActions';
import MemberManager from 'shared/components/MemberManager'; import MemberManager from 'shared/components/MemberManager';
@ -30,6 +32,7 @@ import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor'; import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
import produce from 'immer'; import produce from 'immer';
import Details from './Details'; import Details from './Details';
import MiniProfile from 'shared/components/MiniProfile';
type TaskRouteProps = { type TaskRouteProps = {
taskID: string; taskID: string;
@ -52,14 +55,23 @@ const Title = styled.span`
font-size: 24px; font-size: 24px;
color: #fff; color: #fff;
`; `;
const ProjectMembers = styled.div`
display: flex;
padding-left: 4px;
padding-top: 4px;
align-items: center;
`;
type LabelManagerEditorProps = { type LabelManagerEditorProps = {
labels: Array<Label>; labels: Array<Label>;
projectID: string;
labelColors: Array<LabelColor>;
}; };
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ labels: initialLabels }) => { const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ labels: initialLabels, projectID, labelColors }) => {
const [labels, setLabels] = useState<Array<Label>>(initialLabels); const [labels, setLabels] = useState<Array<Label>>(initialLabels);
const [currentLabel, setCurrentLabel] = useState(''); const [currentLabel, setCurrentLabel] = useState('');
const [createProjectLabel] = useCreateProjectLabelMutation();
const { setTab } = usePopup(); const { setTab } = usePopup();
return ( return (
<> <>
@ -74,26 +86,21 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ labels: initial
setTab(1); setTab(1);
}} }}
onLabelToggle={labelId => { onLabelToggle={labelId => {
setLabels( setCurrentLabel(labelId);
produce(labels, draftState => { setTab(1);
const idx = labels.findIndex(label => label.labelId === labelId);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], active: !labels[idx].active };
}
}),
);
}} }}
/> />
</Popup> </Popup>
<Popup onClose={() => {}} title="Edit label" tab={1}> <Popup onClose={() => {}} title="Edit label" tab={1}>
<LabelEditor <LabelEditor
labelColors={labelColors}
label={labels.find(label => label.labelId === currentLabel) ?? null} label={labels.find(label => label.labelId === currentLabel) ?? null}
onLabelEdit={(_labelId, name, color) => { onLabelEdit={(_labelId, name, color) => {
setLabels( setLabels(
produce(labels, draftState => { produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === currentLabel); const idx = labels.findIndex(label => label.labelId === currentLabel);
if (idx !== -1) { if (idx !== -1) {
draftState[idx] = { ...draftState[idx], name, color }; draftState[idx] = { ...draftState[idx], name, labelColor: color };
} }
}), }),
); );
@ -103,9 +110,12 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ labels: initial
</Popup> </Popup>
<Popup onClose={() => {}} title="Create new label" tab={2}> <Popup onClose={() => {}} title="Create new label" tab={2}>
<LabelEditor <LabelEditor
labelColors={labelColors}
label={null} label={null}
onLabelEdit={(_labelId, name, color) => { onLabelEdit={(_labelId, name, color) => {
setLabels([...labels, { labelId: name, name, color, active: false }]); console.log(name, color);
setLabels([...labels, { labelId: name, name, labelColor: color, active: false }]);
createProjectLabel({ variables: { projectID, labelColorID: color.id, name } });
setTab(0); setTab(0);
}} }}
/> />
@ -124,7 +134,7 @@ const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top:
const initialLabelsPopupState = { taskID: '', isOpen: false, top: 0, left: 0 }; const initialLabelsPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const initialTaskDetailsState = { isOpen: false, taskID: '' }; const initialTaskDetailsState = { isOpen: false, taskID: '' };
const ProjectActions = styled.div` const ProjectBar = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
@ -132,6 +142,11 @@ const ProjectActions = styled.div`
padding: 0 12px; padding: 0 12px;
`; `;
const ProjectActions = styled.div`
display: flex;
align-items: center;
`;
const ProjectAction = styled.div` const ProjectAction = styled.div`
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -351,9 +366,11 @@ const Project = () => {
task: currentTask, task: currentTask,
}); });
}; };
return ( return (
<> <>
<GlobalTopNavbar name={data.findProject.name} /> <GlobalTopNavbar projectMembers={availableMembers} name={data.findProject.name} />
<ProjectBar>
<ProjectActions> <ProjectActions>
<ProjectAction <ProjectAction
ref={$labelsRef} ref={$labelsRef}
@ -361,14 +378,16 @@ const Project = () => {
showPopup( showPopup(
$labelsRef, $labelsRef,
<LabelManagerEditor <LabelManagerEditor
labelColors={data.labelColors}
labels={data.findProject.labels.map(label => { labels={data.findProject.labels.map(label => {
return { return {
labelId: label.id, labelId: label.id,
name: label.name ?? '', name: label.name ?? '',
color: label.colorHex, labelColor: label.labelColor,
active: false, active: false,
}; };
})} })}
projectID={projectId}
/>, />,
); );
}} }}
@ -385,12 +404,29 @@ const Project = () => {
<ProjectActionText>Rules</ProjectActionText> <ProjectActionText>Rules</ProjectActionText>
</ProjectAction> </ProjectAction>
</ProjectActions> </ProjectActions>
</ProjectBar>
<KanbanBoard <KanbanBoard
listsData={currentListsData} listsData={currentListsData}
onCardDrop={onCardDrop} onCardDrop={onCardDrop}
onListDrop={onListDrop} onListDrop={onListDrop}
onCardCreate={onCardCreate} onCardCreate={onCardCreate}
onCreateList={onCreateList} onCreateList={onCreateList}
onCardMemberClick={($targetRef, taskID, memberID) => {
showPopup(
$targetRef,
<Popup title={null} onClose={() => {}} tab={0}>
<MiniProfile
profileIcon={availableMembers[0].profileIcon}
displayName="Jordan Knott"
username="@jordanthedev"
bio="None"
onRemoveFromTask={() => {
/* unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); */
}}
/>
</Popup>,
);
}}
onQuickEditorOpen={onQuickEditorOpen} onQuickEditorOpen={onQuickEditorOpen}
onOpenListActionsPopup={($targetRef, taskGroupID) => { onOpenListActionsPopup={($targetRef, taskGroupID) => {
showPopup( showPopup(

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

@ -84,7 +84,7 @@ type Team = {
type Label = { type Label = {
labelId: string; labelId: string;
name: string; name: string;
color: string; labelColor: LabelColor;
active: boolean; active: boolean;
}; };
@ -117,7 +117,17 @@ type ElementSize = {
height: number; height: number;
}; };
type LabelColor = {
id: string;
name: string;
colorHex: string;
position: number;
};
type OnCardMemberClick = ($targetRef: RefObject<HTMLElement>, taskID: string, memberID: string) => void;
type ElementBounds = { type ElementBounds = {
size: ElementSize; size: ElementSize;
position: ElementPosition; position: ElementPosition;
}; };

View File

@ -1,5 +1,6 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib'; import TextareaAutosize from 'react-autosize-textarea/lib';
import { mixin } from 'shared/utils/styles';
export const Container = styled.div``; export const Container = styled.div``;
@ -27,11 +28,11 @@ export const Wrapper = styled.div<{ editorOpen: boolean }>`
${props => ${props =>
props.editorOpen && props.editorOpen &&
css` css`
background-color: #ebecf0; background-color: #10163a;
border-radius: 3px; border-radius: 3px;
height: auto; height: auto;
min-height: 32px; min-height: 32px;
padding: 4px; padding: 8px;
transition: background 85ms ease-in, opacity 40ms ease-in, border-color 85ms ease-in; transition: background 85ms ease-in, opacity 40ms ease-in, border-color 85ms ease-in;
`} `}
`; `;
@ -53,17 +54,33 @@ export const ListNameEditorWrapper = styled.div`
display: flex; display: flex;
`; `;
export const ListNameEditor = styled(TextareaAutosize)` export const ListNameEditor = styled(TextareaAutosize)`
background: #fff; background-color: ${props => mixin.lighten('#262c49', 0.05)};
border: none; border: none;
box-shadow: inset 0 0 0 2px #0079bf; box-shadow: inset 0 0 0 2px #0079bf;
display: block;
margin: 0;
transition: margin 85ms ease-in, background 85ms ease-in; transition: margin 85ms ease-in, background 85ms ease-in;
width: 100%;
line-height: 20px; line-height: 20px;
padding: 8px 12px; padding: 8px 12px;
font-family: 'Droid Sans';
overflow: hidden;
overflow-wrap: break-word;
resize: none;
height: 54px;
width: 100%;
border: none;
border-radius: 3px;
box-shadow: none;
margin-bottom: 4px;
max-height: 162px;
min-height: 54px;
font-size: 14px; font-size: 14px;
outline: none; line-height: 20px;
color: #c2c6dc;
l &:focus {
background-color: ${props => mixin.lighten('#262c49', 0.05)};
}
`; `;
export const ListAddControls = styled.div` export const ListAddControls = styled.div`
@ -74,10 +91,9 @@ export const ListAddControls = styled.div`
`; `;
export const AddListButton = styled.button` export const AddListButton = styled.button`
background-color: #5aac44;
box-shadow: none; box-shadow: none;
border: none; border: none;
color: #fff; color: #c2c6dc;
float: left; float: left;
margin: 0 4px 0 0; margin: 0 4px 0 0;
cursor: pointer; cursor: pointer;
@ -88,6 +104,8 @@ export const AddListButton = styled.button`
text-align: center; text-align: center;
border-radius: 3px; border-radius: 3px;
font-size: 14px; font-size: 14px;
background: rgb(115, 103, 240);
`; `;
export const CancelAdd = styled.div` export const CancelAdd = styled.div`

View File

@ -45,6 +45,7 @@ const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
value={listName} value={listName}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setListName(e.currentTarget.value)} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setListName(e.currentTarget.value)}
placeholder="Enter a title for this list..."
/> />
</ListNameEditorWrapper> </ListNameEditorWrapper>
<ListAddControls> <ListAddControls>
@ -60,7 +61,7 @@ const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
Save Save
</AddListButton> </AddListButton>
<CancelAdd onClick={() => onCancel()}> <CancelAdd onClick={() => onCancel()}>
<Cross /> <Cross color="#c2c6dc" />
</CancelAdd> </CancelAdd>
</ListAddControls> </ListAddControls>
</> </>

View File

@ -18,13 +18,23 @@ const labelData = [
{ {
labelId: 'development', labelId: 'development',
name: 'Development', name: 'Development',
color: LabelColors.BLUE, labelColor: {
id: '1',
colorHex: LabelColors.BLUE,
name: 'blue',
position: 1,
},
active: false, active: false,
}, },
{ {
labelId: 'general', labelId: 'general',
name: 'General', name: 'General',
color: LabelColors.PINK, labelColor: {
id: '2',
colorHex: LabelColors.PINK,
name: 'pink',
position: 2,
},
active: false, active: false,
}, },
]; ];

View File

@ -1,6 +1,7 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import { RefObject } from 'react';
export const ClockIcon = styled(FontAwesomeIcon)``; export const ClockIcon = styled(FontAwesomeIcon)``;
@ -127,7 +128,7 @@ export const CardMembers = styled.div`
margin: 0 -2px 0 0; margin: 0 -2px 0 0;
`; `;
export const CardMember = styled.div<{ bgColor: string }>` export const CardMember = styled.div<{ bgColor: string; ref: any }>`
height: 28px; height: 28px;
width: 28px; width: 28px;
float: right; float: right;

View File

@ -33,6 +33,31 @@ type Checklist = {
total: number; total: number;
}; };
type MemberProps = {
onCardMemberClick?: OnCardMemberClick;
taskID: string;
member: TaskUser;
};
const Member: React.FC<MemberProps> = ({ onCardMemberClick, taskID, member }) => {
const $targetRef = useRef<HTMLDivElement>();
return (
<CardMember
ref={$targetRef}
onClick={e => {
if (onCardMemberClick) {
e.stopPropagation();
onCardMemberClick($targetRef, taskID, member.userID);
}
}}
key={member.userID}
bgColor={member.profileIcon.bgColor ?? '#7367F0'}
>
<CardMemberInitials>{member.profileIcon.initials}</CardMemberInitials>
</CardMember>
);
};
type Props = { type Props = {
title: string; title: string;
description: string; description: string;
@ -46,6 +71,7 @@ type Props = {
labels?: Label[]; labels?: Label[];
wrapperProps?: any; wrapperProps?: any;
members?: Array<TaskUser> | null; members?: Array<TaskUser> | null;
onCardMemberClick?: OnCardMemberClick;
}; };
const Card = React.forwardRef( const Card = React.forwardRef(
@ -63,6 +89,7 @@ const Card = React.forwardRef(
checklists, checklists,
watched, watched,
members, members,
onCardMemberClick,
}: Props, }: Props,
$cardRef: any, $cardRef: any,
) => { ) => {
@ -109,7 +136,7 @@ const Card = React.forwardRef(
<ListCardLabels> <ListCardLabels>
{labels && {labels &&
labels.map(label => ( labels.map(label => (
<ListCardLabel color={label.color} key={label.name}> <ListCardLabel color={label.labelColor.colorHex} key={label.name}>
{label.name} {label.name}
</ListCardLabel> </ListCardLabel>
))} ))}
@ -141,11 +168,7 @@ const Card = React.forwardRef(
</ListCardBadges> </ListCardBadges>
<CardMembers> <CardMembers>
{members && {members &&
members.map(member => ( members.map(member => <Member taskID={taskID} member={member} onCardMemberClick={onCardMemberClick} />)}
<CardMember key={member.userID} bgColor={member.profileIcon.bgColor ?? '#7367F0'}>
<CardMemberInitials>{member.profileIcon.initials}</CardMemberInitials>
</CardMember>
))}
</CardMembers> </CardMembers>
</ListCardDetails> </ListCardDetails>
</ListCardInnerContainer> </ListCardInnerContainer>

View File

@ -75,10 +75,10 @@ export const ComposerControlsActionsSection = styled.div`
`; `;
export const AddCardButton = styled.button` export const AddCardButton = styled.button`
background-color: #5aac44; background: rgb(115, 103, 240);
box-shadow: none; box-shadow: none;
border: none; border: none;
color: #fff; color: #c2c6dc;
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
font-weight: 400; font-weight: 400;

View File

@ -21,7 +21,14 @@ export const Default = () => {
taskGroup: { name: 'General', taskGroupID: '1' }, taskGroup: { name: 'General', taskGroupID: '1' },
name: 'Hello, world', name: 'Hello, world',
position: 1, position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }], labels: [
{
labelId: 'soft-skills',
labelColor: { id: '1', colorHex: '#fff', name: 'white', position: 1 },
active: true,
name: 'Soft Skills',
},
],
description: 'hello!', description: 'hello!',
members: [ members: [
{ userID: '1', profileIcon: { url: null, initials: null, bgColor: null }, displayName: 'Jordan Knott' }, { userID: '1', profileIcon: { url: null, initials: null, bgColor: null }, displayName: 'Jordan Knott' },

View File

@ -20,13 +20,23 @@ const labelData = [
{ {
labelId: 'development', labelId: 'development',
name: 'Development', name: 'Development',
color: LabelColors.BLUE, labelColor: {
id: '1',
colorHex: LabelColors.BLUE,
name: 'blue',
position: 1,
},
active: false, active: false,
}, },
{ {
labelId: 'general', labelId: 'general',
name: 'General', name: 'General',
color: LabelColors.PINK, labelColor: {
id: '2',
colorHex: LabelColors.PINK,
name: 'pink',
position: 2,
},
active: false, active: false,
}, },
]; ];

View File

@ -96,6 +96,7 @@ export const Default = () => {
onQuickEditorOpen={action('card composer open')} onQuickEditorOpen={action('card composer open')}
onCardDrop={onCardDrop} onCardDrop={onCardDrop}
onListDrop={onListDrop} onListDrop={onListDrop}
onCardMemberClick={action('card member click')}
onCardCreate={action('card create')} onCardCreate={action('card create')}
onCreateList={listName => { onCreateList={listName => {
const [lastColumn] = Object.values(listsData.columns) const [lastColumn] = Object.values(listsData.columns)
@ -209,6 +210,7 @@ export const ListsWithManyList = () => {
onListDrop={onListDrop} onListDrop={onListDrop}
onCreateList={action('create list')} onCreateList={action('create list')}
onExtraMenuOpen={action('extra menu open')} onExtraMenuOpen={action('extra menu open')}
onCardMemberClick={action('card member click')}
/> />
); );
}; };

View File

@ -30,6 +30,7 @@ type Props = {
onQuickEditorOpen: (e: ContextMenuEvent) => void; onQuickEditorOpen: (e: ContextMenuEvent) => void;
onCreateList: (listName: string) => void; onCreateList: (listName: string) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void; onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
onCardMemberClick: OnCardMemberClick;
}; };
const Lists: React.FC<Props> = ({ const Lists: React.FC<Props> = ({
@ -41,6 +42,7 @@ const Lists: React.FC<Props> = ({
onCardCreate, onCardCreate,
onQuickEditorOpen, onQuickEditorOpen,
onCreateList, onCreateList,
onCardMemberClick,
onExtraMenuOpen, onExtraMenuOpen,
}) => { }) => {
const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => { const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => {
@ -161,6 +163,7 @@ const Lists: React.FC<Props> = ({
labels={task.labels} labels={task.labels}
members={task.members} members={task.members}
onClick={() => onCardClick(task)} onClick={() => onCardClick(task)}
onCardMemberClick={onCardMemberClick}
onContextMenu={onQuickEditorOpen} onContextMenu={onQuickEditorOpen}
/> />
); );

View File

@ -4,14 +4,15 @@ import { Checkmark } from 'shared/icons';
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles'; import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
type Props = { type Props = {
labelColors: Array<LabelColor>;
label: Label | null; label: Label | null;
onLabelEdit: (labelId: string | null, labelName: string, color: string) => void; onLabelEdit: (labelId: string | null, labelName: string, labelColor: LabelColor) => void;
}; };
const LabelManager = ({ label, onLabelEdit }: Props) => { const LabelManager = ({ labelColors, label, onLabelEdit }: Props) => {
console.log(label); console.log(label);
const [currentLabel, setCurrentLabel] = useState(label ? label.name : ''); const [currentLabel, setCurrentLabel] = useState(label ? label.name : '');
const [currentColor, setCurrentColor] = useState<string | null>(label ? label.color : null); const [currentColor, setCurrentColor] = useState<LabelColor | null>(label ? label.labelColor : null);
return ( return (
<EditLabelForm> <EditLabelForm>
<FieldLabel>Name</FieldLabel> <FieldLabel>Name</FieldLabel>
@ -26,14 +27,14 @@ const LabelManager = ({ label, onLabelEdit }: Props) => {
/> />
<FieldLabel>Select a color</FieldLabel> <FieldLabel>Select a color</FieldLabel>
<div> <div>
{Object.values(LabelColors).map(labelColor => ( {labelColors.map((labelColor: LabelColor) => (
<LabelBox <LabelBox
color={labelColor} color={labelColor.colorHex}
onClick={() => { onClick={() => {
setCurrentColor(labelColor); setCurrentColor(labelColor);
}} }}
> >
{labelColor === currentColor && <Checkmark color="#fff" size={12} />} {currentColor && labelColor.id === currentColor.id && <Checkmark color="#fff" size={12} />}
</LabelBox> </LabelBox>
))} ))}
</div> </div>

View File

@ -49,7 +49,7 @@ const LabelManager: React.FC<Props> = ({ labels, onLabelToggle, onLabelEdit, onL
</LabelIcon> </LabelIcon>
<CardLabel <CardLabel
key={label.labelId} key={label.labelId}
color={label.color} color={label.labelColor.colorHex}
active={currentLabel === label.labelId} active={currentLabel === label.labelId}
onMouseEnter={() => { onMouseEnter={() => {
setCurrentLabel(label.labelId); setCurrentLabel(label.labelId);

View File

@ -28,13 +28,23 @@ const labelData = [
{ {
labelId: 'development', labelId: 'development',
name: 'Development', name: 'Development',
color: LabelColors.BLUE, labelColor: {
active: true, id: '1',
name: 'white',
colorHex: LabelColors.BLUE,
position: 1,
},
active: false,
}, },
{ {
labelId: 'general', labelId: 'general',
name: 'General', name: 'General',
color: LabelColors.PINK, labelColor: {
id: '1',
name: 'white',
colorHex: LabelColors.PINK,
position: 1,
},
active: false, active: false,
}, },
]; ];
@ -75,13 +85,14 @@ const LabelManagerEditor = () => {
</Popup> </Popup>
<Popup onClose={action('on close')} title="Edit label" tab={1}> <Popup onClose={action('on close')} title="Edit label" tab={1}>
<LabelEditor <LabelEditor
labelColors={[{ id: '1', colorHex: '#c2c6dc', position: 1, name: 'gray' }]}
label={labels.find(label => label.labelId === currentLabel) ?? null} label={labels.find(label => label.labelId === currentLabel) ?? null}
onLabelEdit={(_labelId, name, color) => { onLabelEdit={(_labelId, name, color) => {
setLabels( setLabels(
produce(labels, draftState => { produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === currentLabel); const idx = labels.findIndex(label => label.labelId === currentLabel);
if (idx !== -1) { if (idx !== -1) {
draftState[idx] = { ...draftState[idx], name, color }; draftState[idx] = { ...draftState[idx], name, labelColor: color };
} }
}), }),
); );
@ -92,8 +103,9 @@ const LabelManagerEditor = () => {
<Popup onClose={action('on close')} title="Create new label" tab={2}> <Popup onClose={action('on close')} title="Create new label" tab={2}>
<LabelEditor <LabelEditor
label={null} label={null}
labelColors={[{ id: '1', colorHex: '#c2c6dc', position: 1, name: 'gray' }]}
onLabelEdit={(_labelId, name, color) => { onLabelEdit={(_labelId, name, color) => {
setLabels([...labels, { labelId: name, name, color, active: false }]); setLabels([...labels, { labelId: name, name, labelColor: color, active: false }]);
setTab(0); setTab(0);
}} }}
/> />
@ -141,7 +153,11 @@ export const LabelsLabelEditor = () => {
onClose={() => setPopupOpen(false)} onClose={() => setPopupOpen(false)}
left={10} left={10}
> >
<LabelEditor label={labelData[0]} onLabelEdit={action('label edit')} /> <LabelEditor
label={labelData[0]}
onLabelEdit={action('label edit')}
labelColors={[{ id: '1', colorHex: '#c2c6dc', position: 1, name: 'gray' }]}
/>
</PopupMenu> </PopupMenu>
)} )}
<button type="submit" onClick={() => setPopupOpen(true)}> <button type="submit" onClick={() => setPopupOpen(true)}>
@ -239,7 +255,19 @@ export const DueDateManagerPopup = () => {
taskGroup: { name: 'General', taskGroupID: '1' }, taskGroup: { name: 'General', taskGroupID: '1' },
name: 'Hello, world', name: 'Hello, world',
position: 1, position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }], labels: [
{
labelId: 'soft-skills',
labelColor: {
id: '1',
name: 'white',
colorHex: '#fff',
position: 1,
},
active: true,
name: 'Soft Skills',
},
],
description: 'hello!', description: 'hello!',
members: [ members: [
{ userID: '1', profileIcon: { bgColor: null, url: null, initials: null }, displayName: 'Jordan Knott' }, { userID: '1', profileIcon: { bgColor: null, url: null, initials: null }, displayName: 'Jordan Knott' },
@ -325,4 +353,3 @@ export const MiniProfilePopup = () => {
</> </>
); );
}; };

View File

@ -207,22 +207,24 @@ export const FieldName = styled.input`
margin: 4px 0 12px; margin: 4px 0 12px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
border-radius: 3px;
display: block; display: block;
line-height: 20px; line-height: 20px;
margin-bottom: 12px; margin-bottom: 12px;
padding: 8px 12px; padding: 8px 12px;
background: #262c49; background: #262c49;
outline: none;
color: #c2c6dc;
border-radius: 3px;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
border-color: transparent; border-color: transparent;
border-image: initial; border-image: initial;
border-color: #414561;
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
color: #c2c6dc;
&:focus { &:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px; box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)}; background: ${mixin.darken('#262c49', 0.15)};

View File

@ -21,13 +21,23 @@ const labelData = [
{ {
labelId: 'development', labelId: 'development',
name: 'Development', name: 'Development',
color: LabelColors.BLUE, labelColor: {
id: '1',
name: 'white',
colorHex: LabelColors.BLUE,
position: 1,
},
active: false, active: false,
}, },
{ {
labelId: 'general', labelId: 'general',
name: 'General', name: 'General',
color: LabelColors.PINK, labelColor: {
id: '1',
name: 'white',
colorHex: LabelColors.PINK,
position: 1,
},
active: false, active: false,
}, },
]; ];

View File

@ -72,7 +72,7 @@ const QuickCardEditor = ({
<ListCardLabels> <ListCardLabels>
{labels && {labels &&
labels.map(label => ( labels.map(label => (
<ListCardLabel color={label.color} key={label.name}> <ListCardLabel color={label.labelColor.colorHex} key={label.name}>
{label.name} {label.name}
</ListCardLabel> </ListCardLabel>
))} ))}

View File

@ -0,0 +1,40 @@
import React, { useRef } from 'react';
import styled from 'styled-components';
const TaskDetailAssignee = styled.div`
&:hover {
opacity: 0.8;
}
margin-right: 4px;
`;
const ProfileIcon = styled.div<{ size: string | number }>`
width: ${props => props.size}px;
height: ${props => props.size}px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 400;
background: rgb(115, 103, 240);
font-size: 14px;
cursor: pointer;
`;
type TaskAssigneeProps = {
size: number | string;
member: TaskUser;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
};
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ member, onMemberProfile, size }) => {
const $memberRef = useRef<HTMLDivElement>(null);
return (
<TaskDetailAssignee ref={$memberRef} onClick={() => onMemberProfile($memberRef, member.userID)} key={member.userID}>
<ProfileIcon size={size}>{member.profileIcon.initials ?? ''}</ProfileIcon>
</TaskDetailAssignee>
);
};
export default TaskAssignee;

View File

@ -199,6 +199,7 @@ export const TaskDetailAssignee = styled.div`
} }
margin-right: 4px; margin-right: 4px;
`; `;
export const ProfileIcon = styled.div` export const ProfileIcon = styled.div`
width: 32px; width: 32px;
height: 32px; height: 32px;

View File

@ -33,7 +33,19 @@ export const Default = () => {
taskGroup: { name: 'General', taskGroupID: '1' }, taskGroup: { name: 'General', taskGroupID: '1' },
name: 'Hello, world', name: 'Hello, world',
position: 1, position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }], labels: [
{
labelId: 'soft-skills',
labelColor: {
id: '1',
name: 'white',
colorHex: '#fff',
position: 1,
},
active: true,
name: 'Soft Skills',
},
],
description, description,
members: [ members: [
{ {

View File

@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { Bin, Cross, Plus } from 'shared/icons'; import { Bin, Cross, Plus } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import TaskAssignee from 'shared/components/TaskAssignee';
import { import {
NoDueDateLabel, NoDueDateLabel,
@ -93,19 +94,6 @@ const DetailsEditor: React.FC<DetailsEditorProps> = ({
); );
}; };
type TaskAssigneeProps = {
member: TaskUser;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
};
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ member, onMemberProfile }) => {
const $memberRef = useRef<HTMLDivElement>(null);
return (
<TaskDetailAssignee ref={$memberRef} onClick={() => onMemberProfile($memberRef, member.userID)} key={member.userID}>
<ProfileIcon>{member.profileIcon.initials ?? ''}</ProfileIcon>
</TaskDetailAssignee>
);
};
type TaskDetailsProps = { type TaskDetailsProps = {
task: Task; task: Task;
onTaskNameChange: (task: Task, newName: string) => void; onTaskNameChange: (task: Task, newName: string) => void;
@ -210,7 +198,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
) : ( ) : (
<> <>
{task.members && {task.members &&
task.members.map(member => <TaskAssignee member={member} onMemberProfile={onMemberProfile} />)} task.members.map(member => (
<TaskAssignee size={32} member={member} onMemberProfile={onMemberProfile} />
))}
<TaskDetailsAddMember ref={$addMemberRef} onClick={onAddMember}> <TaskDetailsAddMember ref={$addMemberRef} onClick={onAddMember}>
<TaskDetailsAddMemberIcon> <TaskDetailsAddMemberIcon>
<Plus size={16} color="#c2c6dc" /> <Plus size={16} color="#c2c6dc" />

View File

@ -5,6 +5,11 @@ export const NavbarWrapper = styled.div`
width: 100%; width: 100%;
`; `;
export const ProjectMembers = styled.div`
display: flex;
padding-right: 18px;
align-items: center;
`;
export const NavbarHeader = styled.header` export const NavbarHeader = styled.header`
height: 80px; height: 80px;
padding: 0 1.75rem; padding: 0 1.75rem;
@ -174,3 +179,29 @@ export const ProjectSettingsButton = styled.button`
background: rgb(115, 103, 240); background: rgb(115, 103, 240);
} }
`; `;
export const InviteButton = styled.button`
outline: none;
border: none;
width: 100%;
line-height: 20px;
padding: 6px 12px;
background-color: none;
text-align: center;
color: #c2c6dc;
font-size: 14px;
cursor: pointer;
margin: 0 0 0 8px;
border-radius: 3px;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
border-color: #414561;
&:hover {
background: rgb(115, 103, 240);
}
`;

View File

@ -3,6 +3,7 @@ import { Star, Bell, Cog, AngleDown } from 'shared/icons';
import { import {
NotificationContainer, NotificationContainer,
InviteButton,
GlobalActions, GlobalActions,
ProjectActions, ProjectActions,
ProjectSwitcher, ProjectSwitcher,
@ -21,7 +22,11 @@ import {
ProfileNameWrapper, ProfileNameWrapper,
ProfileNamePrimary, ProfileNamePrimary,
ProfileNameSecondary, ProfileNameSecondary,
ProjectMembers,
} from './Styles'; } from './Styles';
import TaskAssignee from 'shared/components/TaskAssignee';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import MiniProfile from 'shared/components/MiniProfile';
type NavBarProps = { type NavBarProps = {
projectName: string; projectName: string;
@ -31,6 +36,7 @@ type NavBarProps = {
firstName: string; firstName: string;
lastName: string; lastName: string;
initials: string; initials: string;
projectMembers?: Array<TaskUser> | null;
}; };
const NavBar: React.FC<NavBarProps> = ({ const NavBar: React.FC<NavBarProps> = ({
projectName, projectName,
@ -40,6 +46,7 @@ const NavBar: React.FC<NavBarProps> = ({
lastName, lastName,
initials, initials,
bgColor, bgColor,
projectMembers,
}) => { }) => {
const $profileRef: any = useRef(null); const $profileRef: any = useRef(null);
const handleProfileClick = () => { const handleProfileClick = () => {
@ -47,6 +54,21 @@ const NavBar: React.FC<NavBarProps> = ({
const boundingRect = $profileRef.current.getBoundingClientRect(); const boundingRect = $profileRef.current.getBoundingClientRect();
onProfileClick(boundingRect.bottom, boundingRect.right); onProfileClick(boundingRect.bottom, boundingRect.right);
}; };
const { showPopup } = usePopup();
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
showPopup(
$targetRef,
<Popup title={null} onClose={() => {}} tab={0}>
<MiniProfile
profileIcon={projectMembers ? projectMembers[0].profileIcon : { url: null, initials: 'JK', bgColor: '#000' }}
displayName="Jordan Knott"
username="@jordanthedev"
bio="None"
onRemoveFromTask={() => {}}
/>
</Popup>,
);
};
return ( return (
<NavbarWrapper> <NavbarWrapper>
<NavbarHeader> <NavbarHeader>
@ -58,7 +80,9 @@ const NavBar: React.FC<NavBarProps> = ({
<ProjectSettingsButton> <ProjectSettingsButton>
<AngleDown color="#c2c6dc" /> <AngleDown color="#c2c6dc" />
</ProjectSettingsButton> </ProjectSettingsButton>
<Star filled color="#c2c6dc" /> <ProjectSettingsButton>
<Star width={16} height={16} color="#c2c6dc" />
</ProjectSettingsButton>
</ProjectMeta> </ProjectMeta>
<ProjectTabs> <ProjectTabs>
<ProjectTab active>Board</ProjectTab> <ProjectTab active>Board</ProjectTab>
@ -68,6 +92,14 @@ const NavBar: React.FC<NavBarProps> = ({
</ProjectTabs> </ProjectTabs>
</ProjectActions> </ProjectActions>
<GlobalActions> <GlobalActions>
{projectMembers && (
<ProjectMembers>
{projectMembers.map(member => (
<TaskAssignee size={28} member={member} onMemberProfile={onMemberProfile} />
))}
<InviteButton>Invite</InviteButton>
</ProjectMembers>
)}
<NotificationContainer onClick={onNotificationClick}> <NotificationContainer onClick={onNotificationClick}>
<Bell color="#c2c6dc" size={20} /> <Bell color="#c2c6dc" size={20} />
</NotificationContainer> </NotificationContainer>

View File

@ -19,10 +19,18 @@ export type ProjectLabel = {
__typename?: 'ProjectLabel'; __typename?: 'ProjectLabel';
id: Scalars['ID']; id: Scalars['ID'];
createdDate: Scalars['Time']; createdDate: Scalars['Time'];
colorHex: Scalars['String']; labelColor: LabelColor;
name?: Maybe<Scalars['String']>; name?: Maybe<Scalars['String']>;
}; };
export type LabelColor = {
__typename?: 'LabelColor';
id: Scalars['ID'];
name: Scalars['String'];
position: Scalars['Float'];
colorHex: Scalars['String'];
};
export type TaskLabel = { export type TaskLabel = {
__typename?: 'TaskLabel'; __typename?: 'TaskLabel';
id: Scalars['ID']; id: Scalars['ID'];
@ -130,6 +138,7 @@ export type Query = {
findProject: Project; findProject: Project;
findTask: Task; findTask: Task;
projects: Array<Project>; projects: Array<Project>;
labelColors: Array<LabelColor>;
taskGroups: Array<TaskGroup>; taskGroups: Array<TaskGroup>;
me: UserAccount; me: UserAccount;
}; };
@ -401,7 +410,11 @@ export type CreateProjectLabelMutation = (
{ __typename?: 'Mutation' } { __typename?: 'Mutation' }
& { createProjectLabel: ( & { createProjectLabel: (
{ __typename?: 'ProjectLabel' } { __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'createdDate' | 'colorHex' | 'name'> & Pick<ProjectLabel, 'id' | 'createdDate' | 'name'>
& { labelColor: (
{ __typename?: 'LabelColor' }
& Pick<LabelColor, 'id' | 'colorHex'>
) }
) } ) }
); );
@ -499,7 +512,11 @@ export type FindProjectQuery = (
) } ) }
)>, labels: Array<( )>, labels: Array<(
{ __typename?: 'ProjectLabel' } { __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'createdDate' | 'colorHex' | 'name'> & Pick<ProjectLabel, 'id' | 'createdDate' | 'name'>
& { labelColor: (
{ __typename?: 'LabelColor' }
& Pick<LabelColor, 'id' | 'name' | 'colorHex' | 'position'>
) }
)>, taskGroups: Array<( )>, taskGroups: Array<(
{ __typename?: 'TaskGroup' } { __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name' | 'position'> & Pick<TaskGroup, 'id' | 'name' | 'position'>
@ -516,7 +533,10 @@ export type FindProjectQuery = (
)> } )> }
)> } )> }
)> } )> }
) } ), labelColors: Array<(
{ __typename?: 'LabelColor' }
& Pick<LabelColor, 'id' | 'position' | 'colorHex' | 'name'>
)> }
); );
export type FindTaskQueryVariables = { export type FindTaskQueryVariables = {
@ -692,7 +712,10 @@ export const CreateProjectLabelDocument = gql`
createProjectLabel(input: {projectID: $projectID, labelColorID: $labelColorID, name: $name}) { createProjectLabel(input: {projectID: $projectID, labelColorID: $labelColorID, name: $name}) {
id id
createdDate createdDate
labelColor {
id
colorHex colorHex
}
name name
} }
} }
@ -899,7 +922,12 @@ export const FindProjectDocument = gql`
labels { labels {
id id
createdDate createdDate
labelColor {
id
name
colorHex colorHex
position
}
name name
} }
taskGroups { taskGroups {
@ -924,6 +952,12 @@ export const FindProjectDocument = gql`
} }
} }
} }
labelColors {
id
position
colorHex
name
}
} }
`; `;

View File

@ -2,7 +2,10 @@ mutation createProjectLabel($projectID: UUID!, $labelColorID: UUID!, $name: Stri
createProjectLabel(input:{projectID:$projectID, labelColorID: $labelColorID, name: $name}) { createProjectLabel(input:{projectID:$projectID, labelColorID: $labelColorID, name: $name}) {
id id
createdDate createdDate
labelColor {
id
colorHex colorHex
}
name name
} }
} }

View File

@ -14,7 +14,12 @@ query findProject($projectId: String!) {
labels { labels {
id id
createdDate createdDate
labelColor {
id
name
colorHex colorHex
position
}
name name
} }
taskGroups { taskGroups {
@ -39,4 +44,10 @@ query findProject($projectId: String!) {
} }
} }
} }
labelColors {
id
position
colorHex
name
}
} }