feature: add due dates to tasks

This commit is contained in:
Jordan Knott 2020-06-15 17:36:59 -05:00
parent a12e9c1e50
commit b6f0e8b6b2
32 changed files with 816 additions and 222 deletions

View File

@ -94,6 +94,7 @@ type ComplexityRoot struct {
UpdateProjectLabelName func(childComplexity int, input UpdateProjectLabelName) int UpdateProjectLabelName func(childComplexity int, input UpdateProjectLabelName) int
UpdateProjectName func(childComplexity int, input *UpdateProjectName) int UpdateProjectName func(childComplexity int, input *UpdateProjectName) int
UpdateTaskDescription func(childComplexity int, input UpdateTaskDescriptionInput) int UpdateTaskDescription func(childComplexity int, input UpdateTaskDescriptionInput) int
UpdateTaskDueDate func(childComplexity int, input UpdateTaskDueDate) int
UpdateTaskGroupLocation func(childComplexity int, input NewTaskGroupLocation) int UpdateTaskGroupLocation func(childComplexity int, input NewTaskGroupLocation) int
UpdateTaskGroupName func(childComplexity int, input UpdateTaskGroupName) int UpdateTaskGroupName func(childComplexity int, input UpdateTaskGroupName) int
UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int
@ -153,6 +154,7 @@ type ComplexityRoot struct {
Assigned func(childComplexity int) int Assigned func(childComplexity int) int
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
Description func(childComplexity int) int Description func(childComplexity int) int
DueDate func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Labels func(childComplexity int) int Labels func(childComplexity int) int
Name func(childComplexity int) int Name func(childComplexity int) int
@ -228,6 +230,7 @@ type MutationResolver interface {
UpdateTaskDescription(ctx context.Context, input UpdateTaskDescriptionInput) (*pg.Task, error) UpdateTaskDescription(ctx context.Context, input UpdateTaskDescriptionInput) (*pg.Task, error)
UpdateTaskLocation(ctx context.Context, input NewTaskLocation) (*UpdateTaskLocationPayload, error) UpdateTaskLocation(ctx context.Context, input NewTaskLocation) (*UpdateTaskLocationPayload, error)
UpdateTaskName(ctx context.Context, input UpdateTaskName) (*pg.Task, error) UpdateTaskName(ctx context.Context, input UpdateTaskName) (*pg.Task, error)
UpdateTaskDueDate(ctx context.Context, input UpdateTaskDueDate) (*pg.Task, error)
DeleteTask(ctx context.Context, input DeleteTaskInput) (*DeleteTaskPayload, error) DeleteTask(ctx context.Context, input DeleteTaskInput) (*DeleteTaskPayload, error)
AssignTask(ctx context.Context, input *AssignTaskInput) (*pg.Task, error) AssignTask(ctx context.Context, input *AssignTaskInput) (*pg.Task, error)
UnassignTask(ctx context.Context, input *UnassignTaskInput) (*pg.Task, error) UnassignTask(ctx context.Context, input *UnassignTaskInput) (*pg.Task, error)
@ -267,6 +270,7 @@ type TaskResolver interface {
TaskGroup(ctx context.Context, obj *pg.Task) (*pg.TaskGroup, error) TaskGroup(ctx context.Context, obj *pg.Task) (*pg.TaskGroup, error)
Description(ctx context.Context, obj *pg.Task) (*string, error) Description(ctx context.Context, obj *pg.Task) (*string, error)
DueDate(ctx context.Context, obj *pg.Task) (*time.Time, error)
Assigned(ctx context.Context, obj *pg.Task) ([]ProjectMember, error) Assigned(ctx context.Context, obj *pg.Task) ([]ProjectMember, error)
Labels(ctx context.Context, obj *pg.Task) ([]pg.TaskLabel, error) Labels(ctx context.Context, obj *pg.Task) ([]pg.TaskLabel, error)
} }
@ -619,6 +623,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.UpdateTaskDescription(childComplexity, args["input"].(UpdateTaskDescriptionInput)), true return e.complexity.Mutation.UpdateTaskDescription(childComplexity, args["input"].(UpdateTaskDescriptionInput)), true
case "Mutation.updateTaskDueDate":
if e.complexity.Mutation.UpdateTaskDueDate == nil {
break
}
args, err := ec.field_Mutation_updateTaskDueDate_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UpdateTaskDueDate(childComplexity, args["input"].(UpdateTaskDueDate)), true
case "Mutation.updateTaskGroupLocation": case "Mutation.updateTaskGroupLocation":
if e.complexity.Mutation.UpdateTaskGroupLocation == nil { if e.complexity.Mutation.UpdateTaskGroupLocation == nil {
break break
@ -925,6 +941,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Task.Description(childComplexity), true return e.complexity.Task.Description(childComplexity), true
case "Task.dueDate":
if e.complexity.Task.DueDate == nil {
break
}
return e.complexity.Task.DueDate(childComplexity), true
case "Task.id": case "Task.id":
if e.complexity.Task.ID == nil { if e.complexity.Task.ID == nil {
break break
@ -1271,6 +1294,7 @@ type Task {
name: String! name: String!
position: Float! position: Float!
description: String description: String
dueDate: Time
assigned: [ProjectMember!]! assigned: [ProjectMember!]!
labels: [TaskLabel!]! labels: [TaskLabel!]!
} }
@ -1448,6 +1472,11 @@ input UpdateTaskGroupName {
name: String! name: String!
} }
input UpdateTaskDueDate {
taskID: UUID!
dueDate: Time
}
type Mutation { type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken! createRefreshToken(input: NewRefreshToken!): RefreshToken!
@ -1478,6 +1507,7 @@ type Mutation {
updateTaskDescription(input: UpdateTaskDescriptionInput!): Task! updateTaskDescription(input: UpdateTaskDescriptionInput!): Task!
updateTaskLocation(input: NewTaskLocation!): UpdateTaskLocationPayload! updateTaskLocation(input: NewTaskLocation!): UpdateTaskLocationPayload!
updateTaskName(input: UpdateTaskName!): Task! updateTaskName(input: UpdateTaskName!): Task!
updateTaskDueDate(input: UpdateTaskDueDate!): Task!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
assignTask(input: AssignTaskInput): Task! assignTask(input: AssignTaskInput): Task!
unassignTask(input: UnassignTaskInput): Task! unassignTask(input: UnassignTaskInput): Task!
@ -1786,6 +1816,20 @@ func (ec *executionContext) field_Mutation_updateTaskDescription_args(ctx contex
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation_updateTaskDueDate_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 UpdateTaskDueDate
if tmp, ok := rawArgs["input"]; ok {
arg0, err = ec.unmarshalNUpdateTaskDueDate2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUpdateTaskDueDate(ctx, tmp)
if err != nil {
return nil, err
}
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_updateTaskGroupLocation_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Mutation_updateTaskGroupLocation_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@ -3115,6 +3159,47 @@ func (ec *executionContext) _Mutation_updateTaskName(ctx context.Context, field
return ec.marshalNTask2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTask(ctx, field.Selections, res) return ec.marshalNTask2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTask(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation_updateTaskDueDate(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_updateTaskDueDate_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().UpdateTaskDueDate(rctx, args["input"].(UpdateTaskDueDate))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*pg.Task)
fc.Result = res
return ec.marshalNTask2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTask(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_deleteTask(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation_deleteTask(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -4619,6 +4704,37 @@ func (ec *executionContext) _Task_description(ctx context.Context, field graphql
return ec.marshalOString2ᚖstring(ctx, field.Selections, res) return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
} }
func (ec *executionContext) _Task_dueDate(ctx context.Context, field graphql.CollectedField, obj *pg.Task) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Task",
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.Task().DueDate(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*time.Time)
fc.Result = res
return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res)
}
func (ec *executionContext) _Task_assigned(ctx context.Context, field graphql.CollectedField, obj *pg.Task) (ret graphql.Marshaler) { func (ec *executionContext) _Task_assigned(ctx context.Context, field graphql.CollectedField, obj *pg.Task) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -7166,6 +7282,30 @@ func (ec *executionContext) unmarshalInputUpdateTaskDescriptionInput(ctx context
return it, nil return it, nil
} }
func (ec *executionContext) unmarshalInputUpdateTaskDueDate(ctx context.Context, obj interface{}) (UpdateTaskDueDate, error) {
var it UpdateTaskDueDate
var asMap = obj.(map[string]interface{})
for k, v := range asMap {
switch k {
case "taskID":
var err error
it.TaskID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
if err != nil {
return it, err
}
case "dueDate":
var err error
it.DueDate, err = ec.unmarshalOTime2ᚖtimeᚐTime(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputUpdateTaskGroupName(ctx context.Context, obj interface{}) (UpdateTaskGroupName, error) { func (ec *executionContext) unmarshalInputUpdateTaskGroupName(ctx context.Context, obj interface{}) (UpdateTaskGroupName, error) {
var it UpdateTaskGroupName var it UpdateTaskGroupName
var asMap = obj.(map[string]interface{}) var asMap = obj.(map[string]interface{})
@ -7462,6 +7602,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "updateTaskDueDate":
out.Values[i] = ec._Mutation_updateTaskDueDate(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "deleteTask": case "deleteTask":
out.Values[i] = ec._Mutation_deleteTask(ctx, field) out.Values[i] = ec._Mutation_deleteTask(ctx, field)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@ -8012,6 +8157,17 @@ func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj
res = ec._Task_description(ctx, field, obj) res = ec._Task_description(ctx, field, obj)
return res return res
}) })
case "dueDate":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Task_dueDate(ctx, field, obj)
return res
})
case "assigned": case "assigned":
field := field field := field
out.Concurrently(i, func() (res graphql.Marshaler) { out.Concurrently(i, func() (res graphql.Marshaler) {
@ -9265,6 +9421,10 @@ func (ec *executionContext) unmarshalNUpdateTaskDescriptionInput2githubᚗcomᚋ
return ec.unmarshalInputUpdateTaskDescriptionInput(ctx, v) return ec.unmarshalInputUpdateTaskDescriptionInput(ctx, v)
} }
func (ec *executionContext) unmarshalNUpdateTaskDueDate2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUpdateTaskDueDate(ctx context.Context, v interface{}) (UpdateTaskDueDate, error) {
return ec.unmarshalInputUpdateTaskDueDate(ctx, v)
}
func (ec *executionContext) unmarshalNUpdateTaskGroupName2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUpdateTaskGroupName(ctx context.Context, v interface{}) (UpdateTaskGroupName, error) { func (ec *executionContext) unmarshalNUpdateTaskGroupName2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUpdateTaskGroupName(ctx context.Context, v interface{}) (UpdateTaskGroupName, error) {
return ec.unmarshalInputUpdateTaskGroupName(ctx, v) return ec.unmarshalInputUpdateTaskGroupName(ctx, v)
} }
@ -9658,6 +9818,29 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as
return ec.marshalOString2string(ctx, sel, *v) return ec.marshalOString2string(ctx, sel, *v)
} }
func (ec *executionContext) unmarshalOTime2timeᚐTime(ctx context.Context, v interface{}) (time.Time, error) {
return graphql.UnmarshalTime(v)
}
func (ec *executionContext) marshalOTime2timeᚐTime(ctx context.Context, sel ast.SelectionSet, v time.Time) graphql.Marshaler {
return graphql.MarshalTime(v)
}
func (ec *executionContext) unmarshalOTime2ᚖtimeᚐTime(ctx context.Context, v interface{}) (*time.Time, error) {
if v == nil {
return nil, nil
}
res, err := ec.unmarshalOTime2timeᚐTime(ctx, v)
return &res, err
}
func (ec *executionContext) marshalOTime2ᚖtimeᚐTime(ctx context.Context, sel ast.SelectionSet, v *time.Time) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec.marshalOTime2timeᚐTime(ctx, sel, *v)
}
func (ec *executionContext) unmarshalOUnassignTaskInput2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUnassignTaskInput(ctx context.Context, v interface{}) (UnassignTaskInput, error) { func (ec *executionContext) unmarshalOUnassignTaskInput2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐUnassignTaskInput(ctx context.Context, v interface{}) (UnassignTaskInput, error) {
return ec.unmarshalInputUnassignTaskInput(ctx, v) return ec.unmarshalInputUnassignTaskInput(ctx, v)
} }

View File

@ -3,6 +3,8 @@
package graph package graph
import ( import (
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/pg" "github.com/jordanknott/project-citadel/api/pg"
) )
@ -168,6 +170,11 @@ type UpdateTaskDescriptionInput struct {
Description string `json:"description"` Description string `json:"description"`
} }
type UpdateTaskDueDate struct {
TaskID uuid.UUID `json:"taskID"`
DueDate *time.Time `json:"dueDate"`
}
type UpdateTaskGroupName struct { type UpdateTaskGroupName struct {
TaskGroupID uuid.UUID `json:"taskGroupID"` TaskGroupID uuid.UUID `json:"taskGroupID"`
Name string `json:"name"` Name string `json:"name"`

View File

@ -84,6 +84,7 @@ type Task {
name: String! name: String!
position: Float! position: Float!
description: String description: String
dueDate: Time
assigned: [ProjectMember!]! assigned: [ProjectMember!]!
labels: [TaskLabel!]! labels: [TaskLabel!]!
} }
@ -261,6 +262,11 @@ input UpdateTaskGroupName {
name: String! name: String!
} }
input UpdateTaskDueDate {
taskID: UUID!
dueDate: Time
}
type Mutation { type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken! createRefreshToken(input: NewRefreshToken!): RefreshToken!
@ -291,6 +297,7 @@ type Mutation {
updateTaskDescription(input: UpdateTaskDescriptionInput!): Task! updateTaskDescription(input: UpdateTaskDescriptionInput!): Task!
updateTaskLocation(input: NewTaskLocation!): UpdateTaskLocationPayload! updateTaskLocation(input: NewTaskLocation!): UpdateTaskLocationPayload!
updateTaskName(input: UpdateTaskName!): Task! updateTaskName(input: UpdateTaskName!): Task!
updateTaskDueDate(input: UpdateTaskDueDate!): Task!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
assignTask(input: AssignTaskInput): Task! assignTask(input: AssignTaskInput): Task!
unassignTask(input: UnassignTaskInput): Task! unassignTask(input: UnassignTaskInput): Task!

View File

@ -260,6 +260,21 @@ func (r *mutationResolver) UpdateTaskName(ctx context.Context, input UpdateTaskN
return &task, err return &task, err
} }
func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTaskDueDate) (*pg.Task, error) {
var dueDate sql.NullTime
if input.DueDate == nil {
dueDate = sql.NullTime{Valid: false, Time: time.Now()}
} else {
dueDate = sql.NullTime{Valid: true, Time: *input.DueDate}
}
task, err := r.Repository.UpdateTaskDueDate(ctx, pg.UpdateTaskDueDateParams{
TaskID: input.TaskID,
DueDate: dueDate,
})
return &task, err
}
func (r *mutationResolver) DeleteTask(ctx context.Context, input DeleteTaskInput) (*DeleteTaskPayload, error) { func (r *mutationResolver) DeleteTask(ctx context.Context, input DeleteTaskInput) (*DeleteTaskPayload, error) {
taskID, err := uuid.Parse(input.TaskID) taskID, err := uuid.Parse(input.TaskID)
if err != nil { if err != nil {
@ -484,6 +499,13 @@ func (r *taskResolver) Description(ctx context.Context, obj *pg.Task) (*string,
return &task.Description.String, nil return &task.Description.String, nil
} }
func (r *taskResolver) DueDate(ctx context.Context, obj *pg.Task) (*time.Time, error) {
if obj.DueDate.Valid {
return &obj.DueDate.Time, nil
}
return nil, nil
}
func (r *taskResolver) Assigned(ctx context.Context, obj *pg.Task) ([]ProjectMember, error) { func (r *taskResolver) Assigned(ctx context.Context, obj *pg.Task) ([]ProjectMember, error) {
taskMemberLinks, err := r.Repository.GetAssignedMembersForTask(ctx, obj.TaskID) taskMemberLinks, err := r.Repository.GetAssignedMembersForTask(ctx, obj.TaskID)
taskMembers := []ProjectMember{} taskMembers := []ProjectMember{}

View File

@ -30,6 +30,7 @@ type Repository interface {
UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, error) UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, error)
DeleteTaskLabelByID(ctx context.Context, taskLabelID uuid.UUID) error DeleteTaskLabelByID(ctx context.Context, taskLabelID uuid.UUID) error
UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDateParams) (Task, error)
CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error) CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error)
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error) GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error) GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)

View File

@ -56,11 +56,13 @@ type Querier interface {
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error) GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error) UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error)
UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error) UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error) UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)
UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, error) UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, error)
UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescriptionParams) (Task, error) UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescriptionParams) (Task, error)
UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDateParams) (Task, error)
UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error) UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error) UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)

