feat: redesign due date manager

This commit is contained in:
Jordan Knott 2021-11-05 22:35:57 -05:00
parent df6140a10f
commit 0d00fc7518
34 changed files with 2204 additions and 196 deletions

View File

@ -61,7 +61,6 @@ const Routes: React.FC = () => {
setLoading(false); setLoading(false);
}); });
}, []); }, []);
console.log('loading', loading);
if (loading) return null; if (loading) return null;
return ( return (
<Switch> <Switch>

View File

@ -9,7 +9,6 @@ const Auth = () => {
const history = useHistory(); const history = useHistory();
const location = useLocation<{ redirect: string } | undefined>(); const location = useLocation<{ redirect: string } | undefined>();
const { setUser } = useContext(UserContext); const { setUser } = useContext(UserContext);
console.log('auth');
const login = ( const login = (
data: LoginFormData, data: LoginFormData,
setComplete: (val: boolean) => void, setComplete: (val: boolean) => void,

View File

@ -562,13 +562,36 @@ const Projects = () => {
onCancel={() => null} onCancel={() => null}
onDueDateChange={(task, dueDate, hasTime) => { onDueDateChange={(task, dueDate, hasTime) => {
if (dateEditor.task) { if (dateEditor.task) {
updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate, hasTime } }); hidePopup();
setDateEditor((prev) => ({ ...prev, task: { ...task, dueDate: dueDate.toISOString(), hasTime } })); updateTaskDueDate({
variables: {
taskID: dateEditor.task.id,
dueDate,
hasTime,
deleteNotifications: [],
updateNotifications: [],
createNotifications: [],
},
});
setDateEditor((prev) => ({
...prev,
task: { ...task, dueDate: { at: dueDate.toISOString(), notifications: [] }, hasTime },
}));
} }
}} }}
onRemoveDueDate={(task) => { onRemoveDueDate={(task) => {
if (dateEditor.task) { if (dateEditor.task) {
updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate: null, hasTime: false } }); hidePopup();
updateTaskDueDate({
variables: {
taskID: dateEditor.task.id,
dueDate: null,
hasTime: false,
deleteNotifications: [],
updateNotifications: [],
createNotifications: [],
},
});
setDateEditor((prev) => ({ ...prev, task: { ...task, hasTime: false } })); setDateEditor((prev) => ({ ...prev, task: { ...task, hasTime: false } }));
} }
}} }}
@ -655,8 +678,8 @@ const Projects = () => {
if (a.dueDate === null && b.dueDate === null) return 0; if (a.dueDate === null && b.dueDate === null) return 0;
if (a.dueDate === null && b.dueDate !== null) return 1; if (a.dueDate === null && b.dueDate !== null) return 1;
if (a.dueDate !== null && b.dueDate === null) return -1; if (a.dueDate !== null && b.dueDate === null) return -1;
const first = dayjs(a.dueDate); const first = dayjs(a.dueDate.at);
const second = dayjs(b.dueDate); const second = dayjs(b.dueDate.at);
if (first.isSame(second, 'minute')) return 0; if (first.isSame(second, 'minute')) return 0;
if (first.isAfter(second)) return -1; if (first.isAfter(second)) return -1;
return 1; return 1;
@ -792,10 +815,19 @@ const Projects = () => {
history.push(`${match.url}/c/${task.id}`); history.push(`${match.url}/c/${task.id}`);
}} }}
onRemoveDueDate={() => { onRemoveDueDate={() => {
updateTaskDueDate({ variables: { taskID: task.id, dueDate: null, hasTime: false } }); updateTaskDueDate({
variables: {
taskID: task.id,
dueDate: null,
hasTime: false,
deleteNotifications: [],
updateNotifications: [],
createNotifications: [],
},
});
}} }}
project={projectName ?? 'none'} project={projectName ?? 'none'}
dueDate={task.dueDate} dueDate={task.dueDate.at}
hasTime={task.hasTime ?? false} hasTime={task.hasTime ?? false}
name={task.name} name={task.name}
onEditName={(name) => updateTaskName({ variables: { taskID: task.id, name } })} onEditName={(name) => updateTaskName({ variables: { taskID: task.id, name } })}
@ -821,7 +853,9 @@ const Projects = () => {
<EditorCell width={120}> <EditorCell width={120}>
<DueDateEditorLabel> <DueDateEditorLabel>
{dateEditor.task.dueDate {dateEditor.task.dueDate
? dayjs(dateEditor.task.dueDate).format(dateEditor.task.hasTime ? 'MMM D [at] h:mm A' : 'MMM D') ? dayjs(dateEditor.task.dueDate.at).format(
dateEditor.task.hasTime ? 'MMM D [at] h:mm A' : 'MMM D',
)
: ''} : ''}
</DueDateEditorLabel> </DueDateEditorLabel>
</EditorCell> </EditorCell>

View File

@ -446,7 +446,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
checklist: null, checklist: null,
}, },
position, position,
dueDate: null, dueDate: { at: null },
description: null, description: null,
labels: [], labels: [],
assigned: [], assigned: [],
@ -801,12 +801,30 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<DueDateManager <DueDateManager
task={task} task={task}
onRemoveDueDate={(t) => { onRemoveDueDate={(t) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } }); hidePopup();
// hidePopup(); updateTaskDueDate({
variables: {
taskID: t.id,
dueDate: null,
hasTime: false,
deleteNotifications: [],
updateNotifications: [],
createNotifications: [],
},
});
}} }}
onDueDateChange={(t, newDueDate, hasTime) => { onDueDateChange={(t, newDueDate, hasTime) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } }); hidePopup();
// hidePopup(); updateTaskDueDate({
variables: {
taskID: t.id,
dueDate: newDueDate,
hasTime,
deleteNotifications: [],
updateNotifications: [],
createNotifications: [],
},
});
}} }}
onCancel={NOOP} onCancel={NOOP}
/> />

View File

@ -12,6 +12,7 @@ import {
useUpdateTaskChecklistItemLocationMutation, useUpdateTaskChecklistItemLocationMutation,
useCreateTaskChecklistMutation, useCreateTaskChecklistMutation,
useFindTaskQuery, useFindTaskQuery,
DueDateNotificationDuration,
useUpdateTaskDueDateMutation, useUpdateTaskDueDateMutation,
useSetTaskCompleteMutation, useSetTaskCompleteMutation,
useAssignTaskMutation, useAssignTaskMutation,
@ -647,12 +648,79 @@ const Details: React.FC<DetailsProps> = ({
<DueDateManager <DueDateManager
task={task} task={task}
onRemoveDueDate={(t) => { onRemoveDueDate={(t) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } }); updateTaskDueDate({
// hidePopup(); variables: {
taskID: t.id,
dueDate: null,
hasTime: false,
deleteNotifications: t.dueDate.notifications
? t.dueDate.notifications.map((n) => ({ id: n.id }))
: [],
updateNotifications: [],
createNotifications: [],
},
});
hidePopup();
}} }}
onDueDateChange={(t, newDueDate, hasTime) => { onDueDateChange={(t, newDueDate, hasTime, notifications) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } }); const updatedNotifications = notifications.current
// hidePopup(); .filter((c) => c.externalId !== null)
.map((c) => {
let duration = DueDateNotificationDuration.Minute;
switch (c.duration.value) {
case 'hour':
duration = DueDateNotificationDuration.Hour;
break;
case 'day':
duration = DueDateNotificationDuration.Day;
break;
case 'week':
duration = DueDateNotificationDuration.Week;
break;
default:
break;
}
return {
id: c.externalId ?? '',
period: c.period,
duration,
};
});
const newNotifications = notifications.current
.filter((c) => c.externalId === null)
.map((c) => {
let duration = DueDateNotificationDuration.Minute;
switch (c.duration.value) {
case 'hour':
duration = DueDateNotificationDuration.Hour;
break;
case 'day':
duration = DueDateNotificationDuration.Day;
break;
case 'week':
duration = DueDateNotificationDuration.Week;
break;
default:
break;
}
return {
taskID: task.id,
period: c.period,
duration,
};
});
// const updatedNotifications = notifications.filter(c => c.externalId === null);
updateTaskDueDate({
variables: {
taskID: t.id,
dueDate: newDueDate,
hasTime,
createNotifications: newNotifications,
updateNotifications: updatedNotifications,
deleteNotifications: notifications.removed.map((n) => ({ id: n })),
},
});
hidePopup();
}} }}
onCancel={NOOP} onCancel={NOOP}
/> />

View File

