feature: various additions
This commit is contained in:
parent
4c02df9061
commit
6267a37b6e
3
api/Makefile
Normal file
3
api/Makefile
Normal file
@ -0,0 +1,3 @@
|
||||
start:
|
||||
docker container start test-db
|
||||
go run cmd/citadel/main.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)
|
||||
}
|
||||
|
@ -102,8 +102,8 @@ type NewTeam struct {
|
||||
type NewUserAccount struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
FullName string `json:"fullName"`
|
||||
Initials string `json:"initials"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
@ -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"`
|
||||
|
@ -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!
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
@ -0,0 +1 @@
|
||||
ALTER TABLE user_account DROP COLUMN first_name;
|
@ -0,0 +1 @@
|
||||
ALTER TABLE user_account DROP COLUMN last_name;
|
@ -0,0 +1 @@
|
||||
ALTER TABLE user_account ADD COLUMN initials TEXT NOT NULL DEFAULT '';
|
@ -0,0 +1 @@
|
||||
ALTER TABLE user_account ADD COLUMN profile_avatar_url TEXT;
|
@ -87,10 +87,11 @@ 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"`
|
||||
FullName string `json:"full_name"`
|
||||
Initials string `json:"initials"`
|
||||
ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 *;
|
||||
|
||||
|
@ -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 *;
|
||||
|
@ -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": [
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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))
|
||||
})
|
||||
|
@ -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,
|
||||
|
18
api/uploads/Ansible_logo.svg
Normal file
18
api/uploads/Ansible_logo.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="256px" height="315px" viewBox="0 0 256 315" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<g transform="translate(0.000000, 281.859985)" fill="#1A1918">
|
||||
<path d="M67.516834,32.0035248 C66.9753105,32.0035248 66.3906671,31.8666739 65.9847187,31.3176926 L48.6011921,8.45648614 L48.6011921,31.775177 L44.6368825,31.775177 L44.6368825,3.06093065 C44.6368825,1.91840275 45.5828006,1.00382829 46.6646822,1.00382829 C47.2943879,1.00382829 47.8355229,1.18642767 48.239529,1.73540898 L65.5791587,24.5512613 L65.5791587,1.323673 L69.5430798,1.323673 L69.5430798,29.9917765 C69.5430798,31.1808416 68.5971618,32.0035248 67.516834,32.0035248"></path>
|
||||
<path d="M100.306277,32.277976 C96.3435217,32.277976 92.1993517,31.4545041 88.3259436,29.9921314 L89.2722501,26.6994268 C92.8302228,27.8427434 96.7032425,28.665821 100.306277,28.665821 C105.305853,28.665821 107.917907,26.8824205 107.917907,24.3682281 C107.917907,22.0815948 105.890496,20.9386725 98.9097372,17.6463622 C91.9289785,14.3544463 89.1825141,12.2515955 89.1825141,8.13581322 C89.1825141,2.9701831 93.1452698,0.865754758 100.620936,0.865754758 C103.729063,0.865754758 108.502551,1.50662733 111.250957,2.32970491 L110.620086,5.71272332 C107.601695,4.98153702 103.819576,4.47869854 100.711449,4.47869854 C95.4430543,4.47869854 93.2808449,5.48398111 93.2808449,7.99935666 C93.2808449,10.4678006 94.8564686,11.5196204 102.242787,14.949176 C110.034666,18.561331 112.016238,20.39048 112.016238,24.1390915 C112.016238,29.5807898 106.881088,32.277976 100.306277,32.277976"></path>
|
||||
<path d="M130.798131,1.323673 L134.896461,1.323673 L134.896461,31.7747826 L130.798131,31.7747826 L130.798131,1.323673 Z"></path>
|
||||
<path d="M169.757834,17.9669168 L159.578047,17.9669168 L159.578047,28.3009377 L169.757834,28.3009377 C173.225294,28.3009377 175.342053,26.1058013 175.342053,23.0418388 C175.342053,20.1612645 173.135558,17.9669168 169.757834,17.9669168 M167.775097,4.79846437 L159.578047,4.79846437 L159.578047,14.5377556 L167.775097,14.5377556 C171.063473,14.5377556 173.450217,12.434116 173.450217,9.50818761 C173.450217,6.62761329 171.378132,4.79846437 167.775097,4.79846437 M169.802508,31.7750587 L157.552578,31.7750587 C156.38096,31.7750587 155.480104,30.8600898 155.480104,29.7175619 L155.480104,3.38144581 C155.480104,2.2834832 156.38096,1.32394907 157.552578,1.32394907 L167.685749,1.32394907 C173.900839,1.32394907 177.59361,4.38712277 177.59361,9.1883429 C177.59361,11.7940322 176.062271,14.2175165 173.719813,15.6349294 C177.189215,16.6867492 179.529731,19.5669291 179.529731,23.0418388 C179.529731,27.888413 175.522302,31.7750587 169.802508,31.7750587"></path>
|
||||
<path d="M199.212246,31.7750981 C198.088022,31.7750981 197.186778,30.8147752 197.186778,29.7176013 L197.186778,1.32398851 L201.284331,1.32398851 L201.284331,28.0718406 L217.588307,28.0718406 L217.588307,31.7750981 L199.212246,31.7750981 Z"></path>
|
||||
<path d="M245.526181,32.277976 C237.148883,32.277976 232.104634,29.1690539 232.104634,22.9050667 C232.104634,19.9329955 233.771547,17.0981697 237.014473,15.7722536 C234.40203,14.1260985 233.051329,11.7941111 233.051329,9.23377583 C233.051329,3.56451846 237.421198,0.865754758 245.617083,0.865754758 C248.364713,0.865754758 252.237344,1.18638824 255.480658,1.91836331 L255.029647,5.30138173 C251.607638,4.75240042 248.408609,4.43334448 245.301259,4.43334448 C239.986637,4.43334448 237.148883,5.89571709 237.148883,9.4167696 C237.148883,12.2062414 239.761714,14.4005891 243.230339,14.4005891 L250.255383,14.4005891 C251.246364,14.4005891 252.011644,15.1779182 252.011644,16.1378468 C252.011644,17.1435237 251.246364,17.9208529 250.255383,17.9208529 L242.69037,17.9208529 C238.725672,17.9208529 236.204907,19.8864583 236.204907,22.9050667 C236.204907,27.3399049 240.303238,28.665821 245.437222,28.665821 C248.229914,28.665821 252.011644,28.3010166 255.073933,27.5690415 L255.660907,30.9063115 C252.73264,31.7297835 248.903905,32.277976 245.526181,32.277976"></path>
|
||||
<path d="M14.1379982,7.03469555 L21.3157099,25.0189603 L10.4751393,16.3496305 L14.1379982,7.03469555 Z M26.8894398,29.1623494 L15.8495854,2.18772698 C15.5345384,1.41039785 14.9036674,0.99866187 14.1379982,0.99866187 C13.372329,0.99866187 12.6967842,1.41039785 12.3817372,2.18772698 L0.263885888,31.7751375 L4.40844431,31.7751375 L9.20601639,19.5768676 L23.5202622,31.3176531 C24.0959709,31.7897297 24.5112425,32.0034854 25.0523775,32.0034854 C26.1327053,32.0034854 27.0786234,31.1808022 27.0786234,29.9921314 C27.0786234,29.7992781 27.0106416,29.492448 26.8894398,29.1623494 L26.8894398,29.1623494 Z"></path>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M255.878566,127.868028 C255.878566,198.323171 198.768494,255.43274 128.312347,255.43274 C57.8622265,255.43274 0.746127325,198.323171 0.746127325,127.868028 C0.746127325,57.4179077 57.8622265,0.301808544 128.312347,0.301808544 C198.768494,0.301808544 255.878566,57.4179077 255.878566,127.868028" fill="#1A1918"></path>
|
||||
<path d="M130.459863,78.228885 L163.47146,159.705138 L113.608007,120.427335 L130.459863,78.228885 Z M189.104342,178.474123 L138.32577,56.2720498 C136.876246,52.7476854 133.977698,50.8827909 130.459863,50.8827909 C126.935499,50.8827909 123.826002,52.7476854 122.376477,56.2720498 L66.6436045,190.312411 L85.708924,190.312411 L107.771234,135.047143 L173.610097,188.23707 C176.258016,190.378208 178.168617,191.346566 180.652297,191.346566 C185.626186,191.346566 189.973756,187.617782 189.973756,182.235555 C189.973756,181.359612 189.664363,179.969354 189.104342,178.474123 L189.104342,178.474123 Z" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.5 KiB |
BIN
api/uploads/headshot-small.png
Normal file
BIN
api/uploads/headshot-small.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
api/uploads/headshot.png
Normal file
BIN
api/uploads/headshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 558 KiB |
BIN
api/uploads/logo.png
Normal file
BIN
api/uploads/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
91
api/uploads/logo.svg
Normal file
91
api/uploads/logo.svg
Normal file
@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<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="300"
|
||||
height="300"
|
||||
viewBox="0 0 300.00001 300.00001"
|
||||
id="svg4144"
|
||||
version="1.1"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="/home/jordan/blog/static/images/logo.png"
|
||||
inkscape:export-xdpi="12.6"
|
||||
inkscape:export-ydpi="12.6">
|
||||
<defs
|
||||
id="defs4146">
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 531.49605 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="1062.9921 : 531.49605 : 1"
|
||||
inkscape:persp3d-origin="531.49605 : 354.3307 : 1"
|
||||
id="perspective4164" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.979899"
|
||||
inkscape:cx="197.58796"
|
||||
inkscape:cy="137.75021"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="995"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
units="px" />
|
||||
<metadata
|
||||
id="metadata4149">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-752.36208)">
|
||||
<circle
|
||||
style="opacity:0.98000004;fill:none;fill-opacity:1;stroke:#e136ed;stroke-width:13.443;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4692"
|
||||
cx="150"
|
||||
cy="902.36206"
|
||||
r="129.61052"
|
||||
inkscape:export-xdpi="3.7273829"
|
||||
inkscape:export-ydpi="3.7273829" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:'Techno Hideo';-inkscape-font-specification:'Techno Hideo';letter-spacing:0px;word-spacing:0px;fill:#e136ed;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
|
||||
x="85.248688"
|
||||
y="796.60187"
|
||||
id="text4184"
|
||||
transform="scale(0.83194405,1.2020039)"
|
||||
inkscape:export-xdpi="3.7273829"
|
||||
inkscape:export-ydpi="3.7273829"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4186"
|
||||
x="85.248688"
|
||||
y="796.60187"
|
||||
style="font-size:181.42245483px;line-height:1.25;fill:#e136ed;fill-opacity:1;">JK</tspan></text>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
BIN
api/uploads/text4184.png
Normal file
BIN
api/uploads/text4184.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
@ -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
|
||||
|
3
web/Makefile
Normal file
3
web/Makefile
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
start:
|
||||
yarn start
|
@ -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",
|
||||
|
@ -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) => (
|
||||
<Switch>
|
||||
<Route exact path="/login" component={Login} />
|
||||
<MainContent>
|
||||
<Route exact path="/" component={Dashboard} />
|
||||
<Route exact path="/projects" component={Projects} />
|
||||
<Route path="/projects/:projectID" component={Project} />
|
||||
<Route path="/profile" component={Profile} />
|
||||
</MainContent>
|
||||
</Switch>
|
||||
);
|
||||
|
||||
|
@ -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<GlobalTopNavbarProps> = ({ 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<HTMLElement>) => {
|
||||
showPopup(
|
||||
$target,
|
||||
<Popup title={null} tab={0}>
|
||||
<ProfileMenu
|
||||
onLogout={onLogout}
|
||||
onProfile={() => {
|
||||
history.push('/profile');
|
||||
hidePopup();
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
185,
|
||||
);
|
||||
};
|
||||
|
||||
const onOpenSettings = ($target: React.RefObject<HTMLElement>) => {
|
||||
showPopup(
|
||||
@ -40,18 +56,6 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ 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<GlobalTopNavbarProps> = ({ name, projectMembers,
|
||||
<>
|
||||
<TopNavbar
|
||||
projectName={name}
|
||||
bgColor={data ? data.me.profileIcon.bgColor ?? '#7367F0' : '#7367F0'}
|
||||
firstName={data ? data.me.firstName : ''}
|
||||
lastName={data ? data.me.lastName : ''}
|
||||
initials={!data ? '' : data.me.profileIcon.initials ?? ''}
|
||||
user={data ? data.me : null}
|
||||
onNotificationClick={() => {}}
|
||||
projectMembers={projectMembers}
|
||||
onProfileClick={onProfileClick}
|
||||
onSaveProjectName={onSaveProjectName}
|
||||
onOpenSettings={onOpenSettings}
|
||||
/>
|
||||
{menu.isOpen && (
|
||||
<DropdownMenu
|
||||
onCloseDropdown={() => {
|
||||
setMenu({
|
||||
top: 0,
|
||||
left: 0,
|
||||
isOpen: false,
|
||||
});
|
||||
}}
|
||||
onLogout={onLogout}
|
||||
left={menu.left}
|
||||
top={menu.top}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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<string | null>(null);
|
||||
@ -54,9 +48,7 @@ const App = () => {
|
||||
) : (
|
||||
<>
|
||||
<Navbar />
|
||||
<MainContent>
|
||||
<Routes history={history} />
|
||||
</MainContent>
|
||||
</>
|
||||
)}
|
||||
</Router>
|
||||
|
71
web/src/Profile/index.tsx
Normal file
71
web/src/Profile/index.tsx
Normal file
@ -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<HTMLInputElement>(null);
|
||||
const [clearProfileAvatar] = useClearProfileAvatarMutation();
|
||||
const { loading, data, refetch } = useMeQuery();
|
||||
useEffect(() => {
|
||||
document.title = 'Profile | Citadel';
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
style={{ display: 'none' }}
|
||||
ref={$fileUpload}
|
||||
onChange={e => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<GlobalTopNavbar onSaveProjectName={() => {}} name={null} />
|
||||
{!loading && data && (
|
||||
<Settings
|
||||
profile={data.me.profileIcon}
|
||||
onProfileAvatarChange={() => {
|
||||
if ($fileUpload && $fileUpload.current) {
|
||||
$fileUpload.current.click();
|
||||
}
|
||||
}}
|
||||
onProfileAvatarRemove={() => {
|
||||
clearProfileAvatar();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Projects;
|
@ -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<HTMLDivElement>(null);
|
||||
const labelsRef = useRef<Array<ProjectLabel>>([]);
|
||||
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
document.title = `${data.findProject.name} | Citadel`;
|
||||
}
|
||||
}, [data]);
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
@ -562,6 +567,7 @@ const Project = () => {
|
||||
</Popup>,
|
||||
);
|
||||
}}
|
||||
onChangeTaskGroupName={(taskGroupID, name) => {}}
|
||||
onQuickEditorOpen={onQuickEditorOpen}
|
||||
onExtraMenuOpen={(taskGroupID: string, $targetRef: any) => {
|
||||
showPopup(
|
||||
|
@ -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({
|
||||
|
8
web/src/citadel.d.ts
vendored
8
web/src/citadel.d.ts
vendored
@ -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,
|
||||
|
@ -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$;
|
||||
|
25
web/src/shared/components/Admin/Admin.stories.tsx
Normal file
25
web/src/shared/components/Admin/Admin.stories.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<Admin />
|
||||
</>
|
||||
);
|
||||
};
|
323
web/src/shared/components/Admin/index.tsx
Normal file
323
web/src/shared/components/Admin/index.tsx
Normal file
@ -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 <span>Hello!</span>;
|
||||
};
|
||||
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 (
|
||||
<Root>
|
||||
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}>
|
||||
<AgGridReact
|
||||
rowSelection="multiple"
|
||||
defaultColDef={data.defaultColDef}
|
||||
columnDefs={data.columnDefs}
|
||||
rowData={data.rowData}
|
||||
frameworkComponents={data.frameworkComponents}
|
||||
onFirstDataRendered={params => {
|
||||
params.api.sizeColumnsToFit();
|
||||
}}
|
||||
onGridSizeChanged={params => {
|
||||
params.api.sizeColumnsToFit();
|
||||
}}
|
||||
></AgGridReact>
|
||||
</div>
|
||||
</Root>
|
||||
);
|
||||
};
|
||||
|
||||
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<NavItemProps> = ({ active, name, tab, onClick }) => {
|
||||
const $item = useRef<HTMLLIElement>(null);
|
||||
return (
|
||||
<TabNavItem
|
||||
key={name}
|
||||
ref={$item}
|
||||
onClick={() => {
|
||||
if ($item && $item.current) {
|
||||
const pos = $item.current.getBoundingClientRect();
|
||||
onClick(tab, pos.top);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TabNavItemButton active={active}>
|
||||
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
|
||||
<TabNavItemSpan>{name}</TabNavItemSpan>
|
||||
</TabNavItemButton>
|
||||
</TabNavItem>
|
||||
);
|
||||
};
|
||||
|
||||
const Admin = () => {
|
||||
const [currentTop, setTop] = useState(0);
|
||||
const [currentTab, setTab] = useState(0);
|
||||
const $tabNav = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Container>
|
||||
<TabNav ref={$tabNav}>
|
||||
<TabNavContent>
|
||||
{items.map((item, idx) => (
|
||||
<NavItem
|
||||
onClick={(tab, top) => {
|
||||
if ($tabNav && $tabNav.current) {
|
||||
const pos = $tabNav.current.getBoundingClientRect();
|
||||
setTab(tab);
|
||||
setTop(top - pos.top);
|
||||
}
|
||||
}}
|
||||
name={item.name}
|
||||
tab={idx}
|
||||
active={idx === currentTab}
|
||||
/>
|
||||
))}
|
||||
<TabNavLine top={currentTop} />
|
||||
</TabNavContent>
|
||||
</TabNav>
|
||||
<TabContentWrapper>
|
||||
<TabContent>
|
||||
<NewUserButton>
|
||||
<Plus color="rgba(115, 103, 240)" size={10} />
|
||||
<span>Add New</span>
|
||||
</NewUserButton>
|
||||
<ListTable />
|
||||
</TabContent>
|
||||
</TabContentWrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Admin;
|
@ -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(
|
||||
<CardMembers>
|
||||
{members &&
|
||||
members.map(member => (
|
||||
<Member key={member.id} taskID={taskID} member={member} onCardMemberClick={onCardMemberClick} />
|
||||
<TaskAssignee
|
||||
key={member.id}
|
||||
size={28}
|
||||
member={member}
|
||||
onMemberProfile={$target => {
|
||||
if (onCardMemberClick) {
|
||||
onCardMemberClick($target, taskID, member.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</CardMembers>
|
||||
</ListCardDetails>
|
||||
|
@ -33,4 +33,29 @@ const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top, onLogout, onClos
|
||||
);
|
||||
};
|
||||
|
||||
type ProfileMenuProps = {
|
||||
onProfile: () => void;
|
||||
onLogout: () => void;
|
||||
};
|
||||
|
||||
const ProfileMenu: React.FC<ProfileMenuProps> = ({ onProfile, onLogout }) => {
|
||||
return (
|
||||
<>
|
||||
<ActionItem onClick={onProfile}>
|
||||
<User size={16} color="#c2c6dc" />
|
||||
<ActionTitle>Profile</ActionTitle>
|
||||
</ActionItem>
|
||||
<Separator />
|
||||
<ActionsList>
|
||||
<ActionItem onClick={onLogout}>
|
||||
<Exit size={16} color="#c2c6dc" />
|
||||
<ActionTitle>Logout</ActionTitle>
|
||||
</ActionItem>
|
||||
</ActionsList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProfileMenu };
|
||||
|
||||
export default DropdownMenu;
|
||||
|
@ -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,6 +20,8 @@ export default {
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<PopupWrapper>
|
||||
<Popup title={null} tab={0}>
|
||||
<DueDateManager
|
||||
task={{
|
||||
id: '1',
|
||||
@ -43,13 +50,14 @@ export const Default = () => {
|
||||
{
|
||||
id: '1',
|
||||
profileIcon: { url: null, initials: null, bgColor: null },
|
||||
firstName: 'Jordan',
|
||||
lastName: 'Knott',
|
||||
fullName: 'Jordan Knott',
|
||||
},
|
||||
],
|
||||
}}
|
||||
onCancel={action('cancel')}
|
||||
onDueDateChange={action('due date change')}
|
||||
/>
|
||||
</Popup>
|
||||
</PopupWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -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`
|
||||
|
@ -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<DueDateManagerProps> = ({ 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<DueDateManagerProps> = ({ 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<DueDateFormData>();
|
||||
console.log(errors);
|
||||
return (
|
||||
<Wrapper>
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
id="endDate"
|
||||
name="endDate"
|
||||
onChange={e => {
|
||||
setTextStartDate(e.currentTarget.value);
|
||||
}}
|
||||
value={textStartDate}
|
||||
ref={register({
|
||||
required: 'End due date is required.',
|
||||
validate: value => {
|
||||
const isValid = moment(value, 'YYYY-MM-DD').isValid();
|
||||
console.log(`${value} - ${isValid}`);
|
||||
return isValid;
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</form>
|
||||
<DueDatePickerWrapper>
|
||||
<DatePicker inline selected={startDate} onChange={date => setStartDate(date ?? new Date())} />
|
||||
<DatePicker
|
||||
useWeekdaysShort
|
||||
renderCustomHeader={({
|
||||
date,
|
||||
changeYear,
|
||||
changeMonth,
|
||||
decreaseMonth,
|
||||
increaseMonth,
|
||||
prevMonthButtonDisabled,
|
||||
nextMonthButtonDisabled,
|
||||
}) => (
|
||||
<HeaderActions>
|
||||
<HeaderButton onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>
|
||||
Prev
|
||||
</HeaderButton>
|
||||
<HeaderSelectLabel>
|
||||
{months[date.getMonth()]}
|
||||
<HeaderSelect value={getYear(date)} onChange={({ target: { value } }) => changeYear(parseInt(value))}>
|
||||
{years.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</HeaderSelect>
|
||||
</HeaderSelectLabel>
|
||||
<HeaderSelectLabel>
|
||||
{date.getFullYear()}
|
||||
<HeaderSelect
|
||||
value={months[getMonth(date)]}
|
||||
onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
|
||||
>
|
||||
{months.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</HeaderSelect>
|
||||
</HeaderSelectLabel>
|
||||
|
||||
<HeaderButton onClick={increaseMonth} disabled={nextMonthButtonDisabled}>
|
||||
Next
|
||||
</HeaderButton>
|
||||
</HeaderActions>
|
||||
)}
|
||||
selected={startDate}
|
||||
inline
|
||||
onChange={date => setStartDate(date ?? new Date())}
|
||||
/>
|
||||
</DueDatePickerWrapper>
|
||||
<ActionWrapper>
|
||||
<ConfirmAddDueDate onClick={() => onDueDateChange(task, startDate)}>Save</ConfirmAddDueDate>
|
||||
|
@ -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;
|
||||
|
@ -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')}
|
||||
|
@ -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<HTMLElement>) => void;
|
||||
@ -29,6 +30,7 @@ interface SimpleProps {
|
||||
const SimpleLists: React.FC<SimpleProps> = ({
|
||||
taskGroups,
|
||||
onTaskDrop,
|
||||
onChangeTaskGroupName,
|
||||
onTaskGroupDrop,
|
||||
onTaskClick,
|
||||
onCreateTask,
|
||||
@ -135,7 +137,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
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}
|
||||
|
@ -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);
|
||||
`;
|
||||
|
@ -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) => {
|
||||
<Column>
|
||||
<LoginFormWrapper>
|
||||
<LoginFormContainer>
|
||||
<LogoWrapper>
|
||||
<Citadel size={42} />
|
||||
<LogoTitle>Citadel</LogoTitle>
|
||||
</LogoWrapper>
|
||||
<Title>Login</Title>
|
||||
<SubTitle>Welcome back, please login into your account.</SubTitle>
|
||||
<Form onSubmit={handleSubmit(loginSubmit)}>
|
||||
|
@ -39,9 +39,7 @@ const MemberManager: React.FC<MemberManagerProps> = ({
|
||||
<BoardMembersList>
|
||||
{availableMembers
|
||||
.filter(
|
||||
member =>
|
||||
currentSearch === '' ||
|
||||
`${member.firstName} ${member.lastName}`.toLowerCase().startsWith(currentSearch.toLowerCase()),
|
||||
member => currentSearch === '' || member.fullName.toLowerCase().startsWith(currentSearch.toLowerCase()),
|
||||
)
|
||||
.map(member => {
|
||||
return (
|
||||
@ -58,7 +56,7 @@ const MemberManager: React.FC<MemberManagerProps> = ({
|
||||
}}
|
||||
>
|
||||
<ProfileIcon>JK</ProfileIcon>
|
||||
<MemberName>{`${member.firstName} ${member.lastName}`}</MemberName>
|
||||
<MemberName>{member.fullName}</MemberName>
|
||||
{activeMembers.findIndex(m => m.id === member.id) !== -1 && (
|
||||
<ActiveIconWrapper>
|
||||
<Checkmark size={16} color="#42526e" />
|
||||
|
@ -227,8 +227,7 @@ export const MemberManagerPopup = () => {
|
||||
availableMembers={[
|
||||
{
|
||||
id: '1',
|
||||
firstName: 'Jordan',
|
||||
lastName: 'Knott',
|
||||
fullName: 'Jordan Knott',
|
||||
profileIcon: { bgColor: null, url: null, initials: null },
|
||||
},
|
||||
]}
|
||||
@ -293,8 +292,7 @@ export const DueDateManagerPopup = () => {
|
||||
{
|
||||
id: '1',
|
||||
profileIcon: { bgColor: null, url: null, initials: null },
|
||||
firstName: 'Jordan',
|
||||
lastName: 'Knott',
|
||||
fullName: 'Jordan Knott',
|
||||
},
|
||||
],
|
||||
}}
|
||||
|
46
web/src/shared/components/ProfileIcon/index.tsx
Normal file
46
web/src/shared/components/ProfileIcon/index.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
|
||||
margin-left: 10px;
|
||||
width: ${props => props.size}px;
|
||||
height: ${props => props.size}px;
|
||||
border-radius: 9999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
`;
|
||||
|
||||
type ProfileIconProps = {
|
||||
user: TaskUser;
|
||||
onProfileClick: ($target: React.RefObject<HTMLElement>, user: TaskUser) => void;
|
||||
size: number | string;
|
||||
};
|
||||
|
||||
const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size }) => {
|
||||
const $profileRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Container
|
||||
ref={$profileRef}
|
||||
onClick={() => {
|
||||
onProfileClick($profileRef, user);
|
||||
}}
|
||||
size={size}
|
||||
backgroundURL={user.profileIcon.url ?? null}
|
||||
bgColor={user.profileIcon.bgColor ?? null}
|
||||
>
|
||||
{(!user.profileIcon.url && user.profileIcon.initials) ?? ''}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
ProfileIcon.defaultProps = {
|
||||
size: 28,
|
||||
};
|
||||
|
||||
export default ProfileIcon;
|
@ -3,14 +3,14 @@ import TextareaAutosize from 'react-autosize-textarea';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
|
||||
export const Wrapper = styled.div<{ open: boolean }>`
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
bottom: 0;
|
||||
color: #fff;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
z-index: 100;
|
||||
visibility: ${props => (props.open ? 'show' : 'hidden')};
|
||||
`;
|
||||
|
||||
|
30
web/src/shared/components/Settings/Settings.stories.tsx
Normal file
30
web/src/shared/components/Settings/Settings.stories.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { createRef, useState } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import NormalizeStyles from 'App/NormalizeStyles';
|
||||
import BaseStyles from 'App/BaseStyles';
|
||||
import Settings from '.';
|
||||
|
||||
export default {
|
||||
component: Settings,
|
||||
title: 'Settings',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff', default: true },
|
||||
{ name: 'gray', value: '#f8f8f8' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const profile = { url: 'http://localhost:3333/uploads/headshot.png', bgColor: '#000', initials: 'JK' };
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<Settings
|
||||
profile={profile}
|
||||
onProfileAvatarRemove={action('remove')}
|
||||
onProfileAvatarChange={action('profile avatar change')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
362
web/src/shared/components/Settings/index.tsx
Normal file
362
web/src/shared/components/Settings/index.tsx
Normal file
@ -0,0 +1,362 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { User } from 'shared/icons';
|
||||
|
||||
const TextFieldWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
|
||||
margin-bottom: 2.2rem;
|
||||
margin-top: 17px;
|
||||
`;
|
||||
|
||||
const TextFieldLabel = styled.span`
|
||||
padding: 0.7rem !important;
|
||||
color: #c2c6dc;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transition: all 0.2s ease;
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
font-size: 0.85rem;
|
||||
cursor: text;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const TextFieldInput = styled.input`
|
||||
font-size: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
background: #262c49;
|
||||
padding: 0.7rem !important;
|
||||
color: #c2c6dc;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
&:focus {
|
||||
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid rgba(115, 103, 240);
|
||||
}
|
||||
&:focus ~ ${TextFieldLabel} {
|
||||
color: rgba(115, 103, 240);
|
||||
transform: translate(-3px, -90%);
|
||||
}
|
||||
`;
|
||||
|
||||
type TextFieldProps = {
|
||||
label: string;
|
||||
};
|
||||
const TextField: React.FC<TextFieldProps> = ({ label }) => {
|
||||
return (
|
||||
<TextFieldWrapper>
|
||||
<TextFieldInput />
|
||||
<TextFieldLabel>{label}</TextFieldLabel>
|
||||
</TextFieldWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2.2rem !important;
|
||||
`;
|
||||
|
||||
const AvatarContainer = styled.div`
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
margin-bottom: 1rem;
|
||||
margin-right: 1rem;
|
||||
`;
|
||||
const AvatarMask = styled.div<{ background: string }>`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: ${props => props.background};
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const AvatarImg = styled.img<{ src: string }>`
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const ActionButtons = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
`;
|
||||
const UploadButton = styled.div`
|
||||
margin-right: 1rem;
|
||||
padding: 0.75rem 2rem;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
background: rgba(115, 103, 240);
|
||||
`;
|
||||
|
||||
const RemoveButton = styled.button`
|
||||
display: inline-block;
|
||||
border: 1px solid rgba(234, 84, 85, 1);
|
||||
background: transparent;
|
||||
color: rgba(234, 84, 85, 1);
|
||||
padding: 0.75rem 2rem;
|
||||
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const ImgLabel = styled.p`
|
||||
color: #c2c6dc;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 12.25px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const AvatarInitials = styled.span`
|
||||
font-size: 32px;
|
||||
color: #fff;
|
||||
`;
|
||||
|
||||
type AvatarSettingsProps = {
|
||||
onProfileAvatarChange: () => void;
|
||||
onProfileAvatarRemove: () => void;
|
||||
profile: ProfileIcon;
|
||||
};
|
||||
|
||||
const AvatarSettings: React.FC<AvatarSettingsProps> = ({ profile, onProfileAvatarChange, onProfileAvatarRemove }) => {
|
||||
return (
|
||||
<ProfileContainer>
|
||||
<AvatarContainer>
|
||||
<AvatarMask
|
||||
background={profile.url ? 'none' : profile.bgColor ?? 'none'}
|
||||
onClick={() => onProfileAvatarChange()}
|
||||
>
|
||||
{profile.url ? (
|
||||
<AvatarImg alt="" src={profile.url ?? ''} />
|
||||
) : (
|
||||
<AvatarInitials>{profile.initials}</AvatarInitials>
|
||||
)}
|
||||
</AvatarMask>
|
||||
</AvatarContainer>
|
||||
<ActionButtons>
|
||||
<UploadButton onClick={() => onProfileAvatarChange()}>Upload photo</UploadButton>
|
||||
<RemoveButton onClick={() => onProfileAvatarRemove()}>Remove</RemoveButton>
|
||||
<ImgLabel>Allowed JPG, GIF or PNG. Max size of 800kB</ImgLabel>
|
||||
</ActionButtons>
|
||||
</ProfileContainer>
|
||||
);
|
||||
};
|
||||
|
||||
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 TabContentInner = styled.div``;
|
||||
|
||||
const items = [{ name: 'General' }, { name: 'Change Password' }, { name: 'Info' }, { name: 'Notifications' }];
|
||||
type NavItemProps = {
|
||||
active: boolean;
|
||||
name: string;
|
||||
tab: number;
|
||||
onClick: (tab: number, top: number) => void;
|
||||
};
|
||||
const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
|
||||
const $item = useRef<HTMLLIElement>(null);
|
||||
return (
|
||||
<TabNavItem
|
||||
key={name}
|
||||
ref={$item}
|
||||
onClick={() => {
|
||||
if ($item && $item.current) {
|
||||
const pos = $item.current.getBoundingClientRect();
|
||||
onClick(tab, pos.top);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TabNavItemButton active={active}>
|
||||
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
|
||||
<TabNavItemSpan>{name}</TabNavItemSpan>
|
||||
</TabNavItemButton>
|
||||
</TabNavItem>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingActions = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const SaveButton = styled.div`
|
||||
margin-right: 1rem;
|
||||
padding: 0.75rem 2rem;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
background: rgba(115, 103, 240);
|
||||
`;
|
||||
|
||||
type SettingsProps = {
|
||||
onProfileAvatarChange: () => void;
|
||||
onProfileAvatarRemove: () => void;
|
||||
profile: ProfileIcon;
|
||||
};
|
||||
|
||||
const Settings: React.FC<SettingsProps> = ({ onProfileAvatarRemove, onProfileAvatarChange, profile }) => {
|
||||
const [currentTab, setTab] = useState(0);
|
||||
const [currentTop, setTop] = useState(0);
|
||||
const $tabNav = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Container>
|
||||
<TabNav ref={$tabNav}>
|
||||
<TabNavContent>
|
||||
{items.map((item, idx) => (
|
||||
<NavItem
|
||||
onClick={(tab, top) => {
|
||||
if ($tabNav && $tabNav.current) {
|
||||
const pos = $tabNav.current.getBoundingClientRect();
|
||||
setTab(tab);
|
||||
setTop(top - pos.top);
|
||||
}
|
||||
}}
|
||||
name={item.name}
|
||||
tab={idx}
|
||||
active={idx === currentTab}
|
||||
/>
|
||||
))}
|
||||
<TabNavLine top={currentTop} />
|
||||
</TabNavContent>
|
||||
</TabNav>
|
||||
<TabContentWrapper>
|
||||
<TabContent>
|
||||
<AvatarSettings
|
||||
onProfileAvatarRemove={onProfileAvatarRemove}
|
||||
onProfileAvatarChange={onProfileAvatarChange}
|
||||
profile={profile}
|
||||
/>
|
||||
<TextField label="Name" />
|
||||
<TextField label="Initials " />
|
||||
<TextField label="Username " />
|
||||
<TextField label="Email" />
|
||||
<TextField label="Bio" />
|
||||
<SettingActions>
|
||||
<SaveButton>Save Change</SaveButton>
|
||||
</SettingActions>
|
||||
</TabContent>
|
||||
</TabContentWrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
25
web/src/shared/components/Tabs/Tabs.stories.tsx
Normal file
25
web/src/shared/components/Tabs/Tabs.stories.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React, { useRef } from 'react';
|
||||
import NormalizeStyles from 'App/NormalizeStyles';
|
||||
import BaseStyles from 'App/BaseStyles';
|
||||
import Tabs from '.';
|
||||
|
||||
export default {
|
||||
component: Tabs,
|
||||
title: 'Tabs',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'gray', value: '#f8f8f8', default: true },
|
||||
{ name: 'white', value: '#ffffff' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<Tabs />
|
||||
</>
|
||||
);
|
||||
};
|
8
web/src/shared/components/Tabs/index.tsx
Normal file
8
web/src/shared/components/Tabs/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Tabs = () => {
|
||||
return <span>HEllo!</span>;
|
||||
};
|
||||
|
||||
export default Tabs;
|
@ -8,7 +8,7 @@ const TaskDetailAssignee = styled.div`
|
||||
margin-right: 4px;
|
||||
`;
|
||||
|
||||
const ProfileIcon = styled.div<{ size: string | number }>`
|
||||
export const Wrapper = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
|
||||
width: ${props => props.size}px;
|
||||
height: ${props => props.size}px;
|
||||
border-radius: 9999px;
|
||||
@ -16,10 +16,10 @@ const ProfileIcon = styled.div<{ size: string | number }>`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
background: rgb(115, 103, 240);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
`;
|
||||
|
||||
type TaskAssigneeProps = {
|
||||
@ -31,8 +31,17 @@ type TaskAssigneeProps = {
|
||||
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ member, onMemberProfile, size }) => {
|
||||
const $memberRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<TaskDetailAssignee ref={$memberRef} onClick={() => onMemberProfile($memberRef, member.id)} key={member.id}>
|
||||
<ProfileIcon size={size}>{member.profileIcon.initials ?? ''}</ProfileIcon>
|
||||
<TaskDetailAssignee
|
||||
ref={$memberRef}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onMemberProfile($memberRef, member.id);
|
||||
}}
|
||||
key={member.id}
|
||||
>
|
||||
<Wrapper backgroundURL={member.profileIcon.url ?? null} bgColor={member.profileIcon.bgColor ?? null} size={size}>
|
||||
{(!member.profileIcon.url && member.profileIcon.initials) ?? ''}
|
||||
</Wrapper>
|
||||
</TaskDetailAssignee>
|
||||
);
|
||||
};
|
||||
|
@ -55,8 +55,7 @@ export const Default = () => {
|
||||
{
|
||||
id: '1',
|
||||
profileIcon: { bgColor: null, url: null, initials: null },
|
||||
firstName: 'Jordan',
|
||||
lastName: 'Knott',
|
||||
fullName: 'Jordan Knott',
|
||||
},
|
||||
],
|
||||
}}
|
||||
|
@ -68,7 +68,7 @@ export const ProfileNameSecondary = styled.small`
|
||||
color: #c2c6dc;
|
||||
`;
|
||||
|
||||
export const ProfileIcon = styled.div<{ bgColor: string }>`
|
||||
export const ProfileIcon = styled.div<{ bgColor: string | null; backgroundURL: string | null }>`
|
||||
margin-left: 10px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@ -78,8 +78,9 @@ export const ProfileIcon = styled.div<{ bgColor: string }>`
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
background: ${props => props.bgColor};
|
||||
cursor: pointer;
|
||||
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
`;
|
||||
|
||||
export const ProjectMeta = styled.div`
|
||||
|
@ -21,42 +21,25 @@ export default {
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const [menu, setMenu] = useState({
|
||||
top: 0,
|
||||
left: 0,
|
||||
isOpen: false,
|
||||
});
|
||||
const onClick = (bottom: number, right: number) => {
|
||||
setMenu({
|
||||
isOpen: !menu.isOpen,
|
||||
left: right,
|
||||
top: bottom,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<TopNavbar
|
||||
projectName="Projects"
|
||||
bgColor="#7367F0"
|
||||
firstName="Jordan"
|
||||
lastName="Knott"
|
||||
initials="JK"
|
||||
user={{
|
||||
id: '1',
|
||||
fullName: 'Jordan Knott',
|
||||
profileIcon: {
|
||||
url: null,
|
||||
initials: 'JK',
|
||||
bgColor: '#000',
|
||||
},
|
||||
}}
|
||||
onNotificationClick={action('notifications click')}
|
||||
onOpenSettings={action('open settings')}
|
||||
onProfileClick={onClick}
|
||||
onProfileClick={action('profile click')}
|
||||
/>
|
||||
{menu.isOpen && (
|
||||
<DropdownMenu
|
||||
onCloseDropdown={() => {
|
||||
setMenu({ left: 0, top: 0, isOpen: false });
|
||||
}}
|
||||
onLogout={action('on logout')}
|
||||
left={menu.left}
|
||||
top={menu.top}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Star, Ellipsis, Bell, Cog, AngleDown } from 'shared/icons';
|
||||
|
||||
import ProfileIcon from 'shared/components/ProfileIcon';
|
||||
import {
|
||||
NotificationContainer,
|
||||
ProjectNameTextarea,
|
||||
@ -18,7 +18,6 @@ import {
|
||||
Breadcrumbs,
|
||||
BreadcrumpSeparator,
|
||||
ProjectSettingsButton,
|
||||
ProfileIcon,
|
||||
ProfileContainer,
|
||||
ProfileNameWrapper,
|
||||
ProfileNamePrimary,
|
||||
@ -110,14 +109,11 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
|
||||
|
||||
type NavBarProps = {
|
||||
projectName: string | null;
|
||||
onProfileClick: (bottom: number, right: number) => void;
|
||||
onProfileClick: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onSaveProjectName?: (projectName: string) => void;
|
||||
onNotificationClick: () => void;
|
||||
bgColor: string;
|
||||
user: TaskUser | null;
|
||||
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
initials: string;
|
||||
projectMembers?: Array<TaskUser> | null;
|
||||
};
|
||||
|
||||
@ -126,17 +122,14 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
onSaveProjectName,
|
||||
onProfileClick,
|
||||
onNotificationClick,
|
||||
firstName,
|
||||
lastName,
|
||||
initials,
|
||||
bgColor,
|
||||
user,
|
||||
projectMembers,
|
||||
onOpenSettings,
|
||||
}) => {
|
||||
const $profileRef: any = useRef(null);
|
||||
const handleProfileClick = () => {
|
||||
const boundingRect = $profileRef.current.getBoundingClientRect();
|
||||
onProfileClick(boundingRect.bottom, boundingRect.right);
|
||||
const handleProfileClick = ($target: React.RefObject<HTMLElement>) => {
|
||||
if ($target && $target.current) {
|
||||
onProfileClick($target);
|
||||
}
|
||||
};
|
||||
const { showPopup } = usePopup();
|
||||
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
|
||||
@ -189,15 +182,16 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
<NotificationContainer onClick={onNotificationClick}>
|
||||
<Bell color="#c2c6dc" size={20} />
|
||||
</NotificationContainer>
|
||||
|
||||
{user && (
|
||||
<ProfileContainer>
|
||||
<ProfileNameWrapper>
|
||||
<ProfileNamePrimary>{`${firstName} ${lastName}`}</ProfileNamePrimary>
|
||||
<ProfileNamePrimary>{user.fullName}</ProfileNamePrimary>
|
||||
<ProfileNameSecondary>Manager</ProfileNameSecondary>
|
||||
</ProfileNameWrapper>
|
||||
<ProfileIcon ref={$profileRef} onClick={handleProfileClick} bgColor={bgColor}>
|
||||
{initials}
|
||||
</ProfileIcon>
|
||||
<ProfileIcon user={user} size={40} onProfileClick={handleProfileClick} />}
|
||||
</ProfileContainer>
|
||||
)}
|
||||
</GlobalActions>
|
||||
</NavbarHeader>
|
||||
</NavbarWrapper>
|
||||
|
@ -11,10 +11,12 @@ export type Scalars = {
|
||||
Float: number;
|
||||
Time: any;
|
||||
UUID: string;
|
||||
Upload: any;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export type ProjectLabel = {
|
||||
__typename?: 'ProjectLabel';
|
||||
id: Scalars['ID'];
|
||||
@ -48,8 +50,7 @@ export type ProfileIcon = {
|
||||
export type ProjectMember = {
|
||||
__typename?: 'ProjectMember';
|
||||
id: Scalars['ID'];
|
||||
firstName: Scalars['String'];
|
||||
lastName: Scalars['String'];
|
||||
fullName: Scalars['String'];
|
||||
profileIcon: ProfileIcon;
|
||||
};
|
||||
|
||||
@ -66,8 +67,8 @@ export type UserAccount = {
|
||||
id: Scalars['ID'];
|
||||
email: Scalars['String'];
|
||||
createdAt: Scalars['Time'];
|
||||
firstName: Scalars['String'];
|
||||
lastName: Scalars['String'];
|
||||
fullName: Scalars['String'];
|
||||
initials: Scalars['String'];
|
||||
username: Scalars['String'];
|
||||
profileIcon: ProfileIcon;
|
||||
};
|
||||
@ -169,8 +170,8 @@ export type NewRefreshToken = {
|
||||
export type NewUserAccount = {
|
||||
username: Scalars['String'];
|
||||
email: Scalars['String'];
|
||||
firstName: Scalars['String'];
|
||||
lastName: Scalars['String'];
|
||||
fullName: Scalars['String'];
|
||||
initials: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
};
|
||||
|
||||
@ -314,6 +315,7 @@ export type Mutation = {
|
||||
createRefreshToken: RefreshToken;
|
||||
createUserAccount: UserAccount;
|
||||
createTeam: Team;
|
||||
clearProfileAvatar: UserAccount;
|
||||
createProject: Project;
|
||||
updateProjectName: Project;
|
||||
createProjectLabel: ProjectLabel;
|
||||
@ -470,11 +472,26 @@ export type AssignTaskMutation = (
|
||||
& Pick<Task, 'id'>
|
||||
& { assigned: Array<(
|
||||
{ __typename?: 'ProjectMember' }
|
||||
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
|
||||
& Pick<ProjectMember, 'id' | 'fullName'>
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type ClearProfileAvatarMutationVariables = {};
|
||||
|
||||
|
||||
export type ClearProfileAvatarMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { clearProfileAvatar: (
|
||||
{ __typename?: 'UserAccount' }
|
||||
& Pick<UserAccount, 'id' | 'fullName'>
|
||||
& { profileIcon: (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type CreateProjectMutationVariables = {
|
||||
teamID: Scalars['UUID'];
|
||||
userID: Scalars['UUID'];
|
||||
@ -541,7 +558,7 @@ export type CreateTaskMutation = (
|
||||
) }
|
||||
)>, assigned: Array<(
|
||||
{ __typename?: 'ProjectMember' }
|
||||
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
|
||||
& Pick<ProjectMember, 'id' | 'fullName'>
|
||||
& { profileIcon: (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
|
||||
@ -624,7 +641,7 @@ export type FindProjectQuery = (
|
||||
& Pick<Project, 'name'>
|
||||
& { members: Array<(
|
||||
{ __typename?: 'ProjectMember' }
|
||||
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
|
||||
& Pick<ProjectMember, 'id' | 'fullName'>
|
||||
& { profileIcon: (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
|
||||
@ -658,7 +675,7 @@ export type FindProjectQuery = (
|
||||
) }
|
||||
)>, assigned: Array<(
|
||||
{ __typename?: 'ProjectMember' }
|
||||
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
|
||||
& Pick<ProjectMember, 'id' | 'fullName'>
|
||||
& { profileIcon: (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
|
||||
@ -698,7 +715,7 @@ export type FindTaskQuery = (
|
||||
) }
|
||||
)>, assigned: Array<(
|
||||
{ __typename?: 'ProjectMember' }
|
||||
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
|
||||
& Pick<ProjectMember, 'id' | 'fullName'>
|
||||
& { profileIcon: (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
|
||||
@ -732,10 +749,10 @@ export type MeQuery = (
|
||||
{ __typename?: 'Query' }
|
||||
& { me: (
|
||||
{ __typename?: 'UserAccount' }
|
||||
& Pick<UserAccount, 'firstName' | 'lastName'>
|
||||
& Pick<UserAccount, 'id' | 'fullName'>
|
||||
& { profileIcon: (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'initials' | 'bgColor'>
|
||||
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
|
||||
) }
|
||||
) }
|
||||
);
|
||||
@ -783,7 +800,7 @@ export type UnassignTaskMutation = (
|
||||
& Pick<Task, 'id'>
|
||||
& { assigned: Array<(
|
||||
{ __typename?: 'ProjectMember' }
|
||||
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
|
||||
& Pick<ProjectMember, 'id' | 'fullName'>
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
@ -831,7 +848,7 @@ export type UpdateTaskDescriptionMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { updateTaskDescription: (
|
||||
{ __typename?: 'Task' }
|
||||
& Pick<Task, 'id'>
|
||||
& Pick<Task, 'id' | 'description'>
|
||||
) }
|
||||
);
|
||||
|
||||
@ -893,8 +910,7 @@ export const AssignTaskDocument = gql`
|
||||
id
|
||||
assigned {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -925,6 +941,43 @@ 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 ClearProfileAvatarDocument = gql`
|
||||
mutation clearProfileAvatar {
|
||||
clearProfileAvatar {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
initials
|
||||
bgColor
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type ClearProfileAvatarMutationFn = ApolloReactCommon.MutationFunction<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useClearProfileAvatarMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useClearProfileAvatarMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useClearProfileAvatarMutation` 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 [clearProfileAvatarMutation, { data, loading, error }] = useClearProfileAvatarMutation({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useClearProfileAvatarMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>) {
|
||||
return ApolloReactHooks.useMutation<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>(ClearProfileAvatarDocument, baseOptions);
|
||||
}
|
||||
export type ClearProfileAvatarMutationHookResult = ReturnType<typeof useClearProfileAvatarMutation>;
|
||||
export type ClearProfileAvatarMutationResult = ApolloReactCommon.MutationResult<ClearProfileAvatarMutation>;
|
||||
export type ClearProfileAvatarMutationOptions = ApolloReactCommon.BaseMutationOptions<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>;
|
||||
export const CreateProjectDocument = gql`
|
||||
mutation createProject($teamID: UUID!, $userID: UUID!, $name: String!) {
|
||||
createProject(input: {teamID: $teamID, userID: $userID, name: $name}) {
|
||||
@ -1035,8 +1088,7 @@ export const CreateTaskDocument = gql`
|
||||
}
|
||||
assigned {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
@ -1219,8 +1271,7 @@ export const FindProjectDocument = gql`
|
||||
name
|
||||
members {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
@ -1269,8 +1320,7 @@ export const FindProjectDocument = gql`
|
||||
}
|
||||
assigned {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
@ -1341,8 +1391,7 @@ export const FindTaskDocument = gql`
|
||||
}
|
||||
assigned {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
@ -1423,11 +1472,12 @@ export type GetProjectsQueryResult = ApolloReactCommon.QueryResult<GetProjectsQu
|
||||
export const MeDocument = gql`
|
||||
query me {
|
||||
me {
|
||||
firstName
|
||||
lastName
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
initials
|
||||
bgColor
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1513,8 +1563,7 @@ export const UnassignTaskDocument = gql`
|
||||
unassignTask(input: {taskID: $taskID, userID: $userID}) {
|
||||
assigned {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
id
|
||||
}
|
||||
@ -1626,6 +1675,7 @@ export const UpdateTaskDescriptionDocument = gql`
|
||||
mutation updateTaskDescription($taskID: UUID!, $description: String!) {
|
||||
updateTaskDescription(input: {taskID: $taskID, description: $description}) {
|
||||
id
|
||||
description
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -3,8 +3,7 @@ mutation assignTask($taskID: UUID!, $userID: UUID!) {
|
||||
id
|
||||
assigned {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
11
web/src/shared/graphql/clearAvatarProfile.graphqls
Normal file
11
web/src/shared/graphql/clearAvatarProfile.graphqls
Normal file
@ -0,0 +1,11 @@
|
||||
mutation clearProfileAvatar {
|
||||
clearProfileAvatar {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
initials
|
||||
bgColor
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
@ -26,8 +26,7 @@ mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
|
||||
}
|
||||
assigned {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
|
@ -3,8 +3,7 @@ query findProject($projectId: String!) {
|
||||
name
|
||||
members {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
@ -53,8 +52,7 @@ query findProject($projectId: String!) {
|
||||
}
|
||||
assigned {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
|
@ -24,8 +24,7 @@ query findTask($taskID: UUID!) {
|
||||
}
|
||||
assigned {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
|
@ -1,10 +1,11 @@
|
||||
query me {
|
||||
me {
|
||||
firstName
|
||||
lastName
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
initials
|
||||
bgColor
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ mutation unassignTask($taskID: UUID!, $userID: UUID!) {
|
||||
unassignTask(input: {taskID: $taskID, userID: $userID}) {
|
||||
assigned {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
id
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
mutation updateTaskDescription($taskID: UUID!, $description: String!) {
|
||||
updateTaskDescription(input: {taskID: $taskID, description: $description}) {
|
||||
id
|
||||
description
|
||||
}
|
||||
}
|
||||
|
@ -2971,6 +2971,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
|
||||
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
|
||||
|
||||
"@types/axios@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46"
|
||||
integrity sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=
|
||||
dependencies:
|
||||
axios "*"
|
||||
|
||||
"@types/babel-types@*", "@types/babel-types@^7.0.0":
|
||||
version "7.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.7.tgz#667eb1640e8039436028055737d2b9986ee336e3"
|
||||
@ -3035,6 +3042,13 @@
|
||||
dependencies:
|
||||
"@types/color-convert" "*"
|
||||
|
||||
"@types/date-fns@^2.6.0":
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/date-fns/-/date-fns-2.6.0.tgz#b062ca46562002909be0c63a6467ed173136acc1"
|
||||
integrity sha1-sGLKRlYgApCb4MY6ZGftFzE2rME=
|
||||
dependencies:
|
||||
date-fns "*"
|
||||
|
||||
"@types/eslint-visitor-keys@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
||||
@ -3724,6 +3738,18 @@ adjust-sourcemap-loader@2.0.0:
|
||||
object-path "0.11.4"
|
||||
regex-parser "2.2.10"
|
||||
|
||||
ag-grid-community@^23.2.0:
|
||||
version "23.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-23.2.0.tgz#889f52e8eb91c167c2ac7477938cbf498a54f67c"
|
||||
integrity sha512-aG7Ghfu79HeqOCd50GhFSeZUX1Tw9BVUX1VKMuglkAcwYPTQjuYvYT7QVQB5FGzfFjcVq4a1QFfcgdoAcZYJIA==
|
||||
|
||||
ag-grid-react@^23.2.0:
|
||||
version "23.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ag-grid-react/-/ag-grid-react-23.2.0.tgz#00a54cb4e83c0d35a49c202e5833e3bb20d8cfa8"
|
||||
integrity sha512-lDGV+WX0Nj5biNOJRSErFehXG+nqkbuXPMS7YJxEDWJLJxtOF0INP5sL6dtxV12j/XHqXa+M2CgQBXZWZq+EWg==
|
||||
dependencies:
|
||||
prop-types "^15.6.2"
|
||||
|
||||
agent-base@4, agent-base@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
|
||||
@ -4399,6 +4425,18 @@ aws4@^1.8.0:
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
|
||||
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
|
||||
|
||||
axios-auth-refresh@^2.2.7:
|
||||
version "2.2.7"
|
||||
resolved "https://registry.yarnpkg.com/axios-auth-refresh/-/axios-auth-refresh-2.2.7.tgz#922f458129ed653d9bd0d732743bf9bba4524f12"
|
||||
integrity sha512-5gdrwRG3luW/BHIwyh7vZk9AFkC3tOkWhE4yJJW0Dno36kcTHN8V87SxH52m3HF8bQpORaV3RNmuwlOT8pKOOw==
|
||||
|
||||
axios@*, axios@^0.19.2:
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
|
||||
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
|
||||
dependencies:
|
||||
follow-redirects "1.5.10"
|
||||
|
||||
axios@0.19.0:
|
||||
version "0.19.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
|
||||
@ -6351,6 +6389,11 @@ data-urls@^1.0.0, data-urls@^1.1.0:
|
||||
whatwg-mimetype "^2.2.0"
|
||||
whatwg-url "^7.0.0"
|
||||
|
||||
date-fns@*, date-fns@^2.14.0:
|
||||
version "2.14.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.14.0.tgz#359a87a265bb34ef2e38f93ecf63ac453f9bc7ba"
|
||||
integrity sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw==
|
||||
|
||||
date-fns@^1.27.2:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
|
||||
|
Loading…
Reference in New Issue
Block a user