View File

@ -177,6 +177,30 @@ func (q *Queries) UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescr
return i, err return i, err
} }
const updateTaskDueDate = `-- name: UpdateTaskDueDate :one
UPDATE task SET due_date = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date
`
type UpdateTaskDueDateParams struct {
TaskID uuid.UUID `json:"task_id"`
DueDate sql.NullTime `json:"due_date"`
}
func (q *Queries) UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDateParams) (Task, error) {
row := q.db.QueryRowContext(ctx, updateTaskDueDate, arg.TaskID, arg.DueDate)
var i Task
err := row.Scan(
&i.TaskID,
&i.TaskGroupID,
&i.CreatedAt,
&i.Name,
&i.Position,
&i.Description,
&i.DueDate,
)
return i, err
}
const updateTaskLocation = `-- name: UpdateTaskLocation :one const updateTaskLocation = `-- name: UpdateTaskLocation :one
UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date
` `

View File

@ -135,6 +135,28 @@ func (q *Queries) GetTaskGroupsForProject(ctx context.Context, projectID uuid.UU
return items, nil return items, nil
} }
const setTaskGroupName = `-- name: SetTaskGroupName :one
UPDATE task_group SET name = $2 WHERE task_group_id = $1 RETURNING task_group_id, project_id, created_at, name, position
`
type SetTaskGroupNameParams struct {
TaskGroupID uuid.UUID `json:"task_group_id"`
Name string `json:"name"`
}
func (q *Queries) SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error) {
row := q.db.QueryRowContext(ctx, setTaskGroupName, arg.TaskGroupID, arg.Name)
var i TaskGroup
err := row.Scan(
&i.TaskGroupID,
&i.ProjectID,
&i.CreatedAt,
&i.Name,
&i.Position,
)
return i, err
}
const updateTaskGroupLocation = `-- name: UpdateTaskGroupLocation :one const updateTaskGroupLocation = `-- name: UpdateTaskGroupLocation :one
UPDATE task_group SET position = $2 WHERE task_group_id = $1 RETURNING task_group_id, project_id, created_at, name, position UPDATE task_group SET position = $2 WHERE task_group_id = $1 RETURNING task_group_id, project_id, created_at, name, position
` `