@ -39,7 +39,6 @@ const UsersRegister = () => {
.then(async (x) => { .then(async (x) => {
const response = await x.json(); const response = await x.json();
const { setup } = response; const { setup } = response;
console.log(response);
if (setup) { if (setup) {
history.replace(`/confirm?confirmToken=xxxx`); history.replace(`/confirm?confirmToken=xxxx`);
isRedirected = true; isRedirected = true;

View File

@ -1,9 +1,8 @@
import styled from 'styled-components'; import styled, { css } 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 ControlledInput from 'shared/components/ControlledInput'; import ControlledInput from 'shared/components/ControlledInput';
import { Clock } from 'shared/icons'; import { Bell, Clock } from 'shared/icons';
export const Wrapper = styled.div` export const Wrapper = styled.div`
display: flex display: flex
@ -22,27 +21,27 @@ display: flex
& .react-datepicker__close-icon::after { & .react-datepicker__close-icon::after {
background: none; background: none;
font-size: 16px; font-size: 16px;
color: ${props => props.theme.colors.text.primary}; 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};
} }
& .react-datepicker__time-list-item { & .react-datepicker__time-list-item {
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
} }
& .react-datepicker__time-container .react-datepicker__time & .react-datepicker__time-container .react-datepicker__time
.react-datepicker__time-box ul.react-datepicker__time-list .react-datepicker__time-box ul.react-datepicker__time-list
li.react-datepicker__time-list-item:hover { li.react-datepicker__time-list-item:hover {
color: ${props => props.theme.colors.text.secondary}; color: ${(props) => props.theme.colors.text.secondary};
background: ${props => props.theme.colors.bg.secondary}; background: ${(props) => props.theme.colors.bg.secondary};
} }
& .react-datepicker__time-container .react-datepicker__time { & .react-datepicker__time-container .react-datepicker__time {
background: ${props => props.theme.colors.bg.primary}; background: ${(props) => props.theme.colors.bg.primary};
} }
& .react-datepicker--time-only { & .react-datepicker--time-only {
background: ${props => props.theme.colors.bg.primary}; background: ${(props) => props.theme.colors.bg.primary};
border: 1px solid ${props => props.theme.colors.border}; border: 1px solid ${(props) => props.theme.colors.border};
} }
& .react-datepicker * { & .react-datepicker * {
@ -82,12 +81,12 @@ display: flex
} }
& .react-datepicker__day--selected { & .react-datepicker__day--selected {
border-radius: 50%; border-radius: 50%;
background: ${props => props.theme.colors.primary}; background: ${(props) => props.theme.colors.primary};
color: #fff; color: #fff;
} }
& .react-datepicker__day--selected:hover { & .react-datepicker__day--selected:hover {
border-radius: 50%; border-radius: 50%;
background: ${props => props.theme.colors.primary}; background: ${(props) => props.theme.colors.primary};
color: #fff; color: #fff;
} }
& .react-datepicker__header { & .react-datepicker__header {
@ -95,12 +94,12 @@ display: flex
border: none; border: none;
} }
& .react-datepicker__header--time { & .react-datepicker__header--time {
border-bottom: 1px solid ${props => props.theme.colors.border}; border-bottom: 1px solid ${(props) => props.theme.colors.border};
} }
& .react-datepicker__input-container input { & .react-datepicker__input-container input {
border: 1px solid rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.2);
border-color: ${props => props.theme.colors.alternate}; border-color: ${(props) => props.theme.colors.alternate};
background: #262c49; background: #262c49;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15); box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
padding: 0.7rem; padding: 0.7rem;
@ -114,7 +113,7 @@ padding: 0.7rem;
&:focus { &:focus {
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15); box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
border: 1px solid rgba(115, 103, 240); border: 1px solid rgba(115, 103, 240);
background: ${props => props.theme.colors.bg.primary}; background: ${(props) => props.theme.colors.bg.primary};
} }
`; `;
@ -142,9 +141,9 @@ export const AddDateRange = styled.div`
width: 100%; width: 100%;
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 16px;
color: ${props => mixin.rgba(props.theme.colors.primary, 0.8)}; color: ${(props) => mixin.rgba(props.theme.colors.primary, 0.8)};
&:hover { &:hover {
color: ${props => mixin.rgba(props.theme.colors.primary, 1)}; color: ${(props) => mixin.rgba(props.theme.colors.primary, 1)};
text-decoration: underline; text-decoration: underline;
} }
`; `;
@ -201,18 +200,62 @@ export const ActionsWrapper = styled.div`
align-items: center; align-items: center;
& .react-datepicker-wrapper { & .react-datepicker-wrapper {
margin-left: auto; margin-left: auto;
width: 82px; width: 86px;
} }
& .react-datepicker__input-container input { & .react-datepicker__input-container input {
padding-bottom: 4px; padding-bottom: 4px;
padding-top: 4px; padding-top: 4px;
width: 100%; width: 100%;
} }
& .react-period-select__indicators {
display: none;
}
& .react-period {
width: 100%;
max-width: 86px;
}
& .react-period-select__single-value {
color: #c2c6dc;
margin-left: 0;
margin-right: 0;
}
& .react-period-select__value-container {
padding-left: 0;
padding-right: 0;
}
& .react-period-select__control {
border: 1px solid rgba(0, 0, 0, 0.2);
min-height: 30px;
border-color: rgb(65, 69, 97);
background: #262c49;
box-shadow: 0 0 0 0 rgb(0 0 0 / 15%);
color: #c2c6dc;
padding-right: 12px;
padding-left: 12px;
padding-bottom: 4px;
padding-top: 4px;
width: 100%;
position: relative;
border-radius: 5px;
transition: all 0.3s ease;
font-size: 13px;
line-height: 20px;
padding: 0 12px;
}
`; `;
export const ActionClock = styled(Clock)` export const ActionClock = styled(Clock)`
align-self: center; align-self: center;
fill: ${props => props.theme.colors.primary}; fill: ${(props) => props.theme.colors.primary};
margin: 0 8px;
flex: 0 0 auto;
`;
export const ActionBell = styled(Bell)`
align-self: center;
fill: ${(props) => props.theme.colors.primary};
margin: 0 8px; margin: 0 8px;
flex: 0 0 auto; flex: 0 0 auto;
`; `;
@ -222,7 +265,7 @@ export const ActionLabel = styled.div`
line-height: 14px; line-height: 14px;
`; `;
export const ActionIcon = styled.div` export const ActionIcon = styled.div<{ disabled?: boolean }>`
height: 36px; height: 36px;
min-height: 36px; min-height: 36px;
min-width: 36px; min-width: 36px;
@ -232,17 +275,25 @@ export const ActionIcon = styled.div`
cursor: pointer; cursor: pointer;
margin-right: 8px; margin-right: 8px;
svg { svg {
fill: ${props => props.theme.colors.text.primary}; fill: ${(props) => props.theme.colors.text.primary};
transition-duration: 0.2s; transition-duration: 0.2s;
transition-property: background, border, box-shadow, fill; transition-property: background, border, box-shadow, fill;
} }
&:hover svg { &:hover svg {
fill: ${props => props.theme.colors.text.secondary}; fill: ${(props) => props.theme.colors.text.secondary};
} }
${(props) =>
props.disabled &&
css`
opacity: 0.8;
cursor: not-allowed;
`}
align-items: center; align-items: center;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
position: relative;
`; `;
export const ClearButton = styled.div` export const ClearButton = styled.div`
@ -260,8 +311,38 @@ export const ClearButton = styled.div`
justify-content: center; justify-content: center;
transition-duration: 0.2s; transition-duration: 0.2s;
transition-property: background, border, box-shadow, color, fill; transition-property: background, border, box-shadow, color, fill;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
&:hover { &:hover {
color: ${props => props.theme.colors.text.secondary}; color: ${(props) => props.theme.colors.text.secondary};
} }
`; `;
export const ControlWrapper = styled.div`
display: flex;
align-items: center;
margin-top: 8px;
`;
export const RightWrapper = styled.div`
flex: 1 1 50%;
display: flex;
align-items: center;
flex-direction: row-reverse;
`;
export const LeftWrapper = styled.div`
flex: 1 1 50%;
display: flex;
align-items: center;
`;
export const SaveButton = styled(Button)`
padding: 6px 12px;
justify-content: center;
margin-right: 4px;
`;
export const RemoveButton = styled.div`
width: 100%;
justify-content: center;
`;

View File

@ -3,16 +3,21 @@ import dayjs from 'dayjs';
import styled from 'styled-components'; import styled from 'styled-components';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import _ from 'lodash'; import _ from 'lodash';
import { colourStyles } from 'shared/components/Select';
import produce from 'immer';
import Select from 'react-select';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
import { getYear, getMonth } from 'date-fns'; import { getYear, getMonth } from 'date-fns';
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 { Clock, Cross } from 'shared/icons'; import { Bell, Clock, Cross, Plus, Trash } from 'shared/icons';
import Select from 'react-select/src/Select';
import { import {
Wrapper, Wrapper,
RemoveDueDate, RemoveDueDate,
SaveButton,
RightWrapper,
LeftWrapper,
DueDateInput, DueDateInput,
DueDatePickerWrapper, DueDatePickerWrapper,
ConfirmAddDueDate, ConfirmAddDueDate,
@ -24,11 +29,19 @@ import {
ActionsSeparator, ActionsSeparator,
ActionClock, ActionClock,
ActionLabel, ActionLabel,
ControlWrapper,
RemoveButton,
ActionBell,
} from './Styles'; } from './Styles';
type DueDateManagerProps = { type DueDateManagerProps = {
task: Task; task: Task;
onDueDateChange: (task: Task, newDueDate: Date, hasTime: boolean) => void; onDueDateChange: (
task: Task,
newDueDate: Date,
hasTime: boolean,
notifications: { current: Array<NotificationInternal>; removed: Array<string> },
) => void;
onRemoveDueDate: (task: Task) => void; onRemoveDueDate: (task: Task) => void;
onCancel: () => void; onCancel: () => void;
}; };
@ -41,6 +54,39 @@ const FormField = styled.div`
width: 50%; width: 50%;
display: inline-block; display: inline-block;
`; `;
const NotificationCount = styled.input``;
const ActionPlus = styled(Plus)`
position: absolute;
fill: ${(props) => props.theme.colors.bg.primary} !important;
stroke: ${(props) => props.theme.colors.bg.primary};
`;
const ActionInput = styled.input`
border: 1px solid rgba(0, 0, 0, 0.2);
margin-left: auto;
margin-right: 4px;
border-color: rgb(65, 69, 97);
background: #262c49;
box-shadow: 0 0 0 0 rgb(0 0 0 / 15%);
color: #c2c6dc;
position: relative;
border-radius: 5px;
transition: all 0.3s ease;
font-size: 13px;
line-height: 20px;
padding: 0 12px;
padding-bottom: 4px;
padding-top: 4px;
width: 100%;
max-width: 48px;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
`;
const HeaderSelectLabel = styled.div` const HeaderSelectLabel = styled.div`
display: inline-block; display: inline-block;
position: relative; position: relative;
@ -131,8 +177,69 @@ const HeaderActions = styled.div`
} }
`; `;
const notificationPeriodOptions = [
{ value: 'minute', label: 'Minutes' },
{ value: 'hour', label: 'Hours' },
{ value: 'day', label: 'Days' },
{ value: 'week', label: 'Weeks' },
];
type NotificationInternal = {
internalId: string;
externalId: string | null;
period: number;
duration: { value: string; label: string };
};
type NotificationEntryProps = {
notification: NotificationInternal;
onChange: (period: number, duration: { value: string; label: string }) => void;
onRemove: () => void;
};
const NotificationEntry: React.FC<NotificationEntryProps> = ({ notification, onChange, onRemove }) => {
return (
<>
<ActionBell width={16} height={16} />
<ActionLabel>Notification</ActionLabel>
<ActionInput
value={notification.period}
onChange={(e) => {
onChange(parseInt(e.currentTarget.value, 10), notification.duration);
}}
onKeyPress={(e) => {
const isNumber = /^[0-9]$/i.test(e.key);
if (!isNumber && e.key !== 'Backspace') {
e.preventDefault();
}
}}
dir="ltr"
autoComplete="off"
min="0"
type="number"
/>
<Select
menuPlacement="top"
className="react-period"
classNamePrefix="react-period-select"
styles={colourStyles}
isSearchable={false}
defaultValue={notification.duration}
options={notificationPeriodOptions}
onChange={(e) => {
if (e !== null) {
onChange(notification.period, e);
}
}}
/>
<ActionIcon onClick={() => onRemove()}>
<Cross width={16} height={16} />
</ActionIcon>
</>
);
};
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => { const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => {
const currentDueDate = task.dueDate ? dayjs(task.dueDate).toDate() : null; const currentDueDate = task.dueDate.at ? dayjs(task.dueDate.at).toDate() : null;
const { const {
register, register,
handleSubmit, handleSubmit,
@ -145,28 +252,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
const [startDate, setStartDate] = useState<Date | null>(currentDueDate); const [startDate, setStartDate] = useState<Date | null>(currentDueDate);
const [endDate, setEndDate] = useState<Date | null>(currentDueDate); const [endDate, setEndDate] = useState<Date | null>(currentDueDate);
const [hasTime, enableTime] = useState(task.hasTime ?? false); 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(() => {
debouncedChange(startDate, hasTime);
}, [startDate, hasTime]);
const years = _.range(2010, getYear(new Date()) + 10, 1); const years = _.range(2010, getYear(new Date()) + 10, 1);
const months = [ const months = [
'January', 'January',
@ -183,33 +269,41 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
'December', 'December',
]; ];
const onChange = (dates: any) => {
const [start, end] = dates;
setStartDate(start);
setEndDate(end);
};
const [isRange, setIsRange] = useState(false); const [isRange, setIsRange] = useState(false);
const [notDuration, setNotDuration] = useState(10);
const [removedNotifications, setRemovedNotifications] = useState<Array<string>>([]);
const [notifications, setNotifications] = useState<Array<NotificationInternal>>(
task.dueDate.notifications
? task.dueDate.notifications.map((c, idx) => {
const duration =
notificationPeriodOptions.find((o) => o.value === c.duration.toLowerCase()) ?? notificationPeriodOptions[0];
return {
internalId: `n${idx}`,
externalId: c.id,
period: c.period,
duration,
};
})
: [],
);
return ( return (
<Wrapper> <Wrapper>
<DateRangeInputs> <DateRangeInputs>
<DatePicker <DatePicker
selected={startDate} selected={startDate}
onChange={(date) => { onChange={(date) => {
if (!Array.isArray(date)) { if (!Array.isArray(date) && date !== null) {
setStartDate(date); setStartDate(date);
} }
}} }}
popperClassName="picker-hidden" popperClassName="picker-hidden"
dateFormat="yyyy-MM-dd" dateFormat="yyyy-MM-dd"
disabledKeyboardNavigation disabledKeyboardNavigation
isClearable
placeholderText="Select due date" placeholderText="Select due date"
/> />
{isRange ? ( {isRange ? (
<DatePicker <DatePicker
selected={startDate} selected={startDate}
isClearable
onChange={(date) => { onChange={(date) => {
if (!Array.isArray(date)) { if (!Array.isArray(date)) {
setStartDate(date); setStartDate(date);
@ -299,7 +393,78 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
</ActionIcon> </ActionIcon>
</ActionsWrapper> </ActionsWrapper>
)} )}
<ActionsWrapper> {notifications.map((n, idx) => (
<ActionsWrapper key={n.internalId}>
<NotificationEntry
notification={n}
onChange={(period, duration) => {
setNotifications((prev) =>
produce(prev, (draft) => {
draft[idx].duration = duration;
draft[idx].period = period;
}),
);
}}
onRemove={() => {
setNotifications((prev) =>
produce(prev, (draft) => {
draft.splice(idx, 1);
if (n.externalId !== null) {
setRemovedNotifications((prev) => {
if (n.externalId !== null) {
return [...prev, n.externalId];
}
return prev;
});
}
}),
);
}}
/>
</ActionsWrapper>
))}
<ControlWrapper>
<LeftWrapper>
<SaveButton
onClick={() => {
if (startDate && notifications.findIndex((n) => Number.isNaN(n.period)) === -1) {
onDueDateChange(task, startDate, hasTime, { current: notifications, removed: removedNotifications });
}
}}
>
Save
</SaveButton>
{currentDueDate !== null && (
<ActionIcon
onClick={() => {
onRemoveDueDate(task);
}}
>
<Trash width={16} height={16} />
</ActionIcon>
)}
</LeftWrapper>
<RightWrapper>
<ActionIcon
// disabled={notifications.length === 3}
disabled
onClick={() => {
/*
setNotifications((prev) => [
...prev,
{
externalId: null,
internalId: `n${prev.length + 1}`,
duration: notificationPeriodOptions[0],
period: 10,
},
]);
*/
}}
>
<Bell width={16} height={16} />
<ActionPlus width={8} height={8} />
</ActionIcon>
{!hasTime && ( {!hasTime && (
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
@ -314,8 +479,8 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
<Clock width={16} height={16} /> <Clock width={16} height={16} />
</ActionIcon> </ActionIcon>
)} )}
<ClearButton onClick={() => setStartDate(null)}>{hasTime ? 'Clear all' : 'Clear'}</ClearButton> </RightWrapper>
</ActionsWrapper> </ControlWrapper>
</Wrapper> </Wrapper>
); );
}; };

View File

@ -351,10 +351,10 @@ const SimpleLists: React.FC<SimpleProps> = ({
description="" description=""
labels={task.labels.map((label) => label.projectLabel)} labels={task.labels.map((label) => label.projectLabel)}
dueDate={ dueDate={
task.dueDate task.dueDate.at
? { ? {
isPastDue: false, isPastDue: false,
formattedDate: dayjs(task.dueDate).format('MMM D, YYYY'), formattedDate: dayjs(task.dueDate.at).format('MMM D, YYYY'),
} }
: undefined : undefined
} }

View File

@ -24,7 +24,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null)); isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null));
} }
if (task.dueDate) { if (task.dueDate) {
const taskDueDate = dayjs(task.dueDate); const taskDueDate = dayjs(task.dueDate.at);
const today = dayjs(); const today = dayjs();
let start; let start;
let end; let end;
@ -36,61 +36,31 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
isFiltered = shouldFilter(taskDueDate.isSame(today, 'day')); isFiltered = shouldFilter(taskDueDate.isSame(today, 'day'));
break; break;
case DueDateFilterType.TOMORROW: case DueDateFilterType.TOMORROW:
isFiltered = shouldFilter( isFiltered = shouldFilter(taskDueDate.isBefore(today.clone().add(1, 'day').endOf('day')));
taskDueDate.isBefore(
today
.clone()
.add(1, 'day')
.endOf('day'),
),
);
break; break;
case DueDateFilterType.THIS_WEEK: case DueDateFilterType.THIS_WEEK:
start = today start = today.clone().weekday(0).startOf('day');
.clone() end = today.clone().weekday(6).endOf('day');
.weekday(0)
.startOf('day');
end = today
.clone()
.weekday(6)
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
case DueDateFilterType.NEXT_WEEK: case DueDateFilterType.NEXT_WEEK:
start = today start = today.clone().weekday(0).add(7, 'day').startOf('day');
.clone() end = today.clone().weekday(6).add(7, 'day').endOf('day');
.weekday(0)
.add(7, 'day')
.startOf('day');
end = today
.clone()
.weekday(6)
.add(7, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
case DueDateFilterType.ONE_WEEK: case DueDateFilterType.ONE_WEEK:
start = today.clone().startOf('day'); start = today.clone().startOf('day');
end = today end = today.clone().add(7, 'day').endOf('day');
.clone()
.add(7, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
case DueDateFilterType.TWO_WEEKS: case DueDateFilterType.TWO_WEEKS:
start = today.clone().startOf('day'); start = today.clone().startOf('day');
end = today end = today.clone().add(14, 'day').endOf('day');
.clone()
.add(14, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
case DueDateFilterType.THREE_WEEKS: case DueDateFilterType.THREE_WEEKS:
start = today.clone().startOf('day'); start = today.clone().startOf('day');
end = today end = today.clone().add(21, 'day').endOf('day');
.clone()
.add(21, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
default: default:
@ -104,7 +74,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
} }
for (const member of filters.members) { for (const member of filters.members) {
if (task.assigned) { if (task.assigned) {
if (task.assigned.findIndex(m => m.id === member.id) !== -1) { if (task.assigned.findIndex((m) => m.id === member.id) !== -1) {
isFiltered = ShouldFilter.VALID; isFiltered = ShouldFilter.VALID;
} }
} }
@ -116,7 +86,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
} }
for (const label of filters.labels) { for (const label of filters.labels) {
if (task.labels) { if (task.labels) {
if (task.labels.findIndex(m => m.projectLabel.id === label.id) !== -1) { if (task.labels.findIndex((m) => m.projectLabel.id === label.id) !== -1) {
isFiltered = ShouldFilter.VALID; isFiltered = ShouldFilter.VALID;
} }
} }

View File

@ -9,14 +9,22 @@ type ActivityMessageProps = {
}; };
function getVariable(data: Array<TaskActivityData>, name: string) { function getVariable(data: Array<TaskActivityData>, name: string) {
const target = data.find(d => d.name === name); const target = data.find((d) => d.name === name);
return target ? target.value : null; return target ? target.value : null;
} }
function renderDate(timestamp: string | null) { function getVariableBool(data: Array<TaskActivityData>, name: string, defaultValue = false) {
const target = data.find((d) => d.name === name);
return target ? target.value === 'true' : defaultValue;
}
function renderDate(timestamp: string | null, hasTime: boolean) {
if (timestamp) { if (timestamp) {
if (hasTime) {
return dayjs(timestamp).format('MMM D [at] h:mm A'); return dayjs(timestamp).format('MMM D [at] h:mm A');
} }
return dayjs(timestamp).format('MMM D');
}
return null; return null;
} }
@ -30,13 +38,19 @@ const ActivityMessage: React.FC<ActivityMessageProps> = ({ type, data }) => {
message = `moved this task from ${getVariable(data, 'PrevTaskGroup')} to ${getVariable(data, 'CurTaskGroup')}`; message = `moved this task from ${getVariable(data, 'PrevTaskGroup')} to ${getVariable(data, 'CurTaskGroup')}`;
break; break;
case ActivityType.TaskDueDateAdded: case ActivityType.TaskDueDateAdded:
message = `set this task to be due ${renderDate(getVariable(data, 'DueDate'))}`; message = `set this task to be due ${renderDate(
getVariable(data, 'DueDate'),
getVariableBool(data, 'HasTime', true),
)}`;
break; break;
case ActivityType.TaskDueDateRemoved: case ActivityType.TaskDueDateRemoved:
message = `removed the due date from this task`; message = `removed the due date from this task`;
break; break;
case ActivityType.TaskDueDateChanged: case ActivityType.TaskDueDateChanged:
message = `changed the due date of this task to ${renderDate(getVariable(data, 'CurDueDate'))}`; message = `changed the due date of this task to ${renderDate(
getVariable(data, 'CurDueDate'),
getVariableBool(data, 'HasTime', true),
)}`;
break; break;
case ActivityType.TaskMarkedComplete: case ActivityType.TaskMarkedComplete:
message = `marked this task complete`; message = `marked this task complete`;

View File

@ -332,7 +332,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const saveDescription = () => { const saveDescription = () => {
onTaskDescriptionChange(task, taskDescriptionRef.current); onTaskDescriptionChange(task, taskDescriptionRef.current);
}; };
console.log(task.watched);
return ( return (
<Container> <Container>
<LeftSidebar> <LeftSidebar>
@ -351,9 +350,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
} }
}} }}
> >
{task.dueDate ? ( {task.dueDate.at ? (
<SidebarButtonText> <SidebarButtonText>
{dayjs(task.dueDate).format(task.hasTime ? 'MMM D [at] h:mm A' : 'MMMM D')} {dayjs(task.dueDate.at).format(task.hasTime ? 'MMM D [at] h:mm A' : 'MMMM D')}
</SidebarButtonText> </SidebarButtonText>
) : ( ) : (
<SidebarButtonText>No due date</SidebarButtonText> <SidebarButtonText>No due date</SidebarButtonText>
@ -632,6 +631,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
{activityStream.map((stream) => {activityStream.map((stream) =>
stream.data.type === 'comment' ? ( stream.data.type === 'comment' ? (
<StreamComment <StreamComment
key={stream.id}
onExtraActions={onCommentShowActions} onExtraActions={onCommentShowActions}
onCancelCommentEdit={onCancelCommentEdit} onCancelCommentEdit={onCancelCommentEdit}
onUpdateComment={(message) => onUpdateComment(stream.id, message)} onUpdateComment={(message) => onUpdateComment(stream.id, message)}
@ -640,6 +640,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
/> />
) : ( ) : (
<StreamActivity <StreamActivity
key={stream.id}
activity={task.activity && task.activity.find((activity) => activity.id === stream.id)} activity={task.activity && task.activity.find((activity) => activity.id === stream.id)}
/> />
), ),

View File

@ -30,19 +30,16 @@ function plugin(options) {
} }
function getEmoji(match) { function getEmoji(match) {
console.log(match);
const got = emoji.get(match); const got = emoji.get(match);
if (pad && got !== match) { if (pad && got !== match) {
return `${got} `; return `${got} `;
} }
console.log(got);
return ReactDOMServer.renderToStaticMarkup(<Emoji set="google" emoji={match} size={16} />); return ReactDOMServer.renderToStaticMarkup(<Emoji set="google" emoji={match} size={16} />);
} }
function transformer(tree) { function transformer(tree) {
visit(tree, 'paragraph', function (node) { visit(tree, 'paragraph', function (node) {
console.log(tree);
// node.value = node.value.replace(RE_EMOJI, getEmoji); // node.value = node.value.replace(RE_EMOJI, getEmoji);
// jnode.type = 'html'; // jnode.type = 'html';
// jnode.tagName = 'div'; // jnode.tagName = 'div';
@ -58,7 +55,6 @@ function plugin(options) {
if (emoticonEnable) { if (emoticonEnable) {
// node.value = node.value.replace(RE_SHORT, getEmojiByShortCode); // node.value = node.value.replace(RE_SHORT, getEmojiByShortCode);
} }
console.log(node);
}); });
} }

View File

@ -334,7 +334,7 @@ const NavBar: React.FC<NavBarProps> = ({
<ListUnordered width={20} height={20} /> <ListUnordered width={20} height={20} />
</IconContainer> </IconContainer>
<IconContainer onClick={onNotificationClick}> <IconContainer onClick={onNotificationClick}>
<Bell color="#c2c6dc" size={20} /> <Bell width={20} height={20} />
{hasUnread && <NotificationCount />} {hasUnread && <NotificationCount />}
</IconContainer> </IconContainer>
<IconContainer disabled onClick={NOOP}> <IconContainer disabled onClick={NOOP}>

View File

@ -107,6 +107,17 @@ export type CreateTaskCommentPayload = {
comment: TaskComment; comment: TaskComment;
}; };
export type CreateTaskDueDateNotification = {
taskID: Scalars['UUID'];
period: Scalars['Int'];
duration: DueDateNotificationDuration;
};
export type CreateTaskDueDateNotificationsResult = {
__typename?: 'CreateTaskDueDateNotificationsResult';
notifications: Array<DueDateNotification>;
};
export type CreateTeamMember = { export type CreateTeamMember = {
userID: Scalars['UUID']; userID: Scalars['UUID'];
teamID: Scalars['UUID']; teamID: Scalars['UUID'];
@ -200,6 +211,15 @@ export type DeleteTaskCommentPayload = {
commentID: Scalars['UUID']; commentID: Scalars['UUID'];
}; };
export type DeleteTaskDueDateNotification = {
id: Scalars['UUID'];
};
export type DeleteTaskDueDateNotificationsResult = {
__typename?: 'DeleteTaskDueDateNotificationsResult';
notifications: Array<Scalars['UUID']>;
};
export type DeleteTaskGroupInput = { export type DeleteTaskGroupInput = {
taskGroupID: Scalars['UUID']; taskGroupID: Scalars['UUID'];
}; };
@ -265,6 +285,26 @@ export type DeleteUserAccountPayload = {
userAccount: UserAccount; userAccount: UserAccount;
}; };
export type DueDate = {
__typename?: 'DueDate';
at?: Maybe<Scalars['Time']>;
notifications: Array<DueDateNotification>;
};
export type DueDateNotification = {
__typename?: 'DueDateNotification';
id: Scalars['ID'];
period: Scalars['Int'];
duration: DueDateNotificationDuration;
};
export enum DueDateNotificationDuration {
Minute = 'MINUTE',
Hour = 'HOUR',
Day = 'DAY',
Week = 'WEEK'
}
export type DuplicateTaskGroup = { export type DuplicateTaskGroup = {
projectID: Scalars['UUID']; projectID: Scalars['UUID'];
taskGroupID: Scalars['UUID']; taskGroupID: Scalars['UUID'];
@ -393,6 +433,7 @@ export type Mutation = {
createTaskChecklist: TaskChecklist; createTaskChecklist: TaskChecklist;
createTaskChecklistItem: TaskChecklistItem; createTaskChecklistItem: TaskChecklistItem;
createTaskComment: CreateTaskCommentPayload; createTaskComment: CreateTaskCommentPayload;
createTaskDueDateNotifications: CreateTaskDueDateNotificationsResult;
createTaskGroup: TaskGroup; createTaskGroup: TaskGroup;
createTeam: Team; createTeam: Team;
createTeamMember: CreateTeamMemberPayload; createTeamMember: CreateTeamMemberPayload;
@ -406,6 +447,7 @@ export type Mutation = {
deleteTaskChecklist: DeleteTaskChecklistPayload; deleteTaskChecklist: DeleteTaskChecklistPayload;
deleteTaskChecklistItem: DeleteTaskChecklistItemPayload; deleteTaskChecklistItem: DeleteTaskChecklistItemPayload;
deleteTaskComment: DeleteTaskCommentPayload; deleteTaskComment: DeleteTaskCommentPayload;
deleteTaskDueDateNotifications: DeleteTaskDueDateNotificationsResult;
deleteTaskGroup: DeleteTaskGroupPayload; deleteTaskGroup: DeleteTaskGroupPayload;
deleteTaskGroupTasks: DeleteTaskGroupTasksPayload; deleteTaskGroupTasks: DeleteTaskGroupTasksPayload;
deleteTeam: DeleteTeamPayload; deleteTeam: DeleteTeamPayload;
@ -435,6 +477,7 @@ export type Mutation = {
updateTaskComment: UpdateTaskCommentPayload; updateTaskComment: UpdateTaskCommentPayload;
updateTaskDescription: Task; updateTaskDescription: Task;
updateTaskDueDate: Task; updateTaskDueDate: Task;
updateTaskDueDateNotifications: UpdateTaskDueDateNotificationsResult;
updateTaskGroupLocation: TaskGroup; updateTaskGroupLocation: TaskGroup;
updateTaskGroupName: TaskGroup; updateTaskGroupName: TaskGroup;
updateTaskLocation: UpdateTaskLocationPayload; updateTaskLocation: UpdateTaskLocationPayload;
@ -486,6 +529,11 @@ export type MutationCreateTaskCommentArgs = {
}; };
export type MutationCreateTaskDueDateNotificationsArgs = {
input: Array<CreateTaskDueDateNotification>;
};
export type MutationCreateTaskGroupArgs = { export type MutationCreateTaskGroupArgs = {
input: NewTaskGroup; input: NewTaskGroup;
}; };
@ -551,6 +599,11 @@ export type MutationDeleteTaskCommentArgs = {
}; };
export type MutationDeleteTaskDueDateNotificationsArgs = {
input: Array<DeleteTaskDueDateNotification>;
};
export type MutationDeleteTaskGroupArgs = { export type MutationDeleteTaskGroupArgs = {
input: DeleteTaskGroupInput; input: DeleteTaskGroupInput;
}; };
@ -696,6 +749,11 @@ export type MutationUpdateTaskDueDateArgs = {
}; };
export type MutationUpdateTaskDueDateNotificationsArgs = {
input: Array<UpdateTaskDueDateNotification>;
};
export type MutationUpdateTaskGroupLocationArgs = { export type MutationUpdateTaskGroupLocationArgs = {
input: NewTaskGroupLocation; input: NewTaskGroupLocation;
}; };
@ -1078,7 +1136,7 @@ export type Task = {
position: Scalars['Float']; position: Scalars['Float'];
description?: Maybe<Scalars['String']>; description?: Maybe<Scalars['String']>;
watched: Scalars['Boolean']; watched: Scalars['Boolean'];
dueDate?: Maybe<Scalars['Time']>; dueDate: DueDate;
hasTime: Scalars['Boolean']; hasTime: Scalars['Boolean'];
complete: Scalars['Boolean']; complete: Scalars['Boolean'];
completedAt?: Maybe<Scalars['Time']>; completedAt?: Maybe<Scalars['Time']>;
@ -1302,6 +1360,17 @@ export type UpdateTaskDueDate = {
dueDate?: Maybe<Scalars['Time']>; dueDate?: Maybe<Scalars['Time']>;
}; };
export type UpdateTaskDueDateNotification = {
id: Scalars['UUID'];
period: Scalars['Int'];
duration: DueDateNotificationDuration;
};
export type UpdateTaskDueDateNotificationsResult = {
__typename?: 'UpdateTaskDueDateNotificationsResult';
notifications: Array<DueDateNotification>;
};
export type UpdateTaskGroupName = { export type UpdateTaskGroupName = {
taskGroupID: Scalars['UUID']; taskGroupID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
@ -1596,8 +1665,15 @@ export type FindTaskQuery = (
{ __typename?: 'Query' } { __typename?: 'Query' }
& { findTask: ( & { findTask: (
{ __typename?: 'Task' } { __typename?: 'Task' }
& Pick<Task, 'id' | 'shortId' | 'name' | 'watched' | 'description' | 'dueDate' | 'position' | 'complete' | 'hasTime'> & Pick<Task, 'id' | 'shortId' | 'name' | 'watched' | 'description' | 'position' | 'complete' | 'hasTime'>
& { taskGroup: ( & { dueDate: (
{ __typename?: 'DueDate' }
& Pick<DueDate, 'at'>
& { notifications: Array<(
{ __typename?: 'DueDateNotification' }
& Pick<DueDateNotification, 'id' | 'period' | 'duration'>
)> }
), taskGroup: (
{ __typename?: 'TaskGroup' } { __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name'> & Pick<TaskGroup, 'id' | 'name'>
), comments: Array<( ), comments: Array<(
@ -1672,8 +1748,11 @@ export type FindTaskQuery = (
export type TaskFieldsFragment = ( export type TaskFieldsFragment = (
{ __typename?: 'Task' } { __typename?: 'Task' }
& Pick<Task, 'id' | 'shortId' | 'name' | 'description' | 'dueDate' | 'hasTime' | 'complete' | 'watched' | 'completedAt' | 'position'> & Pick<Task, 'id' | 'shortId' | 'name' | 'description' | 'hasTime' | 'complete' | 'watched' | 'completedAt' | 'position'>
& { badges: ( & { dueDate: (
{ __typename?: 'DueDate' }
& Pick<DueDate, 'at'>
), badges: (
{ __typename?: 'TaskBadges' } { __typename?: 'TaskBadges' }
& { checklist?: Maybe<( & { checklist?: Maybe<(
{ __typename?: 'ChecklistBadge' } { __typename?: 'ChecklistBadge' }
@ -1789,10 +1868,13 @@ export type MyTasksQuery = (
{ __typename?: 'MyTasksPayload' } { __typename?: 'MyTasksPayload' }
& { tasks: Array<( & { tasks: Array<(
{ __typename?: 'Task' } { __typename?: 'Task' }
& Pick<Task, 'id' | 'shortId' | 'name' | 'dueDate' | 'hasTime' | 'complete' | 'completedAt'> & Pick<Task, 'id' | 'shortId' | 'name' | 'hasTime' | 'complete' | 'completedAt'>
& { taskGroup: ( & { taskGroup: (
{ __typename?: 'TaskGroup' } { __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name'> & Pick<TaskGroup, 'id' | 'name'>
), dueDate: (
{ __typename?: 'DueDate' }
& Pick<DueDate, 'at'>
) } ) }
)>, projects: Array<( )>, projects: Array<(
{ __typename?: 'ProjectTaskMapping' } { __typename?: 'ProjectTaskMapping' }
@ -2624,6 +2706,9 @@ export type UpdateTaskDueDateMutationVariables = Exact<{
taskID: Scalars['UUID']; taskID: Scalars['UUID'];
dueDate?: Maybe<Scalars['Time']>; dueDate?: Maybe<Scalars['Time']>;
hasTime: Scalars['Boolean']; hasTime: Scalars['Boolean'];
createNotifications: Array<CreateTaskDueDateNotification> | CreateTaskDueDateNotification;
updateNotifications: Array<UpdateTaskDueDateNotification> | UpdateTaskDueDateNotification;
deleteNotifications: Array<DeleteTaskDueDateNotification> | DeleteTaskDueDateNotification;
}>; }>;
@ -2631,7 +2716,26 @@ export type UpdateTaskDueDateMutation = (
{ __typename?: 'Mutation' } { __typename?: 'Mutation' }
& { updateTaskDueDate: ( & { updateTaskDueDate: (
{ __typename?: 'Task' } { __typename?: 'Task' }
& Pick<Task, 'id' | 'dueDate' | 'hasTime'> & Pick<Task, 'id' | 'hasTime'>
& { dueDate: (
{ __typename?: 'DueDate' }
& Pick<DueDate, 'at'>
) }
), createTaskDueDateNotifications: (
{ __typename?: 'CreateTaskDueDateNotificationsResult' }
& { notifications: Array<(
{ __typename?: 'DueDateNotification' }
& Pick<DueDateNotification, 'id' | 'period' | 'duration'>
)> }
), updateTaskDueDateNotifications: (
{ __typename?: 'UpdateTaskDueDateNotificationsResult' }
& { notifications: Array<(
{ __typename?: 'DueDateNotification' }
& Pick<DueDateNotification, 'id' | 'period' | 'duration'>
)> }
), deleteTaskDueDateNotifications: (
{ __typename?: 'DeleteTaskDueDateNotificationsResult' }
& Pick<DeleteTaskDueDateNotificationsResult, 'notifications'>
) } ) }
); );
@ -2866,7 +2970,9 @@ export const TaskFieldsFragmentDoc = gql`
shortId shortId
name name
description description
dueDate dueDate {
at
}
hasTime hasTime
complete complete
watched watched
@ -3346,7 +3452,14 @@ export const FindTaskDocument = gql`
name name
watched watched
description description
dueDate dueDate {
at
notifications {
id
period
duration
}
}
position position
complete complete
hasTime hasTime
@ -3640,7 +3753,9 @@ export const MyTasksDocument = gql`
name name
} }
name name
dueDate dueDate {
at
}
hasTime hasTime
complete complete
completedAt completedAt
@ -5423,14 +5538,33 @@ export type UpdateTaskDescriptionMutationHookResult = ReturnType<typeof useUpdat
export type UpdateTaskDescriptionMutationResult = Apollo.MutationResult<UpdateTaskDescriptionMutation>; export type UpdateTaskDescriptionMutationResult = Apollo.MutationResult<UpdateTaskDescriptionMutation>;
export type UpdateTaskDescriptionMutationOptions = Apollo.BaseMutationOptions<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>; export type UpdateTaskDescriptionMutationOptions = Apollo.BaseMutationOptions<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>;
export const UpdateTaskDueDateDocument = gql` export const UpdateTaskDueDateDocument = gql`
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!) { mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!, $createNotifications: [CreateTaskDueDateNotification!]!, $updateNotifications: [UpdateTaskDueDateNotification!]!, $deleteNotifications: [DeleteTaskDueDateNotification!]!) {
updateTaskDueDate( updateTaskDueDate(
input: {taskID: $taskID, dueDate: $dueDate, hasTime: $hasTime} input: {taskID: $taskID, dueDate: $dueDate, hasTime: $hasTime}
) { ) {
id id
dueDate dueDate {
at
}
hasTime hasTime
} }
createTaskDueDateNotifications(input: $createNotifications) {
notifications {
id
period
duration
}
}
updateTaskDueDateNotifications(input: $updateNotifications) {
notifications {
id
period
duration
}
}
deleteTaskDueDateNotifications(input: $deleteNotifications) {
notifications
}
} }
`; `;
export type UpdateTaskDueDateMutationFn = Apollo.MutationFunction<UpdateTaskDueDateMutation, UpdateTaskDueDateMutationVariables>; export type UpdateTaskDueDateMutationFn = Apollo.MutationFunction<UpdateTaskDueDateMutation, UpdateTaskDueDateMutationVariables>;
@ -5451,6 +5585,9 @@ export type UpdateTaskDueDateMutationFn = Apollo.MutationFunction<UpdateTaskDueD
* taskID: // value for 'taskID' * taskID: // value for 'taskID'
* dueDate: // value for 'dueDate' * dueDate: // value for 'dueDate'
* hasTime: // value for 'hasTime' * hasTime: // value for 'hasTime'
* createNotifications: // value for 'createNotifications'
* updateNotifications: // value for 'updateNotifications'
* deleteNotifications: // value for 'deleteNotifications'
* }, * },
* }); * });
*/ */

