From b6f0e8b6b2246352e4017530bd03a9e04d6babfc Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Mon, 15 Jun 2020 17:36:59 -0500 Subject: [PATCH] feature: add due dates to tasks --- api/graph/generated.go | 183 ++++++++++++++ api/graph/models_gen.go | 7 + api/graph/schema.graphqls | 7 + api/graph/schema.resolvers.go | 22 ++ api/pg/pg.go | 1 + api/pg/querier.go | 2 + api/pg/task.sql.go | 24 ++ api/pg/task_group.sql.go | 22 ++ api/query/task.sql | 3 + web/src/App/BaseStyles.ts | 14 ++ web/src/App/ThemeStyles.ts | 1 + web/src/Projects/Project/Details/index.tsx | 40 ++- web/src/Projects/Project/index.tsx | 10 +- web/src/citadel.d.ts | 4 +- web/src/projects.d.ts | 1 + web/src/shared/components/Button/index.tsx | 14 +- web/src/shared/components/Card/Styles.ts | 6 +- .../DueDateManager/DueDateManager.stories.tsx | 92 +++---- .../components/DueDateManager/Styles.ts | 54 ++++- .../components/DueDateManager/index.tsx | 227 ++++++++++++------ web/src/shared/components/Input/index.tsx | 96 ++++++-- web/src/shared/components/Lists/index.tsx | 9 + .../TaskDetails/TaskDetails.stories.tsx | 1 + .../shared/components/TaskDetails/index.tsx | 14 +- web/src/shared/generated/graphql.tsx | 77 +++++- web/src/shared/graphql/findProject.graphqls | 1 + web/src/shared/graphql/findTask.graphqls | 1 + .../shared/graphql/updateTaskDueDate.graphqls | 12 + web/src/shared/icons/Bolt.tsx | 22 +- web/src/shared/icons/Icon.tsx | 28 +++ web/src/shared/icons/Tags.tsx | 22 +- web/src/shared/icons/ToggleOn.tsx | 21 +- 32 files changed, 816 insertions(+), 222 deletions(-) create mode 100644 web/src/shared/graphql/updateTaskDueDate.graphqls create mode 100644 web/src/shared/icons/Icon.tsx diff --git a/api/graph/generated.go b/api/graph/generated.go index a4fdc23..678d8ff 100644 --- a/api/graph/generated.go +++ b/api/graph/generated.go @@ -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) } diff --git a/api/graph/models_gen.go b/api/graph/models_gen.go index 03f6437..3bd86f9 100644 --- a/api/graph/models_gen.go +++ b/api/graph/models_gen.go @@ -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"` diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index d626453..602eba3 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -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! diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 09fb727..cec7d27 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -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{} diff --git a/api/pg/pg.go b/api/pg/pg.go index 3e08de3..62a3bfe 100644 --- a/api/pg/pg.go +++ b/api/pg/pg.go @@ -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) diff --git a/api/pg/querier.go b/api/pg/querier.go index f2753b6..ca1adf4 100644 --- a/api/pg/querier.go +++ b/api/pg/querier.go @@ -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) diff --git a/api/pg/task.sql.go b/api/pg/task.sql.go index aa06f59..ac6e36e 100644 --- a/api/pg/task.sql.go +++ b/api/pg/task.sql.go @@ -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 ` diff --git a/api/pg/task_group.sql.go b/api/pg/task_group.sql.go index a7e1105..e0cb17b 100644 --- a/api/pg/task_group.sql.go +++ b/api/pg/task_group.sql.go @@ -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 ` diff --git a/api/query/task.sql b/api/query/task.sql index 54de5b8..347a2ac 100644 --- a/api/query/task.sql +++ b/api/query/task.sql @@ -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 *; diff --git a/web/src/App/BaseStyles.ts b/web/src/App/BaseStyles.ts index e96a068..329e9fa 100644 --- a/web/src/App/BaseStyles.ts +++ b/web/src/App/BaseStyles.ts @@ -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)} `; diff --git a/web/src/App/ThemeStyles.ts b/web/src/App/ThemeStyles.ts index 1d46126..a16961f 100644 --- a/web/src/App/ThemeStyles.ts +++ b/web/src/App/ThemeStyles.ts @@ -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', diff --git a/web/src/Projects/Project/Details/index.tsx b/web/src/Projects/Project/Details/index.tsx index bd5a8ed..39c483e 100644 --- a/web/src/Projects/Project/Details/index.tsx +++ b/web/src/Projects/Project/Details/index.tsx @@ -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 = ({ 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 = ({ if (!data) { return
loading
; } + console.log(data.findTask); return ( <> = ({ ); }} onOpenAddLabelPopup={onOpenAddLabelPopup} + onOpenDueDatePopop={(task, $targetRef) => { + showPopup( + $targetRef, + + { + hidePopup(); + }} + > + { + console.log(`${newDueDate}`); + updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } }); + hidePopup(); + }} + onCancel={() => {}} + /> + , + ); + }} /> ); }} diff --git a/web/src/Projects/Project/index.tsx b/web/src/Projects/Project/index.tsx index 4996054..ac796d3 100644 --- a/web/src/Projects/Project/index.tsx +++ b/web/src/Projects/Project/index.tsx @@ -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 = () => { ); }} > - + Labels - + Fields - + Rules diff --git a/web/src/citadel.d.ts b/web/src/citadel.d.ts index 611a015..52a8e83 100644 --- a/web/src/citadel.d.ts +++ b/web/src/citadel.d.ts @@ -32,8 +32,8 @@ type LoginFormData = { }; type DueDateFormData = { - endDate: Date; - endTime: string | null; + endDate: string; + endTime: string; }; type LoginProps = { diff --git a/web/src/projects.d.ts b/web/src/projects.d.ts index 93dcfc7..bb2f007 100644 --- a/web/src/projects.d.ts +++ b/web/src/projects.d.ts @@ -35,6 +35,7 @@ type Task = { taskGroup: InnerTaskGroup; name: string; position: number; + dueDate?: string; labels: TaskLabel[]; description?: string | null; assigned?: Array; diff --git a/web/src/shared/components/Button/index.tsx b/web/src/shared/components/Button/index.tsx index 5e47954..62a528d 100644 --- a/web/src/shared/components/Button/index.tsx +++ b/web/src/shared/components/Button/index.tsx @@ -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 = ({ fontSize = '14px', color = 'primary', variant = 'filled', + type = 'button', onClick, className, children, @@ -128,38 +130,38 @@ const Button: React.FC = ({ switch (variant) { case 'filled': return ( - + {children} ); case 'outline': return ( - + {children} ); case 'flat': return ( - + {children} ); case 'lineDown': return ( - + {children} ); case 'gradient': return ( - + {children} ); case 'relief': return ( - + {children} ); diff --git a/web/src/shared/components/Card/Styles.ts b/web/src/shared/components/Card/Styles.ts index 5ca43fc..ce17e58 100644 --- a/web/src/shared/components/Card/Styles.ts +++ b/web/src/shared/components/Card/Styles.ts @@ -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` diff --git a/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx b/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx index b7a7b0a..838cea9 100644 --- a/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx +++ b/web/src/shared/components/DueDateManager/DueDateManager.stories.tsx @@ -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,44 +24,50 @@ export default { export const Default = () => { return ( - - - + + + + + + - - + taskGroup: { name: 'General', id: '1', position: 1 }, + name: 'Hello, world', + position: 1, + labels: [ + { + id: 'soft-skills', + assignedDate: new Date().toString(), + projectLabel: { + createdDate: new Date().toString(), + id: 'label-soft-skills', + name: 'Soft Skills', + labelColor: { + id: '1', + name: 'white', + colorHex: '#fff', + position: 1, + }, + }, + }, + ], + description: 'hello!', + assigned: [ + { + id: '1', + profileIcon: { url: null, initials: null, bgColor: null }, + fullName: 'Jordan Knott', + }, + ], + }} + onCancel={action('cancel')} + onDueDateChange={action('due date change')} + /> + + + + ); }; diff --git a/web/src/shared/components/DueDateManager/Styles.ts b/web/src/shared/components/DueDateManager/Styles.ts index 96e5800..31d4367 100644 --- a/web/src/shared/components/DueDateManager/Styles.ts +++ b/web/src/shared/components/DueDateManager/Styles.ts @@ -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%; diff --git a/web/src/shared/components/DueDateManager/index.tsx b/web/src/shared/components/DueDateManager/index.tsx index 28d46c1..ffadbd9 100644 --- a/web/src/shared/components/DueDateManager/index.tsx +++ b/web/src/shared/components/DueDateManager/index.tsx @@ -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 = ({ 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,85 +138,143 @@ const DueDateManager: React.FC = ({ task, onDueDateChange, 'November', 'December', ]; - const { register, handleSubmit, errors, setError, formState } = useForm(); + const { register, handleSubmit, errors, setValue, setError, formState } = useForm(); + 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 ( + { + 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 ( -
- { - 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; - }, - })} - /> -
- - ( - - - Prev - - - {months[date.getMonth()]} - changeYear(parseInt(value))}> - {years.map(option => ( - - ))} - - - - {date.getFullYear()} - changeMonth(months.indexOf(value))} - > - {months.map(option => ( - - ))} - - +
+ + { + setTextStartDate(e.currentTarget.value); + }} + value={textStartDate} + ref={register({ + required: 'End date is required.', + })} + /> + + + { + 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={} + /> + + + ( + + + Prev + + + {months[date.getMonth()]} + changeYear(parseInt(value))}> + {years.map(option => ( + + ))} + + + + {date.getFullYear()} + changeMonth(months.indexOf(value))} + > + {months.map(option => ( + + ))} + + - - Next - - - )} - selected={startDate} - inline - onChange={date => setStartDate(date ?? new Date())} - /> - - - onDueDateChange(task, startDate)}>Save - - - - + + Next + + + )} + selected={startDate} + inline + onChange={date => { + setStartDate(date ?? new Date()); + }} + /> + + + { + // 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 + + + + + +
); }; diff --git a/web/src/shared/components/Input/index.tsx b/web/src/shared/components/Input/index.tsx index d596752..669a765 100644 --- a/web/src/shared/components/Input/index.tsx +++ b/web/src/shared/components/Input/index.tsx @@ -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,24 +82,66 @@ type InputProps = { width?: string; placeholder?: string; icon?: JSX.Element; + id?: string; + name?: string; + className?: string; + value?: string; + onClick?: (e: React.MouseEvent) => void; + onChange?: (e: React.ChangeEvent) => void; }; -const Input: React.FC = ({ width = 'auto', variant = 'normal', label, placeholder, icon }) => { - const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561'; - const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)'; - return ( - - - {label && {label}} - {icon && 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) => { + setValue(e.currentTarget.value); + if (onChange) { + onChange(e); + } + }; + return ( + + + {label && {label}} + {icon && icon} + + ); + }, +); export default Input; diff --git a/web/src/shared/components/Lists/index.tsx b/web/src/shared/components/Lists/index.tsx index 83dedad..e4874d9 100644 --- a/web/src/shared/components/Lists/index.tsx +++ b/web/src/shared/components/Lists/index.tsx @@ -12,6 +12,7 @@ import { } from 'shared/utils/draggables'; import { Container, BoardWrapper } from './Styles'; +import moment from 'moment'; interface SimpleProps { taskGroups: Array; @@ -165,6 +166,14 @@ const SimpleLists: React.FC = ({ 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={() => { diff --git a/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx index 84e758e..98ef26d 100644 --- a/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx +++ b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx @@ -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')} /> ); }} diff --git a/web/src/shared/components/TaskDetails/index.tsx b/web/src/shared/components/TaskDetails/index.tsx index 250d752..d7c3e33 100644 --- a/web/src/shared/components/TaskDetails/index.tsx +++ b/web/src/shared/components/TaskDetails/index.tsx @@ -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) => void; onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject) => void; + onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject) => void; onMemberProfile: ($targetRef: React.RefObject, memberID: string) => void; onCloseModal: () => void; }; @@ -118,6 +120,7 @@ const TaskDetails: React.FC = ({ onCloseModal, onOpenAddMemberPopup, onOpenAddLabelPopup, + onOpenDueDatePopop, onMemberProfile, }) => { const [editorOpen, setEditorOpen] = useState(false); @@ -143,6 +146,7 @@ const TaskDetails: React.FC = ({ const onAddMember = () => { onOpenAddMemberPopup(task, $addMemberRef); }; + const $dueDateLabel = useRef(null); const $addLabelRef = useRef(null); const onAddLabel = () => { onOpenAddLabelPopup(task, $addLabelRef); @@ -229,7 +233,15 @@ const TaskDetails: React.FC = ({ Due Date - No due date + {task.dueDate ? ( + onOpenDueDatePopop(task, $dueDateLabel)}> + {moment(task.dueDate).format('MMM D [at] h:mm A')} + + ) : ( + onOpenDueDatePopop(task, $dueDateLabel)}> + No due date + + )} diff --git a/web/src/shared/generated/graphql.tsx b/web/src/shared/generated/graphql.tsx index dbb4e20..c0456ba 100644 --- a/web/src/shared/generated/graphql.tsx +++ b/web/src/shared/generated/graphql.tsx @@ -110,6 +110,7 @@ export type Task = { name: Scalars['String']; position: Scalars['Float']; description?: Maybe; + dueDate?: Maybe; assigned: Array; labels: Array; }; @@ -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; +}; + 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 & { tasks: Array<( { __typename?: 'Task' } - & Pick + & Pick & { taskGroup: ( { __typename?: 'TaskGroup' } & Pick @@ -698,7 +721,7 @@ export type FindTaskQuery = ( { __typename?: 'Query' } & { findTask: ( { __typename?: 'Task' } - & Pick + & Pick & { taskGroup: ( { __typename?: 'TaskGroup' } & Pick @@ -852,6 +875,20 @@ export type UpdateTaskDescriptionMutation = ( ) } ); +export type UpdateTaskDueDateMutationVariables = { + taskID: Scalars['UUID']; + dueDate?: Maybe; +}; + + +export type UpdateTaskDueDateMutation = ( + { __typename?: 'Mutation' } + & { updateTaskDueDate: ( + { __typename?: 'Task' } + & Pick + ) } +); + 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; export type UpdateTaskDescriptionMutationResult = ApolloReactCommon.MutationResult; export type UpdateTaskDescriptionMutationOptions = ApolloReactCommon.BaseMutationOptions; +export const UpdateTaskDueDateDocument = gql` + mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time) { + updateTaskDueDate(input: {taskID: $taskID, dueDate: $dueDate}) { + id + dueDate + } +} + `; +export type UpdateTaskDueDateMutationFn = ApolloReactCommon.MutationFunction; + +/** + * __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) { + return ApolloReactHooks.useMutation(UpdateTaskDueDateDocument, baseOptions); + } +export type UpdateTaskDueDateMutationHookResult = ReturnType; +export type UpdateTaskDueDateMutationResult = ApolloReactCommon.MutationResult; +export type UpdateTaskDueDateMutationOptions = ApolloReactCommon.BaseMutationOptions; export const UpdateTaskGroupLocationDocument = gql` mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) { updateTaskGroupLocation(input: {taskGroupID: $taskGroupID, position: $position}) { diff --git a/web/src/shared/graphql/findProject.graphqls b/web/src/shared/graphql/findProject.graphqls index 75e81a3..f1fe1ac 100644 --- a/web/src/shared/graphql/findProject.graphqls +++ b/web/src/shared/graphql/findProject.graphqls @@ -30,6 +30,7 @@ query findProject($projectId: String!) { name position description + dueDate taskGroup { id name diff --git a/web/src/shared/graphql/findTask.graphqls b/web/src/shared/graphql/findTask.graphqls index 8a8cf5d..611b6bc 100644 --- a/web/src/shared/graphql/findTask.graphqls +++ b/web/src/shared/graphql/findTask.graphqls @@ -3,6 +3,7 @@ query findTask($taskID: UUID!) { id name description + dueDate position taskGroup { id diff --git a/web/src/shared/graphql/updateTaskDueDate.graphqls b/web/src/shared/graphql/updateTaskDueDate.graphqls new file mode 100644 index 0000000..3f4f743 --- /dev/null +++ b/web/src/shared/graphql/updateTaskDueDate.graphqls @@ -0,0 +1,12 @@ +mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time) { + updateTaskDueDate ( + input: { + taskID: $taskID + dueDate: $dueDate + } + ) { + id + dueDate + } +} + diff --git a/web/src/shared/icons/Bolt.tsx b/web/src/shared/icons/Bolt.tsx index 5d1b7fc..7227958 100644 --- a/web/src/shared/icons/Bolt.tsx +++ b/web/src/shared/icons/Bolt.tsx @@ -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 = ({ width = '16px', height = '16px' }) => { return ( - - - + + + ); }; -Bolt.defaultProps = { - size: 16, - color: '#000', -}; - export default Bolt; diff --git a/web/src/shared/icons/Icon.tsx b/web/src/shared/icons/Icon.tsx new file mode 100644 index 0000000..3d9bf6d --- /dev/null +++ b/web/src/shared/icons/Icon.tsx @@ -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 = ({ width, height, viewBox, className, children }) => { + return ( + + {children} + + ); +}; + +export default Icon; diff --git a/web/src/shared/icons/Tags.tsx b/web/src/shared/icons/Tags.tsx index 6a88a38..2cdfb10 100644 --- a/web/src/shared/icons/Tags.tsx +++ b/web/src/shared/icons/Tags.tsx @@ -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 = ({ width = '16px', height = '16px' }) => { return ( - - - + + + ); }; -Tags.defaultProps = { - size: 16, - color: '#000', -}; - export default Tags; diff --git a/web/src/shared/icons/ToggleOn.tsx b/web/src/shared/icons/ToggleOn.tsx index f2cce50..8152bf2 100644 --- a/web/src/shared/icons/ToggleOn.tsx +++ b/web/src/shared/icons/ToggleOn.tsx @@ -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 = ({ width = '16px', height = '16px' }) => { return ( - - - + + + ); }; -ToggleOn.defaultProps = { - size: 16, - color: '#000', -}; - export default ToggleOn;