View File

@ -25,3 +25,6 @@ UPDATE task SET name = $2 WHERE task_id = $1 RETURNING *;
-- name: DeleteTasksByTaskGroupID :execrows -- name: DeleteTasksByTaskGroupID :execrows
DELETE FROM task where task_group_id = $1; DELETE FROM task where task_group_id = $1;
-- name: UpdateTaskDueDate :one
UPDATE task SET due_date = $2 WHERE task_id = $1 RETURNING *;

View File

@ -111,5 +111,19 @@ export default createGlobalStyle`
resize: none; resize: none;
} }
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: #262c49;
border-radius: 20px;
}
::-webkit-scrollbar-thumb {
background: #7367f0;
border-radius: 20px;
}
${mixin.placeholderColor(color.textLight)} ${mixin.placeholderColor(color.textLight)}
`; `;

View File

@ -17,6 +17,7 @@ const theme: DefaultTheme = {
primary: '194, 198, 220', primary: '194, 198, 220',
secondary: '255, 255, 255', secondary: '255, 255, 255',
}, },
border: '65, 69, 97',
bg: { bg: {
primary: '16, 22, 58', primary: '16, 22, 58',
secondary: '38, 44, 73', secondary: '38, 44, 73',

View File

@ -4,9 +4,15 @@ import TaskDetails from 'shared/components/TaskDetails';
import PopupMenu, { Popup, usePopup } from 'shared/components/PopupMenu'; import PopupMenu, { Popup, usePopup } from 'shared/components/PopupMenu';
import MemberManager from 'shared/components/MemberManager'; import MemberManager from 'shared/components/MemberManager';
import { useRouteMatch, useHistory } from 'react-router'; import { useRouteMatch, useHistory } from 'react-router';
import { useFindTaskQuery, useAssignTaskMutation, useUnassignTaskMutation } from 'shared/generated/graphql'; import {
useFindTaskQuery,
useUpdateTaskDueDateMutation,
useAssignTaskMutation,
useUnassignTaskMutation,
} from 'shared/generated/graphql';
import UserIDContext from 'App/context'; import UserIDContext from 'App/context';
import MiniProfile from 'shared/components/MiniProfile'; import MiniProfile from 'shared/components/MiniProfile';
import DueDateManager from 'shared/components/DueDateManager';
type DetailsProps = { type DetailsProps = {
taskID: string; taskID: string;
@ -32,12 +38,18 @@ const Details: React.FC<DetailsProps> = ({
refreshCache, refreshCache,
}) => { }) => {
const { userID } = useContext(UserIDContext); const { userID } = useContext(UserIDContext);
const { showPopup } = usePopup(); const { showPopup, hidePopup } = usePopup();
const history = useHistory(); const history = useHistory();
const match = useRouteMatch(); const match = useRouteMatch();
const [currentMemberTask, setCurrentMemberTask] = useState(''); const [currentMemberTask, setCurrentMemberTask] = useState('');
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState); const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } }); const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => {
refetch();
refreshCache();
},
});
const [assignTask] = useAssignTaskMutation({ const [assignTask] = useAssignTaskMutation({
onCompleted: () => { onCompleted: () => {
refetch(); refetch();
@ -56,6 +68,7 @@ const Details: React.FC<DetailsProps> = ({
if (!data) { if (!data) {
return <div>loading</div>; return <div>loading</div>;
} }
console.log(data.findTask);
return ( return (
<> <>
<Modal <Modal
@ -108,6 +121,29 @@ const Details: React.FC<DetailsProps> = ({
); );
}} }}
onOpenAddLabelPopup={onOpenAddLabelPopup} onOpenAddLabelPopup={onOpenAddLabelPopup}
onOpenDueDatePopop={(task, $targetRef) => {
showPopup(
$targetRef,
<Popup
title={'Change Due Date'}
tab={0}
onClose={() => {
hidePopup();
}}
>
<DueDateManager
task={task}
onDueDateChange={(t, newDueDate) => {
console.log(`${newDueDate}`);
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } });
hidePopup();
}}
onCancel={() => {}}
/>
</Popup>,
);
}}
/> />
); );
}} }}

View File

@ -229,14 +229,14 @@ const ProjectAction = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 15px; font-size: 15px;
color: var(--color-text); color: rgba(${props => props.theme.colors.text.primary});
&:not(:last-child) { &:not(:last-child) {
margin-right: 16px; margin-right: 16px;
} }
&:hover { &:hover {
color: var(--color-text-hover); color: rgba(${props => props.theme.colors.text.secondary});
} }
`; `;
@ -490,15 +490,15 @@ const Project = () => {
); );
}} }}
> >
<Tags size={13} color="var(--color-icon)" /> <Tags width={13} height={13} />
<ProjectActionText>Labels</ProjectActionText> <ProjectActionText>Labels</ProjectActionText>
</ProjectAction> </ProjectAction>
<ProjectAction> <ProjectAction>
<ToggleOn size={13} color="var(--color-icon)" /> <ToggleOn width={13} height={13} />
<ProjectActionText>Fields</ProjectActionText> <ProjectActionText>Fields</ProjectActionText>
</ProjectAction> </ProjectAction>
<ProjectAction> <ProjectAction>
<Bolt size={13} color="var(--color-icon)" /> <Bolt width={13} height={13} />
<ProjectActionText>Rules</ProjectActionText> <ProjectActionText>Rules</ProjectActionText>
</ProjectAction> </ProjectAction>
</ProjectActions> </ProjectActions>

View File

@ -32,8 +32,8 @@ type LoginFormData = {
}; };
type DueDateFormData = { type DueDateFormData = {
endDate: Date; endDate: string;
endTime: string | null; endTime: string;
}; };
type LoginProps = { type LoginProps = {

View File

@ -35,6 +35,7 @@ type Task = {
taskGroup: InnerTaskGroup; taskGroup: InnerTaskGroup;
name: string; name: string;
position: number; position: number;
dueDate?: string;
labels: TaskLabel[]; labels: TaskLabel[];
description?: string | null; description?: string | null;
assigned?: Array<TaskUser>; assigned?: Array<TaskUser>;

View File

@ -107,6 +107,7 @@ type ButtonProps = {
variant?: 'filled' | 'outline' | 'flat' | 'lineDown' | 'gradient' | 'relief'; variant?: 'filled' | 'outline' | 'flat' | 'lineDown' | 'gradient' | 'relief';
color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark'; color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark';
disabled?: boolean; disabled?: boolean;
type?: 'button' | 'submit';
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
}; };
@ -116,6 +117,7 @@ const Button: React.FC<ButtonProps> = ({
fontSize = '14px', fontSize = '14px',
color = 'primary', color = 'primary',
variant = 'filled', variant = 'filled',
type = 'button',
onClick, onClick,
className, className,
children, children,
@ -128,38 +130,38 @@ const Button: React.FC<ButtonProps> = ({
switch (variant) { switch (variant) {
case 'filled': case 'filled':
return ( return (
<Filled onClick={handleClick} className={className} disabled={disabled} color={color}> <Filled type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
</Filled> </Filled>
); );
case 'outline': case 'outline':
return ( return (
<Outline onClick={handleClick} className={className} disabled={disabled} color={color}> <Outline type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
</Outline> </Outline>
); );
case 'flat': case 'flat':
return ( return (
<Flat onClick={handleClick} className={className} disabled={disabled} color={color}> <Flat type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
</Flat> </Flat>
); );
case 'lineDown': case 'lineDown':
return ( return (
<LineDown onClick={handleClick} className={className} disabled={disabled} color={color}> <LineDown type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
<LineX color={color} /> <LineX color={color} />
</LineDown> </LineDown>
); );
case 'gradient': case 'gradient':
return ( return (
<Gradient onClick={handleClick} className={className} disabled={disabled} color={color}> <Gradient type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
</Gradient> </Gradient>
); );
case 'relief': case 'relief':
return ( return (
<Relief onClick={handleClick} className={className} disabled={disabled} color={color}> <Relief type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text fontSize={fontSize}>{children}</Text>
</Relief> </Relief>
); );

View File

@ -22,7 +22,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
padding: 0; padding: 0;
font-size: 16px; font-size: 16px;
line-height: 20px; line-height: 20px;
color: var(--color-input-text-focus); color: rgba(${props => props.theme.colors.text.secondary});
&:focus { &:focus {
border: none; border: none;
outline: none; outline: none;
@ -84,7 +84,7 @@ export const ListCardContainer = styled.div<{ isActive: boolean; editable: boole
position: relative; position: relative;
background-color: ${props => background-color: ${props =>
props.isActive && !props.editable ? mixin.darken('#262c49', 0.1) : 'var(--color-background)'}; props.isActive && !props.editable ? mixin.darken('#262c49', 0.1) : `rgba(${props.theme.colors.bg.secondary})`};
`; `;
export const ListCardInnerContainer = styled.div` export const ListCardInnerContainer = styled.div`
@ -145,7 +145,7 @@ export const CardTitle = styled.span`
overflow: hidden; overflow: hidden;
text-decoration: none; text-decoration: none;
word-wrap: break-word; word-wrap: break-word;
color: var(--color-text); color: rgba(${props => props.theme.colors.text.primary});
`; `;
export const CardMembers = styled.div` export const CardMembers = styled.div`

