feature: add due dates to tasks
This commit is contained in:
parent
a12e9c1e50
commit
b6f0e8b6b2
@ -94,6 +94,7 @@ type ComplexityRoot struct {
|
||||
UpdateProjectLabelName func(childComplexity int, input UpdateProjectLabelName) int
|
||||
UpdateProjectName func(childComplexity int, input *UpdateProjectName) int
|
||||
UpdateTaskDescription func(childComplexity int, input UpdateTaskDescriptionInput) int
|
||||
UpdateTaskDueDate func(childComplexity int, input UpdateTaskDueDate) int
|
||||
UpdateTaskGroupLocation func(childComplexity int, input NewTaskGroupLocation) int
|
||||
UpdateTaskGroupName func(childComplexity int, input UpdateTaskGroupName) int
|
||||
UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int
|
||||
@ -153,6 +154,7 @@ type ComplexityRoot struct {
|
||||
Assigned func(childComplexity int) int
|
||||
CreatedAt func(childComplexity int) int
|
||||
Description func(childComplexity int) int
|
||||
DueDate func(childComplexity int) int
|
||||
ID func(childComplexity int) int
|
||||
Labels 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)
|
||||
UpdateTaskLocation(ctx context.Context, input NewTaskLocation) (*UpdateTaskLocationPayload, 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)
|
||||
AssignTask(ctx context.Context, input *AssignTaskInput) (*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)
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
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":
|
||||
if e.complexity.Mutation.UpdateTaskGroupLocation == nil {
|
||||
break
|
||||
@ -925,6 +941,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
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":
|
||||
if e.complexity.Task.ID == nil {
|
||||
break
|
||||
@ -1271,6 +1294,7 @@ type Task {
|
||||
name: String!
|
||||
position: Float!
|
||||
description: String
|
||||
dueDate: Time
|
||||
assigned: [ProjectMember!]!
|
||||
labels: [TaskLabel!]!
|
||||
}
|
||||
@ -1448,6 +1472,11 @@ input UpdateTaskGroupName {
|
||||
name: String!
|
||||
}
|
||||
|
||||
input UpdateTaskDueDate {
|
||||
taskID: UUID!
|
||||
dueDate: Time
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createRefreshToken(input: NewRefreshToken!): RefreshToken!
|
||||
|
||||
@ -1478,6 +1507,7 @@ type Mutation {
|
||||
updateTaskDescription(input: UpdateTaskDescriptionInput!): Task!
|
||||
updateTaskLocation(input: NewTaskLocation!): UpdateTaskLocationPayload!
|
||||
updateTaskName(input: UpdateTaskName!): Task!
|
||||
updateTaskDueDate(input: UpdateTaskDueDate!): Task!
|
||||
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
|
||||
assignTask(input: AssignTaskInput): Task!
|
||||
unassignTask(input: UnassignTaskInput): Task!
|
||||
@ -1786,6 +1816,20 @@ func (ec *executionContext) field_Mutation_updateTaskDescription_args(ctx contex
|
||||
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) {
|
||||
var err error
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
defer func() {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -7166,6 +7282,30 @@ func (ec *executionContext) unmarshalInputUpdateTaskDescriptionInput(ctx context
|
||||
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) {
|
||||
var it UpdateTaskGroupName
|
||||
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 {
|
||||
invalids++
|
||||
}
|
||||
case "updateTaskDueDate":
|
||||
out.Values[i] = ec._Mutation_updateTaskDueDate(ctx, field)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "deleteTask":
|
||||
out.Values[i] = ec._Mutation_deleteTask(ctx, field)
|
||||
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)
|
||||
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":
|
||||
field := field
|
||||
out.Concurrently(i, func() (res graphql.Marshaler) {
|
||||
@ -9265,6 +9421,10 @@ func (ec *executionContext) unmarshalNUpdateTaskDescriptionInput2githubᚗcomᚋ
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
return ec.unmarshalInputUnassignTaskInput(ctx, v)
|
||||
}
|
||||
|
@ -3,6 +3,8 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jordanknott/project-citadel/api/pg"
|
||||
)
|
||||
@ -168,6 +170,11 @@ type UpdateTaskDescriptionInput struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type UpdateTaskDueDate struct {
|
||||
TaskID uuid.UUID `json:"taskID"`
|
||||
DueDate *time.Time `json:"dueDate"`
|
||||
}
|
||||
|
||||
type UpdateTaskGroupName struct {
|
||||
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||
Name string `json:"name"`
|
||||
|
@ -84,6 +84,7 @@ type Task {
|
||||
name: String!
|
||||
position: Float!
|
||||
description: String
|
||||
dueDate: Time
|
||||
assigned: [ProjectMember!]!
|
||||
labels: [TaskLabel!]!
|
||||
}
|
||||
@ -261,6 +262,11 @@ input UpdateTaskGroupName {
|
||||
name: String!
|
||||
}
|
||||
|
||||
input UpdateTaskDueDate {
|
||||
taskID: UUID!
|
||||
dueDate: Time
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createRefreshToken(input: NewRefreshToken!): RefreshToken!
|
||||
|
||||
@ -291,6 +297,7 @@ type Mutation {
|
||||
updateTaskDescription(input: UpdateTaskDescriptionInput!): Task!
|
||||
updateTaskLocation(input: NewTaskLocation!): UpdateTaskLocationPayload!
|
||||
updateTaskName(input: UpdateTaskName!): Task!
|
||||
updateTaskDueDate(input: UpdateTaskDueDate!): Task!
|
||||
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
|
||||
assignTask(input: AssignTaskInput): Task!
|
||||
unassignTask(input: UnassignTaskInput): Task!
|
||||
|
@ -260,6 +260,21 @@ func (r *mutationResolver) UpdateTaskName(ctx context.Context, input UpdateTaskN
|
||||
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) {
|
||||
taskID, err := uuid.Parse(input.TaskID)
|
||||
if err != nil {
|
||||
@ -484,6 +499,13 @@ func (r *taskResolver) Description(ctx context.Context, obj *pg.Task) (*string,
|
||||
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) {
|
||||
taskMemberLinks, err := r.Repository.GetAssignedMembersForTask(ctx, obj.TaskID)
|
||||
taskMembers := []ProjectMember{}
|
||||
|
@ -30,6 +30,7 @@ type Repository interface {
|
||||
UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, 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)
|
||||
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
|
||||
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
|
||||
|
@ -56,11 +56,13 @@ type Querier interface {
|
||||
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
|
||||
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (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)
|
||||
UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
|
||||
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)
|
||||
UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, 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)
|
||||
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
|
||||
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
|
||||
|
@ -177,6 +177,30 @@ func (q *Queries) UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescr
|
||||
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
|
||||
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
|
||||
`
|
||||
|
@ -135,6 +135,28 @@ func (q *Queries) GetTaskGroupsForProject(ctx context.Context, projectID uuid.UU
|
||||
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
|
||||
UPDATE task_group SET position = $2 WHERE task_group_id = $1 RETURNING task_group_id, project_id, created_at, name, position
|
||||
`
|
||||
|
@ -25,3 +25,6 @@ UPDATE task SET name = $2 WHERE task_id = $1 RETURNING *;
|
||||
|
||||
-- name: DeleteTasksByTaskGroupID :execrows
|
||||
DELETE FROM task where task_group_id = $1;
|
||||
|
||||
-- name: UpdateTaskDueDate :one
|
||||
UPDATE task SET due_date = $2 WHERE task_id = $1 RETURNING *;
|
||||
|
@ -111,5 +111,19 @@ export default createGlobalStyle`
|
||||
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)}
|
||||
`;
|
||||
|
@ -17,6 +17,7 @@ const theme: DefaultTheme = {
|
||||
primary: '194, 198, 220',
|
||||
secondary: '255, 255, 255',
|
||||
},
|
||||
border: '65, 69, 97',
|
||||
bg: {
|
||||
primary: '16, 22, 58',
|
||||
secondary: '38, 44, 73',
|
||||
|
@ -4,9 +4,15 @@ import TaskDetails from 'shared/components/TaskDetails';
|
||||
import PopupMenu, { Popup, usePopup } from 'shared/components/PopupMenu';
|
||||
import MemberManager from 'shared/components/MemberManager';
|
||||
import { useRouteMatch, useHistory } from 'react-router';
|
||||
import { useFindTaskQuery, useAssignTaskMutation, useUnassignTaskMutation } from 'shared/generated/graphql';
|
||||
import {
|
||||
useFindTaskQuery,
|
||||
useUpdateTaskDueDateMutation,
|
||||
useAssignTaskMutation,
|
||||
useUnassignTaskMutation,
|
||||
} from 'shared/generated/graphql';
|
||||
import UserIDContext from 'App/context';
|
||||
import MiniProfile from 'shared/components/MiniProfile';
|
||||
import DueDateManager from 'shared/components/DueDateManager';
|
||||
|
||||
type DetailsProps = {
|
||||
taskID: string;
|
||||
@ -32,12 +38,18 @@ const Details: React.FC<DetailsProps> = ({
|
||||
refreshCache,
|
||||
}) => {
|
||||
const { userID } = useContext(UserIDContext);
|
||||
const { showPopup } = usePopup();
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const history = useHistory();
|
||||
const match = useRouteMatch();
|
||||
const [currentMemberTask, setCurrentMemberTask] = useState('');
|
||||
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
|
||||
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
|
||||
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
|
||||
onCompleted: () => {
|
||||
refetch();
|
||||
refreshCache();
|
||||
},
|
||||
});
|
||||
const [assignTask] = useAssignTaskMutation({
|
||||
onCompleted: () => {
|
||||
refetch();
|
||||
@ -56,6 +68,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
if (!data) {
|
||||
return <div>loading</div>;
|
||||
}
|
||||
console.log(data.findTask);
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
@ -108,6 +121,29 @@ const Details: React.FC<DetailsProps> = ({
|
||||
);
|
||||
}}
|
||||
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>,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -229,14 +229,14 @@ const ProjectAction = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
color: var(--color-text);
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&: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>
|
||||
</ProjectAction>
|
||||
<ProjectAction>
|
||||
<ToggleOn size={13} color="var(--color-icon)" />
|
||||
<ToggleOn width={13} height={13} />
|
||||
<ProjectActionText>Fields</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction>
|
||||
<Bolt size={13} color="var(--color-icon)" />
|
||||
<Bolt width={13} height={13} />
|
||||
<ProjectActionText>Rules</ProjectActionText>
|
||||
</ProjectAction>
|
||||
</ProjectActions>
|
||||
|
4
web/src/citadel.d.ts
vendored
4
web/src/citadel.d.ts
vendored
@ -32,8 +32,8 @@ type LoginFormData = {
|
||||
};
|
||||
|
||||
type DueDateFormData = {
|
||||
endDate: Date;
|
||||
endTime: string | null;
|
||||
endDate: string;
|
||||
endTime: string;
|
||||
};
|
||||
|
||||
type LoginProps = {
|
||||
|
1
web/src/projects.d.ts
vendored
1
web/src/projects.d.ts
vendored
@ -35,6 +35,7 @@ type Task = {
|
||||
taskGroup: InnerTaskGroup;
|
||||
name: string;
|
||||
position: number;
|
||||
dueDate?: string;
|
||||
labels: TaskLabel[];
|
||||
description?: string | null;
|
||||
assigned?: Array<TaskUser>;
|
||||
|
@ -107,6 +107,7 @@ type ButtonProps = {
|
||||
variant?: 'filled' | 'outline' | 'flat' | 'lineDown' | 'gradient' | 'relief';
|
||||
color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark';
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit';
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
@ -116,6 +117,7 @@ const Button: React.FC<ButtonProps> = ({
|
||||
fontSize = '14px',
|
||||
color = 'primary',
|
||||
variant = 'filled',
|
||||
type = 'button',
|
||||
onClick,
|
||||
className,
|
||||
children,
|
||||
@ -128,38 +130,38 @@ const Button: React.FC<ButtonProps> = ({
|
||||
switch (variant) {
|
||||
case 'filled':
|
||||
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>
|
||||
</Filled>
|
||||
);
|
||||
case 'outline':
|
||||
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>
|
||||
</Outline>
|
||||
);
|
||||
case 'flat':
|
||||
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>
|
||||
</Flat>
|
||||
);
|
||||
case 'lineDown':
|
||||
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>
|
||||
<LineX color={color} />
|
||||
</LineDown>
|
||||
);
|
||||
case 'gradient':
|
||||
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>
|
||||
</Gradient>
|
||||
);
|
||||
case 'relief':
|
||||
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>
|
||||
</Relief>
|
||||
);
|
||||
|
@ -22,7 +22,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: var(--color-input-text-focus);
|
||||
color: rgba(${props => props.theme.colors.text.secondary});
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
@ -84,7 +84,7 @@ export const ListCardContainer = styled.div<{ isActive: boolean; editable: boole
|
||||
position: relative;
|
||||
|
||||
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`
|
||||
@ -145,7 +145,7 @@ export const CardTitle = styled.span`
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
word-wrap: break-word;
|
||||
color: var(--color-text);
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
`;
|
||||
|
||||
export const CardMembers = styled.div`
|
||||
|
@ -1,12 +1,16 @@
|
||||
import React, { useRef } from 'react';
|
||||
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 styled from 'styled-components';
|
||||
import DueDateManager from '.';
|
||||
|
||||
const PopupWrapper = styled.div`
|
||||
width: 300px;
|
||||
width: 310px;
|
||||
`;
|
||||
|
||||
export default {
|
||||
component: DueDateManager,
|
||||
title: 'DueDateManager',
|
||||
@ -20,6 +24,10 @@ export default {
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<ThemeProvider theme={theme}>
|
||||
<PopupWrapper>
|
||||
<Popup title={null} tab={0}>
|
||||
<DueDateManager
|
||||
@ -59,5 +67,7 @@ export const Default = () => {
|
||||
/>
|
||||
</Popup>
|
||||
</PopupWrapper>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
import Button from 'shared/components/Button';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
import Input from 'shared/components/Input';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
display: flex
|
||||
@ -8,7 +10,37 @@ display: flex
|
||||
background: #262c49;
|
||||
font-family: 'Droid Sans', sans-serif;
|
||||
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 {
|
||||
color: #c2c6dc;
|
||||
@ -56,6 +88,9 @@ display: flex
|
||||
background: 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;
|
||||
`;
|
||||
|
||||
export const ConfirmAddDueDate = styled.div`
|
||||
background-color: #5aac44;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
export const ConfirmAddDueDate = styled(Button)`
|
||||
float: left;
|
||||
margin: 0 4px 0 0;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
padding: 6px 12px;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export const CancelDueDate = styled.div`
|
||||
@ -92,6 +116,12 @@ export const CancelDueDate = styled.div`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const DueDateInput = styled(Input)`
|
||||
margin-top: 15px;
|
||||
margin-bottom: 5px;
|
||||
padding-right: 10px;
|
||||
`;
|
||||
|
||||
export const ActionWrapper = styled.div`
|
||||
padding-top: 8px;
|
||||
width: 100%;
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, forwardRef } 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 { Wrapper, ActionWrapper, DueDateInput, DueDatePickerWrapper, ConfirmAddDueDate, CancelDueDate } from './Styles';
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import { getYear, getMonth } from 'date-fns';
|
||||
@ -17,6 +17,14 @@ type DueDateManagerProps = {
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const Form = styled.form`
|
||||
padding-top: 25px;
|
||||
`;
|
||||
|
||||
const FormField = styled.div`
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
`;
|
||||
const HeaderSelectLabel = styled.div`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
@ -104,12 +112,17 @@ const HeaderActions = styled.div`
|
||||
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 [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 months = [
|
||||
'January',
|
||||
@ -125,29 +138,75 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
||||
'November',
|
||||
'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);
|
||||
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 (
|
||||
<Wrapper>
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
<Form onSubmit={handleSubmit(saveDueDate)}>
|
||||
<FormField>
|
||||
<DueDateInput
|
||||
id="endDate"
|
||||
name="endDate"
|
||||
width="100%"
|
||||
variant="alternate"
|
||||
label="Date"
|
||||
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;
|
||||
},
|
||||
required: 'End date is required.',
|
||||
})}
|
||||
/>
|
||||
</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>
|
||||
<DatePicker
|
||||
useWeekdaysShort
|
||||
@ -195,15 +254,27 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
||||
)}
|
||||
selected={startDate}
|
||||
inline
|
||||
onChange={date => setStartDate(date ?? new Date())}
|
||||
onChange={date => {
|
||||
setStartDate(date ?? new Date());
|
||||
}}
|
||||
/>
|
||||
</DueDatePickerWrapper>
|
||||
<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}>
|
||||
<Cross size={16} color="#c2c6dc" />
|
||||
</CancelDueDate>
|
||||
</ActionWrapper>
|
||||
</Form>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
|
||||
const InputWrapper = styled.div<{ width: string }>`
|
||||
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};
|
||||
font-size: 14px;
|
||||
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);
|
||||
transform: translate(-3px, -90%);
|
||||
}
|
||||
${props =>
|
||||
props.hasValue &&
|
||||
css`
|
||||
& ~ ${InputLabel} {
|
||||
color: rgba(115, 103, 240);
|
||||
transform: translate(-3px, -90%);
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const Icon = styled.div`
|
||||
@ -68,14 +82,55 @@ type InputProps = {
|
||||
width?: string;
|
||||
placeholder?: string;
|
||||
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 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 (
|
||||
<InputWrapper width={width}>
|
||||
<InputWrapper className={className} width={width}>
|
||||
<InputInput
|
||||
hasValue={value !== ''}
|
||||
ref={$ref}
|
||||
id={id}
|
||||
name={name}
|
||||
onClick={onClick}
|
||||
onChange={handleChange}
|
||||
value={value}
|
||||
hasIcon={typeof icon !== 'undefined'}
|
||||
width={width}
|
||||
placeholder={placeholder}
|
||||
@ -86,6 +141,7 @@ const Input: React.FC<InputProps> = ({ width = 'auto', variant = 'normal', label
|
||||
<Icon>{icon && icon}</Icon>
|
||||
</InputWrapper>
|
||||
);
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export default Input;
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
} from 'shared/utils/draggables';
|
||||
|
||||
import { Container, BoardWrapper } from './Styles';
|
||||
import moment from 'moment';
|
||||
|
||||
interface SimpleProps {
|
||||
taskGroups: Array<TaskGroup>;
|
||||
@ -165,6 +166,14 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
taskGroupID={taskGroup.id}
|
||||
description=""
|
||||
labels={task.labels.map(label => label.projectLabel)}
|
||||
dueDate={
|
||||
task.dueDate
|
||||
? {
|
||||
isPastDue: false,
|
||||
formattedDate: moment(task.dueDate).format('MMM D, YYYY'),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
title={task.name}
|
||||
members={task.assigned}
|
||||
onClick={() => {
|
||||
|
@ -66,6 +66,7 @@ export const Default = () => {
|
||||
onMemberProfile={action('profile')}
|
||||
onOpenAddMemberPopup={action('open add member popup')}
|
||||
onOpenAddLabelPopup={action('open add label popup')}
|
||||
onOpenDueDatePopop={action('open due date popup')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -39,6 +39,7 @@ import {
|
||||
} from './Styles';
|
||||
|
||||
import convertDivElementRefToBounds from 'shared/utils/boundingRect';
|
||||
import moment from 'moment';
|
||||
|
||||
type TaskContentProps = {
|
||||
onEditContent: () => void;
|
||||
@ -106,6 +107,7 @@ type TaskDetailsProps = {
|
||||
onDeleteTask: (task: Task) => void;
|
||||
onOpenAddMemberPopup: (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;
|
||||
onCloseModal: () => void;
|
||||
};
|
||||
@ -118,6 +120,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
onCloseModal,
|
||||
onOpenAddMemberPopup,
|
||||
onOpenAddLabelPopup,
|
||||
onOpenDueDatePopop,
|
||||
onMemberProfile,
|
||||
}) => {
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
@ -143,6 +146,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
const onAddMember = () => {
|
||||
onOpenAddMemberPopup(task, $addMemberRef);
|
||||
};
|
||||
const $dueDateLabel = useRef<HTMLDivElement>(null);
|
||||
const $addLabelRef = useRef<HTMLDivElement>(null);
|
||||
const onAddLabel = () => {
|
||||
onOpenAddLabelPopup(task, $addLabelRef);
|
||||
@ -229,7 +233,15 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
</TaskDetailsAddLabel>
|
||||
</TaskDetailLabels>
|
||||
<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>
|
||||
</TaskDetailsWrapper>
|
||||
</>
|
||||
|
@ -110,6 +110,7 @@ export type Task = {
|
||||
name: Scalars['String'];
|
||||
position: Scalars['Float'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
dueDate?: Maybe<Scalars['Time']>;
|
||||
assigned: Array<ProjectMember>;
|
||||
labels: Array<TaskLabel>;
|
||||
};
|
||||
@ -310,6 +311,16 @@ export type UpdateTaskLocationPayload = {
|
||||
task: Task;
|
||||
};
|
||||
|
||||
export type UpdateTaskGroupName = {
|
||||
taskGroupID: Scalars['UUID'];
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type UpdateTaskDueDate = {
|
||||
taskID: Scalars['UUID'];
|
||||
dueDate?: Maybe<Scalars['Time']>;
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
createRefreshToken: RefreshToken;
|
||||
@ -325,6 +336,7 @@ export type Mutation = {
|
||||
updateProjectLabelColor: ProjectLabel;
|
||||
createTaskGroup: TaskGroup;
|
||||
updateTaskGroupLocation: TaskGroup;
|
||||
updateTaskGroupName: TaskGroup;
|
||||
deleteTaskGroup: DeleteTaskGroupPayload;
|
||||
addTaskLabel: Task;
|
||||
removeTaskLabel: Task;
|
||||
@ -333,6 +345,7 @@ export type Mutation = {
|
||||
updateTaskDescription: Task;
|
||||
updateTaskLocation: UpdateTaskLocationPayload;
|
||||
updateTaskName: Task;
|
||||
updateTaskDueDate: Task;
|
||||
deleteTask: DeleteTaskPayload;
|
||||
assignTask: Task;
|
||||
unassignTask: Task;
|
||||
@ -400,6 +413,11 @@ export type MutationUpdateTaskGroupLocationArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateTaskGroupNameArgs = {
|
||||
input: UpdateTaskGroupName;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteTaskGroupArgs = {
|
||||
input: DeleteTaskGroupInput;
|
||||
};
|
||||
@ -440,6 +458,11 @@ export type MutationUpdateTaskNameArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateTaskDueDateArgs = {
|
||||
input: UpdateTaskDueDate;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteTaskArgs = {
|
||||
input: DeleteTaskInput;
|
||||
};
|
||||
@ -658,7 +681,7 @@ export type FindProjectQuery = (
|
||||
& Pick<TaskGroup, 'id' | 'name' | 'position'>
|
||||
& { tasks: Array<(
|
||||
{ __typename?: 'Task' }
|
||||
& Pick<Task, 'id' | 'name' | 'position' | 'description'>
|
||||
& Pick<Task, 'id' | 'name' | 'position' | 'description' | 'dueDate'>
|
||||
& { taskGroup: (
|
||||
{ __typename?: 'TaskGroup' }
|
||||
& Pick<TaskGroup, 'id' | 'name' | 'position'>
|
||||
@ -698,7 +721,7 @@ export type FindTaskQuery = (
|
||||
{ __typename?: 'Query' }
|
||||
& { findTask: (
|
||||
{ __typename?: 'Task' }
|
||||
& Pick<Task, 'id' | 'name' | 'description' | 'position'>
|
||||
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position'>
|
||||
& { taskGroup: (
|
||||
{ __typename?: 'TaskGroup' }
|
||||
& 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 = {
|
||||
taskGroupID: Scalars['UUID'];
|
||||
position: Scalars['Float'];
|
||||
@ -1298,6 +1335,7 @@ export const FindProjectDocument = gql`
|
||||
name
|
||||
position
|
||||
description
|
||||
dueDate
|
||||
taskGroup {
|
||||
id
|
||||
name
|
||||
@ -1370,6 +1408,7 @@ export const FindTaskDocument = gql`
|
||||
id
|
||||
name
|
||||
description
|
||||
dueDate
|
||||
position
|
||||
taskGroup {
|
||||
id
|
||||
@ -1705,6 +1744,40 @@ export function useUpdateTaskDescriptionMutation(baseOptions?: ApolloReactHooks.
|
||||
export type UpdateTaskDescriptionMutationHookResult = ReturnType<typeof useUpdateTaskDescriptionMutation>;
|
||||
export type UpdateTaskDescriptionMutationResult = ApolloReactCommon.MutationResult<UpdateTaskDescriptionMutation>;
|
||||
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`
|
||||
mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) {
|
||||
updateTaskGroupLocation(input: {taskGroupID: $taskGroupID, position: $position}) {
|
||||
|
@ -30,6 +30,7 @@ query findProject($projectId: String!) {
|
||||
name
|
||||
position
|
||||
description
|
||||
dueDate
|
||||
taskGroup {
|
||||
id
|
||||
name
|
||||
|
@ -3,6 +3,7 @@ query findTask($taskID: UUID!) {
|
||||
id
|
||||
name
|
||||
description
|
||||
dueDate
|
||||
position
|
||||
taskGroup {
|
||||
id
|
||||
|
12
web/src/shared/graphql/updateTaskDueDate.graphqls
Normal file
12
web/src/shared/graphql/updateTaskDueDate.graphqls
Normal file
@ -0,0 +1,12 @@
|
||||
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time) {
|
||||
updateTaskDueDate (
|
||||
input: {
|
||||
taskID: $taskID
|
||||
dueDate: $dueDate
|
||||
}
|
||||
) {
|
||||
id
|
||||
dueDate
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,12 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Bolt = ({ size, color }: Props) => {
|
||||
const Bolt: React.FC<IconProps> = ({ width = '16px', height = '16px' }) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" width={size} height={size}>
|
||||
<path
|
||||
fill={color}
|
||||
d="M296 160H180.6l42.6-129.8C227.2 15 215.7 0 200 0H56C44 0 33.8 8.9 32.2 20.8l-32 240C-1.7 275.2 9.5 288 24 288h118.7L96.6 482.5c-3.6 15.2 8 29.5 23.3 29.5 8.4 0 16.4-4.4 20.8-12l176-304c9.3-15.9-2.2-36-20.7-36z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon width={width} height={height} viewBox="0 0 320 512">
|
||||
<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" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
Bolt.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Bolt;
|
||||
|
28
web/src/shared/icons/Icon.tsx
Normal file
28
web/src/shared/icons/Icon.tsx
Normal 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;
|
@ -1,24 +1,12 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Tags = ({ size, color }: Props) => {
|
||||
const Tags: React.FC<IconProps> = ({ width = '16px', height = '16px' }) => {
|
||||
return (
|
||||
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
|
||||
<path
|
||||
fill={color}
|
||||
d="M497.941 225.941L286.059 14.059A48 48 0 0 0 252.118 0H48C21.49 0 0 21.49 0 48v204.118a48 48 0 0 0 14.059 33.941l211.882 211.882c18.744 18.745 49.136 18.746 67.882 0l204.118-204.118c18.745-18.745 18.745-49.137 0-67.882zM112 160c-26.51 0-48-21.49-48-48s21.49-48 48-48 48 21.49 48 48-21.49 48-48 48zm513.941 133.823L421.823 497.941c-18.745 18.745-49.137 18.745-67.882 0l-.36-.36L527.64 323.522c16.999-16.999 26.36-39.6 26.36-63.64s-9.362-46.641-26.36-63.64L331.397 0h48.721a48 48 0 0 1 33.941 14.059l211.882 211.882c18.745 18.745 18.745 49.137 0 67.882z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon width={width} height={height} viewBox="0 0 640 512">
|
||||
<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" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
Tags.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
|
@ -1,24 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
const ToggleOn = ({ size, color }: Props) => {
|
||||
const ToggleOn: React.FC<IconProps> = ({ width = '16px', height = '16px' }) => {
|
||||
return (
|
||||
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
||||
<path
|
||||
fill={color}
|
||||
d="M384 64H192C86 64 0 150 0 256s86 192 192 192h192c106 0 192-86 192-192S490 64 384 64zm0 320c-70.8 0-128-57.3-128-128 0-70.8 57.3-128 128-128 70.8 0 128 57.3 128 128 0 70.8-57.3 128-128 128z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon width={width} height={height} viewBox="0 0 576 512">
|
||||
<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" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
ToggleOn.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default ToggleOn;
|
||||
|
Loading…
Reference in New Issue
Block a user