feat: redesign task due date manager

This commit is contained in:
Jordan Knott 2021-01-01 14:51:40 -06:00
parent a8b3809515
commit d6101d9221
20 changed files with 450 additions and 167 deletions

View File

@ -126,4 +126,8 @@ export default createGlobalStyle`
} }
${mixin.placeholderColor(color.textLight)} ${mixin.placeholderColor(color.textLight)}
.picker-hidden {
display: none;
}
`; `;

View File

@ -793,12 +793,12 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<DueDateManager <DueDateManager
task={task} task={task}
onRemoveDueDate={t => { onRemoveDueDate={t => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null } }); updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
hidePopup(); // hidePopup();
}} }}
onDueDateChange={(t, newDueDate) => { onDueDateChange={(t, newDueDate, hasTime) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } }); updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } });
hidePopup(); // hidePopup();
}} }}
onCancel={NOOP} onCancel={NOOP}
/> />

View File

@ -632,12 +632,12 @@ const Details: React.FC<DetailsProps> = ({
<DueDateManager <DueDateManager
task={task} task={task}
onRemoveDueDate={t => { onRemoveDueDate={t => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null } }); updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
hidePopup(); // hidePopup();
}} }}
onDueDateChange={(t, newDueDate) => { onDueDateChange={(t, newDueDate, hasTime) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } }); updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } });
hidePopup(); // hidePopup();
}} }}
onCancel={NOOP} onCancel={NOOP}
/> />

View File

@ -2,6 +2,8 @@ import styled from 'styled-components';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import Input from 'shared/components/Input'; import Input from 'shared/components/Input';
import ControlledInput from 'shared/components/ControlledInput';
import { Clock } from 'shared/icons';
export const Wrapper = styled.div` export const Wrapper = styled.div`
display: flex display: flex
@ -17,6 +19,11 @@ display: flex
z-index: 10000; z-index: 10000;
margin-top: 0; margin-top: 0;
} }
& .react-datepicker__close-icon::after {
background: none;
font-size: 16px;
color: ${props => props.theme.colors.text.primary};
}
& .react-datepicker-time__header { & .react-datepicker-time__header {
color: ${props => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
@ -91,6 +98,24 @@ display: flex
border-bottom: 1px solid ${props => props.theme.colors.border}; border-bottom: 1px solid ${props => props.theme.colors.border};
} }
& .react-datepicker__input-container input {
border: 1px solid rgba(0, 0, 0, 0.2);
border-color: ${props => props.theme.colors.alternate};
background: #262c49;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
padding: 0.7rem;
color: #c2c6dc;
position: relative;
border-radius: 5px;
transition: all 0.3s ease;
font-size: 13px;
line-height: 20px;
padding: 0 12px;
&:focus {
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
border: 1px solid rgba(115, 103, 240);
background: ${props => props.theme.colors.bg.primary};
}
`; `;
export const DueDatePickerWrapper = styled.div` export const DueDatePickerWrapper = styled.div`
@ -110,6 +135,44 @@ export const RemoveDueDate = styled(Button)`
margin: 0 0 0 4px; margin: 0 0 0 4px;
`; `;
export const AddDateRange = styled.div`
opacity: 0.6;
display: flex;
align-items: center;
width: 100%;
font-size: 12px;
line-height: 16px;
color: ${props => mixin.rgba(props.theme.colors.primary, 0.8)};
&:hover {
color: ${props => mixin.rgba(props.theme.colors.primary, 1)};
text-decoration: underline;
}
`;
export const DateRangeInputs = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-left: -4px;
& > div:first-child,
& > div:last-child {
flex: 1 1 92px;
margin-bottom: 4px;
margin-left: 4px;
min-width: 92px;
width: initial;
}
& > ${AddDateRange} {
margin-left: 4px;
padding-left: 4px;
}
& > .react-datepicker-wrapper input {
padding-bottom: 4px;
padding-top: 4px;
width: 100%;
}
`;
export const CancelDueDate = styled.div` export const CancelDueDate = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
@ -119,15 +182,86 @@ export const CancelDueDate = styled.div`
cursor: pointer; cursor: pointer;
`; `;
export const DueDateInput = styled(Input)` export const DueDateInput = styled(ControlledInput)`
margin-top: 15px; margin-top: 15px;
margin-bottom: 5px; margin-bottom: 5px;
padding-right: 10px; padding-right: 10px;
`; `;
export const ActionWrapper = styled.div` export const ActionsSeparator = styled.div`
padding-top: 8px; margin-top: 8px;
height: 1px;
width: 100%; width: 100%;
background: #414561;
display: flex; display: flex;
justify-content: space-between; `;
export const ActionsWrapper = styled.div`
margin-top: 8px;
display: flex;
align-items: center;
& .react-datepicker-wrapper {
margin-left: auto;
width: 82px;
}
& .react-datepicker__input-container input {
padding-bottom: 4px;
padding-top: 4px;
width: 100%;
}
`;
export const ActionClock = styled(Clock)`
align-self: center;
fill: ${props => props.theme.colors.primary};
margin: 0 8px;
flex: 0 0 auto;
`;
export const ActionLabel = styled.div`
font-size: 12px;
line-height: 14px;
`;
export const ActionIcon = styled.div`
height: 36px;
min-height: 36px;
min-width: 36px;
width: 36px;
border-radius: 6px;
background: transparent;
cursor: pointer;
margin-right: 8px;
svg {
fill: ${props => props.theme.colors.text.primary};
transition-duration: 0.2s;
transition-property: background, border, box-shadow, fill;
}
&:hover svg {
fill: ${props => props.theme.colors.text.secondary};
}
align-items: center;
display: inline-flex;
justify-content: center;
`;
export const ClearButton = styled.div`
font-weight: 500;
font-size: 13px;
height: 36px;
line-height: 36px;
padding: 0 12px;
margin-left: auto;
cursor: pointer;
align-items: center;
border-radius: 6px;
display: inline-flex;
flex-shrink: 0;
justify-content: center;
transition-duration: 0.2s;
transition-property: background, border, box-shadow, color, fill;
color: ${props => props.theme.colors.text.primary};
&:hover {
color: ${props => props.theme.colors.text.secondary};
}
`; `;

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, forwardRef } from 'react'; import React, { useState, useEffect, forwardRef, useRef, useCallback } from 'react';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import styled from 'styled-components'; import styled from 'styled-components';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
@ -8,11 +8,27 @@ import { getYear, getMonth } from 'date-fns';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import { Wrapper, ActionWrapper, RemoveDueDate, DueDateInput, DueDatePickerWrapper, ConfirmAddDueDate } from './Styles'; import {
Wrapper,
RemoveDueDate,
DueDateInput,
DueDatePickerWrapper,
ConfirmAddDueDate,
DateRangeInputs,
AddDateRange,
ActionIcon,
ActionsWrapper,
ClearButton,
ActionsSeparator,
ActionClock,
ActionLabel,
} from './Styles';
import { Clock, Cross } from 'shared/icons';
import Select from 'react-select/src/Select';
type DueDateManagerProps = { type DueDateManagerProps = {
task: Task; task: Task;
onDueDateChange: (task: Task, newDueDate: Date) => void; onDueDateChange: (task: Task, newDueDate: Date, hasTime: boolean) => void;
onRemoveDueDate: (task: Task) => void; onRemoveDueDate: (task: Task) => void;
onCancel: () => void; onCancel: () => void;
}; };
@ -52,14 +68,20 @@ const HeaderSelect = styled.select`
text-decoration: underline; text-decoration: underline;
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
padding: 4px 6px;
background: none; background: none;
outline: none; outline: none;
border: none; border: none;
border-radius: 3px; border-radius: 3px;
appearance: none; appearance: none;
width: 100%;
display: inline-block;
&:hover { & option {
color: #c2c6dc;
background: ${props => props.theme.colors.bg.primary};
}
& option:hover {
background: ${props => props.theme.colors.bg.secondary}; background: ${props => props.theme.colors.bg.secondary};
border: 1px solid ${props => props.theme.colors.primary}; border: 1px solid ${props => props.theme.colors.primary};
outline: none !important; outline: none !important;
@ -110,15 +132,34 @@ const HeaderActions = styled.div`
`; `;
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => { const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => {
const now = dayjs(); const currentDueDate = task.dueDate ? dayjs(task.dueDate).toDate() : null;
const { register, handleSubmit, errors, setValue, setError, formState, control } = useForm<DueDateFormData>(); const { register, handleSubmit, errors, setValue, setError, formState, control } = useForm<DueDateFormData>();
const [startDate, setStartDate] = useState(new Date());
const [startDate, setStartDate] = useState<Date | null>(currentDueDate);
const [endDate, setEndDate] = useState<Date | null>(currentDueDate);
const [hasTime, enableTime] = useState(task.hasTime ?? false);
const firstRun = useRef<boolean>(true);
const debouncedFunctionRef = useRef((newDate: Date | null, nowHasTime: boolean) => {
if (!firstRun.current) {
if (newDate) {
onDueDateChange(task, newDate, nowHasTime);
} else {
onRemoveDueDate(task);
enableTime(false);
}
} else {
firstRun.current = false;
}
});
const debouncedChange = useCallback(
_.debounce((newDate, nowHasTime) => debouncedFunctionRef.current(newDate, nowHasTime), 500),
[],
);
useEffect(() => { useEffect(() => {
const newDate = dayjs(startDate).format('YYYY-MM-DD'); debouncedChange(startDate, hasTime);
setValue('endDate', newDate); }, [startDate, hasTime]);
}, [startDate]);
const years = _.range(2010, getYear(new Date()) + 10, 1); const years = _.range(2010, getYear(new Date()) + 10, 1);
const months = [ const months = [
'January', 'January',
@ -134,19 +175,21 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
'November', 'November',
'December', 'December',
]; ];
const saveDueDate = (data: any) => {
const newDate = dayjs(`${data.endDate} ${dayjs(data.endTime).format('h:mm A')}`, 'YYYY-MM-DD h:mm A'); const onChange = (dates: any) => {
if (newDate.isValid()) { const [start, end] = dates;
onDueDateChange(task, newDate.toDate()); setStartDate(start);
} setEndDate(end);
}; };
const CustomTimeInput = forwardRef(({ value, onClick }: any, $ref: any) => { const [isRange, setIsRange] = useState(false);
const CustomTimeInput = forwardRef(({ value, onClick, onChange, onBlur, onFocus }: any, $ref: any) => {
return ( return (
<DueDateInput <DueDateInput
id="endTime" id="endTime"
value={value} value={value}
name="endTime" name="endTime"
ref={$ref} onChange={onChange}
width="100%" width="100%"
variant="alternate" variant="alternate"
label="Time" label="Time"
@ -154,114 +197,119 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
/> />
); );
}); });
return ( return (
<Wrapper> <Wrapper>
<Form onSubmit={handleSubmit(saveDueDate)}> <DateRangeInputs>
<FormField> <DatePicker
<DueDateInput selected={startDate}
id="endDate" onChange={date => setStartDate(date)}
name="endDate" popperClassName="picker-hidden"
width="100%" dateFormat="yyyy-MM-dd"
variant="alternate" disabledKeyboardNavigation
label="Date" isClearable
defaultValue={now.format('YYYY-MM-DD')} placeholderText="Select due date"
ref={register({ />
required: 'End date is required.', {isRange ? (
})}
/>
</FormField>
<FormField>
<Controller
control={control}
defaultValue={now.toDate()}
name="endTime"
render={({ onChange, onBlur, value }) => (
<DatePicker
onChange={onChange}
selected={value}
onBlur={onBlur}
showTimeSelect
showTimeSelectOnly
timeIntervals={15}
timeCaption="Time"
dateFormat="h:mm aa"
customInput={<CustomTimeInput />}
/>
)}
/>
</FormField>
<DueDatePickerWrapper>
<DatePicker <DatePicker
useWeekdaysShort
renderCustomHeader={({
date,
changeYear,
changeMonth,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<HeaderActions>
<HeaderButton onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>
Prev
</HeaderButton>
<HeaderSelectLabel>
{months[date.getMonth()]}
<HeaderSelect
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(parseInt(value, 10))}
>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<HeaderSelectLabel>
{date.getFullYear()}
<HeaderSelect
value={months[getMonth(date)]}
onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<HeaderButton onClick={increaseMonth} disabled={nextMonthButtonDisabled}>
Next
</HeaderButton>
</HeaderActions>
)}
selected={startDate} selected={startDate}
inline isClearable
onChange={date => { onChange={date => setStartDate(date)}
if (date) { popperClassName="picker-hidden"
setStartDate(date); dateFormat="yyyy-MM-dd"
} placeholderText="Select from date"
}}
/> />
</DueDatePickerWrapper> ) : (
<ActionWrapper> <AddDateRange>Add date range</AddDateRange>
<ConfirmAddDueDate type="submit" onClick={NOOP}> )}
Save </DateRangeInputs>
</ConfirmAddDueDate> <DatePicker
<RemoveDueDate selected={startDate}
variant="outline" onChange={date => setStartDate(date)}
color="danger" startDate={startDate}
useWeekdaysShort
renderCustomHeader={({
date,
changeYear,
changeMonth,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<HeaderActions>
<HeaderButton onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>
Prev
</HeaderButton>
<HeaderSelectLabel>
{months[date.getMonth()]}
<HeaderSelect
value={months[getMonth(date)]}
onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<HeaderSelectLabel>
{date.getFullYear()}
<HeaderSelect value={getYear(date)} onChange={({ target: { value } }) => changeYear(parseInt(value, 10))}>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<HeaderButton onClick={increaseMonth} disabled={nextMonthButtonDisabled}>
Next
</HeaderButton>
</HeaderActions>
)}
inline
/>
<ActionsSeparator />
{hasTime && (
<ActionsWrapper>
<ActionClock width={16} height={16} />
<ActionLabel>Due Time</ActionLabel>
<DatePicker
selected={startDate}
onChange={date => {
setStartDate(date);
}}
showTimeSelect
showTimeSelectOnly
timeIntervals={15}
timeCaption="Time"
dateFormat="h:mm aa"
/>
<ActionIcon onClick={() => enableTime(false)}>
<Cross width={16} height={16} />
</ActionIcon>
</ActionsWrapper>
)}
<ActionsWrapper>
{!hasTime && (
<ActionIcon
onClick={() => { onClick={() => {
onRemoveDueDate(task); if (startDate === null) {
const today = new Date();
today.setHours(12, 30, 0);
setStartDate(today);
}
enableTime(true);
}} }}
> >
Remove <Clock width={16} height={16} />
</RemoveDueDate> </ActionIcon>
</ActionWrapper> )}
</Form> <ClearButton onClick={() => setStartDate(null)}>{hasTime ? 'Clear all' : 'Clear'}</ClearButton>
</ActionsWrapper>
</Wrapper> </Wrapper>
); );
}; };

View File

@ -341,7 +341,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
}} }}
> >
{task.dueDate ? ( {task.dueDate ? (
<SidebarButtonText>{dayjs(task.dueDate).format('MMM D [at] h:mm A')}</SidebarButtonText> <SidebarButtonText>
{dayjs(task.dueDate).format(task.hasTime ? 'MMM D [at] h:mm A' : 'MMMM D')}
</SidebarButtonText>
) : ( ) : (
<SidebarButtonText>No due date</SidebarButtonText> <SidebarButtonText>No due date</SidebarButtonText>
)} )}

View File

@ -215,6 +215,7 @@ export type Task = {
position: Scalars['Float']; position: Scalars['Float'];
description?: Maybe<Scalars['String']>; description?: Maybe<Scalars['String']>;
dueDate?: Maybe<Scalars['Time']>; dueDate?: Maybe<Scalars['Time']>;
hasTime: Scalars['Boolean'];
complete: Scalars['Boolean']; complete: Scalars['Boolean'];
completedAt?: Maybe<Scalars['Time']>; completedAt?: Maybe<Scalars['Time']>;
assigned: Array<Member>; assigned: Array<Member>;
@ -883,6 +884,7 @@ export type UpdateTaskLocationPayload = {
export type UpdateTaskDueDate = { export type UpdateTaskDueDate = {
taskID: Scalars['UUID']; taskID: Scalars['UUID'];
hasTime: Scalars['Boolean'];
dueDate?: Maybe<Scalars['Time']>; dueDate?: Maybe<Scalars['Time']>;
}; };
@ -1451,7 +1453,7 @@ export type FindTaskQuery = (
{ __typename?: 'Query' } { __typename?: 'Query' }
& { findTask: ( & { findTask: (
{ __typename?: 'Task' } { __typename?: 'Task' }
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position' | 'complete'> & Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position' | 'complete' | 'hasTime'>
& { taskGroup: ( & { taskGroup: (
{ __typename?: 'TaskGroup' } { __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name'> & Pick<TaskGroup, 'id' | 'name'>
@ -2314,6 +2316,7 @@ export type UpdateTaskDescriptionMutation = (
export type UpdateTaskDueDateMutationVariables = Exact<{ export type UpdateTaskDueDateMutationVariables = Exact<{
taskID: Scalars['UUID']; taskID: Scalars['UUID'];
dueDate?: Maybe<Scalars['Time']>; dueDate?: Maybe<Scalars['Time']>;
hasTime: Scalars['Boolean'];
}>; }>;
@ -2321,7 +2324,7 @@ export type UpdateTaskDueDateMutation = (
{ __typename?: 'Mutation' } { __typename?: 'Mutation' }
& { updateTaskDueDate: ( & { updateTaskDueDate: (
{ __typename?: 'Task' } { __typename?: 'Task' }
& Pick<Task, 'id' | 'dueDate'> & Pick<Task, 'id' | 'dueDate' | 'hasTime'>
) } ) }
); );
@ -3017,6 +3020,7 @@ export const FindTaskDocument = gql`
dueDate dueDate
position position
complete complete
hasTime
taskGroup { taskGroup {
id id
name name
@ -4692,10 +4696,13 @@ export type UpdateTaskDescriptionMutationHookResult = ReturnType<typeof useUpdat
export type UpdateTaskDescriptionMutationResult = ApolloReactCommon.MutationResult<UpdateTaskDescriptionMutation>; export type UpdateTaskDescriptionMutationResult = ApolloReactCommon.MutationResult<UpdateTaskDescriptionMutation>;
export type UpdateTaskDescriptionMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>; export type UpdateTaskDescriptionMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>;
export const UpdateTaskDueDateDocument = gql` export const UpdateTaskDueDateDocument = gql`
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time) { mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!) {
updateTaskDueDate(input: {taskID: $taskID, dueDate: $dueDate}) { updateTaskDueDate(
input: {taskID: $taskID, dueDate: $dueDate, hasTime: $hasTime}
) {
id id
dueDate dueDate
hasTime
} }
} }
`; `;
@ -4716,6 +4723,7 @@ export type UpdateTaskDueDateMutationFn = ApolloReactCommon.MutationFunction<Upd
* variables: { * variables: {
* taskID: // value for 'taskID' * taskID: // value for 'taskID'
* dueDate: // value for 'dueDate' * dueDate: // value for 'dueDate'
* hasTime: // value for 'hasTime'
* }, * },
* }); * });
*/ */
@ -5167,4 +5175,4 @@ export function useUsersLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOp
} }
export type UsersQueryHookResult = ReturnType<typeof useUsersQuery>; export type UsersQueryHookResult = ReturnType<typeof useUsersQuery>;
export type UsersLazyQueryHookResult = ReturnType<typeof useUsersLazyQuery>; export type UsersLazyQueryHookResult = ReturnType<typeof useUsersLazyQuery>;
export type UsersQueryResult = ApolloReactCommon.QueryResult<UsersQuery, UsersQueryVariables>; export type UsersQueryResult = ApolloReactCommon.QueryResult<UsersQuery, UsersQueryVariables>;