View File

@ -5,7 +5,14 @@ query findTask($taskID: String!) {
name name
watched watched
description description
dueDate dueDate {
at
notifications {
id
period
duration
}
}
position position
complete complete
hasTime hasTime

View File

@ -6,7 +6,9 @@ const TASK_FRAGMENT = gql`
shortId shortId
name name
description description
dueDate dueDate {
at
}
hasTime hasTime
complete complete
watched watched

View File

@ -12,7 +12,9 @@ query myTasks($status: MyTasksStatus!, $sort: MyTasksSort!) {
name name
} }
name name
dueDate dueDate {
at
}
hasTime hasTime
complete complete
completedAt completedAt

View File

@ -1,4 +1,8 @@
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!) { mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!,
$createNotifications: [CreateTaskDueDateNotification!]!,
$updateNotifications: [UpdateTaskDueDateNotification!]!
$deleteNotifications: [DeleteTaskDueDateNotification!]!
) {
updateTaskDueDate ( updateTaskDueDate (
input: { input: {
taskID: $taskID taskID: $taskID
@ -7,7 +11,26 @@ mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!) {
} }
) { ) {
id id
dueDate dueDate {
at
}
hasTime hasTime
} }
createTaskDueDateNotifications(input: $createNotifications) {
notifications {
id
period
duration
}
}
updateTaskDueDateNotifications(input: $updateNotifications) {
notifications {
id
period
duration
}
}
deleteTaskDueDateNotifications(input: $deleteNotifications) {
notifications
}
} }

View File

@ -5,7 +5,6 @@ function parseJSON<T>(value: string | null): T | undefined {
try { try {
return value === 'undefined' ? undefined : JSON.parse(value ?? ''); return value === 'undefined' ? undefined : JSON.parse(value ?? '');
} catch (error) { } catch (error) {
console.log('parsing error on', { value });
return undefined; return undefined;
} }
} }

View File

@ -1,15 +1,11 @@
import React from 'react'; import React from 'react';
import Icon, { IconProps } from './Icon';
type Props = { const Bell: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
size: number | string;
color: string;
};
const Bell = ({ size, color }: Props) => {
return ( return (
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 448 512"> <Icon width={width} height={height} className={className} viewBox="0 0 448 512">
<path d="M224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64zm215.39-149.71c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71z" /> <path d="M224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64zm215.39-149.71c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71z" />
</svg> </Icon>
); );
}; };

View File

@ -46,7 +46,7 @@ export function sortTasks(a: Task, b: Task, taskSorting: TaskSorting) {
if (b.dueDate && !a.dueDate) { if (b.dueDate && !a.dueDate) {
return 1; return 1;
} }
return dayjs(a.dueDate).diff(dayjs(b.dueDate)); return dayjs(a.dueDate.at).diff(dayjs(b.dueDate.at));
} }
if (taskSorting.type === TaskSortingType.COMPLETE) { if (taskSorting.type === TaskSortingType.COMPLETE) {
if (a.complete && !b.complete) { if (a.complete && !b.complete) {

View File

@ -110,7 +110,7 @@ type Task = {
badges?: TaskBadges; badges?: TaskBadges;
position: number; position: number;
hasTime?: boolean; hasTime?: boolean;
dueDate?: string; dueDate: { at?: string; notifications?: Array<{ id: string; period: number; duration: string }> };
complete?: boolean; complete?: boolean;
completedAt?: string | null; completedAt?: string | null;
labels: TaskLabel[]; labels: TaskLabel[];

View File

@ -187,6 +187,17 @@ type TaskComment struct {
Message string `json:"message"` Message string `json:"message"`
} }
type TaskDueDateReminder struct {
DueDateReminderID uuid.UUID `json:"due_date_reminder_id"`
TaskID uuid.UUID `json:"task_id"`
Period int32 `json:"period"`
Duration string `json:"duration"`
}
type TaskDueDateReminderDuration struct {
Code string `json:"code"`
}
type TaskGroup struct { type TaskGroup struct {
TaskGroupID uuid.UUID `json:"task_group_id"` TaskGroupID uuid.UUID `json:"task_group_id"`
ProjectID uuid.UUID `json:"project_id"` ProjectID uuid.UUID `json:"project_id"`

View File

@ -12,6 +12,7 @@ import (
type Querier interface { type Querier interface {
CreateAuthToken(ctx context.Context, arg CreateAuthTokenParams) (AuthToken, error) CreateAuthToken(ctx context.Context, arg CreateAuthTokenParams) (AuthToken, error)
CreateConfirmToken(ctx context.Context, email string) (UserAccountConfirmToken, error) CreateConfirmToken(ctx context.Context, email string) (UserAccountConfirmToken, error)
CreateDueDateReminder(ctx context.Context, arg CreateDueDateReminderParams) (TaskDueDateReminder, error)
CreateInvitedProjectMember(ctx context.Context, arg CreateInvitedProjectMemberParams) (ProjectMemberInvited, error) CreateInvitedProjectMember(ctx context.Context, arg CreateInvitedProjectMemberParams) (ProjectMemberInvited, error)
CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error) CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error)
CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error) CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error)
@ -40,6 +41,7 @@ type Querier interface {
DeleteAuthTokenByID(ctx context.Context, tokenID uuid.UUID) error DeleteAuthTokenByID(ctx context.Context, tokenID uuid.UUID) error
DeleteAuthTokenByUserID(ctx context.Context, userID uuid.UUID) error DeleteAuthTokenByUserID(ctx context.Context, userID uuid.UUID) error
DeleteConfirmTokenForEmail(ctx context.Context, email string) error DeleteConfirmTokenForEmail(ctx context.Context, email string) error
DeleteDueDateReminder(ctx context.Context, dueDateReminderID uuid.UUID) error
DeleteExpiredTokens(ctx context.Context) error DeleteExpiredTokens(ctx context.Context) error
DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) error DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) error
DeleteInvitedUserAccount(ctx context.Context, userAccountInvitedID uuid.UUID) (UserAccountInvited, error) DeleteInvitedUserAccount(ctx context.Context, userAccountInvitedID uuid.UUID) (UserAccountInvited, error)
@ -80,6 +82,7 @@ type Querier interface {
GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskComment, error) GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskComment, error)
GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error) GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error)
GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error) GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error)
GetDueDateRemindersForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskDueDateReminder, error)
GetInvitedMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]GetInvitedMembersForProjectIDRow, error) GetInvitedMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]GetInvitedMembersForProjectIDRow, error)
GetInvitedUserAccounts(ctx context.Context) ([]UserAccountInvited, error) GetInvitedUserAccounts(ctx context.Context) ([]UserAccountInvited, error)
GetInvitedUserByEmail(ctx context.Context, email string) (UserAccountInvited, error) GetInvitedUserByEmail(ctx context.Context, email string) (UserAccountInvited, error)
@ -150,6 +153,7 @@ type Querier interface {
SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error) SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
SetUserActiveByEmail(ctx context.Context, email string) (UserAccount, error) SetUserActiveByEmail(ctx context.Context, email string) (UserAccount, error)
SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error) SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error)
UpdateDueDateReminder(ctx context.Context, arg UpdateDueDateReminderParams) (TaskDueDateReminder, error)
UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error) UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error)
UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error) UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error) UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)

View File

@ -116,3 +116,16 @@ SELECT task.* FROM task_assigned
-- name: GetCommentCountForTask :one -- name: GetCommentCountForTask :one
SELECT COUNT(*) FROM task_comment WHERE task_id = $1; SELECT COUNT(*) FROM task_comment WHERE task_id = $1;
-- name: CreateDueDateReminder :one
INSERT INTO task_due_date_reminder (task_id, period, duration) VALUES ($1, $2, $3) RETURNING *;
-- name: UpdateDueDateReminder :one
UPDATE task_due_date_reminder SET period = $2, duration = $3 WHERE due_date_reminder_id = $1 RETURNING *;
-- name: GetDueDateRemindersForTaskID :many
SELECT * FROM task_due_date_reminder WHERE task_id = $1;
-- name: DeleteDueDateReminder :exec
DELETE FROM task_due_date_reminder WHERE due_date_reminder_id = $1;

View File

@ -12,6 +12,28 @@ import (
"github.com/lib/pq" "github.com/lib/pq"
) )
const createDueDateReminder = `-- name: CreateDueDateReminder :one
INSERT INTO task_due_date_reminder (task_id, period, duration) VALUES ($1, $2, $3) RETURNING due_date_reminder_id, task_id, period, duration
`
type CreateDueDateReminderParams struct {
TaskID uuid.UUID `json:"task_id"`
Period int32 `json:"period"`
Duration string `json:"duration"`
}
func (q *Queries) CreateDueDateReminder(ctx context.Context, arg CreateDueDateReminderParams) (TaskDueDateReminder, error) {
row := q.db.QueryRowContext(ctx, createDueDateReminder, arg.TaskID, arg.Period, arg.Duration)
var i TaskDueDateReminder
err := row.Scan(
&i.DueDateReminderID,
&i.TaskID,
&i.Period,
&i.Duration,
)
return i, err
}
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, has_time, short_id VALUES($1, $2, $3, $4) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id
@ -144,6 +166,15 @@ func (q *Queries) CreateTaskWatcher(ctx context.Context, arg CreateTaskWatcherPa
return i, err return i, err
} }
const deleteDueDateReminder = `-- name: DeleteDueDateReminder :exec
DELETE FROM task_due_date_reminder WHERE due_date_reminder_id = $1
`
func (q *Queries) DeleteDueDateReminder(ctx context.Context, dueDateReminderID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteDueDateReminder, dueDateReminderID)
return err
}
const deleteTaskByID = `-- name: DeleteTaskByID :exec const deleteTaskByID = `-- name: DeleteTaskByID :exec
DELETE FROM task WHERE task_id = $1 DELETE FROM task WHERE task_id = $1
` `
@ -403,6 +434,38 @@ func (q *Queries) GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([
return items, nil return items, nil
} }
const getDueDateRemindersForTaskID = `-- name: GetDueDateRemindersForTaskID :many
SELECT due_date_reminder_id, task_id, period, duration FROM task_due_date_reminder WHERE task_id = $1
`
func (q *Queries) GetDueDateRemindersForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskDueDateReminder, error) {
rows, err := q.db.QueryContext(ctx, getDueDateRemindersForTaskID, taskID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TaskDueDateReminder
for rows.Next() {
var i TaskDueDateReminder
if err := rows.Scan(
&i.DueDateReminderID,
&i.TaskID,
&i.Period,
&i.Duration,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProjectIDForTask = `-- name: GetProjectIDForTask :one const getProjectIDForTask = `-- name: GetProjectIDForTask :one
SELECT project_id FROM task SELECT project_id FROM task
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
@ -651,6 +714,28 @@ func (q *Queries) SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams
return i, err return i, err
} }
const updateDueDateReminder = `-- name: UpdateDueDateReminder :one
UPDATE task_due_date_reminder SET period = $2, duration = $3 WHERE due_date_reminder_id = $1 RETURNING due_date_reminder_id, task_id, period, duration
`
type UpdateDueDateReminderParams struct {
DueDateReminderID uuid.UUID `json:"due_date_reminder_id"`
Period int32 `json:"period"`
Duration string `json:"duration"`
}
func (q *Queries) UpdateDueDateReminder(ctx context.Context, arg UpdateDueDateReminderParams) (TaskDueDateReminder, error) {
row := q.db.QueryRowContext(ctx, updateDueDateReminder, arg.DueDateReminderID, arg.Period, arg.Duration)
var i TaskDueDateReminder
err := row.Scan(
&i.DueDateReminderID,
&i.TaskID,
&i.Period,
&i.Duration,
)
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 = $3 WHERE task_comment_id = $1 RETURNING task_comment_id, task_id, created_at, updated_at, created_by, pinned, message
` `

File diff suppressed because it is too large Load Diff

View File

@ -60,6 +60,16 @@ type CreateTaskCommentPayload struct {
Comment *db.TaskComment `json:"comment"` Comment *db.TaskComment `json:"comment"`
} }
type CreateTaskDueDateNotification struct {
TaskID uuid.UUID `json:"taskID"`
Period int `json:"period"`
Duration DueDateNotificationDuration `json:"duration"`
}
type CreateTaskDueDateNotificationsResult struct {
Notifications []DueDateNotification `json:"notifications"`
}
type CreateTeamMember struct { type CreateTeamMember struct {
UserID uuid.UUID `json:"userID"` UserID uuid.UUID `json:"userID"`
TeamID uuid.UUID `json:"teamID"` TeamID uuid.UUID `json:"teamID"`
@ -144,6 +154,14 @@ type DeleteTaskCommentPayload struct {
CommentID uuid.UUID `json:"commentID"` CommentID uuid.UUID `json:"commentID"`
} }
type DeleteTaskDueDateNotification struct {
ID uuid.UUID `json:"id"`
}
type DeleteTaskDueDateNotificationsResult struct {
Notifications []uuid.UUID `json:"notifications"`
}
type DeleteTaskGroupInput struct { type DeleteTaskGroupInput struct {
TaskGroupID uuid.UUID `json:"taskGroupID"` TaskGroupID uuid.UUID `json:"taskGroupID"`
} }
@ -203,6 +221,17 @@ type DeleteUserAccountPayload struct {
UserAccount *db.UserAccount `json:"userAccount"` UserAccount *db.UserAccount `json:"userAccount"`
} }
type DueDate struct {
At *time.Time `json:"at"`
Notifications []DueDateNotification `json:"notifications"`
}
type DueDateNotification struct {
ID uuid.UUID `json:"id"`
Period int `json:"period"`
Duration DueDateNotificationDuration `json:"duration"`
}
type DuplicateTaskGroup struct { type DuplicateTaskGroup struct {
ProjectID uuid.UUID `json:"projectID"` ProjectID uuid.UUID `json:"projectID"`
TaskGroupID uuid.UUID `json:"taskGroupID"` TaskGroupID uuid.UUID `json:"taskGroupID"`
@ -599,6 +628,16 @@ type UpdateTaskDueDate struct {
DueDate *time.Time `json:"dueDate"` DueDate *time.Time `json:"dueDate"`
} }
type UpdateTaskDueDateNotification struct {
ID uuid.UUID `json:"id"`
Period int `json:"period"`
Duration DueDateNotificationDuration `json:"duration"`
}
type UpdateTaskDueDateNotificationsResult struct {
Notifications []DueDateNotification `json:"notifications"`
}
type UpdateTaskGroupName struct { type UpdateTaskGroupName struct {
TaskGroupID uuid.UUID `json:"taskGroupID"` TaskGroupID uuid.UUID `json:"taskGroupID"`
Name string `json:"name"` Name string `json:"name"`
@ -821,6 +860,51 @@ func (e ActivityType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String())) fmt.Fprint(w, strconv.Quote(e.String()))
} }
type DueDateNotificationDuration string
const (
DueDateNotificationDurationMinute DueDateNotificationDuration = "MINUTE"
DueDateNotificationDurationHour DueDateNotificationDuration = "HOUR"
DueDateNotificationDurationDay DueDateNotificationDuration = "DAY"
DueDateNotificationDurationWeek DueDateNotificationDuration = "WEEK"
)
var AllDueDateNotificationDuration = []DueDateNotificationDuration{
DueDateNotificationDurationMinute,
DueDateNotificationDurationHour,
DueDateNotificationDurationDay,
DueDateNotificationDurationWeek,
}
func (e DueDateNotificationDuration) IsValid() bool {
switch e {
case DueDateNotificationDurationMinute, DueDateNotificationDurationHour, DueDateNotificationDurationDay, DueDateNotificationDurationWeek:
return true
}
return false
}
func (e DueDateNotificationDuration) String() string {
return string(e)
}
func (e *DueDateNotificationDuration) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = DueDateNotificationDuration(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid DueDateNotificationDuration", str)
}
return nil
}
func (e DueDateNotificationDuration) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type MyTasksSort string type MyTasksSort string
const ( const (

View File

@ -1,3 +1,9 @@
enum DueDateNotificationDuration {
MINUTE
HOUR
DAY
WEEK
}
type TaskLabel { type TaskLabel {
id: ID! id: ID!
@ -20,6 +26,18 @@ type TaskBadges {
comments: CommentsBadge comments: CommentsBadge
} }
type DueDateNotification {
id: ID!
period: Int!
duration: DueDateNotificationDuration!
}
type DueDate {
at: Time
notifications: [DueDateNotification!]!
}
type Task { type Task {
id: ID! id: ID!
shortId: String! shortId: String!
@ -29,7 +47,7 @@ type Task {
position: Float! position: Float!
description: String description: String
watched: Boolean! watched: Boolean!
dueDate: Time dueDate: DueDate!
hasTime: Boolean! hasTime: Boolean!
complete: Boolean! complete: Boolean!
completedAt: Time completedAt: Time
@ -371,8 +389,45 @@ extend type Mutation {
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
unassignTask(input: UnassignTaskInput): unassignTask(input: UnassignTaskInput):
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
createTaskDueDateNotifications(input: [CreateTaskDueDateNotification!]!):
CreateTaskDueDateNotificationsResult!
updateTaskDueDateNotifications(input: [UpdateTaskDueDateNotification!]!):
UpdateTaskDueDateNotificationsResult!
deleteTaskDueDateNotifications(input: [DeleteTaskDueDateNotification!]!):
DeleteTaskDueDateNotificationsResult!
} }
input DeleteTaskDueDateNotification {
id: UUID!
}
type DeleteTaskDueDateNotificationsResult {
notifications: [UUID!]!
}
input UpdateTaskDueDateNotification {
id: UUID!
period: Int!
duration: DueDateNotificationDuration!
}
type UpdateTaskDueDateNotificationsResult {
notifications: [DueDateNotification!]!
}
input CreateTaskDueDateNotification {
taskID: UUID!
period: Int!
duration: DueDateNotificationDuration!
}
type CreateTaskDueDateNotificationsResult {
notifications: [DueDateNotification!]!
}
input ToggleTaskWatch { input ToggleTaskWatch {
taskID: UUID! taskID: UUID!
} }

View File

@ -1,3 +1,9 @@
enum DueDateNotificationDuration {
MINUTE
HOUR
DAY
WEEK
}
type TaskLabel { type TaskLabel {
id: ID! id: ID!
@ -20,6 +26,18 @@ type TaskBadges {
comments: CommentsBadge comments: CommentsBadge
} }
type DueDateNotification {
id: ID!
period: Int!
duration: DueDateNotificationDuration!
}
type DueDate {
at: Time
notifications: [DueDateNotification!]!
}
type Task { type Task {
id: ID! id: ID!
shortId: String! shortId: String!
@ -29,7 +47,7 @@ type Task {
position: Float! position: Float!
description: String description: String
watched: Boolean! watched: Boolean!
dueDate: Time dueDate: DueDate!
hasTime: Boolean! hasTime: Boolean!
complete: Boolean! complete: Boolean!
completedAt: Time completedAt: Time

View File

@ -31,8 +31,45 @@ extend type Mutation {
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
unassignTask(input: UnassignTaskInput): unassignTask(input: UnassignTaskInput):
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
createTaskDueDateNotifications(input: [CreateTaskDueDateNotification!]!):
CreateTaskDueDateNotificationsResult!
updateTaskDueDateNotifications(input: [UpdateTaskDueDateNotification!]!):
UpdateTaskDueDateNotificationsResult!
deleteTaskDueDateNotifications(input: [DeleteTaskDueDateNotification!]!):
DeleteTaskDueDateNotificationsResult!
} }
input DeleteTaskDueDateNotification {
id: UUID!
}
type DeleteTaskDueDateNotificationsResult {
notifications: [UUID!]!
}
input UpdateTaskDueDateNotification {
id: UUID!
period: Int!
duration: DueDateNotificationDuration!
}
type UpdateTaskDueDateNotificationsResult {
notifications: [DueDateNotification!]!
}
input CreateTaskDueDateNotification {
taskID: UUID!
period: Int!
duration: DueDateNotificationDuration!
}
type CreateTaskDueDateNotificationsResult {
notifications: [DueDateNotification!]!
}
input ToggleTaskWatch { input ToggleTaskWatch {
taskID: UUID! taskID: UUID!
} }

View File

@ -8,7 +8,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "strconv"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@ -501,8 +501,20 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa
if err != nil { if err != nil {
return &db.Task{}, err return &db.Task{}, err
} }
isSame := false
if prevTask.DueDate.Valid && input.DueDate != nil {
if prevTask.DueDate.Time == *input.DueDate && prevTask.HasTime == input.HasTime {
isSame = true
}
}
log.WithFields(log.Fields{
"isSame": isSame,
"prev": prevTask.HasTime,
"new": input.HasTime,
}).Info("chekcing same")
data := map[string]string{} data := map[string]string{}
var activityType = TASK_DUE_DATE_ADDED var activityType = TASK_DUE_DATE_ADDED
data["HasTime"] = strconv.FormatBool(input.HasTime)
if input.DueDate == nil && prevTask.DueDate.Valid { if input.DueDate == nil && prevTask.DueDate.Valid {
activityType = TASK_DUE_DATE_REMOVED activityType = TASK_DUE_DATE_REMOVED
data["PrevDueDate"] = prevTask.DueDate.Time.String() data["PrevDueDate"] = prevTask.DueDate.Time.String()
@ -529,6 +541,7 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa
}) })
createdAt := time.Now().UTC() createdAt := time.Now().UTC()
d, _ := json.Marshal(data) d, _ := json.Marshal(data)
if !isSame {
_, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{ _, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{
TaskID: task.TaskID, TaskID: task.TaskID,
Data: d, Data: d,
@ -536,6 +549,7 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa
CreatedAt: createdAt, CreatedAt: createdAt,
ActivityTypeID: activityType, ActivityTypeID: activityType,
}) })
}
} else { } else {
task, err = r.Repository.GetTaskByID(ctx, input.TaskID) task, err = r.Repository.GetTaskByID(ctx, input.TaskID)
} }
@ -670,6 +684,73 @@ func (r *mutationResolver) UnassignTask(ctx context.Context, input *UnassignTask
return &task, nil return &task, nil
} }
func (r *mutationResolver) CreateTaskDueDateNotifications(ctx context.Context, input []CreateTaskDueDateNotification) (*CreateTaskDueDateNotificationsResult, error) {
reminders := []DueDateNotification{}
for _, in := range input {
n, err := r.Repository.CreateDueDateReminder(ctx, db.CreateDueDateReminderParams{
TaskID: in.TaskID,
Period: int32(in.Period),
Duration: in.Duration.String(),
})
if err != nil {
return &CreateTaskDueDateNotificationsResult{}, err
}
duration := DueDateNotificationDuration(n.Duration)
if !duration.IsValid() {
log.WithField("duration", n.Duration).Error("invalid duration found")
return &CreateTaskDueDateNotificationsResult{}, errors.New("invalid duration")
}
reminders = append(reminders, DueDateNotification{
ID: n.DueDateReminderID,
Period: int(n.Period),
Duration: duration,
})
}
return &CreateTaskDueDateNotificationsResult{
Notifications: reminders,
}, nil
}
func (r *mutationResolver) UpdateTaskDueDateNotifications(ctx context.Context, input []UpdateTaskDueDateNotification) (*UpdateTaskDueDateNotificationsResult, error) {
reminders := []DueDateNotification{}
for _, in := range input {
n, err := r.Repository.UpdateDueDateReminder(ctx, db.UpdateDueDateReminderParams{
DueDateReminderID: in.ID,
Period: int32(in.Period),
Duration: in.Duration.String(),
})
if err != nil {
return &UpdateTaskDueDateNotificationsResult{}, err
}
duration := DueDateNotificationDuration(n.Duration)
if !duration.IsValid() {
log.WithField("duration", n.Duration).Error("invalid duration found")
return &UpdateTaskDueDateNotificationsResult{}, errors.New("invalid duration")
}
reminders = append(reminders, DueDateNotification{
ID: n.DueDateReminderID,
Period: int(n.Period),
Duration: duration,
})
}
return &UpdateTaskDueDateNotificationsResult{
Notifications: reminders,
}, nil
}
func (r *mutationResolver) DeleteTaskDueDateNotifications(ctx context.Context, input []DeleteTaskDueDateNotification) (*DeleteTaskDueDateNotificationsResult, error) {
ids := []uuid.UUID{}
for _, n := range input {
err := r.Repository.DeleteDueDateReminder(ctx, n.ID)
if err != nil {
log.WithError(err).Error("error while deleting task due date notification")
return &DeleteTaskDueDateNotificationsResult{}, err
}
ids = append(ids, n.ID)
}
return &DeleteTaskDueDateNotificationsResult{Notifications: ids}, nil
}
func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task, error) { func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task, error) {
var taskID uuid.UUID var taskID uuid.UUID
var err error var err error
@ -724,11 +805,34 @@ func (r *taskResolver) Watched(ctx context.Context, obj *db.Task) (bool, error)
return true, nil return true, nil
} }
func (r *taskResolver) DueDate(ctx context.Context, obj *db.Task) (*time.Time, error) { func (r *taskResolver) DueDate(ctx context.Context, obj *db.Task) (*DueDate, error) {
if obj.DueDate.Valid { nots, err := r.Repository.GetDueDateRemindersForTaskID(ctx, obj.TaskID)
return &obj.DueDate.Time, nil if err != nil {
log.WithError(err).Error("error while fetching due date reminders")
return &DueDate{}, err
} }
return nil, nil reminders := []DueDateNotification{}
for _, n := range nots {
duration := DueDateNotificationDuration(n.Duration)
if !duration.IsValid() {
log.WithField("duration", n.Duration).Error("invalid duration found")
return &DueDate{}, errors.New("invalid duration")
}
reminders = append(reminders, DueDateNotification{
ID: n.DueDateReminderID,
Period: int(n.Period),
Duration: duration,
})
}
var time *time.Time
if obj.DueDate.Valid {
time = &obj.DueDate.Time
}
return &DueDate{
At: time,
Notifications: reminders,
}, nil
} }
func (r *taskResolver) CompletedAt(ctx context.Context, obj *db.Task) (*time.Time, error) { func (r *taskResolver) CompletedAt(ctx context.Context, obj *db.Task) (*time.Time, error) {
@ -904,7 +1008,10 @@ func (r *taskChecklistItemResolver) ID(ctx context.Context, obj *db.TaskChecklis
} }
func (r *taskChecklistItemResolver) DueDate(ctx context.Context, obj *db.TaskChecklistItem) (*time.Time, error) { func (r *taskChecklistItemResolver) DueDate(ctx context.Context, obj *db.TaskChecklistItem) (*time.Time, error) {
panic(fmt.Errorf("not implemented")) if obj.DueDate.Valid {
return &obj.DueDate.Time, nil
}
return nil, nil
} }
func (r *taskCommentResolver) ID(ctx context.Context, obj *db.TaskComment) (uuid.UUID, error) { func (r *taskCommentResolver) ID(ctx context.Context, obj *db.TaskComment) (uuid.UUID, error) {

View File

@ -0,0 +1,15 @@
CREATE TABLE task_due_date_reminder_duration (
code text PRIMARY KEY
);
INSERT INTO task_due_date_reminder_duration VALUES ('MINUTE');
INSERT INTO task_due_date_reminder_duration VALUES ('HOUR');
INSERT INTO task_due_date_reminder_duration VALUES ('DAY');
INSERT INTO task_due_date_reminder_duration VALUES ('WEEK');
CREATE TABLE task_due_date_reminder (
due_date_reminder_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
task_id uuid NOT NULL REFERENCES task(task_id) ON DELETE CASCADE,
period int NOT NULL,
duration text NOT NULL REFERENCES task_due_date_reminder_duration(code) ON DELETE CASCADE
);