View File

@ -1,12 +1,16 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import DueDateManager from '.'; import BaseStyles from 'App/BaseStyles';
import NormalizeStyles from 'App/NormalizeStyles';
import { theme } from 'App/ThemeStyles';
import styled, { ThemeProvider } from 'styled-components';
import { Popup } from '../PopupMenu'; import { Popup } from '../PopupMenu';
import styled from 'styled-components'; import DueDateManager from '.';
const PopupWrapper = styled.div` const PopupWrapper = styled.div`
width: 300px; width: 310px;
`; `;
export default { export default {
component: DueDateManager, component: DueDateManager,
title: 'DueDateManager', title: 'DueDateManager',
@ -20,6 +24,10 @@ export default {
export const Default = () => { export const Default = () => {
return ( return (
<>
<NormalizeStyles />
<BaseStyles />
<ThemeProvider theme={theme}>
<PopupWrapper> <PopupWrapper>
<Popup title={null} tab={0}> <Popup title={null} tab={0}>
<DueDateManager <DueDateManager
@ -59,5 +67,7 @@ export const Default = () => {
/> />
</Popup> </Popup>
</PopupWrapper> </PopupWrapper>
</ThemeProvider>
</>
); );
}; };

View File

@ -1,5 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import Button from 'shared/components/Button';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import Input from 'shared/components/Input';
export const Wrapper = styled.div` export const Wrapper = styled.div`
display: flex display: flex
@ -8,7 +10,37 @@ display: flex
background: #262c49; background: #262c49;
font-family: 'Droid Sans', sans-serif; font-family: 'Droid Sans', sans-serif;
border: none; border: none;
}
& .react-datepicker__triangle {
display: none;
}
& .react-datepicker-popper {
z-index: 10000;
margin-top: 0;
}
& .react-datepicker-time__header {
color: rgba(${props => props.theme.colors.text.primary});
}
& .react-datepicker__time-list-item {
color: rgba(${props => props.theme.colors.text.primary});
}
& .react-datepicker__time-container .react-datepicker__time
.react-datepicker__time-box ul.react-datepicker__time-list
li.react-datepicker__time-list-item:hover {
color: rgba(${props => props.theme.colors.text.secondary});
background: rgba(${props => props.theme.colors.bg.secondary});
}
& .react-datepicker__time-container .react-datepicker__time {
background: rgba(${props => props.theme.colors.bg.primary});
}
& .react-datepicker--time-only {
background: rgba(${props => props.theme.colors.bg.primary});
border: 1px solid rgba(${props => props.theme.colors.border});
}
& .react-datepicker * {
box-sizing: content-box;
} }
& .react-datepicker__day-name { & .react-datepicker__day-name {
color: #c2c6dc; color: #c2c6dc;
@ -56,6 +88,9 @@ display: flex
background: none; background: none;
border: none; border: none;
} }
& .react-datepicker__header--time {
border-bottom: 1px solid rgba(${props => props.theme.colors.border});
}
`; `;
@ -66,21 +101,10 @@ export const DueDatePickerWrapper = styled.div`
justify-content: center; justify-content: center;
`; `;
export const ConfirmAddDueDate = styled.div` export const ConfirmAddDueDate = styled(Button)`
background-color: #5aac44;
box-shadow: none;
border: none;
color: #fff;
float: left; float: left;
margin: 0 4px 0 0; margin: 0 4px 0 0;
cursor: pointer;
display: inline-block;
font-weight: 400;
line-height: 20px;
padding: 6px 12px; padding: 6px 12px;
text-align: center;
border-radius: 3px;
font-size: 14px;
`; `;
export const CancelDueDate = styled.div` export const CancelDueDate = styled.div`
@ -92,6 +116,12 @@ export const CancelDueDate = styled.div`
cursor: pointer; cursor: pointer;
`; `;
export const DueDateInput = styled(Input)`
margin-top: 15px;
margin-bottom: 5px;
padding-right: 10px;
`;
export const ActionWrapper = styled.div` export const ActionWrapper = styled.div`
padding-top: 8px; padding-top: 8px;
width: 100%; width: 100%;

View File

@ -1,11 +1,11 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, forwardRef } from 'react';
import moment from 'moment'; import moment from 'moment';
import styled from 'styled-components'; import styled from 'styled-components';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import { Cross } from 'shared/icons'; import { Cross } from 'shared/icons';
import _ from 'lodash'; import _ from 'lodash';
import { Wrapper, ActionWrapper, DueDatePickerWrapper, ConfirmAddDueDate, CancelDueDate } from './Styles'; import { Wrapper, ActionWrapper, DueDateInput, DueDatePickerWrapper, ConfirmAddDueDate, CancelDueDate } from './Styles';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
import { getYear, getMonth } from 'date-fns'; import { getYear, getMonth } from 'date-fns';
@ -17,6 +17,14 @@ type DueDateManagerProps = {
onCancel: () => void; onCancel: () => void;
}; };
const Form = styled.form`
padding-top: 25px;
`;
const FormField = styled.div`
width: 50%;
display: inline-block;
`;
const HeaderSelectLabel = styled.div` const HeaderSelectLabel = styled.div`
display: inline-block; display: inline-block;
position: relative; position: relative;
@ -104,12 +112,17 @@ const HeaderActions = styled.div`
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onCancel }) => { const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onCancel }) => {
const now = moment(); const now = moment();
const [textStartDate, setTextStartDate] = useState(now.format('YYYY-MM-DD')); const [textStartDate, setTextStartDate] = useState(now.format('YYYY-MM-DD'));
const [startDate, setStartDate] = useState(new Date()); const [startDate, setStartDate] = useState(new Date());
useEffect(() => { useEffect(() => {
setTextStartDate(moment(startDate).format('YYYY-MM-DD')); setTextStartDate(moment(startDate).format('YYYY-MM-DD'));
}, [startDate]); }, [startDate]);
const [textEndTime, setTextEndTime] = useState(now.format('h:mm A'));
const [endTime, setEndTime] = useState(now.toDate());
useEffect(() => {
setTextEndTime(moment(endTime).format('h:mm A'));
}, [endTime]);
const years = _.range(2010, getYear(new Date()) + 10, 1); const years = _.range(2010, getYear(new Date()) + 10, 1);
const months = [ const months = [
'January', 'January',
@ -125,29 +138,75 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
'November', 'November',
'December', 'December',
]; ];
const { register, handleSubmit, errors, setError, formState } = useForm<DueDateFormData>(); const { register, handleSubmit, errors, setValue, setError, formState } = useForm<DueDateFormData>();
const saveDueDate = (data: any) => {
console.log(data);
const newDate = moment(`${data.endDate} ${data.endTime}`, 'YYYY-MM-DD h:mm A');
if (newDate.isValid()) {
onDueDateChange(task, newDate.toDate());
}
};
console.log(errors); console.log(errors);
register({ name: 'endTime' }, { required: 'End time is required' });
useEffect(() => {
setValue('endTime', now.format('h:mm A'));
}, []);
const CustomTimeInput = forwardRef(({ value, onClick }: any, $ref: any) => {
return (
<DueDateInput
id="endTime"
name="endTime"
ref={$ref}
onChange={e => {
console.log(`onCahnge ${e.currentTarget.value}`);
setTextEndTime(e.currentTarget.value);
setValue('endTime', e.currentTarget.value);
}}
width="100%"
variant="alternate"
label="Date"
onClick={onClick}
value={value}
/>
);
});
console.log(`textStartDate ${textStartDate}`);
return ( return (
<Wrapper> <Wrapper>
<form> <Form onSubmit={handleSubmit(saveDueDate)}>
<input <FormField>
type="text" <DueDateInput
id="endDate" id="endDate"
name="endDate" name="endDate"
width="100%"
variant="alternate"
label="Date"
onChange={e => { onChange={e => {
setTextStartDate(e.currentTarget.value); setTextStartDate(e.currentTarget.value);
}} }}
value={textStartDate} value={textStartDate}
ref={register({ ref={register({
required: 'End due date is required.', required: 'End date is required.',
validate: value => {
const isValid = moment(value, 'YYYY-MM-DD').isValid();
console.log(`${value} - ${isValid}`);
return isValid;
},
})} })}
/> />
</form> </FormField>
<FormField>
<DatePicker
selected={endTime}
onChange={date => {
const changedDate = moment(date ?? new Date());
console.log(`changed ${date}`);
setEndTime(changedDate.toDate());
setValue('endTime', changedDate.format('h:mm A'));
}}
showTimeSelect
showTimeSelectOnly
timeIntervals={15}
timeCaption="Time"
dateFormat="h:mm aa"
customInput={<CustomTimeInput />}
/>
</FormField>
<DueDatePickerWrapper> <DueDatePickerWrapper>
<DatePicker <DatePicker
useWeekdaysShort useWeekdaysShort
@ -195,15 +254,27 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
)} )}
selected={startDate} selected={startDate}
inline inline
onChange={date => setStartDate(date ?? new Date())} onChange={date => {
setStartDate(date ?? new Date());
}}
/> />
</DueDatePickerWrapper> </DueDatePickerWrapper>
<ActionWrapper> <ActionWrapper>
<ConfirmAddDueDate onClick={() => onDueDateChange(task, startDate)}>Save</ConfirmAddDueDate> <ConfirmAddDueDate
type="submit"
onClick={() => {
// const newDate = moment(startDate).format('YYYY-MM-DD');
// const newTime = moment(endTime).format('h:mm A');
// onDueDateChange(task, moment(`${newDate} ${newTime}`, 'YYYY-MM-DD h:mm A').toDate());
}}
>
Save
</ConfirmAddDueDate>
<CancelDueDate onClick={onCancel}> <CancelDueDate onClick={onCancel}>
<Cross size={16} color="#c2c6dc" /> <Cross size={16} color="#c2c6dc" />
</CancelDueDate> </CancelDueDate>
</ActionWrapper> </ActionWrapper>
</Form>
</Wrapper> </Wrapper>
); );
}; };

View File

@ -1,5 +1,5 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import styled from 'styled-components/macro'; import styled, { css } from 'styled-components/macro';
const InputWrapper = styled.div<{ width: string }>` const InputWrapper = styled.div<{ width: string }>`
position: relative; position: relative;
@ -32,7 +32,13 @@ const InputLabel = styled.span<{ width: string }>`
} }
`; `;
const InputInput = styled.input<{ hasIcon: boolean; width: string; focusBg: string; borderColor: string }>` const InputInput = styled.input<{
hasValue: boolean;
hasIcon: boolean;
width: string;
focusBg: string;
borderColor: string;
}>`
width: ${props => props.width}; width: ${props => props.width};
font-size: 14px; font-size: 14px;
border: 1px solid rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.2);
@ -54,6 +60,14 @@ const InputInput = styled.input<{ hasIcon: boolean; width: string; focusBg: stri
color: rgba(115, 103, 240); color: rgba(115, 103, 240);
transform: translate(-3px, -90%); transform: translate(-3px, -90%);
} }
${props =>
props.hasValue &&
css`
& ~ ${InputLabel} {
color: rgba(115, 103, 240);
transform: translate(-3px, -90%);
}
`}
`; `;
const Icon = styled.div` const Icon = styled.div`
@ -68,14 +82,55 @@ type InputProps = {
width?: string; width?: string;
placeholder?: string; placeholder?: string;
icon?: JSX.Element; icon?: JSX.Element;
id?: string;
name?: string;
className?: string;
value?: string;
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}; };
const Input: React.FC<InputProps> = ({ width = 'auto', variant = 'normal', label, placeholder, icon }) => { const Input = React.forwardRef(
(
{
width = 'auto',
variant = 'normal',
label,
placeholder,
icon,
name,
onChange,
className,
onClick,
value: initialValue,
id,
}: InputProps,
$ref: any,
) => {
const [value, setValue] = useState(initialValue ?? '');
useEffect(() => {
if (initialValue) {
setValue(initialValue);
}
}, [initialValue]);
const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561'; const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561';
const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)'; const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)';
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.currentTarget.value);
if (onChange) {
onChange(e);
}
};
return ( return (
<InputWrapper width={width}> <InputWrapper className={className} width={width}>
<InputInput <InputInput
hasValue={value !== ''}
ref={$ref}
id={id}
name={name}
onClick={onClick}
onChange={handleChange}
value={value}
hasIcon={typeof icon !== 'undefined'} hasIcon={typeof icon !== 'undefined'}
width={width} width={width}
placeholder={placeholder} placeholder={placeholder}
@ -86,6 +141,7 @@ const Input: React.FC<InputProps> = ({ width = 'auto', variant = 'normal', label
<Icon>{icon && icon}</Icon> <Icon>{icon && icon}</Icon>
</InputWrapper> </InputWrapper>
); );
}; },
);
export default Input; export default Input;

