diff --git a/api/Makefile b/api/Makefile
new file mode 100644
index 0000000..ad81808
--- /dev/null
+++ b/api/Makefile
@@ -0,0 +1,3 @@
+start:
+ docker container start test-db
+ go run cmd/citadel/main.go
diff --git a/api/graph/generated.go b/api/graph/generated.go
index edbfc13..a4fdc23 100644
--- a/api/graph/generated.go
+++ b/api/graph/generated.go
@@ -74,6 +74,7 @@ type ComplexityRoot struct {
Mutation struct {
AddTaskLabel func(childComplexity int, input *AddTaskLabelInput) int
AssignTask func(childComplexity int, input *AssignTaskInput) int
+ ClearProfileAvatar func(childComplexity int) int
CreateProject func(childComplexity int, input NewProject) int
CreateProjectLabel func(childComplexity int, input NewProjectLabel) int
CreateRefreshToken func(childComplexity int, input NewRefreshToken) int
@@ -94,6 +95,7 @@ type ComplexityRoot struct {
UpdateProjectName func(childComplexity int, input *UpdateProjectName) int
UpdateTaskDescription func(childComplexity int, input UpdateTaskDescriptionInput) int
UpdateTaskGroupLocation func(childComplexity int, input NewTaskGroupLocation) int
+ UpdateTaskGroupName func(childComplexity int, input UpdateTaskGroupName) int
UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int
UpdateTaskName func(childComplexity int, input UpdateTaskName) int
}
@@ -123,9 +125,8 @@ type ComplexityRoot struct {
}
ProjectMember struct {
- FirstName func(childComplexity int) int
+ FullName func(childComplexity int) int
ID func(childComplexity int) int
- LastName func(childComplexity int) int
ProfileIcon func(childComplexity int) int
}
@@ -193,9 +194,9 @@ type ComplexityRoot struct {
UserAccount struct {
CreatedAt func(childComplexity int) int
Email func(childComplexity int) int
- FirstName func(childComplexity int) int
+ FullName func(childComplexity int) int
ID func(childComplexity int) int
- LastName func(childComplexity int) int
+ Initials func(childComplexity int) int
ProfileIcon func(childComplexity int) int
Username func(childComplexity int) int
}
@@ -208,6 +209,7 @@ type MutationResolver interface {
CreateRefreshToken(ctx context.Context, input NewRefreshToken) (*pg.RefreshToken, error)
CreateUserAccount(ctx context.Context, input NewUserAccount) (*pg.UserAccount, error)
CreateTeam(ctx context.Context, input NewTeam) (*pg.Team, error)
+ ClearProfileAvatar(ctx context.Context) (*pg.UserAccount, error)
CreateProject(ctx context.Context, input NewProject) (*pg.Project, error)
UpdateProjectName(ctx context.Context, input *UpdateProjectName) (*pg.Project, error)
CreateProjectLabel(ctx context.Context, input NewProjectLabel) (*pg.ProjectLabel, error)
@@ -217,6 +219,7 @@ type MutationResolver interface {
UpdateProjectLabelColor(ctx context.Context, input UpdateProjectLabelColor) (*pg.ProjectLabel, error)
CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*pg.TaskGroup, error)
UpdateTaskGroupLocation(ctx context.Context, input NewTaskGroupLocation) (*pg.TaskGroup, error)
+ UpdateTaskGroupName(ctx context.Context, input UpdateTaskGroupName) (*pg.TaskGroup, error)
DeleteTaskGroup(ctx context.Context, input DeleteTaskGroupInput) (*DeleteTaskGroupPayload, error)
AddTaskLabel(ctx context.Context, input *AddTaskLabelInput) (*pg.Task, error)
RemoveTaskLabel(ctx context.Context, input *RemoveTaskLabelInput) (*pg.Task, error)
@@ -381,6 +384,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.AssignTask(childComplexity, args["input"].(*AssignTaskInput)), true
+ case "Mutation.clearProfileAvatar":
+ if e.complexity.Mutation.ClearProfileAvatar == nil {
+ break
+ }
+
+ return e.complexity.Mutation.ClearProfileAvatar(childComplexity), true
+
case "Mutation.createProject":
if e.complexity.Mutation.CreateProject == nil {
break
@@ -621,6 +631,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.UpdateTaskGroupLocation(childComplexity, args["input"].(NewTaskGroupLocation)), true
+ case "Mutation.updateTaskGroupName":
+ if e.complexity.Mutation.UpdateTaskGroupName == nil {
+ break
+ }
+
+ args, err := ec.field_Mutation_updateTaskGroupName_args(context.TODO(), rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Mutation.UpdateTaskGroupName(childComplexity, args["input"].(UpdateTaskGroupName)), true
+
case "Mutation.updateTaskLocation":
if e.complexity.Mutation.UpdateTaskLocation == nil {
break
@@ -750,12 +772,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ProjectLabel.Name(childComplexity), true
- case "ProjectMember.firstName":
- if e.complexity.ProjectMember.FirstName == nil {
+ case "ProjectMember.fullName":
+ if e.complexity.ProjectMember.FullName == nil {
break
}
- return e.complexity.ProjectMember.FirstName(childComplexity), true
+ return e.complexity.ProjectMember.FullName(childComplexity), true
case "ProjectMember.id":
if e.complexity.ProjectMember.ID == nil {
@@ -764,13 +786,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ProjectMember.ID(childComplexity), true
- case "ProjectMember.lastName":
- if e.complexity.ProjectMember.LastName == nil {
- break
- }
-
- return e.complexity.ProjectMember.LastName(childComplexity), true
-
case "ProjectMember.profileIcon":
if e.complexity.ProjectMember.ProfileIcon == nil {
break
@@ -1071,12 +1086,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.UserAccount.Email(childComplexity), true
- case "UserAccount.firstName":
- if e.complexity.UserAccount.FirstName == nil {
+ case "UserAccount.fullName":
+ if e.complexity.UserAccount.FullName == nil {
break
}
- return e.complexity.UserAccount.FirstName(childComplexity), true
+ return e.complexity.UserAccount.FullName(childComplexity), true
case "UserAccount.id":
if e.complexity.UserAccount.ID == nil {
@@ -1085,12 +1100,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.UserAccount.ID(childComplexity), true
- case "UserAccount.lastName":
- if e.complexity.UserAccount.LastName == nil {
+ case "UserAccount.initials":
+ if e.complexity.UserAccount.Initials == nil {
break
}
- return e.complexity.UserAccount.LastName(childComplexity), true
+ return e.complexity.UserAccount.Initials(childComplexity), true
case "UserAccount.profileIcon":
if e.complexity.UserAccount.ProfileIcon == nil {
@@ -1172,6 +1187,7 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
var sources = []*ast.Source{
&ast.Source{Name: "graph/schema.graphqls", Input: `scalar Time
scalar UUID
+scalar Upload
type ProjectLabel {
id: ID!
@@ -1201,8 +1217,7 @@ type ProfileIcon {
type ProjectMember {
id: ID!
- firstName: String!
- lastName: String!
+ fullName: String!
profileIcon: ProfileIcon!
}
@@ -1217,8 +1232,8 @@ type UserAccount {
id: ID!
email: String!
createdAt: Time!
- firstName: String!
- lastName: String!
+ fullName: String!
+ initials: String!
username: String!
profileIcon: ProfileIcon!
}
@@ -1295,8 +1310,8 @@ input NewRefreshToken {
input NewUserAccount {
username: String!
email: String!
- firstName: String!
- lastName: String!
+ fullName: String!
+ initials: String!
password: String!
}
@@ -1427,12 +1442,19 @@ type UpdateTaskLocationPayload {
previousTaskGroupID: UUID!
task: Task!
}
+
+input UpdateTaskGroupName {
+ taskGroupID: UUID!
+ name: String!
+}
+
type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken!
createUserAccount(input: NewUserAccount!): UserAccount!
createTeam(input: NewTeam!): Team!
+ clearProfileAvatar: UserAccount!
createProject(input: NewProject!): Project!
updateProjectName(input: UpdateProjectName): Project!
@@ -1445,6 +1467,7 @@ type Mutation {
createTaskGroup(input: NewTaskGroup!): TaskGroup!
updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup!
+ updateTaskGroupName(input: UpdateTaskGroupName!): TaskGroup!
deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload!
addTaskLabel(input: AddTaskLabelInput): Task!
@@ -1777,6 +1800,20 @@ func (ec *executionContext) field_Mutation_updateTaskGroupLocation_args(ctx cont
return args, nil
}
+func (ec *executionContext) field_Mutation_updateTaskGroupName_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+ var err error
+ args := map[string]interface{}{}
+ var arg0 UpdateTaskGroupName
+ if tmp, ok := rawArgs["input"]; ok {
+ arg0, err = ec.unmarshalNUpdateTaskGroupName2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUpdateTaskGroupName(ctx, tmp)
+ if err != nil {
+ return nil, err
+ }
+ }
+ args["input"] = arg0
+ return args, nil
+}
+
func (ec *executionContext) field_Mutation_updateTaskLocation_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@@ -2306,6 +2343,40 @@ func (ec *executionContext) _Mutation_createTeam(ctx context.Context, field grap
return ec.marshalNTeam2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTeam(ctx, field.Selections, res)
}
+func (ec *executionContext) _Mutation_clearProfileAvatar(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ fc := &graphql.FieldContext{
+ Object: "Mutation",
+ Field: field,
+ Args: nil,
+ IsMethod: true,
+ }
+
+ ctx = graphql.WithFieldContext(ctx, fc)
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Mutation().ClearProfileAvatar(rctx)
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*pg.UserAccount)
+ fc.Result = res
+ return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐUserAccount(ctx, field.Selections, res)
+}
+
func (ec *executionContext) _Mutation_createProject(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -2675,6 +2746,47 @@ func (ec *executionContext) _Mutation_updateTaskGroupLocation(ctx context.Contex
return ec.marshalNTaskGroup2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTaskGroup(ctx, field.Selections, res)
}
+func (ec *executionContext) _Mutation_updateTaskGroupName(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ fc := &graphql.FieldContext{
+ Object: "Mutation",
+ Field: field,
+ Args: nil,
+ IsMethod: true,
+ }
+
+ ctx = graphql.WithFieldContext(ctx, fc)
+ rawArgs := field.ArgumentMap(ec.Variables)
+ args, err := ec.field_Mutation_updateTaskGroupName_args(ctx, rawArgs)
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ fc.Args = args
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Mutation().UpdateTaskGroupName(rctx, args["input"].(UpdateTaskGroupName))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*pg.TaskGroup)
+ fc.Result = res
+ return ec.marshalNTaskGroup2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTaskGroup(ctx, field.Selections, res)
+}
+
func (ec *executionContext) _Mutation_deleteTaskGroup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -3699,7 +3811,7 @@ func (ec *executionContext) _ProjectMember_id(ctx context.Context, field graphql
return ec.marshalNID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res)
}
-func (ec *executionContext) _ProjectMember_firstName(ctx context.Context, field graphql.CollectedField, obj *ProjectMember) (ret graphql.Marshaler) {
+func (ec *executionContext) _ProjectMember_fullName(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))
@@ -3716,41 +3828,7 @@ func (ec *executionContext) _ProjectMember_firstName(ctx context.Context, field
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.FirstName, nil
- })
- if err != nil {
- ec.Error(ctx, err)
- return graphql.Null
- }
- if resTmp == nil {
- if !graphql.HasFieldError(ctx, fc) {
- ec.Errorf(ctx, "must not be null")
- }
- return graphql.Null
- }
- res := resTmp.(string)
- fc.Result = res
- return ec.marshalNString2string(ctx, field.Selections, res)
-}
-
-func (ec *executionContext) _ProjectMember_lastName(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))
- ret = graphql.Null
- }
- }()
- fc := &graphql.FieldContext{
- Object: "ProjectMember",
- Field: field,
- Args: nil,
- IsMethod: false,
- }
-
- ctx = graphql.WithFieldContext(ctx, fc)
- resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
- ctx = rctx // use context from middleware stack in children
- return obj.LastName, nil
+ return obj.FullName, nil
})
if err != nil {
ec.Error(ctx, err)
@@ -5255,7 +5333,7 @@ func (ec *executionContext) _UserAccount_createdAt(ctx context.Context, field gr
return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res)
}
-func (ec *executionContext) _UserAccount_firstName(ctx context.Context, field graphql.CollectedField, obj *pg.UserAccount) (ret graphql.Marshaler) {
+func (ec *executionContext) _UserAccount_fullName(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))
@@ -5272,7 +5350,7 @@ func (ec *executionContext) _UserAccount_firstName(ctx context.Context, field gr
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.FirstName, nil
+ return obj.FullName, nil
})
if err != nil {
ec.Error(ctx, err)
@@ -5289,7 +5367,7 @@ func (ec *executionContext) _UserAccount_firstName(ctx context.Context, field gr
return ec.marshalNString2string(ctx, field.Selections, res)
}
-func (ec *executionContext) _UserAccount_lastName(ctx context.Context, field graphql.CollectedField, obj *pg.UserAccount) (ret graphql.Marshaler) {
+func (ec *executionContext) _UserAccount_initials(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))
@@ -5306,7 +5384,7 @@ func (ec *executionContext) _UserAccount_lastName(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.LastName, nil
+ return obj.Initials, nil
})
if err != nil {
ec.Error(ctx, err)
@@ -6854,15 +6932,15 @@ func (ec *executionContext) unmarshalInputNewUserAccount(ctx context.Context, ob
if err != nil {
return it, err
}
- case "firstName":
+ case "fullName":
var err error
- it.FirstName, err = ec.unmarshalNString2string(ctx, v)
+ it.FullName, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
- case "lastName":
+ case "initials":
var err error
- it.LastName, err = ec.unmarshalNString2string(ctx, v)
+ it.Initials, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
@@ -7088,6 +7166,30 @@ func (ec *executionContext) unmarshalInputUpdateTaskDescriptionInput(ctx context
return it, nil
}
+func (ec *executionContext) unmarshalInputUpdateTaskGroupName(ctx context.Context, obj interface{}) (UpdateTaskGroupName, error) {
+ var it UpdateTaskGroupName
+ var asMap = obj.(map[string]interface{})
+
+ for k, v := range asMap {
+ switch k {
+ case "taskGroupID":
+ var err error
+ it.TaskGroupID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ case "name":
+ var err error
+ it.Name, err = ec.unmarshalNString2string(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ }
+ }
+
+ return it, nil
+}
+
func (ec *executionContext) unmarshalInputUpdateTaskName(ctx context.Context, obj interface{}) (UpdateTaskName, error) {
var it UpdateTaskName
var asMap = obj.(map[string]interface{})
@@ -7265,6 +7367,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
invalids++
}
+ case "clearProfileAvatar":
+ out.Values[i] = ec._Mutation_clearProfileAvatar(ctx, field)
+ if out.Values[i] == graphql.Null {
+ invalids++
+ }
case "createProject":
out.Values[i] = ec._Mutation_createProject(ctx, field)
if out.Values[i] == graphql.Null {
@@ -7310,6 +7417,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
invalids++
}
+ case "updateTaskGroupName":
+ out.Values[i] = ec._Mutation_updateTaskGroupName(ctx, field)
+ if out.Values[i] == graphql.Null {
+ invalids++
+ }
case "deleteTaskGroup":
out.Values[i] = ec._Mutation_deleteTaskGroup(ctx, field)
if out.Values[i] == graphql.Null {
@@ -7607,13 +7719,8 @@ func (ec *executionContext) _ProjectMember(ctx context.Context, sel ast.Selectio
if out.Values[i] == graphql.Null {
invalids++
}
- case "firstName":
- out.Values[i] = ec._ProjectMember_firstName(ctx, field, obj)
- if out.Values[i] == graphql.Null {
- invalids++
- }
- case "lastName":
- out.Values[i] = ec._ProjectMember_lastName(ctx, field, obj)
+ case "fullName":
+ out.Values[i] = ec._ProjectMember_fullName(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
@@ -8223,13 +8330,13 @@ func (ec *executionContext) _UserAccount(ctx context.Context, sel ast.SelectionS
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
- case "firstName":
- out.Values[i] = ec._UserAccount_firstName(ctx, field, obj)
+ case "fullName":
+ out.Values[i] = ec._UserAccount_fullName(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
- case "lastName":
- out.Values[i] = ec._UserAccount_lastName(ctx, field, obj)
+ case "initials":
+ out.Values[i] = ec._UserAccount_initials(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
@@ -9158,6 +9265,10 @@ func (ec *executionContext) unmarshalNUpdateTaskDescriptionInput2githubᚗcomᚋ
return ec.unmarshalInputUpdateTaskDescriptionInput(ctx, v)
}
+func (ec *executionContext) unmarshalNUpdateTaskGroupName2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUpdateTaskGroupName(ctx context.Context, v interface{}) (UpdateTaskGroupName, error) {
+ return ec.unmarshalInputUpdateTaskGroupName(ctx, v)
+}
+
func (ec *executionContext) marshalNUpdateTaskLocationPayload2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUpdateTaskLocationPayload(ctx context.Context, sel ast.SelectionSet, v UpdateTaskLocationPayload) graphql.Marshaler {
return ec._UpdateTaskLocationPayload(ctx, sel, &v)
}
diff --git a/api/graph/models_gen.go b/api/graph/models_gen.go
index ae5b771..03f6437 100644
--- a/api/graph/models_gen.go
+++ b/api/graph/models_gen.go
@@ -100,11 +100,11 @@ type NewTeam struct {
}
type NewUserAccount struct {
- Username string `json:"username"`
- Email string `json:"email"`
- FirstName string `json:"firstName"`
- LastName string `json:"lastName"`
- Password string `json:"password"`
+ Username string `json:"username"`
+ Email string `json:"email"`
+ FullName string `json:"fullName"`
+ Initials string `json:"initials"`
+ Password string `json:"password"`
}
type ProfileIcon struct {
@@ -115,8 +115,7 @@ type ProfileIcon struct {
type ProjectMember struct {
ID uuid.UUID `json:"id"`
- FirstName string `json:"firstName"`
- LastName string `json:"lastName"`
+ FullName string `json:"fullName"`
ProfileIcon *ProfileIcon `json:"profileIcon"`
}
@@ -169,6 +168,11 @@ type UpdateTaskDescriptionInput struct {
Description string `json:"description"`
}
+type UpdateTaskGroupName struct {
+ TaskGroupID uuid.UUID `json:"taskGroupID"`
+ Name string `json:"name"`
+}
+
type UpdateTaskLocationPayload struct {
PreviousTaskGroupID uuid.UUID `json:"previousTaskGroupID"`
Task *pg.Task `json:"task"`
diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls
index 3878225..d626453 100644
--- a/api/graph/schema.graphqls
+++ b/api/graph/schema.graphqls
@@ -1,5 +1,6 @@
scalar Time
scalar UUID
+scalar Upload
type ProjectLabel {
id: ID!
@@ -29,8 +30,7 @@ type ProfileIcon {
type ProjectMember {
id: ID!
- firstName: String!
- lastName: String!
+ fullName: String!
profileIcon: ProfileIcon!
}
@@ -45,8 +45,8 @@ type UserAccount {
id: ID!
email: String!
createdAt: Time!
- firstName: String!
- lastName: String!
+ fullName: String!
+ initials: String!
username: String!
profileIcon: ProfileIcon!
}
@@ -123,8 +123,8 @@ input NewRefreshToken {
input NewUserAccount {
username: String!
email: String!
- firstName: String!
- lastName: String!
+ fullName: String!
+ initials: String!
password: String!
}
@@ -255,12 +255,19 @@ type UpdateTaskLocationPayload {
previousTaskGroupID: UUID!
task: Task!
}
+
+input UpdateTaskGroupName {
+ taskGroupID: UUID!
+ name: String!
+}
+
type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken!
createUserAccount(input: NewUserAccount!): UserAccount!
createTeam(input: NewTeam!): Team!
+ clearProfileAvatar: UserAccount!
createProject(input: NewProject!): Project!
updateProjectName(input: UpdateProjectName): Project!
@@ -273,6 +280,7 @@ type Mutation {
createTaskGroup(input: NewTaskGroup!): TaskGroup!
updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup!
+ updateTaskGroupName(input: UpdateTaskGroupName!): TaskGroup!
deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload!
addTaskLabel(input: AddTaskLabelInput): Task!
diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go
index e32b8d2..09fb727 100644
--- a/api/graph/schema.resolvers.go
+++ b/api/graph/schema.resolvers.go
@@ -29,7 +29,14 @@ func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefr
func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*pg.UserAccount, error) {
createdAt := time.Now().UTC()
- userAccount, err := r.Repository.CreateUserAccount(ctx, pg.CreateUserAccountParams{input.FirstName, input.LastName, input.Email, input.Username, createdAt, input.Password})
+ userAccount, err := r.Repository.CreateUserAccount(ctx, pg.CreateUserAccountParams{
+ FullName: input.FullName,
+ Initials: input.Initials,
+ Email: input.Email,
+ Username: input.Username,
+ CreatedAt: createdAt,
+ PasswordHash: input.Password,
+ })
return &userAccount, err
}
@@ -43,6 +50,21 @@ func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*pg.T
return &team, err
}
+func (r *mutationResolver) ClearProfileAvatar(ctx context.Context) (*pg.UserAccount, error) {
+ userID, ok := GetUserID(ctx)
+ if !ok {
+ return &pg.UserAccount{}, fmt.Errorf("internal server error")
+ }
+ log.WithFields(log.Fields{
+ "userID": userID,
+ }).Info("getting user account")
+ user, err := r.Repository.UpdateUserAccountProfileAvatarURL(ctx, pg.UpdateUserAccountProfileAvatarURLParams{UserID: userID, ProfileAvatarUrl: sql.NullString{Valid: false, String: ""}})
+ if err != nil {
+ return &pg.UserAccount{}, err
+ }
+ return &user, nil
+}
+
func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) (*pg.Project, error) {
createdAt := time.Now().UTC()
project, err := r.Repository.CreateProject(ctx, pg.CreateProjectParams{input.UserID, input.TeamID, createdAt, input.Name})
@@ -120,6 +142,10 @@ func (r *mutationResolver) UpdateTaskGroupLocation(ctx context.Context, input Ne
return &taskGroup, err
}
+func (r *mutationResolver) UpdateTaskGroupName(ctx context.Context, input UpdateTaskGroupName) (*pg.TaskGroup, error) {
+ panic(fmt.Errorf("not implemented"))
+}
+
func (r *mutationResolver) DeleteTaskGroup(ctx context.Context, input DeleteTaskGroupInput) (*DeleteTaskGroupPayload, error) {
deletedTasks, err := r.Repository.DeleteTasksByTaskGroupID(ctx, input.TaskGroupID)
if err != nil {
@@ -301,9 +327,12 @@ func (r *projectResolver) Owner(ctx context.Context, obj *pg.Project) (*ProjectM
if err != nil {
return &ProjectMember{}, err
}
- initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
- profileIcon := &ProfileIcon{nil, &initials, &user.ProfileBgColor}
- return &ProjectMember{obj.Owner, user.FirstName, user.LastName, profileIcon}, nil
+ var url *string
+ if user.ProfileAvatarUrl.Valid {
+ url = &user.ProfileAvatarUrl.String
+ }
+ profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
+ return &ProjectMember{obj.Owner, user.FullName, profileIcon}, nil
}
func (r *projectResolver) TaskGroups(ctx context.Context, obj *pg.Project) ([]pg.TaskGroup, error) {
@@ -316,9 +345,12 @@ func (r *projectResolver) Members(ctx context.Context, obj *pg.Project) ([]Proje
if err != nil {
return members, err
}
- initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
- profileIcon := &ProfileIcon{nil, &initials, &user.ProfileBgColor}
- members = append(members, ProjectMember{obj.Owner, user.FirstName, user.LastName, profileIcon})
+ var url *string
+ if user.ProfileAvatarUrl.Valid {
+ url = &user.ProfileAvatarUrl.String
+ }
+ profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
+ members = append(members, ProjectMember{obj.Owner, user.FullName, profileIcon})
return members, nil
}
@@ -463,9 +495,12 @@ func (r *taskResolver) Assigned(ctx context.Context, obj *pg.Task) ([]ProjectMem
if err != nil {
return taskMembers, err
}
- initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
- profileIcon := &ProfileIcon{nil, &initials, &user.ProfileBgColor}
- taskMembers = append(taskMembers, ProjectMember{taskMemberLink.UserID, user.FirstName, user.LastName, profileIcon})
+ var url *string
+ if user.ProfileAvatarUrl.Valid {
+ url = &user.ProfileAvatarUrl.String
+ }
+ profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
+ taskMembers = append(taskMembers, ProjectMember{taskMemberLink.UserID, user.FullName, profileIcon})
}
return taskMembers, nil
}
@@ -505,8 +540,11 @@ func (r *userAccountResolver) ID(ctx context.Context, obj *pg.UserAccount) (uuid
}
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}
+ var url *string
+ if obj.ProfileAvatarUrl.Valid {
+ url = &obj.ProfileAvatarUrl.String
+ }
+ profileIcon := &ProfileIcon{url, &obj.Initials, &obj.ProfileBgColor}
return profileIcon, nil
}
@@ -554,39 +592,3 @@ 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) ColorHex(ctx context.Context, obj *pg.TaskLabel) (string, error) {
- projectLabel, err := r.Repository.GetProjectLabelByID(ctx, obj.ProjectLabelID)
- if err != nil {
- return "", err
- }
- labelColor, err := r.Repository.GetLabelColorByID(ctx, projectLabel.LabelColorID)
- if err != nil {
- return "", err
- }
- return labelColor.ColorHex, nil
-}
-func (r *taskLabelResolver) Name(ctx context.Context, obj *pg.TaskLabel) (*string, error) {
- projectLabel, err := r.Repository.GetProjectLabelByID(ctx, obj.ProjectLabelID)
- if err != nil {
- return nil, err
- }
- name := projectLabel.Name
- if !name.Valid {
- return nil, err
- }
- return &name.String, err
-}
-func (r *projectLabelResolver) ColorHex(ctx context.Context, obj *pg.ProjectLabel) (string, error) {
- labelColor, err := r.Repository.GetLabelColorByID(ctx, obj.LabelColorID)
- if err != nil {
- return "", err
- }
- return labelColor.ColorHex, nil
-}
diff --git a/api/migrations/0020_add-full-name-column-to-user_account-table.up.sql b/api/migrations/0020_add-full-name-column-to-user_account-table.up.sql
new file mode 100644
index 0000000..17e8675
--- /dev/null
+++ b/api/migrations/0020_add-full-name-column-to-user_account-table.up.sql
@@ -0,0 +1,3 @@
+ALTER TABLE user_account ADD COLUMN full_name TEXT;
+UPDATE user_account SET full_name = CONCAT(first_name, ' ', last_name);
+ALTER TABLE user_account ALTER COLUMN full_name SET NOT NULL;
diff --git a/api/migrations/0021_drop-first-name-column-from-user_account-table.up.sql b/api/migrations/0021_drop-first-name-column-from-user_account-table.up.sql
new file mode 100644
index 0000000..ff93086
--- /dev/null
+++ b/api/migrations/0021_drop-first-name-column-from-user_account-table.up.sql
@@ -0,0 +1 @@
+ALTER TABLE user_account DROP COLUMN first_name;
diff --git a/api/migrations/0022_drop-last-name-column-from-user_account-table.up.sql b/api/migrations/0022_drop-last-name-column-from-user_account-table.up.sql
new file mode 100644
index 0000000..bfde48e
--- /dev/null
+++ b/api/migrations/0022_drop-last-name-column-from-user_account-table.up.sql
@@ -0,0 +1 @@
+ALTER TABLE user_account DROP COLUMN last_name;
diff --git a/api/migrations/0023_add-initials-column-to-user_account-table.up.sql b/api/migrations/0023_add-initials-column-to-user_account-table.up.sql
new file mode 100644
index 0000000..35a50b4
--- /dev/null
+++ b/api/migrations/0023_add-initials-column-to-user_account-table.up.sql
@@ -0,0 +1 @@
+ALTER TABLE user_account ADD COLUMN initials TEXT NOT NULL DEFAULT '';
diff --git a/api/migrations/0024_add-profile-avatar-url-column-to-user_account-table.up.sql b/api/migrations/0024_add-profile-avatar-url-column-to-user_account-table.up.sql
new file mode 100644
index 0000000..c7fd227
--- /dev/null
+++ b/api/migrations/0024_add-profile-avatar-url-column-to-user_account-table.up.sql
@@ -0,0 +1 @@
+ALTER TABLE user_account ADD COLUMN profile_avatar_url TEXT;
diff --git a/api/pg/models.go b/api/pg/models.go
index 8de99af..9402a2d 100644
--- a/api/pg/models.go
+++ b/api/pg/models.go
@@ -85,12 +85,13 @@ type Team struct {
}
type UserAccount struct {
- UserID uuid.UUID `json:"user_id"`
- CreatedAt time.Time `json:"created_at"`
- FirstName string `json:"first_name"`
- LastName string `json:"last_name"`
- Email string `json:"email"`
- Username string `json:"username"`
- PasswordHash string `json:"password_hash"`
- ProfileBgColor string `json:"profile_bg_color"`
+ UserID uuid.UUID `json:"user_id"`
+ CreatedAt time.Time `json:"created_at"`
+ Email string `json:"email"`
+ Username string `json:"username"`
+ PasswordHash string `json:"password_hash"`
+ ProfileBgColor string `json:"profile_bg_color"`
+ FullName string `json:"full_name"`
+ Initials string `json:"initials"`
+ ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
}
diff --git a/api/pg/pg.go b/api/pg/pg.go
index 0a97df7..3e08de3 100644
--- a/api/pg/pg.go
+++ b/api/pg/pg.go
@@ -17,6 +17,7 @@ type Repository interface {
GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
+ UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error)
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
diff --git a/api/pg/querier.go b/api/pg/querier.go
index 03cfa3d..f2753b6 100644
--- a/api/pg/querier.go
+++ b/api/pg/querier.go
@@ -64,6 +64,7 @@ type Querier interface {
UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
+ UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
}
var _ Querier = (*Queries)(nil)
diff --git a/api/pg/user_accounts.sql.go b/api/pg/user_accounts.sql.go
index 1815f64..c346dcc 100644
--- a/api/pg/user_accounts.sql.go
+++ b/api/pg/user_accounts.sql.go
@@ -5,20 +5,20 @@ package pg
import (
"context"
+ "database/sql"
"time"
"github.com/google/uuid"
)
const createUserAccount = `-- name: CreateUserAccount :one
-INSERT INTO user_account(first_name, last_name, email, username, created_at, password_hash)
- VALUES ($1, $2, $3, $4, $5, $6)
-RETURNING user_id, created_at, first_name, last_name, email, username, password_hash, profile_bg_color
+INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash)
+ VALUES ($1, $2, $3, $4, $5, $6) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url
`
type CreateUserAccountParams struct {
- FirstName string `json:"first_name"`
- LastName string `json:"last_name"`
+ FullName string `json:"full_name"`
+ Initials string `json:"initials"`
Email string `json:"email"`
Username string `json:"username"`
CreatedAt time.Time `json:"created_at"`
@@ -27,8 +27,8 @@ type CreateUserAccountParams struct {
func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error) {
row := q.db.QueryRowContext(ctx, createUserAccount,
- arg.FirstName,
- arg.LastName,
+ arg.FullName,
+ arg.Initials,
arg.Email,
arg.Username,
arg.CreatedAt,
@@ -38,18 +38,19 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
err := row.Scan(
&i.UserID,
&i.CreatedAt,
- &i.FirstName,
- &i.LastName,
&i.Email,
&i.Username,
&i.PasswordHash,
&i.ProfileBgColor,
+ &i.FullName,
+ &i.Initials,
+ &i.ProfileAvatarUrl,
)
return i, err
}
const getAllUserAccounts = `-- name: GetAllUserAccounts :many
-SELECT user_id, created_at, first_name, last_name, email, username, password_hash, profile_bg_color FROM user_account
+SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url FROM user_account
`
func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
@@ -64,12 +65,13 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
if err := rows.Scan(
&i.UserID,
&i.CreatedAt,
- &i.FirstName,
- &i.LastName,
&i.Email,
&i.Username,
&i.PasswordHash,
&i.ProfileBgColor,
+ &i.FullName,
+ &i.Initials,
+ &i.ProfileAvatarUrl,
); err != nil {
return nil, err
}
@@ -85,7 +87,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
}
const getUserAccountByID = `-- name: GetUserAccountByID :one
-SELECT user_id, created_at, first_name, last_name, email, username, password_hash, profile_bg_color FROM user_account WHERE user_id = $1
+SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url FROM user_account WHERE user_id = $1
`
func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
@@ -94,18 +96,19 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use
err := row.Scan(
&i.UserID,
&i.CreatedAt,
- &i.FirstName,
- &i.LastName,
&i.Email,
&i.Username,
&i.PasswordHash,
&i.ProfileBgColor,
+ &i.FullName,
+ &i.Initials,
+ &i.ProfileAvatarUrl,
)
return i, err
}
const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one
-SELECT user_id, created_at, first_name, last_name, email, username, password_hash, profile_bg_color FROM user_account WHERE username = $1
+SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url FROM user_account WHERE username = $1
`
func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
@@ -114,12 +117,40 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string)
err := row.Scan(
&i.UserID,
&i.CreatedAt,
- &i.FirstName,
- &i.LastName,
&i.Email,
&i.Username,
&i.PasswordHash,
&i.ProfileBgColor,
+ &i.FullName,
+ &i.Initials,
+ &i.ProfileAvatarUrl,
+ )
+ return i, err
+}
+
+const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
+UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
+ RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url
+`
+
+type UpdateUserAccountProfileAvatarURLParams struct {
+ UserID uuid.UUID `json:"user_id"`
+ ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
+}
+
+func (q *Queries) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error) {
+ row := q.db.QueryRowContext(ctx, updateUserAccountProfileAvatarURL, arg.UserID, arg.ProfileAvatarUrl)
+ var i UserAccount
+ err := row.Scan(
+ &i.UserID,
+ &i.CreatedAt,
+ &i.Email,
+ &i.Username,
+ &i.PasswordHash,
+ &i.ProfileBgColor,
+ &i.FullName,
+ &i.Initials,
+ &i.ProfileAvatarUrl,
)
return i, err
}
diff --git a/api/query/task_group.sql b/api/query/task_group.sql
index 23138a4..13a51f1 100644
--- a/api/query/task_group.sql
+++ b/api/query/task_group.sql
@@ -11,8 +11,12 @@ SELECT * FROM task_group;
-- name: GetTaskGroupByID :one
SELECT * FROM task_group WHERE task_group_id = $1;
--- name: UpdateTaskGroupLocation :one
-UPDATE task_group SET position = $2 WHERE task_group_id = $1 RETURNING *;
+-- name: SetTaskGroupName :one
+UPDATE task_group SET name = $2 WHERE task_group_id = $1 RETURNING *;
-- name: DeleteTaskGroupByID :execrows
DELETE FROM task_group WHERE task_group_id = $1;
+
+-- name: UpdateTaskGroupLocation :one
+UPDATE task_group SET position = $2 WHERE task_group_id = $1 RETURNING *;
+
diff --git a/api/query/user_accounts.sql b/api/query/user_accounts.sql
index 2c1b155..74427cb 100644
--- a/api/query/user_accounts.sql
+++ b/api/query/user_accounts.sql
@@ -8,6 +8,9 @@ SELECT * FROM user_account;
SELECT * FROM user_account WHERE username = $1;
-- name: CreateUserAccount :one
-INSERT INTO user_account(first_name, last_name, email, username, created_at, password_hash)
- VALUES ($1, $2, $3, $4, $5, $6)
-RETURNING *;
+INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash)
+ VALUES ($1, $2, $3, $4, $5, $6) RETURNING *;
+
+-- name: UpdateUserAccountProfileAvatarURL :one
+UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
+ RETURNING *;
diff --git a/api/router/middleware.go b/api/router/middleware.go
index a4a2ed5..2f6e823 100644
--- a/api/router/middleware.go
+++ b/api/router/middleware.go
@@ -24,6 +24,7 @@ func AuthenticationMiddleware(next http.Handler) http.Handler {
accessClaims, err := ValidateAccessToken(accessTokenString)
if err != nil {
if _, ok := err.(*ErrExpiredToken); ok {
+ w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{
"data": {},
"errors": [
diff --git a/api/router/models.go b/api/router/models.go
index 8f88204..45af546 100644
--- a/api/router/models.go
+++ b/api/router/models.go
@@ -30,3 +30,8 @@ type LogoutResponseData struct {
type RefreshTokenResponseData struct {
AccessToken string `json:"accessToken"`
}
+
+type AvatarUploadResponseData struct {
+ UserID string `json:"userID"`
+ URL string `json:"url"`
+}
diff --git a/api/router/router.go b/api/router/router.go
index 9563abb..aa4669b 100644
--- a/api/router/router.go
+++ b/api/router/router.go
@@ -1,12 +1,16 @@
package router
import (
+ "database/sql"
+ "encoding/json"
+ "io/ioutil"
"net/http"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/cors"
+ "github.com/google/uuid"
"github.com/jmoiron/sqlx"
log "github.com/sirupsen/logrus"
@@ -18,6 +22,45 @@ func (h *CitadelHandler) PingHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
}
+func (h *CitadelHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) {
+ log.Info("preparing to upload file")
+ userID, ok := r.Context().Value("userID").(uuid.UUID)
+ if !ok {
+ log.Error("not a valid uuid")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // Parse our multipart form, 10 << 20 specifies a maximum
+ // upload of 10 MB files.
+ r.ParseMultipartForm(10 << 20)
+
+ file, handler, err := r.FormFile("file")
+ if err != nil {
+ log.WithError(err).Error("issue while uploading file")
+ return
+ }
+ defer file.Close()
+ log.WithFields(log.Fields{"filename": handler.Filename, "size": handler.Size, "header": handler.Header}).Info("file metadata")
+
+ fileBytes, err := ioutil.ReadAll(file)
+ if err != nil {
+ log.WithError(err).Error("while reading file")
+ return
+ }
+ err = ioutil.WriteFile("uploads/"+handler.Filename, fileBytes, 0644)
+ if err != nil {
+ log.WithError(err).Error("while reading file")
+ return
+ }
+
+ h.repo.UpdateUserAccountProfileAvatarURL(r.Context(), pg.UpdateUserAccountProfileAvatarURLParams{UserID: userID, ProfileAvatarUrl: sql.NullString{String: "http://localhost:3333/uploads/" + handler.Filename, Valid: true}})
+ // return that we have successfully uploaded our file!
+ log.Info("file uploaded")
+ json.NewEncoder(w).Encode(AvatarUploadResponseData{URL: "http://localhost:3333/uploads/" + handler.Filename, UserID: userID.String()})
+
+}
+
func NewRouter(db *sqlx.DB) (chi.Router, error) {
formatter := new(log.TextFormatter)
formatter.TimestampFormat = "02-01-2006 15:04:05"
@@ -50,9 +93,13 @@ func NewRouter(db *sqlx.DB) (chi.Router, error) {
r.Group(func(mux chi.Router) {
mux.Mount("/auth", authResource{}.Routes(citadelHandler))
mux.Handle("/__graphql", graph.NewPlaygroundHandler("/graphql"))
+ var imgServer = http.FileServer(http.Dir("./uploads/"))
+ mux.Mount("/uploads/", http.StripPrefix("/uploads/", imgServer))
+
})
r.Group(func(mux chi.Router) {
mux.Use(AuthenticationMiddleware)
+ mux.Post("/users/me/avatar", citadelHandler.ProfileImageUpload)
mux.Get("/ping", citadelHandler.PingHandler)
mux.Handle("/graphql", graph.NewHandler(repository))
})
diff --git a/api/router/tokens.go b/api/router/tokens.go
index c2f3686..cff30f6 100644
--- a/api/router/tokens.go
+++ b/api/router/tokens.go
@@ -43,6 +43,10 @@ func ValidateAccessToken(accessTokenString string) (AccessTokenClaims, error) {
return jwtKey, nil
})
+ if err != nil {
+ return *accessClaims, nil
+ }
+
if accessToken.Valid {
log.WithFields(log.Fields{
"token": accessTokenString,
diff --git a/api/uploads/Ansible_logo.svg b/api/uploads/Ansible_logo.svg
new file mode 100644
index 0000000..2f25933
--- /dev/null
+++ b/api/uploads/Ansible_logo.svg
@@ -0,0 +1,18 @@
+
+
diff --git a/api/uploads/headshot-small.png b/api/uploads/headshot-small.png
new file mode 100644
index 0000000..8146718
Binary files /dev/null and b/api/uploads/headshot-small.png differ
diff --git a/api/uploads/headshot.png b/api/uploads/headshot.png
new file mode 100644
index 0000000..51b0e2c
Binary files /dev/null and b/api/uploads/headshot.png differ
diff --git a/api/uploads/logo.png b/api/uploads/logo.png
new file mode 100644
index 0000000..c20defa
Binary files /dev/null and b/api/uploads/logo.png differ
diff --git a/api/uploads/logo.svg b/api/uploads/logo.svg
new file mode 100644
index 0000000..e842bc5
--- /dev/null
+++ b/api/uploads/logo.svg
@@ -0,0 +1,91 @@
+
+
+
+
diff --git a/api/uploads/text4184.png b/api/uploads/text4184.png
new file mode 100644
index 0000000..d1dba8f
Binary files /dev/null and b/api/uploads/text4184.png differ
diff --git a/web/.editorconfig b/web/.editorconfig
index 11695db..66996e6 100644
--- a/web/.editorconfig
+++ b/web/.editorconfig
@@ -7,3 +7,7 @@ indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
+
+[Makefile]
+indent_style = tab
+indent_size = 2
diff --git a/web/Makefile b/web/Makefile
new file mode 100644
index 0000000..c51deba
--- /dev/null
+++ b/web/Makefile
@@ -0,0 +1,3 @@
+
+start:
+ yarn start
diff --git a/web/package.json b/web/package.json
index 474f066..7277d29 100644
--- a/web/package.json
+++ b/web/package.json
@@ -18,7 +18,9 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
+ "@types/axios": "^0.14.0",
"@types/color": "^3.0.1",
+ "@types/date-fns": "^2.6.0",
"@types/jest": "^24.0.0",
"@types/jwt-decode": "^2.2.1",
"@types/lodash": "^4.14.149",
@@ -32,6 +34,8 @@
"@types/react-select": "^3.0.13",
"@types/styled-components": "^5.0.0",
"@welldone-software/why-did-you-render": "^4.2.2",
+ "ag-grid-community": "^23.2.0",
+ "ag-grid-react": "^23.2.0",
"apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8",
"apollo-link": "^1.2.13",
@@ -39,7 +43,10 @@
"apollo-link-http": "^1.5.16",
"apollo-link-state": "^0.4.2",
"apollo-utilities": "^1.3.3",
+ "axios": "^0.19.2",
+ "axios-auth-refresh": "^2.2.7",
"color": "^3.1.2",
+ "date-fns": "^2.14.0",
"graphql": "^15.0.0",
"graphql-tag": "^2.10.3",
"history": "^4.10.1",
diff --git a/web/src/App/Routes.tsx b/web/src/App/Routes.tsx
index d28ef6b..58f4bab 100644
--- a/web/src/App/Routes.tsx
+++ b/web/src/App/Routes.tsx
@@ -6,7 +6,14 @@ import Dashboard from 'Dashboard';
import Projects from 'Projects';
import Project from 'Projects/Project';
import Login from 'Auth';
+import Profile from 'Profile';
+import styled from 'styled-components';
+const MainContent = styled.div`
+ padding: 0 0 50px 80px;
+ background: #262c49;
+ height: 100%;
+`;
type RoutesProps = {
history: H.History;
};
@@ -14,9 +21,12 @@ type RoutesProps = {
const Routes = ({ history }: RoutesProps) => (
-
-
-
+
+
+
+
+
+
);
diff --git a/web/src/App/TopNavbar.tsx b/web/src/App/TopNavbar.tsx
index 400a936..c6f4db1 100644
--- a/web/src/App/TopNavbar.tsx
+++ b/web/src/App/TopNavbar.tsx
@@ -1,6 +1,6 @@
import React, { useState, useContext } from 'react';
import TopNavbar from 'shared/components/TopNavbar';
-import DropdownMenu from 'shared/components/DropdownMenu';
+import DropdownMenu, { ProfileMenu } from 'shared/components/DropdownMenu';
import ProjectSettings from 'shared/components/ProjectSettings';
import { useHistory } from 'react-router';
import UserIDContext from 'App/context';
@@ -14,21 +14,37 @@ type GlobalTopNavbarProps = {
};
const GlobalTopNavbar: React.FC = ({ name, projectMembers, onSaveProjectName }) => {
const { loading, data } = useMeQuery();
- const { showPopup } = usePopup();
+ const { showPopup, hidePopup } = usePopup();
const history = useHistory();
const { userID, setUserID } = useContext(UserIDContext);
- const [menu, setMenu] = useState({
- top: 0,
- left: 0,
- isOpen: false,
- });
- const onProfileClick = (bottom: number, right: number) => {
- setMenu({
- isOpen: !menu.isOpen,
- left: right,
- top: bottom,
+ const onLogout = () => {
+ fetch('http://localhost:3333/auth/logout', {
+ method: 'POST',
+ credentials: 'include',
+ }).then(async x => {
+ const { status } = x;
+ if (status === 200) {
+ history.replace('/login');
+ setUserID(null);
+ hidePopup();
+ }
});
};
+ const onProfileClick = ($target: React.RefObject) => {
+ showPopup(
+ $target,
+
+ {
+ history.push('/profile');
+ hidePopup();
+ }}
+ />
+ ,
+ 185,
+ );
+ };
const onOpenSettings = ($target: React.RefObject) => {
showPopup(
@@ -40,18 +56,6 @@ const GlobalTopNavbar: React.FC = ({ name, projectMembers,
);
};
- const onLogout = () => {
- fetch('http://localhost:3333/auth/logout', {
- method: 'POST',
- credentials: 'include',
- }).then(async x => {
- const { status } = x;
- if (status === 200) {
- history.replace('/login');
- setUserID(null);
- }
- });
- };
if (!userID) {
return null;
}
@@ -59,30 +63,13 @@ const GlobalTopNavbar: React.FC = ({ name, projectMembers,
<>
{}}
projectMembers={projectMembers}
onProfileClick={onProfileClick}
onSaveProjectName={onSaveProjectName}
onOpenSettings={onOpenSettings}
/>
- {menu.isOpen && (
- {
- setMenu({
- top: 0,
- left: 0,
- isOpen: false,
- });
- }}
- onLogout={onLogout}
- left={menu.left}
- top={menu.top}
- />
- )}
>
);
};
diff --git a/web/src/App/index.tsx b/web/src/App/index.tsx
index 978df4e..557faf8 100644
--- a/web/src/App/index.tsx
+++ b/web/src/App/index.tsx
@@ -13,12 +13,6 @@ import { PopupProvider } from 'shared/components/PopupMenu';
const history = createBrowserHistory();
-const MainContent = styled.div`
- padding: 0 0 50px 80px;
- background: #262c49;
- height: 100%;
-`;
-
const App = () => {
const [loading, setLoading] = useState(true);
const [userID, setUserID] = useState(null);
@@ -54,9 +48,7 @@ const App = () => {
) : (
<>
-
-
-
+
>
)}
diff --git a/web/src/Profile/index.tsx b/web/src/Profile/index.tsx
new file mode 100644
index 0000000..26c4312
--- /dev/null
+++ b/web/src/Profile/index.tsx
@@ -0,0 +1,71 @@
+import React, { useRef, useEffect } from 'react';
+import styled from 'styled-components/macro';
+import GlobalTopNavbar from 'App/TopNavbar';
+import { Link } from 'react-router-dom';
+import { getAccessToken } from 'shared/utils/accessToken';
+import Navbar from 'App/Navbar';
+import Settings from 'shared/components/Settings';
+import UserIDContext from 'App/context';
+import { useMeQuery, useClearProfileAvatarMutation } from 'shared/generated/graphql';
+import axios from 'axios';
+
+const MainContent = styled.div`
+ padding: 0 0 50px 80px;
+ height: 100%;
+ background: #262c49;
+`;
+
+const Projects = () => {
+ const $fileUpload = useRef(null);
+ const [clearProfileAvatar] = useClearProfileAvatarMutation();
+ const { loading, data, refetch } = useMeQuery();
+ useEffect(() => {
+ document.title = 'Profile | Citadel';
+ }, []);
+ return (
+ <>
+ {
+ if (e.target.files) {
+ console.log(e.target.files[0]);
+ const fileData = new FormData();
+ fileData.append('file', e.target.files[0]);
+ const accessToken = getAccessToken();
+ axios
+ .post('http://localhost:3333/users/me/avatar', fileData, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ })
+ .then(res => {
+ if ($fileUpload && $fileUpload.current) {
+ $fileUpload.current.value = '';
+ refetch();
+ }
+ });
+ }
+ }}
+ />
+ {}} name={null} />
+ {!loading && data && (
+ {
+ if ($fileUpload && $fileUpload.current) {
+ $fileUpload.current.click();
+ }
+ }}
+ onProfileAvatarRemove={() => {
+ clearProfileAvatar();
+ }}
+ />
+ )}
+ >
+ );
+};
+
+export default Projects;
diff --git a/web/src/Projects/Project/index.tsx b/web/src/Projects/Project/index.tsx
index 3f36650..368311a 100644
--- a/web/src/Projects/Project/index.tsx
+++ b/web/src/Projects/Project/index.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useRef, useContext } from 'react';
+import React, { useState, useRef, useContext, useEffect } from 'react';
import GlobalTopNavbar from 'App/TopNavbar';
import styled from 'styled-components/macro';
import { Bolt, ToggleOn, Tags } from 'shared/icons';
@@ -428,6 +428,11 @@ const Project = () => {
const $labelsRef = useRef(null);
const labelsRef = useRef>([]);
const taskLabelsRef = useRef>([]);
+ useEffect(() => {
+ if (data) {
+ document.title = `${data.findProject.name} | Citadel`;
+ }
+ }, [data]);
if (loading) {
return (
<>
@@ -562,6 +567,7 @@ const Project = () => {
,
);
}}
+ onChangeTaskGroupName={(taskGroupID, name) => {}}
onQuickEditorOpen={onQuickEditorOpen}
onExtraMenuOpen={(taskGroupID: string, $targetRef: any) => {
showPopup(
diff --git a/web/src/Projects/index.tsx b/web/src/Projects/index.tsx
index c592530..cc7044a 100644
--- a/web/src/Projects/index.tsx
+++ b/web/src/Projects/index.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useContext } from 'react';
+import React, { useState, useContext, useEffect } from 'react';
import styled from 'styled-components/macro';
import GlobalTopNavbar from 'App/TopNavbar';
import { useGetProjectsQuery, useCreateProjectMutation, GetProjectsDocument } from 'shared/generated/graphql';
@@ -28,6 +28,9 @@ const ProjectLink = styled(Link)``;
const Projects = () => {
const { loading, data } = useGetProjectsQuery();
+ useEffect(() => {
+ document.title = 'Citadel';
+ }, []);
const [createProject] = useCreateProjectMutation({
update: (client, newProject) => {
const cacheData: any = client.readQuery({
diff --git a/web/src/citadel.d.ts b/web/src/citadel.d.ts
index d61698e..611a015 100644
--- a/web/src/citadel.d.ts
+++ b/web/src/citadel.d.ts
@@ -18,8 +18,7 @@ type ContextMenuEvent = {
type TaskUser = {
id: string;
- firstName: string;
- lastName: string;
+ fullName: string;
profileIcon: ProfileIcon;
};
@@ -32,6 +31,11 @@ type LoginFormData = {
password: string;
};
+type DueDateFormData = {
+ endDate: Date;
+ endTime: string | null;
+};
+
type LoginProps = {
onSubmit: (
data: LoginFormData,
diff --git a/web/src/index.tsx b/web/src/index.tsx
index 6e8653d..f72da2f 100644
--- a/web/src/index.tsx
+++ b/web/src/index.tsx
@@ -9,9 +9,21 @@ import { onError } from 'apollo-link-error';
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
+import axios from 'axios';
+import createAuthRefreshInterceptor from 'axios-auth-refresh';
import App from './App';
+// Function that will be called to refresh authorization
+const refreshAuthLogic = (failedRequest: any) =>
+ axios.post('http://localhost:3333/auth/refresh_token', {}, { withCredentials: true }).then(tokenRefreshResponse => {
+ setAccessToken(tokenRefreshResponse.data.accessToken);
+ failedRequest.response.config.headers.Authorization = `Bearer ${tokenRefreshResponse.data.accessToken}`;
+ return Promise.resolve();
+ });
+
+createAuthRefreshInterceptor(axios, refreshAuthLogic);
+
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
let forward$;
diff --git a/web/src/shared/components/Admin/Admin.stories.tsx b/web/src/shared/components/Admin/Admin.stories.tsx
new file mode 100644
index 0000000..a037f8c
--- /dev/null
+++ b/web/src/shared/components/Admin/Admin.stories.tsx
@@ -0,0 +1,25 @@
+import React, { useRef } from 'react';
+import Admin from '.';
+import NormalizeStyles from 'App/NormalizeStyles';
+import BaseStyles from 'App/BaseStyles';
+
+export default {
+ component: Admin,
+ title: 'Admin',
+ parameters: {
+ backgrounds: [
+ { name: 'gray', value: '#f8f8f8', default: true },
+ { name: 'white', value: '#ffffff' },
+ ],
+ },
+};
+
+export const Default = () => {
+ return (
+ <>
+
+
+
+ >
+ );
+};
diff --git a/web/src/shared/components/Admin/index.tsx b/web/src/shared/components/Admin/index.tsx
new file mode 100644
index 0000000..c3fb76b
--- /dev/null
+++ b/web/src/shared/components/Admin/index.tsx
@@ -0,0 +1,323 @@
+import React, { useState, useRef } from 'react';
+import styled from 'styled-components';
+import { User, Plus } from 'shared/icons';
+import { AgGridReact } from 'ag-grid-react';
+
+import 'ag-grid-community/dist/styles/ag-grid.css';
+import 'ag-grid-community/dist/styles/ag-theme-material.css';
+
+const NewUserButton = styled.button`
+ outline: none;
+ border: none;
+ cursor: pointer;
+ line-height: 20px;
+ padding: 0.75rem;
+ background-color: transparent;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: rgba(115, 103, 240);
+ font-size: 14px;
+
+ border-radius: 0.5rem;
+ border-width: 1px;
+ border-style: solid;
+ border-color: transparent;
+ border-image: initial;
+ border-color: rgba(115, 103, 240);
+ span {
+ padding-left: 0.5rem;
+ }
+`;
+const GridTable = styled.div`
+ height: 620px;
+`;
+
+const RootWrapper = styled.div`
+ height: 100%;
+ display: flex;
+ position: relative;
+ flex-direction: column;
+ overflow: hidden;
+`;
+
+const Root = styled.div`
+ .ag-theme-material {
+ --ag-foreground-color: #c2c6dc;
+ --ag-secondary-foreground-color: #c2c6dc;
+ --ag-background-color: transparent;
+ --ag-header-background-color: transparent;
+ --ag-header-foreground-color: #c2c6dc;
+ --ag-border-color: #414561;
+
+ --ag-row-hover-color: #262c49;
+ --ag-header-cell-hover-background-color: #262c49;
+ --ag-checkbox-unchecked-color: #c2c6dc;
+ --ag-checkbox-indeterminate-color: rgba(115, 103, 240);
+ --ag-selected-row-background-color: #262c49;
+ --ag-material-primary-color: rgba(115, 103, 240);
+ --ag-material-accent-color: rgba(115, 103, 240);
+ }
+ .ag-theme-material ::-webkit-scrollbar {
+ width: 12px;
+ }
+
+ .ag-theme-material ::-webkit-scrollbar-track {
+ background: #262c49;
+ border-radius: 20px;
+ }
+
+ .ag-theme-material ::-webkit-scrollbar-thumb {
+ background: #7367f0;
+ border-radius: 20px;
+ }
+ .ag-header-cell-text {
+ color: #fff;
+ font-weight: 700;
+ }
+`;
+
+const Header = styled.div`
+ border-bottom: 1px solid #e2e2e2;
+ flex-direction: row;
+ box-sizing: border-box;
+ display: flex;
+ white-space: nowrap;
+ width: 100%;
+ overflow: hidden;
+ background: transparent;
+ border-bottom-color: #414561;
+ color: #fff;
+
+ height: 112px;
+ min-height: 112px;
+`;
+
+const ActionButtons = () => {
+ return Hello!;
+};
+const data = {
+ defaultColDef: {
+ resizable: true,
+ sortable: true,
+ },
+ columnDefs: [
+ {
+ minWidth: 125,
+ width: 125,
+ headerCheckboxSelection: true,
+ checkboxSelection: true,
+ headerName: 'ID',
+ field: 'id',
+ },
+ { minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
+ { minWidth: 225, headerName: 'Email', field: 'email' },
+ { minWidth: 200, headerName: 'Name', editable: true, field: 'full_name' },
+ { minWidth: 200, headerName: 'Role', editable: true, field: 'role' },
+ {
+ minWidth: 200,
+ headerName: 'Actions',
+ cellRenderer: 'actionButtons',
+ },
+ ],
+ frameworkComponents: {
+ actionButtons: ActionButtons,
+ },
+ rowData: [
+ { id: '1', full_name: 'Jordan Knott', username: 'jordan', email: 'jordan@jordanthedev.com', role: 'Admin' },
+ { id: '2', full_name: 'Jordan Test', username: 'jordantest', email: 'jordan@jordanthedev.com', role: 'Admin' },
+ { id: '3', full_name: 'Jordan Other', username: 'alphatest1050', email: 'jordan@jordanthedev.com', role: 'Admin' },
+ { id: '5', full_name: 'Jordan French', username: 'other', email: 'jordan@jordanthedev.com', role: 'Admin' },
+ ],
+};
+const ListTable = () => {
+ return (
+
+
+
{
+ params.api.sizeColumnsToFit();
+ }}
+ onGridSizeChanged={params => {
+ params.api.sizeColumnsToFit();
+ }}
+ >
+
+
+ );
+};
+
+const Wrapper = styled.div`
+ background: #eff2f7;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-wrap: wrap;
+`;
+
+const Container = styled.div`
+ padding: 2.2rem;
+ display: flex;
+ width: 100%;
+ position: relative;
+`;
+
+const TabNav = styled.div`
+ float: left;
+ width: 220px;
+ height: 100%;
+ display: block;
+ position: relative;
+`;
+
+const TabNavContent = styled.ul`
+ display: block;
+ width: auto;
+ border-bottom: 0 !important;
+ border-right: 1px solid rgba(0, 0, 0, 0.05);
+`;
+
+const TabNavItem = styled.li`
+ padding: 0.35rem 0.3rem;
+ display: block;
+ position: relative;
+`;
+const TabNavItemButton = styled.button<{ active: boolean }>`
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+
+ padding-top: 10px !important;
+ padding-bottom: 10px !important;
+ padding-left: 12px !important;
+ padding-right: 8px !important;
+ width: 100%;
+ position: relative;
+
+ color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
+ &:hover {
+ color: rgba(115, 103, 240);
+ }
+ &:hover svg {
+ fill: rgba(115, 103, 240);
+ }
+`;
+
+const TabNavItemSpan = styled.span`
+ text-align: left;
+ padding-left: 9px;
+ font-size: 14px;
+`;
+
+const TabNavLine = styled.span<{ top: number }>`
+ left: auto;
+ right: 0;
+ width: 2px;
+ height: 48px;
+ transform: scaleX(1);
+ top: ${props => props.top}px;
+
+ background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240));
+ box-shadow: 0 0 8px 0 rgba(115, 103, 240);
+ display: block;
+ position: absolute;
+ transition: all 0.2s ease;
+`;
+
+const TabContentWrapper = styled.div`
+ position: relative;
+ display: block;
+ overflow: hidden;
+ width: 100%;
+`;
+
+const TabContent = styled.div`
+ position: relative;
+ width: 100%;
+ display: block;
+ padding: 0;
+ padding: 1.5rem;
+ background-color: #10163a;
+ margin-left: 1rem !important;
+ border-radius: 0.5rem;
+`;
+
+const items = [
+ { name: 'Insights' },
+ { name: 'Members' },
+ { name: 'Teams' },
+ { name: 'Security' },
+ { name: 'Settings' },
+];
+
+type NavItemProps = {
+ active: boolean;
+ name: string;
+ tab: number;
+ onClick: (tab: number, top: number) => void;
+};
+const NavItem: React.FC = ({ active, name, tab, onClick }) => {
+ const $item = useRef(null);
+ return (
+ {
+ if ($item && $item.current) {
+ const pos = $item.current.getBoundingClientRect();
+ onClick(tab, pos.top);
+ }
+ }}
+ >
+
+
+ {name}
+
+
+ );
+};
+
+const Admin = () => {
+ const [currentTop, setTop] = useState(0);
+ const [currentTab, setTab] = useState(0);
+ const $tabNav = useRef(null);
+ return (
+
+
+
+ {items.map((item, idx) => (
+ {
+ if ($tabNav && $tabNav.current) {
+ const pos = $tabNav.current.getBoundingClientRect();
+ setTab(tab);
+ setTop(top - pos.top);
+ }
+ }}
+ name={item.name}
+ tab={idx}
+ active={idx === currentTab}
+ />
+ ))}
+
+
+
+
+
+
+
+ Add New
+
+
+
+
+
+ );
+};
+
+export default Admin;
diff --git a/web/src/shared/components/Card/index.tsx b/web/src/shared/components/Card/index.tsx
index 191d964..d8c6c22 100644
--- a/web/src/shared/components/Card/index.tsx
+++ b/web/src/shared/components/Card/index.tsx
@@ -21,6 +21,7 @@ import {
CardTitle,
CardMembers,
} from './Styles';
+import TaskAssignee from 'shared/components/TaskAssignee';
type DueDate = {
isPastDue: boolean;
@@ -143,7 +144,16 @@ const Card = React.forwardRef(
{members &&
members.map(member => (
-
+ {
+ if (onCardMemberClick) {
+ onCardMemberClick($target, taskID, member.id);
+ }
+ }}
+ />
))}
diff --git a/web/src/shared/components/DropdownMenu/index.tsx b/web/src/shared/components/DropdownMenu/index.tsx
index 0fadfec..45e6038 100644
--- a/web/src/shared/components/DropdownMenu/index.tsx
+++ b/web/src/shared/components/DropdownMenu/index.tsx
@@ -33,4 +33,29 @@ const DropdownMenu: React.FC = ({ left, top, onLogout, onClos
);
};
+type ProfileMenuProps = {
+ onProfile: () => void;
+ onLogout: () => void;
+};
+
+const ProfileMenu: React.FC = ({ onProfile, onLogout }) => {
+ return (
+ <>
+
+
+ Profile
+
+
+
+
+
+ Logout
+
+
+ >
+ );
+};
+
+export { ProfileMenu };
+
export default DropdownMenu;
diff --git a/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx b/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx
index 2c66a67..b7a7b0a 100644
--- a/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx
+++ b/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx
@@ -1,7 +1,12 @@
import React, { useRef } from 'react';
import { action } from '@storybook/addon-actions';
import DueDateManager from '.';
+import { Popup } from '../PopupMenu';
+import styled from 'styled-components';
+const PopupWrapper = styled.div`
+ width: 300px;
+`;
export default {
component: DueDateManager,
title: 'DueDateManager',
@@ -15,41 +20,44 @@ export default {
export const Default = () => {
return (
-
+
+
+ taskGroup: { name: 'General', id: '1', position: 1 },
+ name: 'Hello, world',
+ position: 1,
+ labels: [
+ {
+ id: 'soft-skills',
+ assignedDate: new Date().toString(),
+ projectLabel: {
+ createdDate: new Date().toString(),
+ id: 'label-soft-skills',
+ name: 'Soft Skills',
+ labelColor: {
+ id: '1',
+ name: 'white',
+ colorHex: '#fff',
+ position: 1,
+ },
+ },
+ },
+ ],
+ description: 'hello!',
+ assigned: [
+ {
+ id: '1',
+ profileIcon: { url: null, initials: null, bgColor: null },
+ fullName: 'Jordan Knott',
+ },
+ ],
+ }}
+ onCancel={action('cancel')}
+ onDueDateChange={action('due date change')}
+ />
+
+
);
};
diff --git a/web/src/shared/components/DueDateManager/Styles.ts b/web/src/shared/components/DueDateManager/Styles.ts
index 2f032de..96e5800 100644
--- a/web/src/shared/components/DueDateManager/Styles.ts
+++ b/web/src/shared/components/DueDateManager/Styles.ts
@@ -1,8 +1,62 @@
import styled from 'styled-components';
+import { mixin } from 'shared/utils/styles';
export const Wrapper = styled.div`
display: flex
flex-direction: column;
+ & .react-datepicker {
+ background: #262c49;
+ font-family: 'Droid Sans', sans-serif;
+ border: none;
+
+ }
+ & .react-datepicker__day-name {
+ color: #c2c6dc;
+ outline: none;
+ box-shadow: none;
+ padding: 4px;
+ font-size: 12px40px
+ line-height: 40px;
+ }
+ & .react-datepicker__day-name:hover {
+ background: #10163a;
+ }
+ & .react-datepicker__month {
+ margin: 0;
+ }
+
+ & .react-datepicker__day,
+ & .react-datepicker__time-name {
+ color: #c2c6dc;
+ outline: none;
+ box-shadow: none;
+ padding: 4px;
+ font-size: 14px;
+ }
+
+ & .react-datepicker__day--outside-month {
+ opacity: 0.6;
+ }
+
+ & .react-datepicker__day:hover {
+ border-radius: 50%;
+ background: #10163a;
+ }
+ & .react-datepicker__day--selected {
+ border-radius: 50%;
+ background: rgba(115, 103, 240);
+ color: #fff;
+ }
+ & .react-datepicker__day--selected:hover {
+ border-radius: 50%;
+ background: rgba(115, 103, 240);
+ color: #fff;
+ }
+ & .react-datepicker__header {
+ background: none;
+ border: none;
+ }
+
`;
export const DueDatePickerWrapper = styled.div`
diff --git a/web/src/shared/components/DueDateManager/index.tsx b/web/src/shared/components/DueDateManager/index.tsx
index bc3bb3b..28d46c1 100644
--- a/web/src/shared/components/DueDateManager/index.tsx
+++ b/web/src/shared/components/DueDateManager/index.tsx
@@ -1,23 +1,202 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
+import moment from 'moment';
+import styled from 'styled-components';
import DatePicker from 'react-datepicker';
import { Cross } from 'shared/icons';
+import _ from 'lodash';
import { Wrapper, ActionWrapper, DueDatePickerWrapper, ConfirmAddDueDate, CancelDueDate } from './Styles';
import 'react-datepicker/dist/react-datepicker.css';
+import { getYear, getMonth } from 'date-fns';
+import { useForm } from 'react-hook-form';
type DueDateManagerProps = {
task: Task;
onDueDateChange: (task: Task, newDueDate: Date) => void;
onCancel: () => void;
};
-const DueDateManager: React.FC = ({ task, onDueDateChange, onCancel }) => {
- const [startDate, setStartDate] = useState(new Date());
+const HeaderSelectLabel = styled.div`
+ display: inline-block;
+ position: relative;
+ z-index: 9999;
+ border-radius: 3px;
+ cursor: pointer;
+ padding: 6px 10px;
+ text-decoration: underline;
+ margin: 6px 0;
+ font-size: 14px;
+ line-height: 16px;
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 4px;
+ padding-right: 4px;
+ color: #c2c6dc;
+
+ &:hover {
+ background: rgba(115, 103, 240);
+ color: #c2c6dc;
+ }
+`;
+
+const HeaderSelect = styled.select`
+ text-decoration: underline;
+ font-size: 14px;
+ text-align: center;
+ padding: 4px 6px;
+ background: none;
+ outline: none;
+ border: none;
+ border-radius: 3px;
+ appearance: none;
+
+ &:hover {
+ background: #262c49;
+ border: 1px solid rgba(115, 103, 240);
+ outline: none !important;
+ box-shadow: none;
+ color: #c2c6dc;
+ }
+
+ &::-ms-expand {
+ display: none;
+ }
+
+ cursor: pointer;
+ position: absolute;
+ z-index: 9998;
+ margin: 0;
+ left: 0;
+ top: 5px;
+ opacity: 0;
+`;
+
+const HeaderButton = styled.button`
+ cursor: pointer;
+ color: #c2c6dc;
+ text-decoration: underline;
+ font-size: 14px;
+ text-align: center;
+ padding: 6px 10px;
+ margin: 6px 0;
+ background: none;
+ outline: none;
+ border: none;
+ border-radius: 3px;
+ &:hover {
+ background: rgba(115, 103, 240);
+ color: #fff;
+ }
+`;
+
+const HeaderActions = styled.div`
+ position: relative;
+ text-align: center;
+ & > button:first-child {
+ float: left;
+ }
+ & > button:last-child {
+ float: right;
+ }
+`;
+
+const DueDateManager: React.FC = ({ task, onDueDateChange, onCancel }) => {
+ const now = moment();
+ const [textStartDate, setTextStartDate] = useState(now.format('YYYY-MM-DD'));
+
+ const [startDate, setStartDate] = useState(new Date());
+ useEffect(() => {
+ setTextStartDate(moment(startDate).format('YYYY-MM-DD'));
+ }, [startDate]);
+
+ const years = _.range(2010, getYear(new Date()) + 10, 1);
+ const months = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+ ];
+ const { register, handleSubmit, errors, setError, formState } = useForm();
+ console.log(errors);
return (
+
- setStartDate(date ?? new Date())} />
+ (
+
+
+ Prev
+
+
+ {months[date.getMonth()]}
+ changeYear(parseInt(value))}>
+ {years.map(option => (
+
+ ))}
+
+
+
+ {date.getFullYear()}
+ changeMonth(months.indexOf(value))}
+ >
+ {months.map(option => (
+
+ ))}
+
+
+
+
+ Next
+
+
+ )}
+ selected={startDate}
+ inline
+ onChange={date => setStartDate(date ?? new Date())}
+ />
onDueDateChange(task, startDate)}>Save
diff --git a/web/src/shared/components/List/Styles.ts b/web/src/shared/components/List/Styles.ts
index 7ad9640..6392e2b 100644
--- a/web/src/shared/components/List/Styles.ts
+++ b/web/src/shared/components/List/Styles.ts
@@ -64,6 +64,7 @@ export const HeaderEditTarget = styled.div<{ isHidden: boolean }>`
export const HeaderName = styled(TextareaAutosize)`
font-family: 'Droid Sans';
+ font-size: 14px;
border: none;
resize: none;
overflow: hidden;
diff --git a/web/src/shared/components/Lists/Lists.stories.tsx b/web/src/shared/components/Lists/Lists.stories.tsx
index e4665b4..7878d21 100644
--- a/web/src/shared/components/Lists/Lists.stories.tsx
+++ b/web/src/shared/components/Lists/Lists.stories.tsx
@@ -171,6 +171,7 @@ export const ListsWithManyList = () => {
onCreateTask={action('card create')}
onTaskDrop={onCardDrop}
onTaskGroupDrop={onListDrop}
+ onChangeTaskGroupName={action('change group name')}
onCreateTaskGroup={action('create list')}
onExtraMenuOpen={action('extra menu open')}
onCardMemberClick={action('card member click')}
diff --git a/web/src/shared/components/Lists/index.tsx b/web/src/shared/components/Lists/index.tsx
index f1187e4..83dedad 100644
--- a/web/src/shared/components/Lists/index.tsx
+++ b/web/src/shared/components/Lists/index.tsx
@@ -20,6 +20,7 @@ interface SimpleProps {
onTaskClick: (task: Task) => void;
onCreateTask: (taskGroupID: string, name: string) => void;
+ onChangeTaskGroupName: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: (e: ContextMenuEvent) => void;
onCreateTaskGroup: (listName: string) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject) => void;
@@ -29,6 +30,7 @@ interface SimpleProps {
const SimpleLists: React.FC = ({
taskGroups,
onTaskDrop,
+ onChangeTaskGroupName,
onTaskGroupDrop,
onTaskClick,
onCreateTask,
@@ -135,7 +137,7 @@ const SimpleLists: React.FC = ({
name={taskGroup.name}
onOpenComposer={id => setCurrentComposer(id)}
isComposerOpen={currentComposer === taskGroup.id}
- onSaveName={name => {}}
+ onSaveName={name => onChangeTaskGroupName(taskGroup.id, name)}
ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps}
headerProps={columnDragProvided.dragHandleProps}
diff --git a/web/src/shared/components/Login/Styles.ts b/web/src/shared/components/Login/Styles.ts
index af0e1c4..872d51b 100644
--- a/web/src/shared/components/Login/Styles.ts
+++ b/web/src/shared/components/Login/Styles.ts
@@ -101,3 +101,24 @@ export const RegisterButton = styled.button`
color: rgba(115, 103, 240);
cursor: pointer;
`;
+
+export const LogoTitle = styled.div`
+ font-size: 24px;
+ font-weight: 600;
+ margin-left: 12px;
+ transition: visibility, opacity, transform 0.25s ease;
+ color: #7367f0;
+`;
+
+export const LogoWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: row;
+ position: relative;
+ width: 100%;
+ padding-bottom: 16px;
+ margin-bottom: 24px;
+ color: rgb(222, 235, 255);
+ border-bottom: 1px solid rgba(65, 69, 97, 0.65);
+`;
diff --git a/web/src/shared/components/Login/index.tsx b/web/src/shared/components/Login/index.tsx
index f4d1732..2ad0213 100644
--- a/web/src/shared/components/Login/index.tsx
+++ b/web/src/shared/components/Login/index.tsx
@@ -1,10 +1,12 @@
import React, { useState } from 'react';
import AccessAccount from 'shared/undraw/AccessAccount';
-import { User, Lock } from 'shared/icons';
+import { User, Lock, Citadel } from 'shared/icons';
import { useForm } from 'react-hook-form';
import {
Form,
+ LogoWrapper,
+ LogoTitle,
ActionButtons,
RegisterButton,
LoginButton,
@@ -35,6 +37,10 @@ const Login = ({ onSubmit }: LoginProps) => {
+
+
+ Citadel
+
Login
Welcome back, please login into your account.