feat: redesign due date manager
This commit is contained in:
parent
df6140a10f
commit
0d00fc7518
@ -61,7 +61,6 @@ const Routes: React.FC = () => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
console.log('loading', loading);
|
||||
if (loading) return null;
|
||||
return (
|
||||
<Switch>
|
||||
|
@ -9,7 +9,6 @@ const Auth = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation<{ redirect: string } | undefined>();
|
||||
const { setUser } = useContext(UserContext);
|
||||
console.log('auth');
|
||||
const login = (
|
||||
data: LoginFormData,
|
||||
setComplete: (val: boolean) => void,
|
||||
|
@ -562,13 +562,36 @@ const Projects = () => {
|
||||
onCancel={() => null}
|
||||
onDueDateChange={(task, dueDate, hasTime) => {
|
||||
if (dateEditor.task) {
|
||||
updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate, hasTime } });
|
||||
setDateEditor((prev) => ({ ...prev, task: { ...task, dueDate: dueDate.toISOString(), hasTime } }));
|
||||
hidePopup();
|
||||
updateTaskDueDate({
|
||||
variables: {
|
||||
taskID: dateEditor.task.id,
|
||||
dueDate,
|
||||
hasTime,
|
||||
deleteNotifications: [],
|
||||
updateNotifications: [],
|
||||
createNotifications: [],
|
||||
},
|
||||
});
|
||||
setDateEditor((prev) => ({
|
||||
...prev,
|
||||
task: { ...task, dueDate: { at: dueDate.toISOString(), notifications: [] }, hasTime },
|
||||
}));
|
||||
}
|
||||
}}
|
||||
onRemoveDueDate={(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 } }));
|
||||
}
|
||||
}}
|
||||
@ -655,8 +678,8 @@ const Projects = () => {
|
||||
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;
|
||||
const first = dayjs(a.dueDate);
|
||||
const second = dayjs(b.dueDate);
|
||||
const first = dayjs(a.dueDate.at);
|
||||
const second = dayjs(b.dueDate.at);
|
||||
if (first.isSame(second, 'minute')) return 0;
|
||||
if (first.isAfter(second)) return -1;
|
||||
return 1;
|
||||
@ -792,10 +815,19 @@ const Projects = () => {
|
||||
history.push(`${match.url}/c/${task.id}`);
|
||||
}}
|
||||
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'}
|
||||
dueDate={task.dueDate}
|
||||
dueDate={task.dueDate.at}
|
||||
hasTime={task.hasTime ?? false}
|
||||
name={task.name}
|
||||
onEditName={(name) => updateTaskName({ variables: { taskID: task.id, name } })}
|
||||
@ -821,7 +853,9 @@ const Projects = () => {
|
||||
<EditorCell width={120}>
|
||||
<DueDateEditorLabel>
|
||||
{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>
|
||||
</EditorCell>
|
||||
|
@ -446,7 +446,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
||||
checklist: null,
|
||||
},
|
||||
position,
|
||||
dueDate: null,
|
||||
dueDate: { at: null },
|
||||
description: null,
|
||||
labels: [],
|
||||
assigned: [],
|
||||
@ -801,12 +801,30 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
||||
<DueDateManager
|
||||
task={task}
|
||||
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) => {
|
||||
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } });
|
||||
// hidePopup();
|
||||
hidePopup();
|
||||
updateTaskDueDate({
|
||||
variables: {
|
||||
taskID: t.id,
|
||||
dueDate: newDueDate,
|
||||
hasTime,
|
||||
deleteNotifications: [],
|
||||
updateNotifications: [],
|
||||
createNotifications: [],
|
||||
},
|
||||
});
|
||||
}}
|
||||
onCancel={NOOP}
|
||||
/>
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
useUpdateTaskChecklistItemLocationMutation,
|
||||
useCreateTaskChecklistMutation,
|
||||
useFindTaskQuery,
|
||||
DueDateNotificationDuration,
|
||||
useUpdateTaskDueDateMutation,
|
||||
useSetTaskCompleteMutation,
|
||||
useAssignTaskMutation,
|
||||
@ -647,12 +648,79 @@ const Details: React.FC<DetailsProps> = ({
|
||||
<DueDateManager
|
||||
task={task}
|
||||
onRemoveDueDate={(t) => {
|
||||
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
|
||||
// hidePopup();
|
||||
updateTaskDueDate({
|
||||
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) => {
|
||||
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } });
|
||||
// hidePopup();
|
||||
onDueDateChange={(t, newDueDate, hasTime, notifications) => {
|
||||
const updatedNotifications = 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 {
|
||||
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}
|
||||
/>
|
||||
|
@ -39,7 +39,6 @@ const UsersRegister = () => {
|
||||
.then(async (x) => {
|
||||
const response = await x.json();
|
||||
const { setup } = response;
|
||||
console.log(response);
|
||||
if (setup) {
|
||||
history.replace(`/confirm?confirmToken=xxxx`);
|
||||
isRedirected = true;
|
||||
|
@ -1,9 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
import Button from 'shared/components/Button';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
import Input from 'shared/components/Input';
|
||||
import ControlledInput from 'shared/components/ControlledInput';
|
||||
import { Clock } from 'shared/icons';
|
||||
import { Bell, Clock } from 'shared/icons';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
display: flex
|
||||
@ -22,27 +21,27 @@ display: flex
|
||||
& .react-datepicker__close-icon::after {
|
||||
background: none;
|
||||
font-size: 16px;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
}
|
||||
|
||||
& .react-datepicker-time__header {
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
}
|
||||
& .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-box ul.react-datepicker__time-list
|
||||
li.react-datepicker__time-list-item:hover {
|
||||
color: ${props => props.theme.colors.text.secondary};
|
||||
background: ${props => props.theme.colors.bg.secondary};
|
||||
color: ${(props) => props.theme.colors.text.secondary};
|
||||
background: ${(props) => props.theme.colors.bg.secondary};
|
||||
}
|
||||
& .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 {
|
||||
background: ${props => props.theme.colors.bg.primary};
|
||||
border: 1px solid ${props => props.theme.colors.border};
|
||||
background: ${(props) => props.theme.colors.bg.primary};
|
||||
border: 1px solid ${(props) => props.theme.colors.border};
|
||||
}
|
||||
|
||||
& .react-datepicker * {
|
||||
@ -82,12 +81,12 @@ display: flex
|
||||
}
|
||||
& .react-datepicker__day--selected {
|
||||
border-radius: 50%;
|
||||
background: ${props => props.theme.colors.primary};
|
||||
background: ${(props) => props.theme.colors.primary};
|
||||
color: #fff;
|
||||
}
|
||||
& .react-datepicker__day--selected:hover {
|
||||
border-radius: 50%;
|
||||
background: ${props => props.theme.colors.primary};
|
||||
background: ${(props) => props.theme.colors.primary};
|
||||
color: #fff;
|
||||
}
|
||||
& .react-datepicker__header {
|
||||
@ -95,12 +94,12 @@ display: flex
|
||||
border: none;
|
||||
}
|
||||
& .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 {
|
||||
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;
|
||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
|
||||
padding: 0.7rem;
|
||||
@ -114,7 +113,7 @@ padding: 0.7rem;
|
||||
&: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};
|
||||
background: ${(props) => props.theme.colors.bg.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
@ -142,9 +141,9 @@ export const AddDateRange = styled.div`
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: ${props => mixin.rgba(props.theme.colors.primary, 0.8)};
|
||||
color: ${(props) => mixin.rgba(props.theme.colors.primary, 0.8)};
|
||||
&:hover {
|
||||
color: ${props => mixin.rgba(props.theme.colors.primary, 1)};
|
||||
color: ${(props) => mixin.rgba(props.theme.colors.primary, 1)};
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
@ -201,18 +200,62 @@ export const ActionsWrapper = styled.div`
|
||||
align-items: center;
|
||||
& .react-datepicker-wrapper {
|
||||
margin-left: auto;
|
||||
width: 82px;
|
||||
width: 86px;
|
||||
}
|
||||
& .react-datepicker__input-container input {
|
||||
padding-bottom: 4px;
|
||||
padding-top: 4px;
|
||||
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)`
|
||||
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;
|
||||
flex: 0 0 auto;
|
||||
`;
|
||||
@ -222,7 +265,7 @@ export const ActionLabel = styled.div`
|
||||
line-height: 14px;
|
||||
`;
|
||||
|
||||
export const ActionIcon = styled.div`
|
||||
export const ActionIcon = styled.div<{ disabled?: boolean }>`
|
||||
height: 36px;
|
||||
min-height: 36px;
|
||||
min-width: 36px;
|
||||
@ -232,17 +275,25 @@ export const ActionIcon = styled.div`
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
svg {
|
||||
fill: ${props => props.theme.colors.text.primary};
|
||||
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};
|
||||
fill: ${(props) => props.theme.colors.text.secondary};
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.disabled &&
|
||||
css`
|
||||
opacity: 0.8;
|
||||
cursor: not-allowed;
|
||||
`}
|
||||
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ClearButton = styled.div`
|
||||
@ -260,8 +311,38 @@ export const ClearButton = styled.div`
|
||||
justify-content: center;
|
||||
transition-duration: 0.2s;
|
||||
transition-property: background, border, box-shadow, color, fill;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
&: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;
|
||||
`;
|
||||
|
@ -3,16 +3,21 @@ import dayjs from 'dayjs';
|
||||
import styled from 'styled-components';
|
||||
import DatePicker from 'react-datepicker';
|
||||
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 { getYear, getMonth } from 'date-fns';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import NOOP from 'shared/utils/noop';
|
||||
import { Clock, Cross } from 'shared/icons';
|
||||
import Select from 'react-select/src/Select';
|
||||
import { Bell, Clock, Cross, Plus, Trash } from 'shared/icons';
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
RemoveDueDate,
|
||||
SaveButton,
|
||||
RightWrapper,
|
||||
LeftWrapper,
|
||||
DueDateInput,
|
||||
DueDatePickerWrapper,
|
||||
ConfirmAddDueDate,
|
||||
@ -24,11 +29,19 @@ import {
|
||||
ActionsSeparator,
|
||||
ActionClock,
|
||||
ActionLabel,
|
||||
ControlWrapper,
|
||||
RemoveButton,
|
||||
ActionBell,
|
||||
} from './Styles';
|
||||
|
||||
type DueDateManagerProps = {
|
||||
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;
|
||||
onCancel: () => void;
|
||||
};
|
||||
@ -41,6 +54,39 @@ const FormField = styled.div`
|
||||
width: 50%;
|
||||
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`
|
||||
display: inline-block;
|
||||
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 currentDueDate = task.dueDate ? dayjs(task.dueDate).toDate() : null;
|
||||
const currentDueDate = task.dueDate.at ? dayjs(task.dueDate.at).toDate() : null;
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@ -145,28 +252,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
||||
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(() => {
|
||||
debouncedChange(startDate, hasTime);
|
||||
}, [startDate, hasTime]);
|
||||
const years = _.range(2010, getYear(new Date()) + 10, 1);
|
||||
const months = [
|
||||
'January',
|
||||
@ -183,33 +269,41 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
||||
'December',
|
||||
];
|
||||
|
||||
const onChange = (dates: any) => {
|
||||
const [start, end] = dates;
|
||||
setStartDate(start);
|
||||
setEndDate(end);
|
||||
};
|
||||
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 (
|
||||
<Wrapper>
|
||||
<DateRangeInputs>
|
||||
<DatePicker
|
||||
selected={startDate}
|
||||
onChange={(date) => {
|
||||
if (!Array.isArray(date)) {
|
||||
if (!Array.isArray(date) && date !== null) {
|
||||
setStartDate(date);
|
||||
}
|
||||
}}
|
||||
popperClassName="picker-hidden"
|
||||
dateFormat="yyyy-MM-dd"
|
||||
disabledKeyboardNavigation
|
||||
isClearable
|
||||
placeholderText="Select due date"
|
||||
/>
|
||||
{isRange ? (
|
||||
<DatePicker
|
||||
selected={startDate}
|
||||
isClearable
|
||||
onChange={(date) => {
|
||||
if (!Array.isArray(date)) {
|
||||
setStartDate(date);
|
||||
@ -299,7 +393,78 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
||||
</ActionIcon>
|
||||
</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 && (
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
@ -314,8 +479,8 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
||||
<Clock width={16} height={16} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<ClearButton onClick={() => setStartDate(null)}>{hasTime ? 'Clear all' : 'Clear'}</ClearButton>
|
||||
</ActionsWrapper>
|
||||
</RightWrapper>
|
||||
</ControlWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
@ -351,10 +351,10 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
description=""
|
||||
labels={task.labels.map((label) => label.projectLabel)}
|
||||
dueDate={
|
||||
task.dueDate
|
||||
task.dueDate.at
|
||||
? {
|
||||
isPastDue: false,
|
||||
formattedDate: dayjs(task.dueDate).format('MMM D, YYYY'),
|
||||
formattedDate: dayjs(task.dueDate.at).format('MMM D, YYYY'),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
|
||||
isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null));
|
||||
}
|
||||
if (task.dueDate) {
|
||||
const taskDueDate = dayjs(task.dueDate);
|
||||
const taskDueDate = dayjs(task.dueDate.at);
|
||||
const today = dayjs();
|
||||
let start;
|
||||
let end;
|
||||
@ -36,61 +36,31 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
|
||||
isFiltered = shouldFilter(taskDueDate.isSame(today, 'day'));
|
||||
break;
|
||||
case DueDateFilterType.TOMORROW:
|
||||
isFiltered = shouldFilter(
|
||||
taskDueDate.isBefore(
|
||||
today
|
||||
.clone()
|
||||
.add(1, 'day')
|
||||
.endOf('day'),
|
||||
),
|
||||
);
|
||||
isFiltered = shouldFilter(taskDueDate.isBefore(today.clone().add(1, 'day').endOf('day')));
|
||||
break;
|
||||
case DueDateFilterType.THIS_WEEK:
|
||||
start = today
|
||||
.clone()
|
||||
.weekday(0)
|
||||
.startOf('day');
|
||||
end = today
|
||||
.clone()
|
||||
.weekday(6)
|
||||
.endOf('day');
|
||||
start = today.clone().weekday(0).startOf('day');
|
||||
end = today.clone().weekday(6).endOf('day');
|
||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||
break;
|
||||
case DueDateFilterType.NEXT_WEEK:
|
||||
start = today
|
||||
.clone()
|
||||
.weekday(0)
|
||||
.add(7, 'day')
|
||||
.startOf('day');
|
||||
end = today
|
||||
.clone()
|
||||
.weekday(6)
|
||||
.add(7, 'day')
|
||||
.endOf('day');
|
||||
start = today.clone().weekday(0).add(7, 'day').startOf('day');
|
||||
end = today.clone().weekday(6).add(7, 'day').endOf('day');
|
||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||
break;
|
||||
case DueDateFilterType.ONE_WEEK:
|
||||
start = today.clone().startOf('day');
|
||||
end = today
|
||||
.clone()
|
||||
.add(7, 'day')
|
||||
.endOf('day');
|
||||
end = today.clone().add(7, 'day').endOf('day');
|
||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||
break;
|
||||
case DueDateFilterType.TWO_WEEKS:
|
||||
start = today.clone().startOf('day');
|
||||
end = today
|
||||
.clone()
|
||||
.add(14, 'day')
|
||||
.endOf('day');
|
||||
end = today.clone().add(14, 'day').endOf('day');
|
||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||
break;
|
||||
case DueDateFilterType.THREE_WEEKS:
|
||||
start = today.clone().startOf('day');
|
||||
end = today
|
||||
.clone()
|
||||
.add(21, 'day')
|
||||
.endOf('day');
|
||||
end = today.clone().add(21, 'day').endOf('day');
|
||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||
break;
|
||||
default:
|
||||
@ -104,7 +74,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
|
||||
}
|
||||
for (const member of filters.members) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -116,7 +86,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
|
||||
}
|
||||
for (const label of filters.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;
|
||||
}
|
||||
}
|
||||
|
@ -9,14 +9,22 @@ type ActivityMessageProps = {
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 (hasTime) {
|
||||
return dayjs(timestamp).format('MMM D [at] h:mm A');
|
||||
}
|
||||
return dayjs(timestamp).format('MMM D');
|
||||
}
|
||||
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')}`;
|
||||
break;
|
||||
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;
|
||||
case ActivityType.TaskDueDateRemoved:
|
||||
message = `removed the due date from this task`;
|
||||
break;
|
||||
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;
|
||||
case ActivityType.TaskMarkedComplete:
|
||||
message = `marked this task complete`;
|
||||
|
@ -332,7 +332,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
const saveDescription = () => {
|
||||
onTaskDescriptionChange(task, taskDescriptionRef.current);
|
||||
};
|
||||
console.log(task.watched);
|
||||
return (
|
||||
<Container>
|
||||
<LeftSidebar>
|
||||
@ -351,9 +350,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{task.dueDate ? (
|
||||
{task.dueDate.at ? (
|
||||
<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>No due date</SidebarButtonText>
|
||||
@ -632,6 +631,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
{activityStream.map((stream) =>
|
||||
stream.data.type === 'comment' ? (
|
||||
<StreamComment
|
||||
key={stream.id}
|
||||
onExtraActions={onCommentShowActions}
|
||||
onCancelCommentEdit={onCancelCommentEdit}
|
||||
onUpdateComment={(message) => onUpdateComment(stream.id, message)}
|
||||
@ -640,6 +640,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
/>
|
||||
) : (
|
||||
<StreamActivity
|
||||
key={stream.id}
|
||||
activity={task.activity && task.activity.find((activity) => activity.id === stream.id)}
|
||||
/>
|
||||
),
|
||||
|
@ -30,19 +30,16 @@ function plugin(options) {
|
||||
}
|
||||
|
||||
function getEmoji(match) {
|
||||
console.log(match);
|
||||
const got = emoji.get(match);
|
||||
if (pad && got !== match) {
|
||||
return `${got} `;
|
||||
}
|
||||
|
||||
console.log(got);
|
||||
return ReactDOMServer.renderToStaticMarkup(<Emoji set="google" emoji={match} size={16} />);
|
||||
}
|
||||
|
||||
function transformer(tree) {
|
||||
visit(tree, 'paragraph', function (node) {
|
||||
console.log(tree);
|
||||
// node.value = node.value.replace(RE_EMOJI, getEmoji);
|
||||
// jnode.type = 'html';
|
||||
// jnode.tagName = 'div';
|
||||
@ -58,7 +55,6 @@ function plugin(options) {
|
||||
if (emoticonEnable) {
|
||||
// node.value = node.value.replace(RE_SHORT, getEmojiByShortCode);
|
||||
}
|
||||
console.log(node);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -334,7 +334,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
<ListUnordered width={20} height={20} />
|
||||
</IconContainer>
|
||||
<IconContainer onClick={onNotificationClick}>
|
||||
<Bell color="#c2c6dc" size={20} />
|
||||
<Bell width={20} height={20} />
|
||||
{hasUnread && <NotificationCount />}
|
||||
</IconContainer>
|
||||
<IconContainer disabled onClick={NOOP}>
|
||||
|
@ -107,6 +107,17 @@ export type CreateTaskCommentPayload = {
|
||||
comment: TaskComment;
|
||||
};
|
||||
|
||||
export type CreateTaskDueDateNotification = {
|
||||
taskID: Scalars['UUID'];
|
||||
period: Scalars['Int'];
|
||||
duration: DueDateNotificationDuration;
|
||||
};
|
||||
|
||||
export type CreateTaskDueDateNotificationsResult = {
|
||||
__typename?: 'CreateTaskDueDateNotificationsResult';
|
||||
notifications: Array<DueDateNotification>;
|
||||
};
|
||||
|
||||
export type CreateTeamMember = {
|
||||
userID: Scalars['UUID'];
|
||||
teamID: Scalars['UUID'];
|
||||
@ -200,6 +211,15 @@ export type DeleteTaskCommentPayload = {
|
||||
commentID: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type DeleteTaskDueDateNotification = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type DeleteTaskDueDateNotificationsResult = {
|
||||
__typename?: 'DeleteTaskDueDateNotificationsResult';
|
||||
notifications: Array<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type DeleteTaskGroupInput = {
|
||||
taskGroupID: Scalars['UUID'];
|
||||
};
|
||||
@ -265,6 +285,26 @@ export type DeleteUserAccountPayload = {
|
||||
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 = {
|
||||
projectID: Scalars['UUID'];
|
||||
taskGroupID: Scalars['UUID'];
|
||||
@ -393,6 +433,7 @@ export type Mutation = {
|
||||
createTaskChecklist: TaskChecklist;
|
||||
createTaskChecklistItem: TaskChecklistItem;
|
||||
createTaskComment: CreateTaskCommentPayload;
|
||||
createTaskDueDateNotifications: CreateTaskDueDateNotificationsResult;
|
||||
createTaskGroup: TaskGroup;
|
||||
createTeam: Team;
|
||||
createTeamMember: CreateTeamMemberPayload;
|
||||
@ -406,6 +447,7 @@ export type Mutation = {
|
||||
deleteTaskChecklist: DeleteTaskChecklistPayload;
|
||||
deleteTaskChecklistItem: DeleteTaskChecklistItemPayload;
|
||||
deleteTaskComment: DeleteTaskCommentPayload;
|
||||
deleteTaskDueDateNotifications: DeleteTaskDueDateNotificationsResult;
|
||||
deleteTaskGroup: DeleteTaskGroupPayload;
|
||||
deleteTaskGroupTasks: DeleteTaskGroupTasksPayload;
|
||||
deleteTeam: DeleteTeamPayload;
|
||||
@ -435,6 +477,7 @@ export type Mutation = {
|
||||
updateTaskComment: UpdateTaskCommentPayload;
|
||||
updateTaskDescription: Task;
|
||||
updateTaskDueDate: Task;
|
||||
updateTaskDueDateNotifications: UpdateTaskDueDateNotificationsResult;
|
||||
updateTaskGroupLocation: TaskGroup;
|
||||
updateTaskGroupName: TaskGroup;
|
||||
updateTaskLocation: UpdateTaskLocationPayload;
|
||||
@ -486,6 +529,11 @@ export type MutationCreateTaskCommentArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateTaskDueDateNotificationsArgs = {
|
||||
input: Array<CreateTaskDueDateNotification>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateTaskGroupArgs = {
|
||||
input: NewTaskGroup;
|
||||
};
|
||||
@ -551,6 +599,11 @@ export type MutationDeleteTaskCommentArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteTaskDueDateNotificationsArgs = {
|
||||
input: Array<DeleteTaskDueDateNotification>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteTaskGroupArgs = {
|
||||
input: DeleteTaskGroupInput;
|
||||
};
|
||||
@ -696,6 +749,11 @@ export type MutationUpdateTaskDueDateArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateTaskDueDateNotificationsArgs = {
|
||||
input: Array<UpdateTaskDueDateNotification>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateTaskGroupLocationArgs = {
|
||||
input: NewTaskGroupLocation;
|
||||
};
|
||||
@ -1078,7 +1136,7 @@ export type Task = {
|
||||
position: Scalars['Float'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
watched: Scalars['Boolean'];
|
||||
dueDate?: Maybe<Scalars['Time']>;
|
||||
dueDate: DueDate;
|
||||
hasTime: Scalars['Boolean'];
|
||||
complete: Scalars['Boolean'];
|
||||
completedAt?: Maybe<Scalars['Time']>;
|
||||
@ -1302,6 +1360,17 @@ export type UpdateTaskDueDate = {
|
||||
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 = {
|
||||
taskGroupID: Scalars['UUID'];
|
||||
name: Scalars['String'];
|
||||
@ -1596,8 +1665,15 @@ export type FindTaskQuery = (
|
||||
{ __typename?: 'Query' }
|
||||
& { findTask: (
|
||||
{ __typename?: 'Task' }
|
||||
& Pick<Task, 'id' | 'shortId' | 'name' | 'watched' | 'description' | 'dueDate' | 'position' | 'complete' | 'hasTime'>
|
||||
& { taskGroup: (
|
||||
& Pick<Task, 'id' | 'shortId' | 'name' | 'watched' | 'description' | 'position' | 'complete' | 'hasTime'>
|
||||
& { dueDate: (
|
||||
{ __typename?: 'DueDate' }
|
||||
& Pick<DueDate, 'at'>
|
||||
& { notifications: Array<(
|
||||
{ __typename?: 'DueDateNotification' }
|
||||
& Pick<DueDateNotification, 'id' | 'period' | 'duration'>
|
||||
)> }
|
||||
), taskGroup: (
|
||||
{ __typename?: 'TaskGroup' }
|
||||
& Pick<TaskGroup, 'id' | 'name'>
|
||||
), comments: Array<(
|
||||
@ -1672,8 +1748,11 @@ export type FindTaskQuery = (
|
||||
|
||||
export type TaskFieldsFragment = (
|
||||
{ __typename?: 'Task' }
|
||||
& Pick<Task, 'id' | 'shortId' | 'name' | 'description' | 'dueDate' | 'hasTime' | 'complete' | 'watched' | 'completedAt' | 'position'>
|
||||
& { badges: (
|
||||
& Pick<Task, 'id' | 'shortId' | 'name' | 'description' | 'hasTime' | 'complete' | 'watched' | 'completedAt' | 'position'>
|
||||
& { dueDate: (
|
||||
{ __typename?: 'DueDate' }
|
||||
& Pick<DueDate, 'at'>
|
||||
), badges: (
|
||||
{ __typename?: 'TaskBadges' }
|
||||
& { checklist?: Maybe<(
|
||||
{ __typename?: 'ChecklistBadge' }
|
||||
@ -1789,10 +1868,13 @@ export type MyTasksQuery = (
|
||||
{ __typename?: 'MyTasksPayload' }
|
||||
& { tasks: Array<(
|
||||
{ __typename?: 'Task' }
|
||||
& Pick<Task, 'id' | 'shortId' | 'name' | 'dueDate' | 'hasTime' | 'complete' | 'completedAt'>
|
||||
& Pick<Task, 'id' | 'shortId' | 'name' | 'hasTime' | 'complete' | 'completedAt'>
|
||||
& { taskGroup: (
|
||||
{ __typename?: 'TaskGroup' }
|
||||
& Pick<TaskGroup, 'id' | 'name'>
|
||||
), dueDate: (
|
||||
{ __typename?: 'DueDate' }
|
||||
& Pick<DueDate, 'at'>
|
||||
) }
|
||||
)>, projects: Array<(
|
||||
{ __typename?: 'ProjectTaskMapping' }
|
||||
@ -2624,6 +2706,9 @@ export type UpdateTaskDueDateMutationVariables = Exact<{
|
||||
taskID: Scalars['UUID'];
|
||||
dueDate?: Maybe<Scalars['Time']>;
|
||||
hasTime: Scalars['Boolean'];
|
||||
createNotifications: Array<CreateTaskDueDateNotification> | CreateTaskDueDateNotification;
|
||||
updateNotifications: Array<UpdateTaskDueDateNotification> | UpdateTaskDueDateNotification;
|
||||
deleteNotifications: Array<DeleteTaskDueDateNotification> | DeleteTaskDueDateNotification;
|
||||
}>;
|
||||
|
||||
|
||||
@ -2631,7 +2716,26 @@ export type UpdateTaskDueDateMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { updateTaskDueDate: (
|
||||
{ __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
|
||||
name
|
||||
description
|
||||
dueDate
|
||||
dueDate {
|
||||
at
|
||||
}
|
||||
hasTime
|
||||
complete
|
||||
watched
|
||||
@ -3346,7 +3452,14 @@ export const FindTaskDocument = gql`
|
||||
name
|
||||
watched
|
||||
description
|
||||
dueDate
|
||||
dueDate {
|
||||
at
|
||||
notifications {
|
||||
id
|
||||
period
|
||||
duration
|
||||
}
|
||||
}
|
||||
position
|
||||
complete
|
||||
hasTime
|
||||
@ -3640,7 +3753,9 @@ export const MyTasksDocument = gql`
|
||||
name
|
||||
}
|
||||
name
|
||||
dueDate
|
||||
dueDate {
|
||||
at
|
||||
}
|
||||
hasTime
|
||||
complete
|
||||
completedAt
|
||||
@ -5423,14 +5538,33 @@ export type UpdateTaskDescriptionMutationHookResult = ReturnType<typeof useUpdat
|
||||
export type UpdateTaskDescriptionMutationResult = Apollo.MutationResult<UpdateTaskDescriptionMutation>;
|
||||
export type UpdateTaskDescriptionMutationOptions = Apollo.BaseMutationOptions<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>;
|
||||
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(
|
||||
input: {taskID: $taskID, dueDate: $dueDate, hasTime: $hasTime}
|
||||
) {
|
||||
id
|
||||
dueDate
|
||||
dueDate {
|
||||
at
|
||||
}
|
||||
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>;
|
||||
@ -5451,6 +5585,9 @@ export type UpdateTaskDueDateMutationFn = Apollo.MutationFunction<UpdateTaskDueD
|
||||
* taskID: // value for 'taskID'
|
||||
* dueDate: // value for 'dueDate'
|
||||
* hasTime: // value for 'hasTime'
|
||||
* createNotifications: // value for 'createNotifications'
|
||||
* updateNotifications: // value for 'updateNotifications'
|
||||
* deleteNotifications: // value for 'deleteNotifications'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
@ -5,7 +5,14 @@ query findTask($taskID: String!) {
|
||||
name
|
||||
watched
|
||||
description
|
||||
dueDate
|
||||
dueDate {
|
||||
at
|
||||
notifications {
|
||||
id
|
||||
period
|
||||
duration
|
||||
}
|
||||
}
|
||||
position
|
||||
complete
|
||||
hasTime
|
||||
|
@ -6,7 +6,9 @@ const TASK_FRAGMENT = gql`
|
||||
shortId
|
||||
name
|
||||
description
|
||||
dueDate
|
||||
dueDate {
|
||||
at
|
||||
}
|
||||
hasTime
|
||||
complete
|
||||
watched
|
||||
|
@ -12,7 +12,9 @@ query myTasks($status: MyTasksStatus!, $sort: MyTasksSort!) {
|
||||
name
|
||||
}
|
||||
name
|
||||
dueDate
|
||||
dueDate {
|
||||
at
|
||||
}
|
||||
hasTime
|
||||
complete
|
||||
completedAt
|
||||
|
@ -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 (
|
||||
input: {
|
||||
taskID: $taskID
|
||||
@ -7,7 +11,26 @@ mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!) {
|
||||
}
|
||||
) {
|
||||
id
|
||||
dueDate
|
||||
dueDate {
|
||||
at
|
||||
}
|
||||
hasTime
|
||||
}
|
||||
createTaskDueDateNotifications(input: $createNotifications) {
|
||||
notifications {
|
||||
id
|
||||
period
|
||||
duration
|
||||
}
|
||||
}
|
||||
updateTaskDueDateNotifications(input: $updateNotifications) {
|
||||
notifications {
|
||||
id
|
||||
period
|
||||
duration
|
||||
}
|
||||
}
|
||||
deleteTaskDueDateNotifications(input: $deleteNotifications) {
|
||||
notifications
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ function parseJSON<T>(value: string | null): T | undefined {
|
||||
try {
|
||||
return value === 'undefined' ? undefined : JSON.parse(value ?? '');
|
||||
} catch (error) {
|
||||
console.log('parsing error on', { value });
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,11 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Bell = ({ size, color }: Props) => {
|
||||
const Bell: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||
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" />
|
||||
</svg>
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -46,7 +46,7 @@ export function sortTasks(a: Task, b: Task, taskSorting: TaskSorting) {
|
||||
if (b.dueDate && !a.dueDate) {
|
||||
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 (a.complete && !b.complete) {
|
||||
|
2
frontend/src/types.d.ts
vendored
2
frontend/src/types.d.ts
vendored
@ -110,7 +110,7 @@ type Task = {
|
||||
badges?: TaskBadges;
|
||||
position: number;
|
||||
hasTime?: boolean;
|
||||
dueDate?: string;
|
||||
dueDate: { at?: string; notifications?: Array<{ id: string; period: number; duration: string }> };
|
||||
complete?: boolean;
|
||||
completedAt?: string | null;
|
||||
labels: TaskLabel[];
|
||||
|
@ -187,6 +187,17 @@ type TaskComment struct {
|
||||
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 {
|
||||
TaskGroupID uuid.UUID `json:"task_group_id"`
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
type Querier interface {
|
||||
CreateAuthToken(ctx context.Context, arg CreateAuthTokenParams) (AuthToken, 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)
|
||||
CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error)
|
||||
CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error)
|
||||
@ -40,6 +41,7 @@ type Querier interface {
|
||||
DeleteAuthTokenByID(ctx context.Context, tokenID uuid.UUID) error
|
||||
DeleteAuthTokenByUserID(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteConfirmTokenForEmail(ctx context.Context, email string) error
|
||||
DeleteDueDateReminder(ctx context.Context, dueDateReminderID uuid.UUID) error
|
||||
DeleteExpiredTokens(ctx context.Context) error
|
||||
DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) 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)
|
||||
GetConfirmTokenByEmail(ctx context.Context, email string) (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)
|
||||
GetInvitedUserAccounts(ctx context.Context) ([]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)
|
||||
SetUserActiveByEmail(ctx context.Context, email string) (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)
|
||||
UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
|
||||
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)
|
||||
|
@ -116,3 +116,16 @@ SELECT task.* FROM task_assigned
|
||||
|
||||
-- name: GetCommentCountForTask :one
|
||||
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;
|
||||
|
@ -12,6 +12,28 @@ import (
|
||||
"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
|
||||
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
|
||||
@ -144,6 +166,15 @@ func (q *Queries) CreateTaskWatcher(ctx context.Context, arg CreateTaskWatcherPa
|
||||
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
|
||||
DELETE FROM task WHERE task_id = $1
|
||||
`
|
||||
@ -403,6 +434,38 @@ func (q *Queries) GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([
|
||||
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
|
||||
SELECT project_id FROM task
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
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
@ -60,6 +60,16 @@ type CreateTaskCommentPayload struct {
|
||||
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 {
|
||||
UserID uuid.UUID `json:"userID"`
|
||||
TeamID uuid.UUID `json:"teamID"`
|
||||
@ -144,6 +154,14 @@ type DeleteTaskCommentPayload struct {
|
||||
CommentID uuid.UUID `json:"commentID"`
|
||||
}
|
||||
|
||||
type DeleteTaskDueDateNotification struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
}
|
||||
|
||||
type DeleteTaskDueDateNotificationsResult struct {
|
||||
Notifications []uuid.UUID `json:"notifications"`
|
||||
}
|
||||
|
||||
type DeleteTaskGroupInput struct {
|
||||
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||
}
|
||||
@ -203,6 +221,17 @@ type DeleteUserAccountPayload struct {
|
||||
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 {
|
||||
ProjectID uuid.UUID `json:"projectID"`
|
||||
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||
@ -599,6 +628,16 @@ type UpdateTaskDueDate struct {
|
||||
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 {
|
||||
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||
Name string `json:"name"`
|
||||
@ -821,6 +860,51 @@ func (e ActivityType) MarshalGQL(w io.Writer) {
|
||||
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
|
||||
|
||||
const (
|
||||
|
@ -1,3 +1,9 @@
|
||||
enum DueDateNotificationDuration {
|
||||
MINUTE
|
||||
HOUR
|
||||
DAY
|
||||
WEEK
|
||||
}
|
||||
|
||||
type TaskLabel {
|
||||
id: ID!
|
||||
@ -20,6 +26,18 @@ type TaskBadges {
|
||||
comments: CommentsBadge
|
||||
}
|
||||
|
||||
type DueDateNotification {
|
||||
id: ID!
|
||||
period: Int!
|
||||
duration: DueDateNotificationDuration!
|
||||
}
|
||||
|
||||
type DueDate {
|
||||
at: Time
|
||||
notifications: [DueDateNotification!]!
|
||||
}
|
||||
|
||||
|
||||
type Task {
|
||||
id: ID!
|
||||
shortId: String!
|
||||
@ -29,7 +47,7 @@ type Task {
|
||||
position: Float!
|
||||
description: String
|
||||
watched: Boolean!
|
||||
dueDate: Time
|
||||
dueDate: DueDate!
|
||||
hasTime: Boolean!
|
||||
complete: Boolean!
|
||||
completedAt: Time
|
||||
@ -371,8 +389,45 @@ extend type Mutation {
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
unassignTask(input: UnassignTaskInput):
|
||||
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 {
|
||||
taskID: UUID!
|
||||
}
|
||||
|
@ -1,3 +1,9 @@
|
||||
enum DueDateNotificationDuration {
|
||||
MINUTE
|
||||
HOUR
|
||||
DAY
|
||||
WEEK
|
||||
}
|
||||
|
||||
type TaskLabel {
|
||||
id: ID!
|
||||
@ -20,6 +26,18 @@ type TaskBadges {
|
||||
comments: CommentsBadge
|
||||
}
|
||||
|
||||
type DueDateNotification {
|
||||
id: ID!
|
||||
period: Int!
|
||||
duration: DueDateNotificationDuration!
|
||||
}
|
||||
|
||||
type DueDate {
|
||||
at: Time
|
||||
notifications: [DueDateNotification!]!
|
||||
}
|
||||
|
||||
|
||||
type Task {
|
||||
id: ID!
|
||||
shortId: String!
|
||||
@ -29,7 +47,7 @@ type Task {
|
||||
position: Float!
|
||||
description: String
|
||||
watched: Boolean!
|
||||
dueDate: Time
|
||||
dueDate: DueDate!
|
||||
hasTime: Boolean!
|
||||
complete: Boolean!
|
||||
completedAt: Time
|
||||
|
@ -31,8 +31,45 @@ extend type Mutation {
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
unassignTask(input: UnassignTaskInput):
|
||||
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 {
|
||||
taskID: UUID!
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -501,8 +501,20 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa
|
||||
if err != nil {
|
||||
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{}
|
||||
var activityType = TASK_DUE_DATE_ADDED
|
||||
data["HasTime"] = strconv.FormatBool(input.HasTime)
|
||||
if input.DueDate == nil && prevTask.DueDate.Valid {
|
||||
activityType = TASK_DUE_DATE_REMOVED
|
||||
data["PrevDueDate"] = prevTask.DueDate.Time.String()
|
||||
@ -529,6 +541,7 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa
|
||||
})
|
||||
createdAt := time.Now().UTC()
|
||||
d, _ := json.Marshal(data)
|
||||
if !isSame {
|
||||
_, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{
|
||||
TaskID: task.TaskID,
|
||||
Data: d,
|
||||
@ -536,6 +549,7 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa
|
||||
CreatedAt: createdAt,
|
||||
ActivityTypeID: activityType,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
task, err = r.Repository.GetTaskByID(ctx, input.TaskID)
|
||||
}
|
||||
@ -670,6 +684,73 @@ func (r *mutationResolver) UnassignTask(ctx context.Context, input *UnassignTask
|
||||
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) {
|
||||
var taskID uuid.UUID
|
||||
var err error
|
||||
@ -724,11 +805,34 @@ func (r *taskResolver) Watched(ctx context.Context, obj *db.Task) (bool, error)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *taskResolver) DueDate(ctx context.Context, obj *db.Task) (*time.Time, error) {
|
||||
if obj.DueDate.Valid {
|
||||
return &obj.DueDate.Time, nil
|
||||
func (r *taskResolver) DueDate(ctx context.Context, obj *db.Task) (*DueDate, error) {
|
||||
nots, err := r.Repository.GetDueDateRemindersForTaskID(ctx, obj.TaskID)
|
||||
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) {
|
||||
@ -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) {
|
||||
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) {
|
||||
|
15
migrations/0070_add-task_due_date_notification.up.sql
Normal file
15
migrations/0070_add-task_due_date_notification.up.sql
Normal 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
|
||||
);
|
Loading…
Reference in New Issue
Block a user