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);
});
}, []);
console.log('loading', loading);
if (loading) return null;
return (
<Switch>

View File

@ -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,

View File

@ -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>

View File

@ -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}
/>

View File

@ -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}
/>

View File

@ -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;

View File

@ -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;
`;

View File

@ -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>
);
};

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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`;

View File

@ -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)}
/>
),

View File

@ -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);
});
}

View File

@ -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}>

View File

@ -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'
* },
* });
*/

View File

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

View File

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

View File

@ -12,7 +12,9 @@ query myTasks($status: MyTasksStatus!, $sort: MyTasksSort!) {
name
}
name
dueDate
dueDate {
at
}
hasTime
complete
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 (
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
}
}

View File

@ -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;
}
}

View File

@ -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>
);
};

View File

@ -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) {

View File

@ -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[];

View File

@ -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"`

View File

@ -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)

View File

@ -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;

View File

@ -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

View File

@ -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 (

View File

@ -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!
}

View File

@ -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

View File

@ -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!
}

View File

@ -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) {

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
);