View File

@ -12,6 +12,7 @@ import {
} from 'shared/utils/draggables'; } from 'shared/utils/draggables';
import { Container, BoardWrapper } from './Styles'; import { Container, BoardWrapper } from './Styles';
import moment from 'moment';
interface SimpleProps { interface SimpleProps {
taskGroups: Array<TaskGroup>; taskGroups: Array<TaskGroup>;
@ -165,6 +166,14 @@ const SimpleLists: React.FC<SimpleProps> = ({
taskGroupID={taskGroup.id} taskGroupID={taskGroup.id}
description="" description=""
labels={task.labels.map(label => label.projectLabel)} labels={task.labels.map(label => label.projectLabel)}
dueDate={
task.dueDate
? {
isPastDue: false,
formattedDate: moment(task.dueDate).format('MMM D, YYYY'),
}
: undefined
}
title={task.name} title={task.name}
members={task.assigned} members={task.assigned}
onClick={() => { onClick={() => {

View File

@ -66,6 +66,7 @@ export const Default = () => {
onMemberProfile={action('profile')} onMemberProfile={action('profile')}
onOpenAddMemberPopup={action('open add member popup')} onOpenAddMemberPopup={action('open add member popup')}
onOpenAddLabelPopup={action('open add label popup')} onOpenAddLabelPopup={action('open add label popup')}
onOpenDueDatePopop={action('open due date popup')}
/> />
); );
}} }}

View File

@ -39,6 +39,7 @@ import {
} from './Styles'; } from './Styles';
import convertDivElementRefToBounds from 'shared/utils/boundingRect'; import convertDivElementRefToBounds from 'shared/utils/boundingRect';
import moment from 'moment';
type TaskContentProps = { type TaskContentProps = {
onEditContent: () => void; onEditContent: () => void;
@ -106,6 +107,7 @@ type TaskDetailsProps = {
onDeleteTask: (task: Task) => void; onDeleteTask: (task: Task) => void;
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void; onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void; onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void; onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
onCloseModal: () => void; onCloseModal: () => void;
}; };
@ -118,6 +120,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onCloseModal, onCloseModal,
onOpenAddMemberPopup, onOpenAddMemberPopup,
onOpenAddLabelPopup, onOpenAddLabelPopup,
onOpenDueDatePopop,
onMemberProfile, onMemberProfile,
}) => { }) => {
const [editorOpen, setEditorOpen] = useState(false); const [editorOpen, setEditorOpen] = useState(false);
@ -143,6 +146,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const onAddMember = () => { const onAddMember = () => {
onOpenAddMemberPopup(task, $addMemberRef); onOpenAddMemberPopup(task, $addMemberRef);
}; };
const $dueDateLabel = useRef<HTMLDivElement>(null);
const $addLabelRef = useRef<HTMLDivElement>(null); const $addLabelRef = useRef<HTMLDivElement>(null);
const onAddLabel = () => { const onAddLabel = () => {
onOpenAddLabelPopup(task, $addLabelRef); onOpenAddLabelPopup(task, $addLabelRef);
@ -229,7 +233,15 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
</TaskDetailsAddLabel> </TaskDetailsAddLabel>
</TaskDetailLabels> </TaskDetailLabels>
<TaskDetailSectionTitle>Due Date</TaskDetailSectionTitle> <TaskDetailSectionTitle>Due Date</TaskDetailSectionTitle>
<NoDueDateLabel>No due date</NoDueDateLabel> {task.dueDate ? (
<NoDueDateLabel ref={$dueDateLabel} onClick={() => onOpenDueDatePopop(task, $dueDateLabel)}>
{moment(task.dueDate).format('MMM D [at] h:mm A')}
</NoDueDateLabel>
) : (
<NoDueDateLabel ref={$dueDateLabel} onClick={() => onOpenDueDatePopop(task, $dueDateLabel)}>
No due date
</NoDueDateLabel>
)}
</TaskDetailsSidebar> </TaskDetailsSidebar>
</TaskDetailsWrapper> </TaskDetailsWrapper>
</> </>

View File

@ -110,6 +110,7 @@ export type Task = {
name: Scalars['String']; name: Scalars['String'];
position: Scalars['Float']; position: Scalars['Float'];
description?: Maybe<Scalars['String']>; description?: Maybe<Scalars['String']>;
dueDate?: Maybe<Scalars['Time']>;
assigned: Array<ProjectMember>; assigned: Array<ProjectMember>;
labels: Array<TaskLabel>; labels: Array<TaskLabel>;
}; };
@ -310,6 +311,16 @@ export type UpdateTaskLocationPayload = {
task: Task; task: Task;
}; };
export type UpdateTaskGroupName = {
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
};
export type UpdateTaskDueDate = {
taskID: Scalars['UUID'];
dueDate?: Maybe<Scalars['Time']>;
};
export type Mutation = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
createRefreshToken: RefreshToken; createRefreshToken: RefreshToken;
@ -325,6 +336,7 @@ export type Mutation = {
updateProjectLabelColor: ProjectLabel; updateProjectLabelColor: ProjectLabel;
createTaskGroup: TaskGroup; createTaskGroup: TaskGroup;
updateTaskGroupLocation: TaskGroup; updateTaskGroupLocation: TaskGroup;
updateTaskGroupName: TaskGroup;
deleteTaskGroup: DeleteTaskGroupPayload; deleteTaskGroup: DeleteTaskGroupPayload;
addTaskLabel: Task; addTaskLabel: Task;
removeTaskLabel: Task; removeTaskLabel: Task;
@ -333,6 +345,7 @@ export type Mutation = {
updateTaskDescription: Task; updateTaskDescription: Task;
updateTaskLocation: UpdateTaskLocationPayload; updateTaskLocation: UpdateTaskLocationPayload;
updateTaskName: Task; updateTaskName: Task;
updateTaskDueDate: Task;
deleteTask: DeleteTaskPayload; deleteTask: DeleteTaskPayload;
assignTask: Task; assignTask: Task;
unassignTask: Task; unassignTask: Task;
@ -400,6 +413,11 @@ export type MutationUpdateTaskGroupLocationArgs = {
}; };
export type MutationUpdateTaskGroupNameArgs = {
input: UpdateTaskGroupName;
};
export type MutationDeleteTaskGroupArgs = { export type MutationDeleteTaskGroupArgs = {
input: DeleteTaskGroupInput; input: DeleteTaskGroupInput;
}; };
@ -440,6 +458,11 @@ export type MutationUpdateTaskNameArgs = {
}; };
export type MutationUpdateTaskDueDateArgs = {
input: UpdateTaskDueDate;
};
export type MutationDeleteTaskArgs = { export type MutationDeleteTaskArgs = {
input: DeleteTaskInput; input: DeleteTaskInput;
}; };
@ -658,7 +681,7 @@ export type FindProjectQuery = (
& Pick<TaskGroup, 'id' | 'name' | 'position'> & Pick<TaskGroup, 'id' | 'name' | 'position'>
& { tasks: Array<( & { tasks: Array<(
{ __typename?: 'Task' } { __typename?: 'Task' }
& Pick<Task, 'id' | 'name' | 'position' | 'description'> & Pick<Task, 'id' | 'name' | 'position' | 'description' | 'dueDate'>
& { taskGroup: ( & { taskGroup: (
{ __typename?: 'TaskGroup' } { __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name' | 'position'> & Pick<TaskGroup, 'id' | 'name' | 'position'>
@ -698,7 +721,7 @@ export type FindTaskQuery = (
{ __typename?: 'Query' } { __typename?: 'Query' }
& { findTask: ( & { findTask: (
{ __typename?: 'Task' } { __typename?: 'Task' }
& Pick<Task, 'id' | 'name' | 'description' | 'position'> & Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position'>
& { taskGroup: ( & { taskGroup: (
{ __typename?: 'TaskGroup' } { __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id'> & Pick<TaskGroup, 'id'>
@ -852,6 +875,20 @@ export type UpdateTaskDescriptionMutation = (
) } ) }
); );
export type UpdateTaskDueDateMutationVariables = {
taskID: Scalars['UUID'];
dueDate?: Maybe<Scalars['Time']>;
};
export type UpdateTaskDueDateMutation = (
{ __typename?: 'Mutation' }
& { updateTaskDueDate: (
{ __typename?: 'Task' }
& Pick<Task, 'id' | 'dueDate'>
) }
);
export type UpdateTaskGroupLocationMutationVariables = { export type UpdateTaskGroupLocationMutationVariables = {
taskGroupID: Scalars['UUID']; taskGroupID: Scalars['UUID'];
position: Scalars['Float']; position: Scalars['Float'];
@ -1298,6 +1335,7 @@ export const FindProjectDocument = gql`
name name
position position
description description
dueDate
taskGroup { taskGroup {
id id
name name
@ -1370,6 +1408,7 @@ export const FindTaskDocument = gql`
id id
name name
description description
dueDate
position position
taskGroup { taskGroup {
id id
@ -1705,6 +1744,40 @@ export function useUpdateTaskDescriptionMutation(baseOptions?: ApolloReactHooks.
export type UpdateTaskDescriptionMutationHookResult = ReturnType<typeof useUpdateTaskDescriptionMutation>; export type UpdateTaskDescriptionMutationHookResult = ReturnType<typeof useUpdateTaskDescriptionMutation>;
export type UpdateTaskDescriptionMutationResult = ApolloReactCommon.MutationResult<UpdateTaskDescriptionMutation>; export type UpdateTaskDescriptionMutationResult = ApolloReactCommon.MutationResult<UpdateTaskDescriptionMutation>;
export type UpdateTaskDescriptionMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>; export type UpdateTaskDescriptionMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>;
export const UpdateTaskDueDateDocument = gql`
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time) {
updateTaskDueDate(input: {taskID: $taskID, dueDate: $dueDate}) {
id
dueDate
}
}
`;
export type UpdateTaskDueDateMutationFn = ApolloReactCommon.MutationFunction<UpdateTaskDueDateMutation, UpdateTaskDueDateMutationVariables>;
/**
* __useUpdateTaskDueDateMutation__
*
* To run a mutation, you first call `useUpdateTaskDueDateMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateTaskDueDateMutation` 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 [updateTaskDueDateMutation, { data, loading, error }] = useUpdateTaskDueDateMutation({
* variables: {
* taskID: // value for 'taskID'
* dueDate: // value for 'dueDate'
* },
* });
*/
export function useUpdateTaskDueDateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTaskDueDateMutation, UpdateTaskDueDateMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateTaskDueDateMutation, UpdateTaskDueDateMutationVariables>(UpdateTaskDueDateDocument, baseOptions);
}
export type UpdateTaskDueDateMutationHookResult = ReturnType<typeof useUpdateTaskDueDateMutation>;
export type UpdateTaskDueDateMutationResult = ApolloReactCommon.MutationResult<UpdateTaskDueDateMutation>;
export type UpdateTaskDueDateMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskDueDateMutation, UpdateTaskDueDateMutationVariables>;
export const UpdateTaskGroupLocationDocument = gql` export const UpdateTaskGroupLocationDocument = gql`
mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) { mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) {
updateTaskGroupLocation(input: {taskGroupID: $taskGroupID, position: $position}) { updateTaskGroupLocation(input: {taskGroupID: $taskGroupID, position: $position}) {

View File

@ -30,6 +30,7 @@ query findProject($projectId: String!) {
name name
position position
description description
dueDate
taskGroup { taskGroup {
id id
name name

View File

@ -3,6 +3,7 @@ query findTask($taskID: UUID!) {
id id
name name
description description
dueDate
position position
taskGroup { taskGroup {
id id

View File

@ -0,0 +1,12 @@
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time) {
updateTaskDueDate (
input: {
taskID: $taskID
dueDate: $dueDate
}
) {
id
dueDate
}
}

View File

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

View File

@ -0,0 +1,28 @@
import React from 'react';
import styled from 'styled-components/macro';
export type IconProps = {
width: number | string;
height: number | string;
};
type Props = {
width: number | string;
height: number | string;
viewBox: string;
className?: string;
};
const Svg = styled.svg`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const Icon: React.FC<Props> = ({ width, height, viewBox, className, children }) => {
return (
<Svg className={className} width={width} height={height} xmlns="http://www.w3.org/2000/svg" viewBox={viewBox}>
{children}
</Svg>
);
};
export default Icon;

View File

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

View File

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