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,23 +393,94 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
</ActionIcon>
</ActionsWrapper>
)}
<ActionsWrapper>
{!hasTime && (
<ActionIcon
{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 === null) {
const today = new Date();
today.setHours(12, 30, 0);
setStartDate(today);
if (startDate && notifications.findIndex((n) => Number.isNaN(n.period)) === -1) {
onDueDateChange(task, startDate, hasTime, { current: notifications, removed: removedNotifications });
}
enableTime(true);
}}
>
<Clock width={16} height={16} />
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>
)}
<ClearButton onClick={() => setStartDate(null)}>{hasTime ? 'Clear all' : 'Clear'}</ClearButton>
</ActionsWrapper>
{!hasTime && (
<ActionIcon
onClick={() => {
if (startDate === null) {
const today = new Date();
today.setHours(12, 30, 0);
setStartDate(today);
}
enableTime(true);
}}
>
<Clock width={16} height={16} />
</ActionIcon>
)}
</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,13 +9,21 @@ 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) {
return dayjs(timestamp).format('MMM D [at] h:mm A');
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[];