feature: add more to project pane

This commit is contained in:
Jordan Knott 2020-05-26 19:53:31 -05:00
parent 7e78ee36b4
commit fba4de631f
64 changed files with 1845 additions and 582 deletions

View File

@ -41,9 +41,11 @@ type ResolverRoot interface {
Project() ProjectResolver
ProjectLabel() ProjectLabelResolver
Query() QueryResolver
RefreshToken() RefreshTokenResolver
Task() TaskResolver
TaskGroup() TaskGroupResolver
TaskLabel() TaskLabelResolver
Team() TeamResolver
UserAccount() UserAccountResolver
}
@ -90,27 +92,27 @@ type ComplexityRoot struct {
Project struct {
CreatedAt func(childComplexity int) int
ID func(childComplexity int) int
Labels func(childComplexity int) int
Members func(childComplexity int) int
Name func(childComplexity int) int
Owner func(childComplexity int) int
ProjectID func(childComplexity int) int
TaskGroups func(childComplexity int) int
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
ColorHex func(childComplexity int) int
CreatedDate func(childComplexity int) int
ID func(childComplexity int) int
Name func(childComplexity int) int
}
ProjectMember struct {
FirstName func(childComplexity int) int
ID func(childComplexity int) int
LastName func(childComplexity int) int
ProfileIcon func(childComplexity int) int
UserID func(childComplexity int) int
}
Query struct {
@ -126,7 +128,7 @@ type ComplexityRoot struct {
RefreshToken struct {
CreatedAt func(childComplexity int) int
ExpiresAt func(childComplexity int) int
TokenID func(childComplexity int) int
ID func(childComplexity int) int
UserID func(childComplexity int) int
}
@ -134,43 +136,43 @@ type ComplexityRoot struct {
Assigned func(childComplexity int) int
CreatedAt func(childComplexity int) int
Description func(childComplexity int) int
ID func(childComplexity int) int
Labels func(childComplexity int) int
Name func(childComplexity int) int
Position func(childComplexity int) int
TaskGroup func(childComplexity int) int
TaskID func(childComplexity int) int
}
TaskGroup struct {
CreatedAt func(childComplexity int) int
Name func(childComplexity int) int
Position func(childComplexity int) int
ProjectID func(childComplexity int) int
TaskGroupID func(childComplexity int) int
Tasks func(childComplexity int) int
CreatedAt func(childComplexity int) int
ID func(childComplexity int) int
Name func(childComplexity int) int
Position func(childComplexity int) int
ProjectID func(childComplexity int) int
Tasks func(childComplexity int) int
}
TaskLabel struct {
AssignedDate func(childComplexity int) int
ColorHex func(childComplexity int) int
ID func(childComplexity int) int
Name func(childComplexity int) int
ProjectLabelID func(childComplexity int) int
TaskLabelID func(childComplexity int) int
}
Team struct {
CreatedAt func(childComplexity int) int
ID func(childComplexity int) int
Name func(childComplexity int) int
TeamID func(childComplexity int) int
}
UserAccount struct {
CreatedAt func(childComplexity int) int
Email func(childComplexity int) int
FirstName func(childComplexity int) int
ID func(childComplexity int) int
LastName func(childComplexity int) int
ProfileIcon func(childComplexity int) int
UserID func(childComplexity int) int
Username func(childComplexity int) int
}
}
@ -196,6 +198,8 @@ type MutationResolver interface {
LogoutUser(ctx context.Context, input LogoutUser) (bool, error)
}
type ProjectResolver interface {
ID(ctx context.Context, obj *pg.Project) (uuid.UUID, error)
Team(ctx context.Context, obj *pg.Project) (*pg.Team, error)
Owner(ctx context.Context, obj *pg.Project) (*ProjectMember, error)
TaskGroups(ctx context.Context, obj *pg.Project) ([]pg.TaskGroup, error)
@ -203,6 +207,8 @@ type ProjectResolver interface {
Labels(ctx context.Context, obj *pg.Project) ([]pg.ProjectLabel, error)
}
type ProjectLabelResolver interface {
ID(ctx context.Context, obj *pg.ProjectLabel) (uuid.UUID, error)
ColorHex(ctx context.Context, obj *pg.ProjectLabel) (string, error)
Name(ctx context.Context, obj *pg.ProjectLabel) (*string, error)
}
@ -215,7 +221,11 @@ type QueryResolver interface {
TaskGroups(ctx context.Context) ([]pg.TaskGroup, error)
Me(ctx context.Context) (*pg.UserAccount, error)
}
type RefreshTokenResolver interface {
ID(ctx context.Context, obj *pg.RefreshToken) (uuid.UUID, error)
}
type TaskResolver interface {
ID(ctx context.Context, obj *pg.Task) (uuid.UUID, error)
TaskGroup(ctx context.Context, obj *pg.Task) (*pg.TaskGroup, error)
Description(ctx context.Context, obj *pg.Task) (*string, error)
@ -223,15 +233,23 @@ type TaskResolver interface {
Labels(ctx context.Context, obj *pg.Task) ([]pg.TaskLabel, error)
}
type TaskGroupResolver interface {
ID(ctx context.Context, obj *pg.TaskGroup) (uuid.UUID, error)
ProjectID(ctx context.Context, obj *pg.TaskGroup) (string, error)
Tasks(ctx context.Context, obj *pg.TaskGroup) ([]pg.Task, error)
}
type TaskLabelResolver interface {
ID(ctx context.Context, obj *pg.TaskLabel) (uuid.UUID, error)
ColorHex(ctx context.Context, obj *pg.TaskLabel) (string, error)
Name(ctx context.Context, obj *pg.TaskLabel) (*string, error)
}
type TeamResolver interface {
ID(ctx context.Context, obj *pg.Team) (uuid.UUID, error)
}
type UserAccountResolver interface {
ID(ctx context.Context, obj *pg.UserAccount) (uuid.UUID, error)
ProfileIcon(ctx context.Context, obj *pg.UserAccount) (*ProfileIcon, error)
}
@ -522,6 +540,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Project.CreatedAt(childComplexity), true
case "Project.id":
if e.complexity.Project.ID == nil {
break
}
return e.complexity.Project.ID(childComplexity), true
case "Project.labels":
if e.complexity.Project.Labels == nil {
break
@ -550,13 +575,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Project.Owner(childComplexity), true
case "Project.projectID":
if e.complexity.Project.ProjectID == nil {
break
}
return e.complexity.Project.ProjectID(childComplexity), true
case "Project.taskGroups":
if e.complexity.Project.TaskGroups == nil {
break
@ -585,6 +603,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ProjectLabel.CreatedDate(childComplexity), true
case "ProjectLabel.id":
if e.complexity.ProjectLabel.ID == nil {
break
}
return e.complexity.ProjectLabel.ID(childComplexity), true
case "ProjectLabel.name":
if e.complexity.ProjectLabel.Name == nil {
break
@ -592,13 +617,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
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
@ -606,6 +624,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ProjectMember.FirstName(childComplexity), true
case "ProjectMember.id":
if e.complexity.ProjectMember.ID == nil {
break
}
return e.complexity.ProjectMember.ID(childComplexity), true
case "ProjectMember.lastName":
if e.complexity.ProjectMember.LastName == nil {
break
@ -620,13 +645,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ProjectMember.ProfileIcon(childComplexity), true
case "ProjectMember.userID":
if e.complexity.ProjectMember.UserID == nil {
break
}
return e.complexity.ProjectMember.UserID(childComplexity), true
case "Query.findProject":
if e.complexity.Query.FindProject == nil {
break
@ -710,12 +728,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.RefreshToken.ExpiresAt(childComplexity), true
case "RefreshToken.tokenId":
if e.complexity.RefreshToken.TokenID == nil {
case "RefreshToken.id":
if e.complexity.RefreshToken.ID == nil {
break
}
return e.complexity.RefreshToken.TokenID(childComplexity), true
return e.complexity.RefreshToken.ID(childComplexity), true
case "RefreshToken.userId":
if e.complexity.RefreshToken.UserID == nil {
@ -745,6 +763,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Task.Description(childComplexity), true
case "Task.id":
if e.complexity.Task.ID == nil {
break
}
return e.complexity.Task.ID(childComplexity), true
case "Task.labels":
if e.complexity.Task.Labels == nil {
break
@ -773,13 +798,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Task.TaskGroup(childComplexity), true
case "Task.taskID":
if e.complexity.Task.TaskID == nil {
break
}
return e.complexity.Task.TaskID(childComplexity), true
case "TaskGroup.createdAt":
if e.complexity.TaskGroup.CreatedAt == nil {
break
@ -787,6 +805,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.TaskGroup.CreatedAt(childComplexity), true
case "TaskGroup.id":
if e.complexity.TaskGroup.ID == nil {
break
}
return e.complexity.TaskGroup.ID(childComplexity), true
case "TaskGroup.name":
if e.complexity.TaskGroup.Name == nil {
break
@ -808,13 +833,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.TaskGroup.ProjectID(childComplexity), true
case "TaskGroup.taskGroupID":
if e.complexity.TaskGroup.TaskGroupID == nil {
break
}
return e.complexity.TaskGroup.TaskGroupID(childComplexity), true
case "TaskGroup.tasks":
if e.complexity.TaskGroup.Tasks == nil {
break
@ -836,6 +854,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.TaskLabel.ColorHex(childComplexity), true
case "TaskLabel.id":
if e.complexity.TaskLabel.ID == nil {
break
}
return e.complexity.TaskLabel.ID(childComplexity), true
case "TaskLabel.name":
if e.complexity.TaskLabel.Name == nil {
break
@ -850,13 +875,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.TaskLabel.ProjectLabelID(childComplexity), true
case "TaskLabel.taskLabelID":
if e.complexity.TaskLabel.TaskLabelID == nil {
break
}
return e.complexity.TaskLabel.TaskLabelID(childComplexity), true
case "Team.createdAt":
if e.complexity.Team.CreatedAt == nil {
break
@ -864,6 +882,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Team.CreatedAt(childComplexity), true
case "Team.id":
if e.complexity.Team.ID == nil {
break
}
return e.complexity.Team.ID(childComplexity), true
case "Team.name":
if e.complexity.Team.Name == nil {
break
@ -871,13 +896,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Team.Name(childComplexity), true
case "Team.teamID":
if e.complexity.Team.TeamID == nil {
break
}
return e.complexity.Team.TeamID(childComplexity), true
case "UserAccount.createdAt":
if e.complexity.UserAccount.CreatedAt == nil {
break
@ -899,6 +917,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.UserAccount.FirstName(childComplexity), true
case "UserAccount.id":
if e.complexity.UserAccount.ID == nil {
break
}
return e.complexity.UserAccount.ID(childComplexity), true
case "UserAccount.lastName":
if e.complexity.UserAccount.LastName == nil {
break
@ -913,13 +938,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.UserAccount.ProfileIcon(childComplexity), true
case "UserAccount.userID":
if e.complexity.UserAccount.UserID == nil {
break
}
return e.complexity.UserAccount.UserID(childComplexity), true
case "UserAccount.username":
if e.complexity.UserAccount.Username == nil {
break
@ -995,14 +1013,14 @@ var sources = []*ast.Source{
scalar UUID
type ProjectLabel {
projectLabelID: ID!
id: ID!
createdDate: Time!
colorHex: String!
name: String
}
type TaskLabel {
taskLabelID: ID!
id: ID!
projectLabelID: UUID!
assignedDate: Time!
colorHex: String!
@ -1016,21 +1034,21 @@ type ProfileIcon {
}
type ProjectMember {
userID: ID!
id: ID!
firstName: String!
lastName: String!
profileIcon: ProfileIcon!
}
type RefreshToken {
tokenId: ID!
id: ID!
userId: UUID!
expiresAt: Time!
createdAt: Time!
}
type UserAccount {
userID: ID!
id: ID!
email: String!
createdAt: Time!
firstName: String!
@ -1040,13 +1058,13 @@ type UserAccount {
}
type Team {
teamID: ID!
id: ID!
createdAt: Time!
name: String!
}
type Project {
projectID: ID!
id: ID!
createdAt: Time!
name: String!
team: Team!
@ -1057,7 +1075,7 @@ type Project {
}
type TaskGroup {
taskGroupID: ID!
id: ID!
projectID: String!
createdAt: Time!
name: String!
@ -1066,7 +1084,7 @@ type TaskGroup {
}
type Task {
taskID: ID!
id: ID!
taskGroup: TaskGroup!
createdAt: Time!
name: String!
@ -2563,7 +2581,7 @@ func (ec *executionContext) _ProfileIcon_bgColor(ctx context.Context, field grap
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) {
func (ec *executionContext) _Project_id(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))
@ -2574,13 +2592,13 @@ func (ec *executionContext) _Project_projectID(ctx context.Context, field graphq
Object: "Project",
Field: field,
Args: nil,
IsMethod: false,
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 obj.ProjectID, nil
return ec.resolvers.Project().ID(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -2835,7 +2853,7 @@ func (ec *executionContext) _Project_labels(ctx context.Context, field graphql.C
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) {
func (ec *executionContext) _ProjectLabel_id(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))
@ -2846,13 +2864,13 @@ func (ec *executionContext) _ProjectLabel_projectLabelID(ctx context.Context, fi
Object: "ProjectLabel",
Field: field,
Args: nil,
IsMethod: false,
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 obj.ProjectLabelID, nil
return ec.resolvers.ProjectLabel().ID(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -2968,7 +2986,7 @@ func (ec *executionContext) _ProjectLabel_name(ctx context.Context, field graphq
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _ProjectMember_userID(ctx context.Context, field graphql.CollectedField, obj *ProjectMember) (ret graphql.Marshaler) {
func (ec *executionContext) _ProjectMember_id(ctx context.Context, field graphql.CollectedField, obj *ProjectMember) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
@ -2985,7 +3003,7 @@ func (ec *executionContext) _ProjectMember_userID(ctx context.Context, field gra
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.UserID, nil
return obj.ID, nil
})
if err != nil {
ec.Error(ctx, err)
@ -3439,7 +3457,7 @@ func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.C
return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res)
}
func (ec *executionContext) _RefreshToken_tokenId(ctx context.Context, field graphql.CollectedField, obj *pg.RefreshToken) (ret graphql.Marshaler) {
func (ec *executionContext) _RefreshToken_id(ctx context.Context, field graphql.CollectedField, obj *pg.RefreshToken) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
@ -3450,13 +3468,13 @@ func (ec *executionContext) _RefreshToken_tokenId(ctx context.Context, field gra
Object: "RefreshToken",
Field: field,
Args: nil,
IsMethod: false,
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 obj.TokenID, nil
return ec.resolvers.RefreshToken().ID(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -3575,7 +3593,7 @@ func (ec *executionContext) _RefreshToken_createdAt(ctx context.Context, field g
return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res)
}
func (ec *executionContext) _Task_taskID(ctx context.Context, field graphql.CollectedField, obj *pg.Task) (ret graphql.Marshaler) {
func (ec *executionContext) _Task_id(ctx context.Context, field graphql.CollectedField, obj *pg.Task) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
@ -3586,13 +3604,13 @@ func (ec *executionContext) _Task_taskID(ctx context.Context, field graphql.Coll
Object: "Task",
Field: field,
Args: nil,
IsMethod: false,
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 obj.TaskID, nil
return ec.resolvers.Task().ID(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -3844,7 +3862,7 @@ func (ec *executionContext) _Task_labels(ctx context.Context, field graphql.Coll
return ec.marshalNTaskLabel2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTaskLabelᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _TaskGroup_taskGroupID(ctx context.Context, field graphql.CollectedField, obj *pg.TaskGroup) (ret graphql.Marshaler) {
func (ec *executionContext) _TaskGroup_id(ctx context.Context, field graphql.CollectedField, obj *pg.TaskGroup) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
@ -3855,13 +3873,13 @@ func (ec *executionContext) _TaskGroup_taskGroupID(ctx context.Context, field gr
Object: "TaskGroup",
Field: field,
Args: nil,
IsMethod: false,
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 obj.TaskGroupID, nil
return ec.resolvers.TaskGroup().ID(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -4048,7 +4066,7 @@ func (ec *executionContext) _TaskGroup_tasks(ctx context.Context, field graphql.
return ec.marshalNTask2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTaskᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _TaskLabel_taskLabelID(ctx context.Context, field graphql.CollectedField, obj *pg.TaskLabel) (ret graphql.Marshaler) {
func (ec *executionContext) _TaskLabel_id(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))
@ -4059,13 +4077,13 @@ func (ec *executionContext) _TaskLabel_taskLabelID(ctx context.Context, field gr
Object: "TaskLabel",
Field: field,
Args: nil,
IsMethod: false,
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 obj.TaskLabelID, nil
return ec.resolvers.TaskLabel().ID(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -4215,7 +4233,7 @@ func (ec *executionContext) _TaskLabel_name(ctx context.Context, field graphql.C
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) {
func (ec *executionContext) _Team_id(ctx context.Context, field graphql.CollectedField, obj *pg.Team) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
@ -4226,13 +4244,13 @@ func (ec *executionContext) _Team_teamID(ctx context.Context, field graphql.Coll
Object: "Team",
Field: field,
Args: nil,
IsMethod: false,
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 obj.TeamID, nil
return ec.resolvers.Team().ID(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -4317,7 +4335,7 @@ func (ec *executionContext) _Team_name(ctx context.Context, field graphql.Collec
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _UserAccount_userID(ctx context.Context, field graphql.CollectedField, obj *pg.UserAccount) (ret graphql.Marshaler) {
func (ec *executionContext) _UserAccount_id(ctx context.Context, field graphql.CollectedField, obj *pg.UserAccount) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
@ -4328,13 +4346,13 @@ func (ec *executionContext) _UserAccount_userID(ctx context.Context, field graph
Object: "UserAccount",
Field: field,
Args: nil,
IsMethod: false,
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 obj.UserID, nil
return ec.resolvers.UserAccount().ID(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -6365,11 +6383,20 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet,
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("Project")
case "projectID":
out.Values[i] = ec._Project_projectID(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
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._Project_id(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "createdAt":
out.Values[i] = ec._Project_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
@ -6472,11 +6499,20 @@ func (ec *executionContext) _ProjectLabel(ctx context.Context, sel ast.Selection
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 "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._ProjectLabel_id(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "createdDate":
out.Values[i] = ec._ProjectLabel_createdDate(ctx, field, obj)
if out.Values[i] == graphql.Null {
@ -6529,8 +6565,8 @@ func (ec *executionContext) _ProjectMember(ctx context.Context, sel ast.Selectio
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("ProjectMember")
case "userID":
out.Values[i] = ec._ProjectMember_userID(ctx, field, obj)
case "id":
out.Values[i] = ec._ProjectMember_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
@ -6699,25 +6735,34 @@ func (ec *executionContext) _RefreshToken(ctx context.Context, sel ast.Selection
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("RefreshToken")
case "tokenId":
out.Values[i] = ec._RefreshToken_tokenId(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
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._RefreshToken_id(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "userId":
out.Values[i] = ec._RefreshToken_userId(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
atomic.AddUint32(&invalids, 1)
}
case "expiresAt":
out.Values[i] = ec._RefreshToken_expiresAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
atomic.AddUint32(&invalids, 1)
}
case "createdAt":
out.Values[i] = ec._RefreshToken_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
atomic.AddUint32(&invalids, 1)
}
default:
panic("unknown field " + strconv.Quote(field.Name))
@ -6741,11 +6786,20 @@ func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("Task")
case "taskID":
out.Values[i] = ec._Task_taskID(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
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._Task_id(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "taskGroup":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
@ -6836,11 +6890,20 @@ func (ec *executionContext) _TaskGroup(ctx context.Context, sel ast.SelectionSet
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("TaskGroup")
case "taskGroupID":
out.Values[i] = ec._TaskGroup_taskGroupID(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
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._TaskGroup_id(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "projectID":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
@ -6906,11 +6969,20 @@ func (ec *executionContext) _TaskLabel(ctx context.Context, sel ast.SelectionSet
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("TaskLabel")
case "taskLabelID":
out.Values[i] = ec._TaskLabel_taskLabelID(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
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._TaskLabel_id(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "projectLabelID":
out.Values[i] = ec._TaskLabel_projectLabelID(ctx, field, obj)
if out.Values[i] == graphql.Null {
@ -6968,20 +7040,29 @@ func (ec *executionContext) _Team(ctx context.Context, sel ast.SelectionSet, obj
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("Team")
case "teamID":
out.Values[i] = ec._Team_teamID(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
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._Team_id(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "createdAt":
out.Values[i] = ec._Team_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
atomic.AddUint32(&invalids, 1)
}
case "name":
out.Values[i] = ec._Team_name(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
atomic.AddUint32(&invalids, 1)
}
default:
panic("unknown field " + strconv.Quote(field.Name))
@ -7005,11 +7086,20 @@ func (ec *executionContext) _UserAccount(ctx context.Context, sel ast.SelectionS
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("UserAccount")
case "userID":
out.Values[i] = ec._UserAccount_userID(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
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._UserAccount_id(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "email":
out.Values[i] = ec._UserAccount_email(ctx, field, obj)
if out.Values[i] == graphql.Null {

View File

@ -110,7 +110,7 @@ type ProfileIcon struct {
}
type ProjectMember struct {
UserID uuid.UUID `json:"userID"`
ID uuid.UUID `json:"id"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
ProfileIcon *ProfileIcon `json:"profileIcon"`

View File

@ -2,14 +2,14 @@ scalar Time
scalar UUID
type ProjectLabel {
projectLabelID: ID!
id: ID!
createdDate: Time!
colorHex: String!
name: String
}
type TaskLabel {
taskLabelID: ID!
id: ID!
projectLabelID: UUID!
assignedDate: Time!
colorHex: String!
@ -23,21 +23,21 @@ type ProfileIcon {
}
type ProjectMember {
userID: ID!
id: ID!
firstName: String!
lastName: String!
profileIcon: ProfileIcon!
}
type RefreshToken {
tokenId: ID!
id: ID!
userId: UUID!
expiresAt: Time!
createdAt: Time!
}
type UserAccount {
userID: ID!
id: ID!
email: String!
createdAt: Time!
firstName: String!
@ -47,13 +47,13 @@ type UserAccount {
}
type Team {
teamID: ID!
id: ID!
createdAt: Time!
name: String!
}
type Project {
projectID: ID!
id: ID!
createdAt: Time!
name: String!
team: Team!
@ -64,7 +64,7 @@ type Project {
}
type TaskGroup {
taskGroupID: ID!
id: ID!
projectID: String!
createdAt: Time!
name: String!
@ -73,7 +73,7 @@ type TaskGroup {
}
type Task {
taskID: ID!
id: ID!
taskGroup: TaskGroup!
createdAt: Time!
name: String!

View File

@ -190,6 +190,10 @@ func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bo
return true, err
}
func (r *projectResolver) ID(ctx context.Context, obj *pg.Project) (uuid.UUID, error) {
return obj.ProjectID, nil
}
func (r *projectResolver) Team(ctx context.Context, obj *pg.Project) (*pg.Team, error) {
team, err := r.Repository.GetTeamByID(ctx, obj.TeamID)
return &team, err
@ -226,6 +230,10 @@ func (r *projectResolver) Labels(ctx context.Context, obj *pg.Project) ([]pg.Pro
return labels, err
}
func (r *projectLabelResolver) ID(ctx context.Context, obj *pg.ProjectLabel) (uuid.UUID, error) {
return obj.ProjectLabelID, nil
}
func (r *projectLabelResolver) ColorHex(ctx context.Context, obj *pg.ProjectLabel) (string, error) {
labelColor, err := r.Repository.GetLabelColorByID(ctx, obj.LabelColorID)
if err != nil {
@ -235,7 +243,11 @@ func (r *projectLabelResolver) ColorHex(ctx context.Context, obj *pg.ProjectLabe
}
func (r *projectLabelResolver) Name(ctx context.Context, obj *pg.ProjectLabel) (*string, error) {
panic(fmt.Errorf("not implemented"))
var name *string
if obj.Name.Valid {
name = &obj.Name.String
}
return name, nil
}
func (r *queryResolver) Users(ctx context.Context) ([]pg.UserAccount, error) {
@ -311,6 +323,14 @@ func (r *queryResolver) Me(ctx context.Context) (*pg.UserAccount, error) {
return &user, err
}
func (r *refreshTokenResolver) ID(ctx context.Context, obj *pg.RefreshToken) (uuid.UUID, error) {
return obj.TokenID, nil
}
func (r *taskResolver) ID(ctx context.Context, obj *pg.Task) (uuid.UUID, error) {
return obj.TaskID, nil
}
func (r *taskResolver) TaskGroup(ctx context.Context, obj *pg.Task) (*pg.TaskGroup, error) {
taskGroup, err := r.Repository.GetTaskGroupByID(ctx, obj.TaskGroupID)
return &taskGroup, err
@ -349,6 +369,10 @@ func (r *taskResolver) Labels(ctx context.Context, obj *pg.Task) ([]pg.TaskLabel
return r.Repository.GetTaskLabelsForTaskID(ctx, obj.TaskID)
}
func (r *taskGroupResolver) ID(ctx context.Context, obj *pg.TaskGroup) (uuid.UUID, error) {
return obj.TaskGroupID, nil
}
func (r *taskGroupResolver) ProjectID(ctx context.Context, obj *pg.TaskGroup) (string, error) {
return obj.ProjectID.String(), nil
}
@ -358,6 +382,10 @@ func (r *taskGroupResolver) Tasks(ctx context.Context, obj *pg.TaskGroup) ([]pg.
return tasks, err
}
func (r *taskLabelResolver) ID(ctx context.Context, obj *pg.TaskLabel) (uuid.UUID, error) {
return obj.TaskLabelID, nil
}
func (r *taskLabelResolver) ColorHex(ctx context.Context, obj *pg.TaskLabel) (string, error) {
projectLabel, err := r.Repository.GetProjectLabelByID(ctx, obj.ProjectLabelID)
if err != nil {
@ -382,6 +410,14 @@ func (r *taskLabelResolver) Name(ctx context.Context, obj *pg.TaskLabel) (*strin
return &name.String, err
}
func (r *teamResolver) ID(ctx context.Context, obj *pg.Team) (uuid.UUID, error) {
return obj.TeamID, nil
}
func (r *userAccountResolver) ID(ctx context.Context, obj *pg.UserAccount) (uuid.UUID, error) {
return obj.UserID, nil
}
func (r *userAccountResolver) ProfileIcon(ctx context.Context, obj *pg.UserAccount) (*ProfileIcon, error) {
initials := string([]rune(obj.FirstName)[0]) + string([]rune(obj.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials, &obj.ProfileBgColor}
@ -400,6 +436,9 @@ func (r *Resolver) ProjectLabel() ProjectLabelResolver { return &projectLabelRes
// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
// RefreshToken returns RefreshTokenResolver implementation.
func (r *Resolver) RefreshToken() RefreshTokenResolver { return &refreshTokenResolver{r} }
// Task returns TaskResolver implementation.
func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
@ -409,6 +448,9 @@ func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r}
// TaskLabel returns TaskLabelResolver implementation.
func (r *Resolver) TaskLabel() TaskLabelResolver { return &taskLabelResolver{r} }
// Team returns TeamResolver implementation.
func (r *Resolver) Team() TeamResolver { return &teamResolver{r} }
// UserAccount returns UserAccountResolver implementation.
func (r *Resolver) UserAccount() UserAccountResolver { return &userAccountResolver{r} }
@ -416,20 +458,9 @@ type mutationResolver struct{ *Resolver }
type projectResolver struct{ *Resolver }
type projectLabelResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type refreshTokenResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }
type taskGroupResolver struct{ *Resolver }
type taskLabelResolver struct{ *Resolver }
type teamResolver 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 *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
}

View File

@ -0,0 +1,7 @@
ALTER TABLE task_assigned
DROP CONSTRAINT task_assigned_task_id_fkey,
ADD CONSTRAINT task_assigned_task_id_fkey
FOREIGN KEY (task_id)
REFERENCES task(task_id)
ON DELETE CASCADE;

69
assets/favicon.svg Normal file
View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64"
height="64"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg8"
sodipodi:docname="favicon.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<metadata
id="metadata14">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs12" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1908"
inkscape:window-height="983"
id="namedview10"
showgrid="false"
inkscape:zoom="7.375"
inkscape:cx="5.2026609"
inkscape:cy="37.687216"
inkscape:window-x="6"
inkscape:window-y="6"
inkscape:window-maximized="0"
inkscape:current-layer="g6" />
<g
transform="translate(-.26 -24.137) scale(.1249)"
id="g6"
style="stroke-width:17.47648118;stroke-miterlimit:4;stroke-dasharray:none">
<path
d="M50.886 286.515l-40.4-44.46 44.459-40.401 40.401 44.46z"
fill="none"
stroke="#000"
strokeWidth="11.90597031"
id="path2"
style="stroke-width:7.94385508;stroke-miterlimit:4;stroke-dasharray:none;stroke:#7367f0;stroke-opacity:1" />
<circle
cx="52.917"
cy="244.083"
r="11.025"
fill="#000"
id="circle4"
style="stroke-width:17.47648118;stroke-miterlimit:4;stroke-dasharray:none;fill:#7367f0;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 295 KiB

View File

@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>Citadel</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -5,7 +5,10 @@ import { useHistory } from 'react-router';
import UserIDContext from 'App/context';
import { useMeQuery } from 'shared/generated/graphql';
const GlobalTopNavbar: React.FC = () => {
type GlobalTopNavbarProps = {
name: string;
};
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ name }) => {
const { loading, data } = useMeQuery();
const history = useHistory();
const { userID, setUserID } = useContext(UserIDContext);
@ -41,6 +44,7 @@ const GlobalTopNavbar: React.FC = () => {
return (
<>
<TopNavbar
projectName={name}
bgColor={data ? data.me.profileIcon.bgColor ?? '#7367F0' : '#7367F0'}
firstName={data ? data.me.firstName : ''}
lastName={data ? data.me.lastName : ''}

View File

@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
import jwtDecode from 'jwt-decode';
import { createBrowserHistory } from 'history';
import { setAccessToken } from 'shared/utils/accessToken';
import GlobalTopNavbar from 'App/TopNavbar';
import styled from 'styled-components';
import NormalizeStyles from './NormalizeStyles';
import BaseStyles from './BaseStyles';
@ -10,6 +9,7 @@ import Routes from './Routes';
import { UserIDContext } from './context';
import Navbar from './Navbar';
import { Router } from 'react-router';
import { PopupProvider } from 'shared/components/PopupMenu';
const history = createBrowserHistory();
@ -45,21 +45,22 @@ const App = () => {
return (
<>
<UserIDContext.Provider value={{ userID, setUserID }}>
<NormalizeStyles />
<BaseStyles />
<Router history={history}>
{loading ? (
<div>loading</div>
) : (
<>
<Navbar />
<MainContent>
<GlobalTopNavbar />
<Routes history={history} />
</MainContent>
</>
)}
</Router>
<PopupProvider>
<NormalizeStyles />
<BaseStyles />
<Router history={history}>
{loading ? (
<div>loading</div>
) : (
<>
<Navbar />
<MainContent>
<Routes history={history} />
</MainContent>
</>
)}
</Router>
</PopupProvider>
</UserIDContext.Provider>
</>
);

View File

@ -1,11 +1,12 @@
import React, { useState, useContext } from 'react';
import Modal from 'shared/components/Modal';
import TaskDetails from 'shared/components/TaskDetails';
import PopupMenu from 'shared/components/PopupMenu';
import PopupMenu, { Popup, usePopup } from 'shared/components/PopupMenu';
import MemberManager from 'shared/components/MemberManager';
import { useRouteMatch, useHistory } from 'react-router';
import { useFindTaskQuery, useAssignTaskMutation, useUnassignTaskMutation } from 'shared/generated/graphql';
import UserIDContext from 'App/context';
import MiniProfile from 'shared/components/MiniProfile';
type DetailsProps = {
taskID: string;
@ -13,7 +14,7 @@ type DetailsProps = {
onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
onOpenAddLabelPopup: (task: Task, bounds: ElementBounds) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
availableMembers: Array<TaskUser>;
refreshCache: () => void;
};
@ -31,8 +32,10 @@ const Details: React.FC<DetailsProps> = ({
refreshCache,
}) => {
const { userID } = useContext(UserIDContext);
const { showPopup } = usePopup();
const history = useHistory();
const match = useRouteMatch();
const [currentMemberTask, setCurrentMemberTask] = useState('');
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const [assignTask] = useAssignTaskMutation({
@ -55,7 +58,7 @@ const Details: React.FC<DetailsProps> = ({
}
const taskMembers = data.findTask.assigned.map(assigned => {
return {
userID: assigned.userID,
userID: assigned.id,
displayName: `${assigned.firstName} ${assigned.lastName}`,
profileIcon: {
url: null,
@ -76,6 +79,8 @@ const Details: React.FC<DetailsProps> = ({
<TaskDetails
task={{
...data.findTask,
taskID: data.findTask.id,
taskGroup: { taskGroupID: data.findTask.taskGroup.id },
members: taskMembers,
description: data.findTask.description ?? '',
labels: [],
@ -84,42 +89,48 @@ const Details: React.FC<DetailsProps> = ({
onTaskDescriptionChange={onTaskDescriptionChange}
onDeleteTask={onDeleteTask}
onCloseModal={() => history.push(projectURL)}
onOpenAddMemberPopup={(task, bounds) => {
console.log(task, bounds);
setMemberPopupData({
isOpen: true,
taskID: task.taskID,
top: bounds.position.top + bounds.size.height + 10,
left: bounds.position.left,
});
onMemberProfile={($targetRef, memberID) => {
showPopup(
$targetRef,
<Popup title={null} onClose={() => {}} tab={0}>
<MiniProfile
profileIcon={taskMembers[0].profileIcon}
displayName="Jordan Knott"
username="@jordanthedev"
bio="None"
onRemoveFromTask={() => {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
}}
/>
</Popup>,
);
}}
onOpenAddMemberPopup={(task, $targetRef) => {
console.log(`task: ${task.taskID}`);
showPopup(
$targetRef,
<Popup title="Members" tab={0} onClose={() => {}}>
<MemberManager
availableMembers={availableMembers}
activeMembers={taskMembers}
onMemberChange={(member, isActive) => {
console.log(`is active ${member.userID} - ${isActive}`);
if (isActive) {
assignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
} else {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
}
console.log(member, isActive);
}}
/>
</Popup>,
);
}}
onOpenAddLabelPopup={onOpenAddLabelPopup}
/>
);
}}
/>
{memberPopupData.isOpen && (
<PopupMenu
title="Members"
top={memberPopupData.top}
onClose={() => setMemberPopupData(initialMemberPopupState)}
left={memberPopupData.left}
>
<MemberManager
availableMembers={availableMembers}
activeMembers={taskMembers}
onMemberChange={(member, isActive) => {
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);
}}
/>
</PopupMenu>
)}
</>
);
};

View File

@ -6,7 +6,7 @@ import { Board } from './Styles';
type KanbanBoardProps = {
listsData: BoardState;
onOpenListActionsPopup: (isOpen: boolean, left: number, top: number, taskGroupID: string) => void;
onOpenListActionsPopup: ($targetRef: React.RefObject<HTMLElement>, taskGroupID: string) => void;
onCardDrop: (task: Task) => void;
onListDrop: (taskGroup: TaskGroup) => void;
onCardCreate: (taskGroupID: string, name: string) => void;
@ -31,8 +31,8 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
onCardClick={task => {
history.push(`${match.url}/c/${task.taskID}`);
}}
onExtraMenuOpen={(taskGroupID, pos, size) => {
onOpenListActionsPopup(true, pos.left, pos.top + size.height + 5, taskGroupID);
onExtraMenuOpen={(taskGroupID, $targetRef) => {
onOpenListActionsPopup($targetRef, taskGroupID);
}}
onQuickEditorOpen={onQuickEditorOpen}
onCardCreate={onCardCreate}

View File

@ -1,6 +1,9 @@
import React, { useState } from 'react';
import React, { useState, useRef } from 'react';
import * as BoardStateUtils from 'shared/utils/boardState';
import GlobalTopNavbar from 'App/TopNavbar';
import styled from 'styled-components/macro';
import { Bolt, ToggleOn, Tags } from 'shared/icons';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps } from 'react-router-dom';
import {
useFindProjectQuery,
@ -13,14 +16,19 @@ import {
useDeleteTaskGroupMutation,
useUpdateTaskDescriptionMutation,
useAssignTaskMutation,
DeleteTaskDocument,
FindProjectDocument,
} from 'shared/generated/graphql';
import QuickCardEditor from 'shared/components/QuickCardEditor';
import PopupMenu from 'shared/components/PopupMenu';
import ListActions from 'shared/components/ListActions';
import MemberManager from 'shared/components/MemberManager';
import { LabelsPopup } from 'shared/components/PopupMenu/PopupMenu.stories';
import KanbanBoard from 'Projects/Project/KanbanBoard';
import { mixin } from 'shared/utils/styles';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
import produce from 'immer';
import Details from './Details';
type TaskRouteProps = {
@ -45,6 +53,67 @@ const Title = styled.span`
color: #fff;
`;
type LabelManagerEditorProps = {
labels: Array<Label>;
};
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ labels: initialLabels }) => {
const [labels, setLabels] = useState<Array<Label>>(initialLabels);
const [currentLabel, setCurrentLabel] = useState('');
const { setTab } = usePopup();
return (
<>
<Popup title="Labels" tab={0} onClose={() => {}}>
<LabelManager
labels={labels}
onLabelCreate={() => {
setTab(2);
}}
onLabelEdit={labelId => {
setCurrentLabel(labelId);
setTab(1);
}}
onLabelToggle={labelId => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === labelId);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], active: !labels[idx].active };
}
}),
);
}}
/>
</Popup>
<Popup onClose={() => {}} title="Edit label" tab={1}>
<LabelEditor
label={labels.find(label => label.labelId === currentLabel) ?? null}
onLabelEdit={(_labelId, name, color) => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === currentLabel);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], name, color };
}
}),
);
setTab(0);
}}
/>
</Popup>
<Popup onClose={() => {}} title="Create new label" tab={2}>
<LabelEditor
label={null}
onLabelEdit={(_labelId, name, color) => {
setLabels([...labels, { labelId: name, name, color, active: false }]);
setTab(0);
}}
/>
</Popup>
</>
);
};
interface ProjectParams {
projectId: string;
}
@ -55,6 +124,34 @@ const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top:
const initialLabelsPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const initialTaskDetailsState = { isOpen: false, taskID: '' };
const ProjectActions = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
height: 40px;
padding: 0 12px;
`;
const ProjectAction = styled.div`
cursor: pointer;
display: flex;
align-items: center;
font-size: 15px;
color: #c2c6dc;
&:not(:last-child) {
margin-right: 16px;
}
&:hover {
color: ${mixin.lighten('#c2c6dc', 0.25)};
}
`;
const ProjectActionText = styled.span`
padding-left: 4px;
`;
const Project = () => {
const { projectId } = useParams<ProjectParams>();
const match = useRouteMatch();
@ -70,17 +167,16 @@ const Project = () => {
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
onCompleted: deletedTaskGroupData => {
setListsData(
BoardStateUtils.deleteTaskGroup(listsData, deletedTaskGroupData.deleteTaskGroup.taskGroup.taskGroupID),
);
setListsData(BoardStateUtils.deleteTaskGroup(listsData, deletedTaskGroupData.deleteTaskGroup.taskGroup.id));
},
});
const [createTaskGroup] = useCreateTaskGroupMutation({
onCompleted: newTaskGroupData => {
const newTaskGroup = {
...newTaskGroupData.createTaskGroup,
taskGroupID: newTaskGroupData.createTaskGroup.id,
tasks: [],
...newTaskGroupData.createTaskGroup,
};
setListsData(BoardStateUtils.addTaskGroup(listsData, newTaskGroup));
},
@ -90,10 +186,43 @@ const Project = () => {
onCompleted: newTaskData => {
const newTask = {
...newTaskData.createTask,
taskID: newTaskData.createTask.id,
taskGroup: { taskGroupID: newTaskData.createTask.taskGroup.id },
labels: [],
};
setListsData(BoardStateUtils.addTask(listsData, newTask));
},
update: (client, newTaskData) => {
const cacheData: any = client.readQuery({
query: FindProjectDocument,
variables: {
projectId: projectId,
},
});
console.log(cacheData);
console.log(newTaskData);
const newTaskGroups = produce(cacheData.findProject.taskGroups, (draftState: any) => {
const targetIndex = draftState.findIndex(
(taskGroup: any) => taskGroup.id === newTaskData.data.createTask.taskGroup.id,
);
draftState[targetIndex] = {
...draftState[targetIndex],
tasks: [...draftState[targetIndex].tasks, { ...newTaskData.data.createTask }],
};
});
console.log(newTaskGroups);
const newData = {
...cacheData.findProject,
taskGroups: newTaskGroups,
};
client.writeQuery({
query: FindProjectDocument,
variables: {
projectId: projectId,
},
data: { findProject: newData },
});
},
});
const [deleteTask] = useDeleteTaskMutation({
@ -105,50 +234,14 @@ const Project = () => {
const [updateTaskName] = useUpdateTaskNameMutation({
onCompleted: newTaskData => {
setListsData(
BoardStateUtils.updateTaskName(listsData, newTaskData.updateTaskName.taskID, newTaskData.updateTaskName.name),
BoardStateUtils.updateTaskName(listsData, newTaskData.updateTaskName.id, newTaskData.updateTaskName.name),
);
},
});
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] = {
taskGroupID: taskGroup.taskGroupID,
name: taskGroup.name,
position: taskGroup.position,
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,
labels: [],
position: task.position,
description: task.description ?? undefined,
members: taskMembers,
};
});
});
setListsData(newListsData);
},
});
console.log(`loading ${loading} - ${data}`);
const onCardCreate = (taskGroupID: string, name: string) => {
const taskGroupTasks = Object.values(listsData.tasks).filter(
@ -163,15 +256,6 @@ const Project = () => {
createTask({ variables: { taskGroupID, name, position } });
};
const onQuickEditorOpen = (e: ContextMenuEvent) => {
const currentTask = Object.values(listsData.tasks).find(task => task.taskID === e.taskID);
setQuickCardEditor({
top: e.top,
left: e.left,
isOpen: true,
task: currentTask,
});
};
const onCardDrop = (droppedTask: Task) => {
updateTaskLocation({
variables: {
@ -202,10 +286,50 @@ const Project = () => {
const [assignTask] = useAssignTaskMutation();
const { showPopup } = usePopup();
const $labelsRef = useRef<HTMLDivElement>(null);
if (loading) {
return <Title>Error Loading</Title>;
return (
<>
<GlobalTopNavbar name="Project" />
<Title>Error Loading</Title>
</>
);
}
if (data) {
const currentListsData: BoardState = { tasks: {}, columns: {} };
data.findProject.taskGroups.forEach(taskGroup => {
currentListsData.columns[taskGroup.id] = {
taskGroupID: taskGroup.id,
name: taskGroup.name,
position: taskGroup.position,
tasks: [],
};
taskGroup.tasks.forEach(task => {
const taskMembers = task.assigned.map(assigned => {
return {
userID: assigned.id,
displayName: `${assigned.firstName} ${assigned.lastName}`,
profileIcon: {
url: null,
initials: assigned.profileIcon.initials ?? '',
bgColor: assigned.profileIcon.bgColor ?? '#7367F0',
},
};
});
currentListsData.tasks[task.id] = {
taskID: task.id,
taskGroup: {
taskGroupID: taskGroup.id,
},
name: task.name,
labels: [],
position: task.position,
description: task.description ?? undefined,
members: taskMembers,
};
});
});
const availableMembers = data.findProject.members.map(member => {
return {
displayName: `${member.firstName} ${member.lastName}`,
@ -214,38 +338,75 @@ const Project = () => {
initials: member.profileIcon.initials ?? null,
bgColor: member.profileIcon.bgColor ?? null,
},
userID: member.userID,
userID: member.id,
};
});
const onQuickEditorOpen = (e: ContextMenuEvent) => {
const currentTask = Object.values(currentListsData.tasks).find(task => task.taskID === e.taskID);
console.log(`currentTask: ${currentTask?.taskID}`);
setQuickCardEditor({
top: e.top,
left: e.left,
isOpen: true,
task: currentTask,
});
};
return (
<>
<GlobalTopNavbar name={data.findProject.name} />
<ProjectActions>
<ProjectAction
ref={$labelsRef}
onClick={() => {
showPopup(
$labelsRef,
<LabelManagerEditor
labels={data.findProject.labels.map(label => {
return {
labelId: label.id,
name: label.name ?? '',
color: label.colorHex,
active: false,
};
})}
/>,
);
}}
>
<Tags size={13} color="#c2c6dc" />
<ProjectActionText>Labels</ProjectActionText>
</ProjectAction>
<ProjectAction>
<ToggleOn size={13} color="#c2c6dc" />
<ProjectActionText>Fields</ProjectActionText>
</ProjectAction>
<ProjectAction>
<Bolt size={13} color="#c2c6dc" />
<ProjectActionText>Rules</ProjectActionText>
</ProjectAction>
</ProjectActions>
<KanbanBoard
listsData={listsData}
listsData={currentListsData}
onCardDrop={onCardDrop}
onListDrop={onListDrop}
onCardCreate={onCardCreate}
onCreateList={onCreateList}
onQuickEditorOpen={onQuickEditorOpen}
onOpenListActionsPopup={(isOpen, left, top, taskGroupID) => {
setPopupData({ isOpen, top, left, taskGroupID });
onOpenListActionsPopup={($targetRef, taskGroupID) => {
showPopup(
$targetRef,
<Popup title="List actions" tab={0} onClose={() => {}}>
<ListActions
taskGroupID={taskGroupID}
onArchiveTaskGroup={tgID => {
deleteTaskGroup({ variables: { taskGroupID: tgID } });
setPopupData(initialPopupState);
}}
/>
</Popup>,
);
}}
/>
{popupData.isOpen && (
<PopupMenu
title="List Actions"
top={popupData.top}
onClose={() => setPopupData(initialPopupState)}
left={popupData.left}
>
<ListActions
taskGroupID={popupData.taskGroupID}
onArchiveTaskGroup={taskGroupID => {
deleteTaskGroup({ variables: { taskGroupID } });
setPopupData(initialPopupState);
}}
/>
</PopupMenu>
)}
{quickCardEditor.isOpen && (
<QuickCardEditor
isOpen
@ -257,7 +418,35 @@ const Project = () => {
updateTaskName({ variables: { taskID: cardId, name: cardName } });
}}
onOpenPopup={() => console.log()}
onArchiveCard={(_listId: string, cardId: string) => deleteTask({ variables: { taskID: cardId } })}
onArchiveCard={(_listId: string, cardId: string) =>
deleteTask({
variables: { taskID: cardId },
update: client => {
const cacheData: any = client.readQuery({
query: FindProjectDocument,
variables: {
projectId: projectId,
},
});
const newData = {
...cacheData.findProject,
taskGroups: cacheData.findProject.taskGroups.map((taskGroup: any) => {
return {
...taskGroup,
tasks: taskGroup.tasks.filter((t: any) => t.id !== cardId),
};
}),
};
client.writeQuery({
query: FindProjectDocument,
variables: {
projectId: projectId,
},
data: { findProject: newData },
});
},
})
}
labels={[]}
top={quickCardEditor.top}
left={quickCardEditor.left}
@ -269,7 +458,7 @@ const Project = () => {
<Details
refreshCache={() => {
console.log('beep 2!');
refetch();
// refetch();
}}
availableMembers={availableMembers}
projectURL={match.url}
@ -284,7 +473,7 @@ const Project = () => {
setTaskDetails(initialTaskDetailsState);
deleteTask({ variables: { taskID: deletedTask.taskID } });
}}
onOpenAddLabelPopup={(task, bounds) => {}}
onOpenAddLabelPopup={(task, $targetRef) => {}}
/>
)}
/>

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import styled from 'styled-components/macro';
import GlobalTopNavbar from 'App/TopNavbar';
import { useGetProjectsQuery } from 'shared/generated/graphql';
import ProjectGridItem from 'shared/components/ProjectGridItem';
@ -40,13 +41,18 @@ const Projects = () => {
if (data) {
const { projects } = data;
return (
<ProjectGrid>
{projects.map(project => (
<ProjectLink key={project.projectID} to={`/projects/${project.projectID}`}>
<ProjectGridItem project={{ ...project, teamTitle: project.team.name, taskGroups: [] }} />
</ProjectLink>
))}
</ProjectGrid>
<>
<GlobalTopNavbar name="Projects" />
<ProjectGrid>
{projects.map(project => (
<ProjectLink key={project.id} to={`/projects/${project.id}`}>
<ProjectGridItem
project={{ ...project, projectID: project.id, teamTitle: project.team.name, taskGroups: [] }}
/>
</ProjectLink>
))}
</ProjectGrid>
</>
);
}
return <div>Error!</div>;

View File

@ -52,7 +52,7 @@ type Task = {
name: string;
position: number;
labels: Label[];
description?: string;
description?: string | null;
members?: Array<TaskUser>;
};

View File

@ -16,7 +16,7 @@ export const CardComposerWrapper = styled.div<{ isOpen: boolean }>`
`;
export const ListCard = styled.div`
background-color: #fff;
background-color: ${props => mixin.lighten('#262c49', 0.05)};
border-radius: 3px;
${mixin.boxShadowCard}
cursor: pointer;
@ -55,9 +55,10 @@ export const ListCardEditor = styled(TextareaAutosize)`
padding: 0;
font-size: 14px;
line-height: 20px;
&:focus {
border: none;
outline: none;
color: #c2c6dc;
l &:focus {
background-color: ${props => mixin.lighten('#262c49', 0.05)};
}
`;

View File

@ -4,8 +4,8 @@ export const Container = styled.div<{ left: number; top: number }>`
position: absolute;
left: ${props => props.left}px;
top: ${props => props.top}px;
padding-top: 10px;
position: absolute;
padding-top: 10px;
height: auto;
width: auto;
transform: translate(-100%);
@ -18,12 +18,12 @@ export const Wrapper = styled.div`
padding-top: 8px;
border-radius: 5px;
box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.1);
position: relative;
margin: 0;
color: #c2c6dc;
background: #262c49;
border: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
`;

View File

@ -21,7 +21,7 @@ export const AddCardContainer = styled.div`
export const AddCardButton = styled.a`
border-radius: 3px;
color: #5e6c84;
color: #c2c6dc;
display: flex;
align-items: center;
cursor: pointer;
@ -32,9 +32,9 @@ export const AddCardButton = styled.a`
text-decoration: none;
user-select: none;
&:hover {
background-color: rgba(9, 30, 66, 0.08);
color: #172b4d;
color: #c2c6dc;
text-decoration: none;
background: rgb(115, 103, 240);
}
`;
export const Wrapper = styled.div`
@ -125,4 +125,5 @@ export const ListExtraMenuButtonWrapper = styled.div`
top: 4px;
z-index: 1;
padding: 6px;
padding-bottom: 0;
`;

View File

@ -26,7 +26,7 @@ type Props = {
wrapperProps?: any;
headerProps?: any;
index?: number;
onExtraMenuOpen: (taskGroupID: string, pos: ElementPosition, size: ElementSize) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
};
const List = React.forwardRef(
@ -78,20 +78,7 @@ const List = React.forwardRef(
const handleExtraMenuOpen = () => {
if ($extraActionsRef && $extraActionsRef.current) {
const pos = $extraActionsRef.current.getBoundingClientRect();
onExtraMenuOpen(
id,
{
top: pos.top,
left: pos.left,
right: pos.right,
bottom: pos.bottom,
},
{
width: pos.width,
height: pos.height,
},
);
onExtraMenuOpen(id, $extraActionsRef);
}
};
useOnEscapeKeyDown(isEditingTitle, onEscape);
@ -116,7 +103,7 @@ const List = React.forwardRef(
{children && children}
<AddCardContainer hidden={isComposerOpen}>
<AddCardButton onClick={() => onOpenComposer(id)}>
<Plus size={12} color="#42526e" />
<Plus size={12} color="#c2c6dc" />
<AddCardButtonText>Add another card</AddCardButtonText>
</AddCardButton>
</AddCardContainer>

View File

@ -14,19 +14,19 @@ export const ListActionItem = styled.span`
cursor: pointer;
display: block;
font-size: 14px;
color: #172b4d;
color: #c2c6dc;
font-weight: 400;
padding: 6px 12px;
position: relative;
margin: 0 -12px;
text-decoration: none;
&:hover {
background-color: rgba(9, 30, 66, 0.04);
background: rgb(115, 103, 240);
}
`;
export const ListSeparator = styled.hr`
background-color: rgba(9, 30, 66, 0.13);
background-color: #414561;
border: 0;
height: 1px;
margin: 8px 0;

View File

@ -29,7 +29,7 @@ type Props = {
onCardCreate: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: (e: ContextMenuEvent) => void;
onCreateList: (listName: string) => void;
onExtraMenuOpen: (taskGroupID: string, pos: ElementPosition, size: ElementSize) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
};
const Lists: React.FC<Props> = ({

View File

@ -1,5 +1,6 @@
import styled from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
import { mixin } from 'shared/utils/styles';
export const MemberManagerWrapper = styled.div``;
@ -11,17 +12,27 @@ export const MemberManagerSearchWrapper = styled.div`
export const MemberManagerSearch = styled(TextareaAutosize)`
margin: 4px 0 12px;
width: 100%;
background-color: #ebecf0;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
line-height: 20px;
padding: 8px 12px;
font-size: 14px;
color: #172b4d;
font-family: 'Droid Sans';
font-weight: 400;
background: #262c49;
outline: none;
color: #c2c6dc;
border-color: #414561;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const BoardMembersLabel = styled.h4`
color: #5e6c84;
color: #c2c6dc;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
@ -52,7 +63,7 @@ export const BoardMemberListItemContent = styled.div`
white-space: nowrap;
padding: 4px;
margin-bottom: 2px;
color: #172b4d;
color: #c2c6dc;
`;
export const ProfileIcon = styled.div`
@ -62,7 +73,7 @@ export const ProfileIcon = styled.div`
display: flex;
align-items: center;
justify-content: center;
color: #fff;
color: #c2c6dc;
font-weight: 700;
background: rgb(115, 103, 240);
cursor: pointer;

View File

@ -43,7 +43,7 @@ const MemberManager: React.FC<MemberManagerProps> = ({
)
.map(member => {
return (
<BoardMembersListItem>
<BoardMembersListItem key={member.userID}>
<BoardMemberListItemContent
onClick={() => {
const isActive = activeMembers.findIndex(m => m.userID === member.userID) !== -1;

View File

@ -11,7 +11,11 @@ export const ProfileIcon = styled.div<{ bgColor: string }>`
margin: 2px;
background-color: ${props => props.bgColor};
border-radius: 25em;
display: block;
font-size: 16px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
height: 50px;
overflow: hidden;
position: relative;
@ -20,6 +24,7 @@ export const ProfileIcon = styled.div<{ bgColor: string }>`
`;
export const ProfileInfo = styled.div`
color: #c2c6dc;
margin: 0 0 0 64px;
word-wrap: break-word;
`;
@ -29,11 +34,11 @@ export const InfoTitle = styled.h3`
font-size: 16px;
font-weight: 600;
line-height: 20px;
color: #172b4d;
color: #c2c6dc;
`;
export const InfoUsername = styled.p`
color: #5e6c84;
color: #c2c6dc;
font-size: 14px;
line-height: 20px;
`;
@ -41,10 +46,29 @@ export const InfoUsername = styled.p`
export const InfoBio = styled.p`
font-size: 14px;
line-height: 20px;
color: #5e6c84;
color: #c2c6dc;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
padding: 0;
`;
export const MiniProfileActions = styled.ul`
list-style-type: none;
`;
export const MiniProfileActionWrapper = styled.li``;
export const MiniProfileActionItem = styled.span`
color: #c2c6dc;
cursor: pointer;
display: block;
font-weight: 400;
padding: 6px 12px;
position: relative;
text-decoration: none;
&:hover {
background: rgb(115, 103, 240);
}
`;

View File

@ -1,14 +1,25 @@
import React from 'react';
import { Profile, ProfileIcon, ProfileInfo, InfoTitle, InfoUsername, InfoBio } from './Styles';
import {
Profile,
ProfileIcon,
ProfileInfo,
InfoTitle,
InfoUsername,
InfoBio,
MiniProfileActions,
MiniProfileActionWrapper,
MiniProfileActionItem,
} from './Styles';
type MiniProfileProps = {
displayName: string;
username: string;
bio: string;
profileIcon: ProfileIcon;
onRemoveFromTask: () => void;
};
const MiniProfile: React.FC<MiniProfileProps> = ({ displayName, username, bio, profileIcon }) => {
const MiniProfile: React.FC<MiniProfileProps> = ({ displayName, username, bio, profileIcon, onRemoveFromTask }) => {
return (
<>
<Profile>
@ -19,6 +30,17 @@ const MiniProfile: React.FC<MiniProfileProps> = ({ displayName, username, bio, p
<InfoBio>{bio}</InfoBio>
</ProfileInfo>
</Profile>
<MiniProfileActions>
<MiniProfileActionWrapper>
<MiniProfileActionItem
onClick={() => {
onRemoveFromTask();
}}
>
Remove from card
</MiniProfileActionItem>
</MiniProfileActionWrapper>
</MiniProfileActions>
</>
);
};

View File

@ -2,7 +2,7 @@ import styled from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const ScrollOverlay = styled.div`
z-index: 1000000;
z-index: 3000;
position: fixed;
top: 0;
left: 0;

View File

@ -4,25 +4,51 @@ import { Checkmark } from 'shared/icons';
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
type Props = {
label: Label;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
label: Label | null;
onLabelEdit: (labelId: string | null, labelName: string, color: string) => void;
};
const LabelManager = ({ label, onLabelEdit }: Props) => {
const [currentLabel, setCurrentLabel] = useState('');
console.log(label);
const [currentLabel, setCurrentLabel] = useState(label ? label.name : '');
const [currentColor, setCurrentColor] = useState<string | null>(label ? label.color : null);
return (
<EditLabelForm>
<FieldLabel>Name</FieldLabel>
<FieldName id="labelName" type="text" name="name" value={currentLabel} />
<FieldName
id="labelName"
type="text"
name="name"
onChange={e => {
setCurrentLabel(e.currentTarget.value);
}}
value={currentLabel}
/>
<FieldLabel>Select a color</FieldLabel>
<div>
{Object.values(LabelColors).map(labelColor => (
<LabelBox color={labelColor}>
<Checkmark color="#fff" size={12} />
<LabelBox
color={labelColor}
onClick={() => {
setCurrentColor(labelColor);
}}
>
{labelColor === currentColor && <Checkmark color="#fff" size={12} />}
</LabelBox>
))}
</div>
<div>
<SaveButton type="submit" value="Save" />
<SaveButton
onClick={e => {
e.preventDefault();
console.log(currentColor);
if (currentColor) {
onLabelEdit(label ? label.labelId : null, currentLabel, currentColor);
}
}}
type="submit"
value="Save"
/>
<DeleteButton type="submit" value="Delete" />
</div>
</EditLabelForm>

View File

@ -1,46 +1,78 @@
import React, { useState } from 'react';
import { Pencil, Checkmark } from 'shared/icons';
import { LabelSearch, ActiveIcon, Labels, Label, CardLabel, Section, SectionTitle, LabelIcon } from './Styles';
import {
LabelSearch,
ActiveIcon,
Labels,
Label,
CardLabel,
Section,
SectionTitle,
LabelIcon,
CreateLabelButton,
} from './Styles';
type Props = {
labels?: Label[];
onLabelToggle: (labelId: string) => void;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
onLabelEdit: (labelId: string) => void;
onLabelCreate: () => void;
};
const LabelManager: React.FC<Props> = ({ labels, onLabelToggle, onLabelEdit }) => {
const LabelManager: React.FC<Props> = ({ labels, onLabelToggle, onLabelEdit, onLabelCreate }) => {
const [currentLabel, setCurrentLabel] = useState('');
const [currentSearch, setCurrentSearch] = useState('');
return (
<>
<LabelSearch type="text" />
<LabelSearch
type="text"
placeholder="search labels..."
onChange={e => {
setCurrentSearch(e.currentTarget.value);
}}
value={currentSearch}
/>
<Section>
<SectionTitle>Labels</SectionTitle>
<Labels>
{labels &&
labels.map(label => (
<Label>
<LabelIcon>
<Pencil />
</LabelIcon>
<CardLabel
key={label.labelId}
color={label.color}
active={currentLabel === label.labelId}
onMouseEnter={() => {
setCurrentLabel(label.labelId);
}}
onClick={() => onLabelToggle(label.labelId)}
>
{label.name}
{label.active && (
<ActiveIcon>
<Checkmark color="#fff" />
</ActiveIcon>
)}
</CardLabel>
</Label>
))}
labels
.filter(label => currentSearch === '' || label.name.toLowerCase().startsWith(currentSearch.toLowerCase()))
.map(label => (
<Label key={label.labelId}>
<LabelIcon
onClick={() => {
onLabelEdit(label.labelId);
}}
>
<Pencil color="#c2c6dc" />
</LabelIcon>
<CardLabel
key={label.labelId}
color={label.color}
active={currentLabel === label.labelId}
onMouseEnter={() => {
setCurrentLabel(label.labelId);
}}
onClick={() => onLabelToggle(label.labelId)}
>
{label.name}
{label.active && (
<ActiveIcon>
<Checkmark color="#fff" />
</ActiveIcon>
)}
</CardLabel>
</Label>
))}
</Labels>
<CreateLabelButton
onClick={() => {
onLabelCreate();
}}
>
Create a new label
</CreateLabelButton>
</Section>
</>
);

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, createRef } from 'react';
import { action } from '@storybook/addon-actions';
import LabelColors from 'shared/constants/labelColors';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
@ -7,10 +7,12 @@ 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 styled from 'styled-components';
import PopupMenu from '.';
import PopupMenu, { PopupProvider, usePopup, Popup } from '.';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import produce from 'immer';
export default {
component: PopupMenu,
@ -37,19 +39,93 @@ const labelData = [
},
];
const OpenLabelBtn = styled.span``;
type TabProps = {
tab: number;
};
const LabelManagerEditor = () => {
const [labels, setLabels] = useState(labelData);
const [currentLabel, setCurrentLabel] = useState('');
const { setTab } = usePopup();
return (
<>
<Popup title="Labels" tab={0} onClose={action('on close')}>
<LabelManager
labels={labels}
onLabelCreate={() => {
setTab(2);
}}
onLabelEdit={labelId => {
setCurrentLabel(labelId);
setTab(1);
}}
onLabelToggle={labelId => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === labelId);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], active: !labels[idx].active };
}
}),
);
}}
/>
</Popup>
<Popup onClose={action('on close')} title="Edit label" tab={1}>
<LabelEditor
label={labels.find(label => label.labelId === currentLabel) ?? null}
onLabelEdit={(_labelId, name, color) => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === currentLabel);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], name, color };
}
}),
);
setTab(0);
}}
/>
</Popup>
<Popup onClose={action('on close')} title="Create new label" tab={2}>
<LabelEditor
label={null}
onLabelEdit={(_labelId, name, color) => {
setLabels([...labels, { labelId: name, name, color, active: false }]);
setTab(0);
}}
/>
</Popup>
</>
);
};
const OpenLabelsButton = () => {
const $buttonRef = createRef<HTMLButtonElement>();
const [currentLabel, setCurrentLabel] = useState('');
const [labels, setLabels] = useState(labelData);
const { showPopup, setTab } = usePopup();
console.log(labels);
return (
<OpenLabelBtn
ref={$buttonRef}
onClick={() => {
showPopup($buttonRef, <LabelManagerEditor />);
}}
>
Open
</OpenLabelBtn>
);
};
export const LabelsPopup = () => {
const [isPopupOpen, setPopupOpen] = useState(false);
return (
<>
{isPopupOpen && (
<PopupMenu title="Label" top={10} onClose={() => setPopupOpen(false)} left={10}>
<LabelManager labels={labelData} onLabelToggle={action('label toggle')} onLabelEdit={action('label edit')} />
</PopupMenu>
)}
<button type="submit" onClick={() => setPopupOpen(true)}>
Open
</button>
</>
<PopupProvider>
<OpenLabelsButton />
</PopupProvider>
);
};
@ -58,7 +134,13 @@ export const LabelsLabelEditor = () => {
return (
<>
{isPopupOpen && (
<PopupMenu title="Change Label" top={10} onClose={() => setPopupOpen(false)} left={10}>
<PopupMenu
onPrevious={action('on previous')}
title="Change Label"
top={10}
onClose={() => setPopupOpen(false)}
left={10}
>
<LabelEditor label={labelData[0]} onLabelEdit={action('label edit')} />
</PopupMenu>
)}
@ -201,12 +283,19 @@ export const MiniProfilePopup = () => {
<NormalizeStyles />
<BaseStyles />
{popupData.isOpen && (
<PopupMenu title="Due Date" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<PopupMenu
noHeader
title="Due Date"
top={popupData.top}
onClose={() => setPopupData(initalState)}
left={popupData.left}
>
<MiniProfile
displayName="Jordan Knott"
profileIcon={{ url: null, bgColor: '#000', initials: 'JK' }}
username="@jordanthedev"
bio="Stuff and things"
onRemoveFromTask={action('mini profile')}
/>
</PopupMenu>
)}
@ -236,3 +325,4 @@ export const MiniProfilePopup = () => {
</>
);
};

View File

@ -1,20 +1,34 @@
import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const Container = styled.div<{ top: number; left: number; ref: any }>`
export const Container = styled.div<{ invert: boolean; top: number; left: number; ref: any }>`
left: ${props => props.left}px;
top: ${props => props.top}px;
background: #fff;
border-radius: 3px;
box-shadow: 0 8px 16px -4px rgba(9, 30, 66, 0.25), 0 0 0 1px rgba(9, 30, 66, 0.08);
display: block;
position: absolute;
width: 304px;
z-index: 100000000000;
&:focus {
outline: none;
border: none;
}
width: 316px;
padding-top: 10px;
height: auto;
z-index: 40000;
${props =>
props.invert &&
css`
transform: translate(-100%);
`}
`;
export const Wrapper = styled.div`
padding: 5px;
padding-top: 8px;
border-radius: 5px;
box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1);
position: relative;
margin: 0;
color: #c2c6dc;
background: #262c49;
border: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
`;
export const Header = styled.div`
@ -26,10 +40,10 @@ export const Header = styled.div`
export const HeaderTitle = styled.span`
box-sizing: border-box;
color: #5e6c84;
color: #c2c6dc;
display: block;
line-height: 40px;
border-bottom: 1px solid rgba(9, 30, 66, 0.13);
border-bottom: 1px solid #414561;
margin: 0 12px;
overflow: hidden;
padding: 0 32px;
@ -46,23 +60,30 @@ export const Content = styled.div`
padding: 0 12px 12px;
`;
export const LabelSearch = styled.input`
box-sizing: border-box;
display: block;
transition-property: background-color, border-color, box-shadow;
transition-duration: 85ms;
transition-timing-function: ease;
margin: 4px 0 12px;
width: 100%;
background-color: #fafbfc;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
color: #172b4d;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
display: block;
line-height: 20px;
padding: 8px 12px;
font-size: 14px;
font-family: 'Droid Sans';
font-weight: 400;
transition-property: background-color, border-color, box-shadow;
transition-duration: 85ms;
transition-timing-function: ease;
background: #262c49;
outline: none;
color: #c2c6dc;
border-color: #414561;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const Section = styled.div`
@ -70,7 +91,7 @@ export const Section = styled.div`
`;
export const SectionTitle = styled.h4`
color: #5e6c84;
color: #c2c6dc;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
@ -95,7 +116,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
props.active &&
css`
margin-left: 4px;
box-shadow: -8px 0 ${mixin.darken(props.color, 0.15)};
box-shadow: -8px 0 ${mixin.darken(props.color, 0.12)};
border-radius: 3px;
`}
@ -113,6 +134,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-height: 31px;
`;
export const CloseButton = styled.div`
@ -126,8 +148,6 @@ export const CloseButton = styled.div`
align-items: center;
justify-content: center;
z-index: 40;
height: 20px;
width: 20px;
cursor: pointer;
`;
@ -142,14 +162,14 @@ export const LabelIcon = styled.div`
align-items: center;
justify-content: center;
height: 20px;
height: 100%;
font-size: 16px;
line-height: 20px;
width: 20px;
width: auto;
cursor: pointer;
&:hover {
background: rgba(9, 30, 66, 0.08);
background: rgb(115, 103, 240);
}
`;
@ -186,19 +206,27 @@ export const FieldLabel = styled.label`
export const FieldName = styled.input`
margin: 4px 0 12px;
width: 100%;
background-color: #fafbfc;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
color: #172b4d;
box-sizing: border-box;
border-radius: 3px;
display: block;
line-height: 20px;
margin-bottom: 12px;
padding: 8px 12px;
background: #262c49;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
font-size: 12px;
font-weight: 400;
color: #c2c6dc;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const LabelBox = styled.span<{ color: string }>`
@ -208,6 +236,7 @@ export const LabelBox = styled.span<{ color: string }>`
padding: 0;
width: 48px;
cursor: pointer;
background-color: ${props => props.color};
border-radius: 4px;
color: #fff;
@ -217,6 +246,7 @@ export const LabelBox = styled.span<{ color: string }>`
`;
export const SaveButton = styled.input`
cursor: pointer;
background-color: #5aac44;
box-shadow: none;
border: none;
@ -239,8 +269,7 @@ export const DeleteButton = styled.input`
border: none;
color: #fff;
cursor: pointer;
display: inline-block;
font-weight: 400;
type="submit"font-weight: 400;
line-height: 20px;
margin: 8px 4px 0 0;
padding: 6px 12px;
@ -248,3 +277,53 @@ export const DeleteButton = styled.input`
border-radius: 3px;
float: right;
`;
export const CreateLabelButton = styled.button`
outline: none;
border: none;
width: 100%;
border-radius: 3px;
line-height: 20px;
margin-bottom: 8px;
padding: 6px 12px;
background-color: none;
text-align: center;
color: #c2c6dc;
margin: 8px 4px 0 0;
font-size: 14px;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const PreviousButton = styled.div`
padding: 10px 12px 10px 8px;
position: absolute;
top: 0;
left: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
z-index: 40;
cursor: pointer;
`;
export const ContainerDiamond = styled.div<{ invert: boolean }>`
top: 10px;
${props => (props.invert ? 'right: 10px; ' : 'left: 15px;')}
position: absolute;
width: 10px;
height: 10px;
display: block;
transform: rotate(45deg) translate(-7px);
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
z-index: 10;
background: #262c49;
border-color: #414561;
`;

View File

@ -1,30 +1,219 @@
import React, { useRef } from 'react';
import { Cross } from 'shared/icons';
import React, { useRef, createContext, RefObject, useState, useContext } from 'react';
import { Cross, AngleLeft } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { Container, Header, HeaderTitle, Content, CloseButton } from './Styles';
import { createPortal } from 'react-dom';
import produce from 'immer';
import {
Container,
ContainerDiamond,
Header,
HeaderTitle,
Content,
CloseButton,
PreviousButton,
Wrapper,
} from './Styles';
type Props = {
title: string;
type PopupContextState = {
show: (target: RefObject<HTMLElement>, content: JSX.Element) => void;
setTab: (newTab: number) => void;
getCurrentTab: () => number;
};
type PopupProps = {
title: string | null;
onClose: () => void;
tab: number;
};
type PopupContainerProps = {
top: number;
left: number;
invert: boolean;
onClose: () => void;
};
const PopupMenu: React.FC<Props> = ({ title, top, left, onClose, children }) => {
const PopupContainer: React.FC<PopupContainerProps> = ({ top, left, onClose, children, invert }) => {
const $containerRef = useRef();
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container left={left} top={top} ref={$containerRef} invert={invert}>
{children}
</Container>
);
};
const PopupContext = createContext<PopupContextState>({
show: () => {},
setTab: () => {},
getCurrentTab: () => 0,
});
export const usePopup = () => {
const ctx = useContext<PopupContextState>(PopupContext);
return { showPopup: ctx.show, setTab: ctx.setTab, getCurrentTab: ctx.getCurrentTab };
};
type PopupState = {
isOpen: boolean;
left: number;
top: number;
invert: boolean;
currentTab: number;
previousTab: number;
content: JSX.Element | null;
};
const { Provider, Consumer } = PopupContext;
const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
const defaultState = {
isOpen: false,
left: 0,
top: 0,
invert: false,
currentTab: 0,
previousTab: 0,
content: null,
};
export const PopupProvider: React.FC = ({ children }) => {
const [currentState, setState] = useState<PopupState>(defaultState);
const show = (target: RefObject<HTMLElement>, content: JSX.Element) => {
console.log(target);
if (target && target.current) {
const bounds = target.current.getBoundingClientRect();
if (bounds.left + 304 + 30 > window.innerWidth) {
console.log('open!');
setState({
isOpen: true,
left: bounds.left + bounds.width,
top: bounds.top + bounds.height,
invert: true,
currentTab: 0,
previousTab: 0,
content,
});
} else {
console.log('open NOT INVERT!');
setState({
isOpen: true,
left: bounds.left,
top: bounds.top + bounds.height,
invert: false,
currentTab: 0,
previousTab: 0,
content,
});
}
}
};
const portalTarget = canUseDOM ? document.body : null; // appease flow
const setTab = (newTab: number) => {
setState((prevState: PopupState) => {
return {
...prevState,
previousTab: currentState.currentTab,
currentTab: newTab,
};
});
};
const getCurrentTab = () => {
return currentState.currentTab;
};
return (
<Provider value={{ show, setTab, getCurrentTab }}>
{portalTarget &&
currentState.isOpen &&
createPortal(
<PopupContainer
invert={currentState.invert}
top={currentState.top}
left={currentState.left}
onClose={() => setState(defaultState)}
>
{currentState.content}
<ContainerDiamond invert={currentState.invert} />
</PopupContainer>,
portalTarget,
)}
{children}
</Provider>
);
};
type Props = {
title: string | null;
top: number;
left: number;
onClose: () => void;
onPrevious?: () => void | null;
noHeader?: boolean | null;
};
const PopupMenu: React.FC<Props> = ({ title, top, left, onClose, noHeader, children, onPrevious }) => {
const $containerRef = useRef();
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container left={left} top={top} ref={$containerRef}>
<Header>
<HeaderTitle>{title}</HeaderTitle>
<CloseButton onClick={() => onClose()}>
<Cross />
</CloseButton>
</Header>
<Content>{children}</Content>
<Container invert={false} left={left} top={top} ref={$containerRef}>
<Wrapper>
{onPrevious && (
<PreviousButton onClick={onPrevious}>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{noHeader ? (
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
) : (
<Header>
<HeaderTitle>{title}</HeaderTitle>
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
</Header>
)}
<Content>{children}</Content>
</Wrapper>
</Container>
);
};
export const Popup: React.FC<PopupProps> = ({ title, onClose, tab, children }) => {
const { getCurrentTab, setTab } = usePopup();
if (getCurrentTab() !== tab) {
return null;
}
return (
<>
<Wrapper>
{tab > 0 && (
<PreviousButton
onClick={() => {
setTab(0);
}}
>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{title && (
<Header>
<HeaderTitle>{title}</HeaderTitle>
</Header>
)}
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
<Content>{children}</Content>
</Wrapper>
</>
);
};
export default PopupMenu;

View File

@ -61,7 +61,7 @@ export const Default = () => {
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
onExtraMenuOpen={(taskGroupID, pos, size) => console.log(taskGroupID, pos, size)}
onExtraMenuOpen={(taskGroupID, $targetRef) => console.log(taskGroupID, $targetRef)}
>
<ListCards>
<Card

View File

@ -2,7 +2,7 @@ import styled, { keyframes } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea';
export const Wrapper = styled.div<{ open: boolean }>`
background: rgba(0, 0, 0, 0.6);
background: rgba(0, 0, 0, 0.4);
bottom: 0;
color: #fff;
left: 0;

View File

@ -286,4 +286,7 @@ export const UnassignedLabel = styled.div`
color: rgb(137, 147, 164);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
height: 32px;
`;

View File

@ -47,6 +47,7 @@ export const Default = () => {
onTaskDescriptionChange={(_task, desc) => setDescription(desc)}
onDeleteTask={action('delete task')}
onCloseModal={action('close modal')}
onMemberProfile={action('profile')}
onOpenAddMemberPopup={action('open add member popup')}
onOpenAddLabelPopup={action('open add label popup')}
/>

View File

@ -93,13 +93,27 @@ 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 = {
task: Task;
onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
onOpenAddMemberPopup: (task: Task, bounds: ElementBounds) => void;
onOpenAddLabelPopup: (task: Task, bounds: ElementBounds) => void;
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
onCloseModal: () => void;
};
@ -111,6 +125,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onCloseModal,
onOpenAddMemberPopup,
onOpenAddLabelPopup,
onMemberProfile,
}) => {
const [editorOpen, setEditorOpen] = useState(false);
const [description, setDescription] = useState(task.description ?? '');
@ -130,23 +145,14 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const $unassignedRef = useRef<HTMLDivElement>(null);
const $addMemberRef = useRef<HTMLDivElement>(null);
const onUnassignedClick = () => {
const bounds = convertDivElementRefToBounds($unassignedRef);
if (bounds) {
onOpenAddMemberPopup(task, bounds);
}
onOpenAddMemberPopup(task, $unassignedRef);
};
const onAddMember = () => {
const bounds = convertDivElementRefToBounds($addMemberRef);
if (bounds) {
onOpenAddMemberPopup(task, bounds);
}
onOpenAddMemberPopup(task, $addMemberRef);
};
const $addLabelRef = useRef<HTMLDivElement>(null);
const onAddLabel = () => {
const bounds = convertDivElementRefToBounds($addLabelRef);
if (bounds) {
onOpenAddLabelPopup(task, bounds);
}
onOpenAddLabelPopup(task, $addLabelRef);
};
console.log(task);
return (
@ -204,14 +210,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
) : (
<>
{task.members &&
task.members.map(member => {
console.log(member);
return (
<TaskDetailAssignee key={member.userID}>
<ProfileIcon>{member.profileIcon.initials ?? ''}</ProfileIcon>
</TaskDetailAssignee>
);
})}
task.members.map(member => <TaskAssignee member={member} onMemberProfile={onMemberProfile} />)}
<TaskDetailsAddMember ref={$addMemberRef} onClick={onAddMember}>
<TaskDetailsAddMemberIcon>
<Plus size={16} color="#c2c6dc" />

View File

@ -1,4 +1,5 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const NavbarWrapper = styled.div`
width: 100%;
@ -76,8 +77,10 @@ export const ProfileIcon = styled.div<{ bgColor: string }>`
`;
export const ProjectMeta = styled.div`
align-items: center;
display: flex;
padding-top: 9px;
margin-left: -14px;
align-items: center;
max-width: 100%;
min-height: 51px;
`;
@ -91,11 +94,11 @@ export const ProjectTabs = styled.div`
max-width: 100%;
`;
export const ProjectTab = styled.span`
export const ProjectTab = styled.span<{ active?: boolean }>`
font-size: 80%;
color: #c2c6dc;
font-size: 15px;
cursor: default;
cursor: pointer;
display: flex;
line-height: normal;
min-width: 1px;
@ -103,16 +106,71 @@ export const ProjectTab = styled.span`
transition-property: box-shadow, color;
white-space: nowrap;
flex: 0 1 auto;
padding-bottom: 12px;
box-shadow: inset 0 -2px #d85dd8;
color: #d85dd8;
&:not(:last-child) {
margin-right: 20px;
}
${props =>
props.active
? css`
box-shadow: inset 0 -2px #d85dd8;
color: #d85dd8;
`
: css`
&:hover {
box-shadow: inset 0 -2px #cbd4db;
color: ${mixin.lighten('#c2c6dc', 0.25)};
}
`}
`;
export const ProjectName = styled.h1`
color: #c2c6dc;
margin-top: 9px;
font-weight: 600;
font-size: 20px;
padding: 6px 10px 6px 8px;
`;
export const ProjectSwitcher = styled.button`
font-size: 20px;
outline: none;
border: none;
width: 100px;
border-radius: 3px;
line-height: 20px;
padding: 6px 4px;
background-color: none;
text-align: center;
color: #c2c6dc;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const Separator = styled.div`
color: #c2c6dc;
font-size: 16px;
padding-left: 4px;
padding-right: 4px;
`;
export const ProjectSettingsButton = styled.button`
outline: none;
border: none;
border-radius: 3px;
line-height: 20px;
width: 28px;
height: 28px;
background-color: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;

View File

@ -38,6 +38,7 @@ export const Default = () => {
<NormalizeStyles />
<BaseStyles />
<TopNavbar
projectName="Projects"
bgColor="#7367F0"
firstName="Jordan"
lastName="Knott"

View File

@ -1,10 +1,12 @@
import React, { useRef } from 'react';
import { Bell } from 'shared/icons';
import { Star, Bell, Cog, AngleDown } from 'shared/icons';
import {
NotificationContainer,
GlobalActions,
ProjectActions,
ProjectSwitcher,
Separator,
ProjectMeta,
ProjectName,
ProjectTabs,
@ -13,6 +15,7 @@ import {
NavbarHeader,
Breadcrumbs,
BreadcrumpSeparator,
ProjectSettingsButton,
ProfileIcon,
ProfileContainer,
ProfileNameWrapper,
@ -21,6 +24,7 @@ import {
} from './Styles';
type NavBarProps = {
projectName: string;
onProfileClick: (bottom: number, right: number) => void;
onNotificationClick: () => void;
bgColor: string;
@ -29,6 +33,7 @@ type NavBarProps = {
initials: string;
};
const NavBar: React.FC<NavBarProps> = ({
projectName,
onProfileClick,
onNotificationClick,
firstName,
@ -47,10 +52,19 @@ const NavBar: React.FC<NavBarProps> = ({
<NavbarHeader>
<ProjectActions>
<ProjectMeta>
<ProjectName>Production Team</ProjectName>
<ProjectSwitcher>Projects</ProjectSwitcher>
<Separator>»</Separator>
<ProjectName>{projectName}</ProjectName>
<ProjectSettingsButton>
<AngleDown color="#c2c6dc" />
</ProjectSettingsButton>
<Star filled color="#c2c6dc" />
</ProjectMeta>
<ProjectTabs>
<ProjectTab>Board</ProjectTab>
<ProjectTab active>Board</ProjectTab>
<ProjectTab>Calender</ProjectTab>
<ProjectTab>Timeline</ProjectTab>
<ProjectTab>Wiki</ProjectTab>
</ProjectTabs>
</ProjectActions>
<GlobalActions>

View File

@ -17,7 +17,7 @@ export type Scalars = {
export type ProjectLabel = {
__typename?: 'ProjectLabel';
projectLabelID: Scalars['ID'];
id: Scalars['ID'];
createdDate: Scalars['Time'];
colorHex: Scalars['String'];
name?: Maybe<Scalars['String']>;
@ -25,7 +25,7 @@ export type ProjectLabel = {
export type TaskLabel = {
__typename?: 'TaskLabel';
taskLabelID: Scalars['ID'];
id: Scalars['ID'];
projectLabelID: Scalars['UUID'];
assignedDate: Scalars['Time'];
colorHex: Scalars['String'];
@ -41,7 +41,7 @@ export type ProfileIcon = {
export type ProjectMember = {
__typename?: 'ProjectMember';
userID: Scalars['ID'];
id: Scalars['ID'];
firstName: Scalars['String'];
lastName: Scalars['String'];
profileIcon: ProfileIcon;
@ -49,7 +49,7 @@ export type ProjectMember = {
export type RefreshToken = {
__typename?: 'RefreshToken';
tokenId: Scalars['ID'];
id: Scalars['ID'];
userId: Scalars['UUID'];
expiresAt: Scalars['Time'];
createdAt: Scalars['Time'];
@ -57,7 +57,7 @@ export type RefreshToken = {
export type UserAccount = {
__typename?: 'UserAccount';
userID: Scalars['ID'];
id: Scalars['ID'];
email: Scalars['String'];
createdAt: Scalars['Time'];
firstName: Scalars['String'];
@ -68,14 +68,14 @@ export type UserAccount = {
export type Team = {
__typename?: 'Team';
teamID: Scalars['ID'];
id: Scalars['ID'];
createdAt: Scalars['Time'];
name: Scalars['String'];
};
export type Project = {
__typename?: 'Project';
projectID: Scalars['ID'];
id: Scalars['ID'];
createdAt: Scalars['Time'];
name: Scalars['String'];
team: Team;
@ -87,7 +87,7 @@ export type Project = {
export type TaskGroup = {
__typename?: 'TaskGroup';
taskGroupID: Scalars['ID'];
id: Scalars['ID'];
projectID: Scalars['String'];
createdAt: Scalars['Time'];
name: Scalars['String'];
@ -97,7 +97,7 @@ export type TaskGroup = {
export type Task = {
__typename?: 'Task';
taskID: Scalars['ID'];
id: Scalars['ID'];
taskGroup: TaskGroup;
createdAt: Scalars['Time'];
name: Scalars['String'];
@ -382,14 +382,29 @@ export type AssignTaskMutation = (
{ __typename?: 'Mutation' }
& { assignTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID'>
& Pick<Task, 'id'>
& { assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
)> }
) }
);
export type CreateProjectLabelMutationVariables = {
projectID: Scalars['UUID'];
labelColorID: Scalars['UUID'];
name: Scalars['String'];
};
export type CreateProjectLabelMutation = (
{ __typename?: 'Mutation' }
& { createProjectLabel: (
{ __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'createdDate' | 'colorHex' | 'name'>
) }
);
export type CreateTaskMutationVariables = {
taskGroupID: Scalars['String'];
name: Scalars['String'];
@ -401,11 +416,18 @@ export type CreateTaskMutation = (
{ __typename?: 'Mutation' }
& { createTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'position'>
& Pick<Task, 'id' | 'name' | 'position' | 'description'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID'>
) }
& Pick<TaskGroup, 'id'>
), assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) }
)> }
) }
);
@ -420,7 +442,7 @@ export type CreateTaskGroupMutation = (
{ __typename?: 'Mutation' }
& { createTaskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID' | 'name' | 'position'>
& Pick<TaskGroup, 'id' | 'name' | 'position'>
) }
);
@ -449,10 +471,10 @@ export type DeleteTaskGroupMutation = (
& Pick<DeleteTaskGroupPayload, 'ok' | 'affectedRows'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID'>
& Pick<TaskGroup, 'id'>
& { tasks: Array<(
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name'>
& Pick<Task, 'id' | 'name'>
)> }
) }
) }
@ -470,20 +492,23 @@ export type FindProjectQuery = (
& Pick<Project, 'name'>
& { members: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) }
)>, labels: Array<(
{ __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'createdDate' | 'colorHex' | 'name'>
)>, taskGroups: Array<(
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID' | 'name' | 'position'>
& Pick<TaskGroup, 'id' | 'name' | 'position'>
& { tasks: Array<(
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'position' | 'description'>
& Pick<Task, 'id' | 'name' | 'position' | 'description'>
& { assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
@ -503,13 +528,13 @@ export type FindTaskQuery = (
{ __typename?: 'Query' }
& { findTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'description' | 'position'>
& Pick<Task, 'id' | 'name' | 'description' | 'position'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID'>
& Pick<TaskGroup, 'id'>
), assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
@ -525,10 +550,10 @@ export type GetProjectsQuery = (
{ __typename?: 'Query' }
& { projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'projectID' | 'name'>
& Pick<Project, 'id' | 'name'>
& { team: (
{ __typename?: 'Team' }
& Pick<Team, 'teamID' | 'name'>
& Pick<Team, 'id' | 'name'>
) }
)> }
);
@ -558,10 +583,10 @@ export type UnassignTaskMutation = (
{ __typename?: 'Mutation' }
& { unassignTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID'>
& Pick<Task, 'id'>
& { assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
)> }
) }
);
@ -576,7 +601,7 @@ export type UpdateTaskDescriptionMutation = (
{ __typename?: 'Mutation' }
& { updateTaskDescription: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID'>
& Pick<Task, 'id'>
) }
);
@ -590,7 +615,7 @@ export type UpdateTaskGroupLocationMutation = (
{ __typename?: 'Mutation' }
& { updateTaskGroupLocation: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID' | 'position'>
& Pick<TaskGroup, 'id' | 'position'>
) }
);
@ -605,7 +630,7 @@ export type UpdateTaskLocationMutation = (
{ __typename?: 'Mutation' }
& { updateTaskLocation: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'createdAt' | 'name' | 'position'>
& Pick<Task, 'id' | 'createdAt' | 'name' | 'position'>
) }
);
@ -619,7 +644,7 @@ export type UpdateTaskNameMutation = (
{ __typename?: 'Mutation' }
& { updateTaskName: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'position'>
& Pick<Task, 'id' | 'name' | 'position'>
) }
);
@ -627,12 +652,12 @@ export type UpdateTaskNameMutation = (
export const AssignTaskDocument = gql`
mutation assignTask($taskID: UUID!, $userID: UUID!) {
assignTask(input: {taskID: $taskID, userID: $userID}) {
id
assigned {
userID
id
firstName
lastName
}
taskID
}
}
`;
@ -662,15 +687,63 @@ export function useAssignTaskMutation(baseOptions?: ApolloReactHooks.MutationHoo
export type AssignTaskMutationHookResult = ReturnType<typeof useAssignTaskMutation>;
export type AssignTaskMutationResult = ApolloReactCommon.MutationResult<AssignTaskMutation>;
export type AssignTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<AssignTaskMutation, AssignTaskMutationVariables>;
export const CreateProjectLabelDocument = gql`
mutation createProjectLabel($projectID: UUID!, $labelColorID: UUID!, $name: String!) {
createProjectLabel(input: {projectID: $projectID, labelColorID: $labelColorID, name: $name}) {
id
createdDate
colorHex
name
}
}
`;
export type CreateProjectLabelMutationFn = ApolloReactCommon.MutationFunction<CreateProjectLabelMutation, CreateProjectLabelMutationVariables>;
/**
* __useCreateProjectLabelMutation__
*
* To run a mutation, you first call `useCreateProjectLabelMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateProjectLabelMutation` 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 [createProjectLabelMutation, { data, loading, error }] = useCreateProjectLabelMutation({
* variables: {
* projectID: // value for 'projectID'
* labelColorID: // value for 'labelColorID'
* name: // value for 'name'
* },
* });
*/
export function useCreateProjectLabelMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<CreateProjectLabelMutation, CreateProjectLabelMutationVariables>) {
return ApolloReactHooks.useMutation<CreateProjectLabelMutation, CreateProjectLabelMutationVariables>(CreateProjectLabelDocument, baseOptions);
}
export type CreateProjectLabelMutationHookResult = ReturnType<typeof useCreateProjectLabelMutation>;
export type CreateProjectLabelMutationResult = ApolloReactCommon.MutationResult<CreateProjectLabelMutation>;
export type CreateProjectLabelMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateProjectLabelMutation, CreateProjectLabelMutationVariables>;
export const CreateTaskDocument = gql`
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) {
taskID
taskGroup {
taskGroupID
}
id
name
position
description
taskGroup {
id
}
assigned {
id
firstName
lastName
profileIcon {
url
initials
bgColor
}
}
}
}
`;
@ -704,7 +777,7 @@ export type CreateTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<Cr
export const CreateTaskGroupDocument = gql`
mutation createTaskGroup($projectID: String!, $name: String!, $position: Float!) {
createTaskGroup(input: {projectID: $projectID, name: $name, position: $position}) {
taskGroupID
id
name
position
}
@ -775,9 +848,9 @@ export const DeleteTaskGroupDocument = gql`
ok
affectedRows
taskGroup {
taskGroupID
id
tasks {
taskID
id
name
}
}
@ -814,7 +887,7 @@ export const FindProjectDocument = gql`
findProject(input: {projectId: $projectId}) {
name
members {
userID
id
firstName
lastName
profileIcon {
@ -823,17 +896,23 @@ export const FindProjectDocument = gql`
bgColor
}
}
labels {
id
createdDate
colorHex
name
}
taskGroups {
taskGroupID
id
name
position
tasks {
taskID
id
name
position
description
assigned {
userID
id
firstName
lastName
profileIcon {
@ -876,15 +955,15 @@ export type FindProjectQueryResult = ApolloReactCommon.QueryResult<FindProjectQu
export const FindTaskDocument = gql`
query findTask($taskID: UUID!) {
findTask(input: {taskID: $taskID}) {
taskID
id
name
description
position
taskGroup {
taskGroupID
id
}
assigned {
userID
id
firstName
lastName
profileIcon {
@ -925,10 +1004,10 @@ export type FindTaskQueryResult = ApolloReactCommon.QueryResult<FindTaskQuery, F
export const GetProjectsDocument = gql`
query getProjects {
projects {
projectID
id
name
team {
teamID
id
name
}
}
@ -1000,11 +1079,11 @@ export const UnassignTaskDocument = gql`
mutation unassignTask($taskID: UUID!, $userID: UUID!) {
unassignTask(input: {taskID: $taskID, userID: $userID}) {
assigned {
userID
id
firstName
lastName
}
taskID
id
}
}
`;
@ -1037,7 +1116,7 @@ export type UnassignTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<
export const UpdateTaskDescriptionDocument = gql`
mutation updateTaskDescription($taskID: UUID!, $description: String!) {
updateTaskDescription(input: {taskID: $taskID, description: $description}) {
taskID
id
}
}
`;
@ -1070,7 +1149,7 @@ export type UpdateTaskDescriptionMutationOptions = ApolloReactCommon.BaseMutatio
export const UpdateTaskGroupLocationDocument = gql`
mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) {
updateTaskGroupLocation(input: {taskGroupID: $taskGroupID, position: $position}) {
taskGroupID
id
position
}
}
@ -1104,7 +1183,7 @@ export type UpdateTaskGroupLocationMutationOptions = ApolloReactCommon.BaseMutat
export const UpdateTaskLocationDocument = gql`
mutation updateTaskLocation($taskID: String!, $taskGroupID: String!, $position: Float!) {
updateTaskLocation(input: {taskID: $taskID, taskGroupID: $taskGroupID, position: $position}) {
taskID
id
createdAt
name
position
@ -1141,7 +1220,7 @@ export type UpdateTaskLocationMutationOptions = ApolloReactCommon.BaseMutationOp
export const UpdateTaskNameDocument = gql`
mutation updateTaskName($taskID: String!, $name: String!) {
updateTaskName(input: {taskID: $taskID, name: $name}) {
taskID
id
name
position
}

View File

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

View File

@ -0,0 +1,8 @@
mutation createProjectLabel($projectID: UUID!, $labelColorID: UUID!, $name: String!) {
createProjectLabel(input:{projectID:$projectID, labelColorID: $labelColorID, name: $name}) {
id
createdDate
colorHex
name
}
}

View File

@ -1,10 +1,21 @@
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) {
taskID
taskGroup {
taskGroupID
}
id
name
position
description
taskGroup {
id
}
assigned {
id
firstName
lastName
profileIcon {
url
initials
bgColor
}
}
}
}

View File

@ -2,7 +2,7 @@ mutation createTaskGroup( $projectID: String!, $name: String!, $position: Float!
createTaskGroup(
input: { projectID: $projectID, name: $name, position: $position }
) {
taskGroupID
id
name
position
}

View File

@ -3,9 +3,9 @@ mutation deleteTaskGroup($taskGroupID: UUID!) {
ok
affectedRows
taskGroup {
taskGroupID
id
tasks {
taskID
id
name
}
}

View File

@ -2,7 +2,7 @@ query findProject($projectId: String!) {
findProject(input: { projectId: $projectId }) {
name
members {
userID
id
firstName
lastName
profileIcon {
@ -11,17 +11,23 @@ query findProject($projectId: String!) {
bgColor
}
}
labels {
id
createdDate
colorHex
name
}
taskGroups {
taskGroupID
id
name
position
tasks {
taskID
id
name
position
description
assigned {
userID
id
firstName
lastName
profileIcon {

View File

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

View File

@ -1,9 +1,9 @@
query getProjects {
projects {
projectID
id
name
team {
teamID
id
name
}
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) {
updateTaskGroupLocation(input:{taskGroupID:$taskGroupID, position: $position}) {
taskGroupID
id
position
}
}

View File

@ -1,6 +1,6 @@
mutation updateTaskLocation($taskID: String!, $taskGroupID: String!, $position: Float!) {
updateTaskLocation(input: { taskID: $taskID, taskGroupID: $taskGroupID, position: $position }) {
taskID
id
createdAt
name
position

View File

@ -1,6 +1,6 @@
mutation updateTaskName($taskID: String!, $name: String!) {
updateTaskName(input: { taskID: $taskID, name: $name }) {
taskID
id
name
position
}

View File

@ -0,0 +1,27 @@
import React from 'react';
type Props = {
width: number | string;
height: number | string;
color: string;
};
const AngleDown = ({ width, height, color }: Props) => {
return (
<svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
<path
fill={color}
d="M143 352.3L7 216.3c-9.4-9.4-9.4-24.6 0-33.9l22.6-22.6c9.4-9.4 24.6-9.4 33.9 0l96.4 96.4 96.4-96.4c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9l-136 136c-9.2 9.4-24.4 9.4-33.8 0z"
/>
</svg>
);
};
AngleDown.defaultProps = {
width: 24,
height: 16,
color: '#000',
};
export default AngleDown;

View File

@ -0,0 +1,24 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const AngleLeft = ({ size, color }: Props) => {
return (
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512">
<path
fill={color}
d="M31.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L127.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L201.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34z"
/>
</svg>
);
};
AngleLeft.defaultProps = {
size: 16,
color: '#000',
};
export default AngleLeft;

View File

@ -7,8 +7,8 @@ type Props = {
const Bell = ({ size, color }: Props) => {
return (
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
<path d="M16.023 12.5c0-4.5-4-3.5-4-7 0-0.29-0.028-0.538-0.079-0.749-0.263-1.766-1.44-3.183-2.965-3.615 0.014-0.062 0.021-0.125 0.021-0.191 0-0.52-0.45-0.945-1-0.945s-1 0.425-1 0.945c0 0.065 0.007 0.129 0.021 0.191-1.71 0.484-2.983 2.208-3.020 4.273-0.001 0.030-0.001 0.060-0.001 0.091 0 3.5-4 2.5-4 7 0 1.191 2.665 2.187 6.234 2.439 0.336 0.631 1.001 1.061 1.766 1.061s1.43-0.43 1.766-1.061c3.568-0.251 6.234-1.248 6.234-2.439 0-0.004-0-0.007-0-0.011l0.024 0.011zM12.91 13.345c-0.847 0.226-1.846 0.389-2.918 0.479-0.089-1.022-0.947-1.824-1.992-1.824s-1.903 0.802-1.992 1.824c-1.072-0.090-2.071-0.253-2.918-0.479-1.166-0.311-1.724-0.659-1.928-0.845 0.204-0.186 0.762-0.534 1.928-0.845 1.356-0.362 3.1-0.561 4.91-0.561s3.554 0.199 4.91 0.561c1.166 0.311 1.724 0.659 1.928 0.845-0.204 0.186-0.762 0.534-1.928 0.845z" />
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 448 512">
<path d="M439.39 362.29c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71zM67.53 368c21.22-27.97 44.42-74.33 44.53-159.42 0-.2-.06-.38-.06-.58 0-61.86 50.14-112 112-112s112 50.14 112 112c0 .2-.06.38-.06.58.11 85.1 23.31 131.46 44.53 159.42H67.53zM224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64z" />
</svg>
);
};

View File

@ -0,0 +1,24 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const Bolt = ({ size, color }: Props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" width={size} height={size}>
<path
fill={color}
d="M296 160H180.6l42.6-129.8C227.2 15 215.7 0 200 0H56C44 0 33.8 8.9 32.2 20.8l-32 240C-1.7 275.2 9.5 288 24 288h118.7L96.6 482.5c-3.6 15.2 8 29.5 23.3 29.5 8.4 0 16.4-4.4 20.8-12l176-304c9.3-15.9-2.2-36-20.7-36z"
/>
</svg>
);
};
Bolt.defaultProps = {
size: 16,
color: '#000',
};
export default Bolt;

View File

@ -0,0 +1,24 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const Cog = ({ size, color }: Props) => {
return (
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path
fill={color}
d="M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z"
/>
</svg>
);
};
Cog.defaultProps = {
size: 16,
color: '#000',
};
export default Cog;

View File

@ -7,8 +7,8 @@ type Props = {
const Cross = ({ size, color }: Props) => {
return (
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
<path d="M15.854 12.854c-0-0-0-0-0-0l-4.854-4.854 4.854-4.854c0-0 0-0 0-0 0.052-0.052 0.090-0.113 0.114-0.178 0.066-0.178 0.028-0.386-0.114-0.529l-2.293-2.293c-0.143-0.143-0.351-0.181-0.529-0.114-0.065 0.024-0.126 0.062-0.178 0.114 0 0-0 0-0 0l-4.854 4.854-4.854-4.854c-0-0-0-0-0-0-0.052-0.052-0.113-0.090-0.178-0.114-0.178-0.066-0.386-0.029-0.529 0.114l-2.293 2.293c-0.143 0.143-0.181 0.351-0.114 0.529 0.024 0.065 0.062 0.126 0.114 0.178 0 0 0 0 0 0l4.854 4.854-4.854 4.854c-0 0-0 0-0 0-0.052 0.052-0.090 0.113-0.114 0.178-0.066 0.178-0.029 0.386 0.114 0.529l2.293 2.293c0.143 0.143 0.351 0.181 0.529 0.114 0.065-0.024 0.126-0.062 0.178-0.114 0-0 0-0 0-0l4.854-4.854 4.854 4.854c0 0 0 0 0 0 0.052 0.052 0.113 0.090 0.178 0.114 0.178 0.066 0.386 0.029 0.529-0.114l2.293-2.293c0.143-0.143 0.181-0.351 0.114-0.529-0.024-0.065-0.062-0.126-0.114-0.178z" />
<svg fill={color} width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512">
<path d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z" />
</svg>
);
};

View File

@ -0,0 +1,36 @@
import React from 'react';
type Props = {
width: number | string;
height: number | string;
color: string;
filled: boolean;
};
const Star = ({ width, height, color, filled }: Props) => {
return (
<svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
{filled ? (
<path
fill={color}
d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"
/>
) : (
<path
fill={color}
d="M528.1 171.5L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6zM388.6 312.3l23.7 138.4L288 385.4l-124.3 65.3 23.7-138.4-100.6-98 139-20.2 62.2-126 62.2 126 139 20.2-100.6 98z"
/>
)}
</svg>
);
};
Star.defaultProps = {
width: 24,
height: 16,
color: '#000',
filled: false,
};
export default Star;

View File

@ -0,0 +1,24 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const Tags = ({ size, color }: Props) => {
return (
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
<path
fill={color}
d="M497.941 225.941L286.059 14.059A48 48 0 0 0 252.118 0H48C21.49 0 0 21.49 0 48v204.118a48 48 0 0 0 14.059 33.941l211.882 211.882c18.744 18.745 49.136 18.746 67.882 0l204.118-204.118c18.745-18.745 18.745-49.137 0-67.882zM112 160c-26.51 0-48-21.49-48-48s21.49-48 48-48 48 21.49 48 48-21.49 48-48 48zm513.941 133.823L421.823 497.941c-18.745 18.745-49.137 18.745-67.882 0l-.36-.36L527.64 323.522c16.999-16.999 26.36-39.6 26.36-63.64s-9.362-46.641-26.36-63.64L331.397 0h48.721a48 48 0 0 1 33.941 14.059l211.882 211.882c18.745 18.745 18.745 49.137 0 67.882z"
/>
</svg>
);
};
Tags.defaultProps = {
size: 16,
color: '#000',
};
export default Tags;

View File

@ -0,0 +1,24 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const ToggleOn = ({ size, color }: Props) => {
return (
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path
fill={color}
d="M384 64H192C86 64 0 150 0 256s86 192 192 192h192c106 0 192-86 192-192S490 64 384 64zm0 320c-70.8 0-128-57.3-128-128 0-70.8 57.3-128 128-128 70.8 0 128 57.3 128 128 0 70.8-57.3 128-128 128z"
/>
</svg>
);
};
ToggleOn.defaultProps = {
size: 16,
color: '#000',
};
export default ToggleOn;

View File

@ -1,6 +1,10 @@
import Cross from './Cross';
import Cog from './Cog';
import Bolt from './Bolt';
import Plus from './Plus';
import Bell from './Bell';
import AngleLeft from './AngleLeft';
import AngleDown from './AngleDown';
import Bin from './Bin';
import Pencil from './Pencil';
import Checkmark from './Checkmark';
@ -13,5 +17,31 @@ import Stack from './Stack';
import Question from './Question';
import Exit from './Exit';
import Ellipsis from './Ellipsis';
import ToggleOn from './ToggleOn';
import Tags from './Tags';
import Star from './Star';
export { Cross, Plus, Bell, Ellipsis, Bin, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock };
export {
Star,
AngleDown,
Cross,
Cog,
Bolt,
Plus,
Bell,
AngleLeft,
Tags,
Ellipsis,
Bin,
Exit,
Pencil,
Stack,
Question,
Home,
Citadel,
Checkmark,
User,
Users,
Lock,
ToggleOn,
};

View File

@ -2,7 +2,7 @@ import produce from 'immer';
export const addTask = (currentState: BoardState, newTask: Task) => {
return produce(currentState, (draftState: BoardState) => {
currentState.tasks[newTask.taskID] = newTask;
draftState.tasks[newTask.taskID] = newTask;
});
};