View File

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

View File

@ -1,11 +1,13 @@
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time) { mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!) {
updateTaskDueDate ( updateTaskDueDate (
input: { input: {
taskID: $taskID taskID: $taskID
dueDate: $dueDate dueDate: $dueDate
hasTime: $hasTime
} }
) { ) {
id id
dueDate dueDate
hasTime
} }
} }

View File

@ -102,6 +102,7 @@ type Task = {
name: string; name: string;
badges?: TaskBadges; badges?: TaskBadges;
position: number; position: number;
hasTime?: boolean;
dueDate?: string; dueDate?: string;
complete?: boolean; complete?: boolean;
completedAt?: string | null; completedAt?: string | null;

View File

@ -102,6 +102,7 @@ type Task struct {
DueDate sql.NullTime `json:"due_date"` DueDate sql.NullTime `json:"due_date"`
Complete bool `json:"complete"` Complete bool `json:"complete"`
CompletedAt sql.NullTime `json:"completed_at"` CompletedAt sql.NullTime `json:"completed_at"`
HasTime bool `json:"has_time"`
} }
type TaskActivity struct { type TaskActivity struct {

View File

@ -34,7 +34,7 @@ UPDATE task SET name = $2 WHERE task_id = $1 RETURNING *;
DELETE FROM task where task_group_id = $1; DELETE FROM task where task_group_id = $1;
-- name: UpdateTaskDueDate :one -- name: UpdateTaskDueDate :one
UPDATE task SET due_date = $2 WHERE task_id = $1 RETURNING *; UPDATE task SET due_date = $2, has_time = $3 WHERE task_id = $1 RETURNING *;
-- name: SetTaskComplete :one -- name: SetTaskComplete :one
UPDATE task SET complete = $2, completed_at = $3 WHERE task_id = $1 RETURNING *; UPDATE task SET complete = $2, completed_at = $3 WHERE task_id = $1 RETURNING *;

View File

@ -13,7 +13,7 @@ import (
const createTask = `-- name: CreateTask :one const createTask = `-- name: CreateTask :one
INSERT INTO task (task_group_id, created_at, name, position) INSERT INTO task (task_group_id, created_at, name, position)
VALUES($1, $2, $3, $4) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at VALUES($1, $2, $3, $4) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type CreateTaskParams struct { type CreateTaskParams struct {
@ -41,13 +41,14 @@ func (q *Queries) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, e
&i.DueDate, &i.DueDate,
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime,
) )
return i, err return i, err
} }
const createTaskAll = `-- name: CreateTaskAll :one const createTaskAll = `-- name: CreateTaskAll :one
INSERT INTO task (task_group_id, created_at, name, position, description, complete, due_date) INSERT INTO task (task_group_id, created_at, name, position, description, complete, due_date)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type CreateTaskAllParams struct { type CreateTaskAllParams struct {
@ -81,6 +82,7 @@ func (q *Queries) CreateTaskAll(ctx context.Context, arg CreateTaskAllParams) (T
&i.DueDate, &i.DueDate,
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime,
) )
return i, err return i, err
} }
@ -158,7 +160,7 @@ func (q *Queries) DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid
} }
const getAllTasks = `-- name: GetAllTasks :many const getAllTasks = `-- name: GetAllTasks :many
SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at FROM task SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time FROM task
` `
func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) { func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
@ -180,6 +182,7 @@ func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
&i.DueDate, &i.DueDate,
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -243,7 +246,7 @@ func (q *Queries) GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uu
} }
const getTaskByID = `-- name: GetTaskByID :one const getTaskByID = `-- name: GetTaskByID :one
SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at FROM task WHERE task_id = $1 SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time FROM task WHERE task_id = $1
` `
func (q *Queries) GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, error) { func (q *Queries) GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, error) {
@ -259,12 +262,13 @@ func (q *Queries) GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, erro
&i.DueDate, &i.DueDate,
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime,
) )
return i, err return i, err
} }
const getTasksForTaskGroupID = `-- name: GetTasksForTaskGroupID :many const getTasksForTaskGroupID = `-- name: GetTasksForTaskGroupID :many
SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at FROM task WHERE task_group_id = $1 SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time FROM task WHERE task_group_id = $1
` `
func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error) { func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error) {
@ -286,6 +290,7 @@ func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.U
&i.DueDate, &i.DueDate,
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -301,7 +306,7 @@ func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.U
} }
const setTaskComplete = `-- name: SetTaskComplete :one const setTaskComplete = `-- name: SetTaskComplete :one
UPDATE task SET complete = $2, completed_at = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at UPDATE task SET complete = $2, completed_at = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type SetTaskCompleteParams struct { type SetTaskCompleteParams struct {
@ -323,12 +328,13 @@ func (q *Queries) SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams
&i.DueDate, &i.DueDate,
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime,
) )
return i, err return i, err
} }
const updateTaskComment = `-- name: UpdateTaskComment :one const updateTaskComment = `-- name: UpdateTaskComment :one
UPDATE task_comment SET message = $2, updated_at = $3 WHERE task_comment_id = $1 RETURNING task_comment_id, task_id, created_at, updated_at, created_by, pinned, message UPDATE task_comment SET message = $2, updated_at = COALESCE($3, updated_at) WHERE task_comment_id = $1 RETURNING task_comment_id, task_id, created_at, updated_at, created_by, pinned, message
` `
type UpdateTaskCommentParams struct { type UpdateTaskCommentParams struct {
@ -353,7 +359,7 @@ func (q *Queries) UpdateTaskComment(ctx context.Context, arg UpdateTaskCommentPa
} }
const updateTaskDescription = `-- name: UpdateTaskDescription :one const updateTaskDescription = `-- name: UpdateTaskDescription :one
UPDATE task SET description = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at UPDATE task SET description = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type UpdateTaskDescriptionParams struct { type UpdateTaskDescriptionParams struct {
@ -374,21 +380,23 @@ func (q *Queries) UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescr
&i.DueDate, &i.DueDate,
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime,
) )
return i, err return i, err
} }
const updateTaskDueDate = `-- name: UpdateTaskDueDate :one 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, complete, completed_at UPDATE task SET due_date = $2, has_time = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type UpdateTaskDueDateParams struct { type UpdateTaskDueDateParams struct {
TaskID uuid.UUID `json:"task_id"` TaskID uuid.UUID `json:"task_id"`
DueDate sql.NullTime `json:"due_date"` DueDate sql.NullTime `json:"due_date"`
HasTime bool `json:"has_time"`
} }
func (q *Queries) UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDateParams) (Task, error) { func (q *Queries) UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDateParams) (Task, error) {
row := q.db.QueryRowContext(ctx, updateTaskDueDate, arg.TaskID, arg.DueDate) row := q.db.QueryRowContext(ctx, updateTaskDueDate, arg.TaskID, arg.DueDate, arg.HasTime)
var i Task var i Task
err := row.Scan( err := row.Scan(
&i.TaskID, &i.TaskID,
@ -400,12 +408,13 @@ func (q *Queries) UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDatePa
&i.DueDate, &i.DueDate,
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime,
) )
return i, err return i, err
} }
const updateTaskLocation = `-- name: UpdateTaskLocation :one const updateTaskLocation = `-- name: UpdateTaskLocation :one
UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at 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, complete, completed_at, has_time
` `
type UpdateTaskLocationParams struct { type UpdateTaskLocationParams struct {
@ -427,12 +436,13 @@ func (q *Queries) UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocation
&i.DueDate, &i.DueDate,
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime,
) )
return i, err return i, err
} }
const updateTaskName = `-- name: UpdateTaskName :one const updateTaskName = `-- name: UpdateTaskName :one
UPDATE task SET name = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at UPDATE task SET name = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type UpdateTaskNameParams struct { type UpdateTaskNameParams struct {
@ -453,12 +463,13 @@ func (q *Queries) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams)
&i.DueDate, &i.DueDate,
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime,
) )
return i, err return i, err
} }
const updateTaskPosition = `-- name: UpdateTaskPosition :one const updateTaskPosition = `-- name: UpdateTaskPosition :one
UPDATE task SET position = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at UPDATE task SET position = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type UpdateTaskPositionParams struct { type UpdateTaskPositionParams struct {
@ -479,6 +490,7 @@ func (q *Queries) UpdateTaskPosition(ctx context.Context, arg UpdateTaskPosition
&i.DueDate, &i.DueDate,
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime,
) )
return i, err return i, err
} }

View File

@ -383,6 +383,7 @@ type ComplexityRoot struct {
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
Description func(childComplexity int) int Description func(childComplexity int) int
DueDate func(childComplexity int) int DueDate func(childComplexity int) int
HasTime func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Labels func(childComplexity int) int Labels func(childComplexity int) int
Name func(childComplexity int) int Name func(childComplexity int) int
@ -2376,6 +2377,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Task.DueDate(childComplexity), true return e.complexity.Task.DueDate(childComplexity), true
case "Task.hasTime":
if e.complexity.Task.HasTime == nil {
break
}
return e.complexity.Task.HasTime(childComplexity), true
case "Task.id": case "Task.id":
if e.complexity.Task.ID == nil { if e.complexity.Task.ID == nil {
break break
@ -3135,6 +3143,7 @@ type Task {
position: Float! position: Float!
description: String description: String
dueDate: Time dueDate: Time
hasTime: Boolean!
complete: Boolean! complete: Boolean!
completedAt: Time completedAt: Time
assigned: [Member!]! assigned: [Member!]!
@ -3477,6 +3486,7 @@ type UpdateTaskLocationPayload {
input UpdateTaskDueDate { input UpdateTaskDueDate {
taskID: UUID! taskID: UUID!
hasTime: Boolean!
dueDate: Time dueDate: Time
} }
@ -13558,6 +13568,40 @@ func (ec *executionContext) _Task_dueDate(ctx context.Context, field graphql.Col
return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res)
} }
func (ec *executionContext) _Task_hasTime(ctx context.Context, field graphql.CollectedField, obj *db.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: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.HasTime, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Task_complete(ctx context.Context, field graphql.CollectedField, obj *db.Task) (ret graphql.Marshaler) { func (ec *executionContext) _Task_complete(ctx context.Context, field graphql.CollectedField, obj *db.Task) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -18596,6 +18640,12 @@ func (ec *executionContext) unmarshalInputUpdateTaskDueDate(ctx context.Context,
if err != nil { if err != nil {
return it, err return it, err
} }
case "hasTime":
var err error
it.HasTime, err = ec.unmarshalNBoolean2bool(ctx, v)
if err != nil {
return it, err
}
case "dueDate": case "dueDate":
var err error var err error
it.DueDate, err = ec.unmarshalOTime2ᚖtimeᚐTime(ctx, v) it.DueDate, err = ec.unmarshalOTime2ᚖtimeᚐTime(ctx, v)
@ -20968,6 +21018,11 @@ func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj
res = ec._Task_dueDate(ctx, field, obj) res = ec._Task_dueDate(ctx, field, obj)
return res return res
}) })
case "hasTime":
out.Values[i] = ec._Task_hasTime(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
case "complete": case "complete":
out.Values[i] = ec._Task_complete(ctx, field, obj) out.Values[i] = ec._Task_complete(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {

View File

@ -519,6 +519,7 @@ type UpdateTaskDescriptionInput struct {
type UpdateTaskDueDate struct { type UpdateTaskDueDate struct {
TaskID uuid.UUID `json:"taskID"` TaskID uuid.UUID `json:"taskID"`
HasTime bool `json:"hasTime"`
DueDate *time.Time `json:"dueDate"` DueDate *time.Time `json:"dueDate"`
} }

View File

@ -175,6 +175,7 @@ type Task {
position: Float! position: Float!
description: String description: String
dueDate: Time dueDate: Time
hasTime: Boolean!
complete: Boolean! complete: Boolean!
completedAt: Time completedAt: Time
assigned: [Member!]! assigned: [Member!]!
@ -517,6 +518,7 @@ type UpdateTaskLocationPayload {
input UpdateTaskDueDate { input UpdateTaskDueDate {
taskID: UUID! taskID: UUID!
hasTime: Boolean!
dueDate: Time dueDate: Time
} }
@ -943,3 +945,4 @@ type DeleteUserAccountPayload {
ok: Boolean! ok: Boolean!
userAccount: UserAccount! userAccount: UserAccount!
} }

View File

@ -423,28 +423,35 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa
activityType = TASK_DUE_DATE_CHANGED activityType = TASK_DUE_DATE_CHANGED
data["PrevDueDate"] = prevTask.DueDate.Time.String() data["PrevDueDate"] = prevTask.DueDate.Time.String()
data["CurDueDate"] = input.DueDate.String() data["CurDueDate"] = input.DueDate.String()
} else { } else if input.DueDate != nil {
data["DueDate"] = input.DueDate.String() data["DueDate"] = input.DueDate.String()
} }
var dueDate sql.NullTime var dueDate sql.NullTime
log.WithField("dueDate", input.DueDate).Info("before ptr!")
if input.DueDate == nil { if input.DueDate == nil {
dueDate = sql.NullTime{Valid: false, Time: time.Now()} dueDate = sql.NullTime{Valid: false, Time: time.Now()}
} else { } else {
dueDate = sql.NullTime{Valid: true, Time: *input.DueDate} dueDate = sql.NullTime{Valid: true, Time: *input.DueDate}
} }
task, err := r.Repository.UpdateTaskDueDate(ctx, db.UpdateTaskDueDateParams{ var task db.Task
TaskID: input.TaskID, if !(input.DueDate == nil && !prevTask.DueDate.Valid) {
DueDate: dueDate, task, err = r.Repository.UpdateTaskDueDate(ctx, db.UpdateTaskDueDateParams{
}) TaskID: input.TaskID,
createdAt := time.Now().UTC() DueDate: dueDate,
d, err := json.Marshal(data) HasTime: input.HasTime,
_, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{ })
TaskID: task.TaskID, createdAt := time.Now().UTC()
Data: d, d, _ := json.Marshal(data)
CausedBy: userID, _, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{
CreatedAt: createdAt, TaskID: task.TaskID,
ActivityTypeID: activityType, Data: d,
}) CausedBy: userID,
CreatedAt: createdAt,
ActivityTypeID: activityType,
})
} else {
task, err = r.Repository.GetTaskByID(ctx, input.TaskID)
}
return &task, err return &task, err
} }

View File

@ -175,6 +175,7 @@ type Task {
position: Float! position: Float!
description: String description: String
dueDate: Time dueDate: Time
hasTime: Boolean!
complete: Boolean! complete: Boolean!
completedAt: Time completedAt: Time
assigned: [Member!]! assigned: [Member!]!

View File

@ -49,6 +49,7 @@ type UpdateTaskLocationPayload {
input UpdateTaskDueDate { input UpdateTaskDueDate {
taskID: UUID! taskID: UUID!
hasTime: Boolean!
dueDate: Time dueDate: Time
} }

View File

@ -0,0 +1,2 @@
ALTER TABLE task ADD COLUMN has_time boolean NOT NULL DEFAULT false;
UPDATE task SET has_time = true;