feat: redesign due date manager
This commit is contained in:
		| @@ -61,7 +61,6 @@ const Routes: React.FC = () => { | ||||
|       setLoading(false); | ||||
|     }); | ||||
|   }, []); | ||||
|   console.log('loading', loading); | ||||
|   if (loading) return null; | ||||
|   return ( | ||||
|     <Switch> | ||||
|   | ||||
| @@ -9,7 +9,6 @@ const Auth = () => { | ||||
|   const history = useHistory(); | ||||
|   const location = useLocation<{ redirect: string } | undefined>(); | ||||
|   const { setUser } = useContext(UserContext); | ||||
|   console.log('auth'); | ||||
|   const login = ( | ||||
|     data: LoginFormData, | ||||
|     setComplete: (val: boolean) => void, | ||||
|   | ||||
| @@ -562,13 +562,36 @@ const Projects = () => { | ||||
|             onCancel={() => null} | ||||
|             onDueDateChange={(task, dueDate, hasTime) => { | ||||
|               if (dateEditor.task) { | ||||
|                 updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate, hasTime } }); | ||||
|                 setDateEditor((prev) => ({ ...prev, task: { ...task, dueDate: dueDate.toISOString(), hasTime } })); | ||||
|                 hidePopup(); | ||||
|                 updateTaskDueDate({ | ||||
|                   variables: { | ||||
|                     taskID: dateEditor.task.id, | ||||
|                     dueDate, | ||||
|                     hasTime, | ||||
|                     deleteNotifications: [], | ||||
|                     updateNotifications: [], | ||||
|                     createNotifications: [], | ||||
|                   }, | ||||
|                 }); | ||||
|                 setDateEditor((prev) => ({ | ||||
|                   ...prev, | ||||
|                   task: { ...task, dueDate: { at: dueDate.toISOString(), notifications: [] }, hasTime }, | ||||
|                 })); | ||||
|               } | ||||
|             }} | ||||
|             onRemoveDueDate={(task) => { | ||||
|               if (dateEditor.task) { | ||||
|                 updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate: null, hasTime: false } }); | ||||
|                 hidePopup(); | ||||
|                 updateTaskDueDate({ | ||||
|                   variables: { | ||||
|                     taskID: dateEditor.task.id, | ||||
|                     dueDate: null, | ||||
|                     hasTime: false, | ||||
|                     deleteNotifications: [], | ||||
|                     updateNotifications: [], | ||||
|                     createNotifications: [], | ||||
|                   }, | ||||
|                 }); | ||||
|                 setDateEditor((prev) => ({ ...prev, task: { ...task, hasTime: false } })); | ||||
|               } | ||||
|             }} | ||||
| @@ -655,8 +678,8 @@ const Projects = () => { | ||||
|               if (a.dueDate === null && b.dueDate === null) return 0; | ||||
|               if (a.dueDate === null && b.dueDate !== null) return 1; | ||||
|               if (a.dueDate !== null && b.dueDate === null) return -1; | ||||
|               const first = dayjs(a.dueDate); | ||||
|               const second = dayjs(b.dueDate); | ||||
|               const first = dayjs(a.dueDate.at); | ||||
|               const second = dayjs(b.dueDate.at); | ||||
|               if (first.isSame(second, 'minute')) return 0; | ||||
|               if (first.isAfter(second)) return -1; | ||||
|               return 1; | ||||
| @@ -792,10 +815,19 @@ const Projects = () => { | ||||
|                                   history.push(`${match.url}/c/${task.id}`); | ||||
|                                 }} | ||||
|                                 onRemoveDueDate={() => { | ||||
|                                   updateTaskDueDate({ variables: { taskID: task.id, dueDate: null, hasTime: false } }); | ||||
|                                   updateTaskDueDate({ | ||||
|                                     variables: { | ||||
|                                       taskID: task.id, | ||||
|                                       dueDate: null, | ||||
|                                       hasTime: false, | ||||
|                                       deleteNotifications: [], | ||||
|                                       updateNotifications: [], | ||||
|                                       createNotifications: [], | ||||
|                                     }, | ||||
|                                   }); | ||||
|                                 }} | ||||
|                                 project={projectName ?? 'none'} | ||||
|                                 dueDate={task.dueDate} | ||||
|                                 dueDate={task.dueDate.at} | ||||
|                                 hasTime={task.hasTime ?? false} | ||||
|                                 name={task.name} | ||||
|                                 onEditName={(name) => updateTaskName({ variables: { taskID: task.id, name } })} | ||||
| @@ -821,7 +853,9 @@ const Projects = () => { | ||||
|                 <EditorCell width={120}> | ||||
|                   <DueDateEditorLabel> | ||||
|                     {dateEditor.task.dueDate | ||||
|                       ? dayjs(dateEditor.task.dueDate).format(dateEditor.task.hasTime ? 'MMM D [at] h:mm A' : 'MMM D') | ||||
|                       ? dayjs(dateEditor.task.dueDate.at).format( | ||||
|                           dateEditor.task.hasTime ? 'MMM D [at] h:mm A' : 'MMM D', | ||||
|                         ) | ||||
|                       : ''} | ||||
|                   </DueDateEditorLabel> | ||||
|                 </EditorCell> | ||||
|   | ||||
| @@ -446,7 +446,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick | ||||
|                 checklist: null, | ||||
|               }, | ||||
|               position, | ||||
|               dueDate: null, | ||||
|               dueDate: { at: null }, | ||||
|               description: null, | ||||
|               labels: [], | ||||
|               assigned: [], | ||||
| @@ -801,12 +801,30 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick | ||||
|                   <DueDateManager | ||||
|                     task={task} | ||||
|                     onRemoveDueDate={(t) => { | ||||
|                       updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } }); | ||||
|                       // hidePopup(); | ||||
|                       hidePopup(); | ||||
|                       updateTaskDueDate({ | ||||
|                         variables: { | ||||
|                           taskID: t.id, | ||||
|                           dueDate: null, | ||||
|                           hasTime: false, | ||||
|                           deleteNotifications: [], | ||||
|                           updateNotifications: [], | ||||
|                           createNotifications: [], | ||||
|                         }, | ||||
|                       }); | ||||
|                     }} | ||||
|                     onDueDateChange={(t, newDueDate, hasTime) => { | ||||
|                       updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } }); | ||||
|                       // hidePopup(); | ||||
|                       hidePopup(); | ||||
|                       updateTaskDueDate({ | ||||
|                         variables: { | ||||
|                           taskID: t.id, | ||||
|                           dueDate: newDueDate, | ||||
|                           hasTime, | ||||
|                           deleteNotifications: [], | ||||
|                           updateNotifications: [], | ||||
|                           createNotifications: [], | ||||
|                         }, | ||||
|                       }); | ||||
|                     }} | ||||
|                     onCancel={NOOP} | ||||
|                   /> | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import { | ||||
|   useUpdateTaskChecklistItemLocationMutation, | ||||
|   useCreateTaskChecklistMutation, | ||||
|   useFindTaskQuery, | ||||
|   DueDateNotificationDuration, | ||||
|   useUpdateTaskDueDateMutation, | ||||
|   useSetTaskCompleteMutation, | ||||
|   useAssignTaskMutation, | ||||
| @@ -647,12 +648,79 @@ const Details: React.FC<DetailsProps> = ({ | ||||
|                     <DueDateManager | ||||
|                       task={task} | ||||
|                       onRemoveDueDate={(t) => { | ||||
|                         updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } }); | ||||
|                         // hidePopup(); | ||||
|                         updateTaskDueDate({ | ||||
|                           variables: { | ||||
|                             taskID: t.id, | ||||
|                             dueDate: null, | ||||
|                             hasTime: false, | ||||
|                             deleteNotifications: t.dueDate.notifications | ||||
|                               ? t.dueDate.notifications.map((n) => ({ id: n.id })) | ||||
|                               : [], | ||||
|                             updateNotifications: [], | ||||
|                             createNotifications: [], | ||||
|                           }, | ||||
|                         }); | ||||
|                         hidePopup(); | ||||
|                       }} | ||||
|                       onDueDateChange={(t, newDueDate, hasTime) => { | ||||
|                         updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } }); | ||||
|                         // hidePopup(); | ||||
|                       onDueDateChange={(t, newDueDate, hasTime, notifications) => { | ||||
|                         const updatedNotifications = notifications.current | ||||
|                           .filter((c) => c.externalId !== null) | ||||
|                           .map((c) => { | ||||
|                             let duration = DueDateNotificationDuration.Minute; | ||||
|                             switch (c.duration.value) { | ||||
|                               case 'hour': | ||||
|                                 duration = DueDateNotificationDuration.Hour; | ||||
|                                 break; | ||||
|                               case 'day': | ||||
|                                 duration = DueDateNotificationDuration.Day; | ||||
|                                 break; | ||||
|                               case 'week': | ||||
|                                 duration = DueDateNotificationDuration.Week; | ||||
|                                 break; | ||||
|                               default: | ||||
|                                 break; | ||||
|                             } | ||||
|                             return { | ||||
|                               id: c.externalId ?? '', | ||||
|                               period: c.period, | ||||
|                               duration, | ||||
|                             }; | ||||
|                           }); | ||||
|                         const newNotifications = notifications.current | ||||
|                           .filter((c) => c.externalId === null) | ||||
|                           .map((c) => { | ||||
|                             let duration = DueDateNotificationDuration.Minute; | ||||
|                             switch (c.duration.value) { | ||||
|                               case 'hour': | ||||
|                                 duration = DueDateNotificationDuration.Hour; | ||||
|                                 break; | ||||
|                               case 'day': | ||||
|                                 duration = DueDateNotificationDuration.Day; | ||||
|                                 break; | ||||
|                               case 'week': | ||||
|                                 duration = DueDateNotificationDuration.Week; | ||||
|                                 break; | ||||
|                               default: | ||||
|                                 break; | ||||
|                             } | ||||
|                             return { | ||||
|                               taskID: task.id, | ||||
|                               period: c.period, | ||||
|                               duration, | ||||
|                             }; | ||||
|                           }); | ||||
|                         // const updatedNotifications = notifications.filter(c => c.externalId === null); | ||||
|                         updateTaskDueDate({ | ||||
|                           variables: { | ||||
|                             taskID: t.id, | ||||
|                             dueDate: newDueDate, | ||||
|                             hasTime, | ||||
|                             createNotifications: newNotifications, | ||||
|                             updateNotifications: updatedNotifications, | ||||
|                             deleteNotifications: notifications.removed.map((n) => ({ id: n })), | ||||
|                           }, | ||||
|                         }); | ||||
|                         hidePopup(); | ||||
|                       }} | ||||
|                       onCancel={NOOP} | ||||
|                     /> | ||||
|   | ||||
| @@ -39,7 +39,6 @@ const UsersRegister = () => { | ||||
|                 .then(async (x) => { | ||||
|                   const response = await x.json(); | ||||
|                   const { setup } = response; | ||||
|                   console.log(response); | ||||
|                   if (setup) { | ||||
|                     history.replace(`/confirm?confirmToken=xxxx`); | ||||
|                     isRedirected = true; | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| import styled from 'styled-components'; | ||||
| import styled, { css } from 'styled-components'; | ||||
| import Button from 'shared/components/Button'; | ||||
| import { mixin } from 'shared/utils/styles'; | ||||
| import Input from 'shared/components/Input'; | ||||
| import ControlledInput from 'shared/components/ControlledInput'; | ||||
| import { Clock } from 'shared/icons'; | ||||
| import { Bell, Clock } from 'shared/icons'; | ||||
|  | ||||
| export const Wrapper = styled.div` | ||||
| display: flex | ||||
| @@ -22,27 +21,27 @@ display: flex | ||||
|   & .react-datepicker__close-icon::after { | ||||
|     background: none; | ||||
|     font-size: 16px; | ||||
|     color: ${props => props.theme.colors.text.primary}; | ||||
|     color: ${(props) => props.theme.colors.text.primary}; | ||||
|   } | ||||
|  | ||||
|   & .react-datepicker-time__header { | ||||
|     color: ${props => props.theme.colors.text.primary}; | ||||
|     color: ${(props) => props.theme.colors.text.primary}; | ||||
|   } | ||||
|   & .react-datepicker__time-list-item { | ||||
|     color: ${props => props.theme.colors.text.primary}; | ||||
|     color: ${(props) => props.theme.colors.text.primary}; | ||||
|   } | ||||
|   & .react-datepicker__time-container .react-datepicker__time | ||||
|   .react-datepicker__time-box ul.react-datepicker__time-list | ||||
|   li.react-datepicker__time-list-item:hover { | ||||
|     color: ${props => props.theme.colors.text.secondary}; | ||||
|     background: ${props => props.theme.colors.bg.secondary}; | ||||
|     color: ${(props) => props.theme.colors.text.secondary}; | ||||
|     background: ${(props) => props.theme.colors.bg.secondary}; | ||||
|   } | ||||
|   & .react-datepicker__time-container .react-datepicker__time { | ||||
|     background: ${props => props.theme.colors.bg.primary}; | ||||
|     background: ${(props) => props.theme.colors.bg.primary}; | ||||
|   } | ||||
|   & .react-datepicker--time-only { | ||||
|     background: ${props => props.theme.colors.bg.primary}; | ||||
|     border: 1px solid ${props => props.theme.colors.border}; | ||||
|     background: ${(props) => props.theme.colors.bg.primary}; | ||||
|     border: 1px solid ${(props) => props.theme.colors.border}; | ||||
|   } | ||||
|  | ||||
|   & .react-datepicker * { | ||||
| @@ -82,12 +81,12 @@ display: flex | ||||
|   } | ||||
|   & .react-datepicker__day--selected { | ||||
|     border-radius: 50%; | ||||
|     background: ${props => props.theme.colors.primary}; | ||||
|     background: ${(props) => props.theme.colors.primary}; | ||||
|     color: #fff; | ||||
|   } | ||||
|   & .react-datepicker__day--selected:hover { | ||||
|     border-radius: 50%; | ||||
|     background: ${props => props.theme.colors.primary}; | ||||
|     background: ${(props) => props.theme.colors.primary}; | ||||
|     color: #fff; | ||||
|   } | ||||
|   & .react-datepicker__header { | ||||
| @@ -95,12 +94,12 @@ display: flex | ||||
|     border: none; | ||||
|   } | ||||
|   & .react-datepicker__header--time { | ||||
|     border-bottom: 1px solid ${props => props.theme.colors.border}; | ||||
|     border-bottom: 1px solid ${(props) => props.theme.colors.border}; | ||||
|   } | ||||
|  | ||||
|   & .react-datepicker__input-container input { | ||||
|   border: 1px solid rgba(0, 0, 0, 0.2); | ||||
|   border-color: ${props => props.theme.colors.alternate}; | ||||
|   border-color: ${(props) => props.theme.colors.alternate}; | ||||
|   background: #262c49; | ||||
|   box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15); | ||||
| padding: 0.7rem; | ||||
| @@ -114,7 +113,7 @@ padding: 0.7rem; | ||||
|   &:focus { | ||||
|     box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15); | ||||
|     border: 1px solid rgba(115, 103, 240); | ||||
|     background: ${props => props.theme.colors.bg.primary}; | ||||
|     background: ${(props) => props.theme.colors.bg.primary}; | ||||
|   } | ||||
| `; | ||||
|  | ||||
| @@ -142,9 +141,9 @@ export const AddDateRange = styled.div` | ||||
|   width: 100%; | ||||
|   font-size: 12px; | ||||
|   line-height: 16px; | ||||
|   color: ${props => mixin.rgba(props.theme.colors.primary, 0.8)}; | ||||
|   color: ${(props) => mixin.rgba(props.theme.colors.primary, 0.8)}; | ||||
|   &:hover { | ||||
|     color: ${props => mixin.rgba(props.theme.colors.primary, 1)}; | ||||
|     color: ${(props) => mixin.rgba(props.theme.colors.primary, 1)}; | ||||
|     text-decoration: underline; | ||||
|   } | ||||
| `; | ||||
| @@ -201,18 +200,62 @@ export const ActionsWrapper = styled.div` | ||||
|   align-items: center; | ||||
|   & .react-datepicker-wrapper { | ||||
|     margin-left: auto; | ||||
|     width: 82px; | ||||
|     width: 86px; | ||||
|   } | ||||
|   & .react-datepicker__input-container input { | ||||
|     padding-bottom: 4px; | ||||
|     padding-top: 4px; | ||||
|     width: 100%; | ||||
|   } | ||||
|  | ||||
|   & .react-period-select__indicators { | ||||
|     display: none; | ||||
|   } | ||||
|   & .react-period { | ||||
|     width: 100%; | ||||
|     max-width: 86px; | ||||
|   } | ||||
|  | ||||
|   & .react-period-select__single-value { | ||||
|     color: #c2c6dc; | ||||
|     margin-left: 0; | ||||
|     margin-right: 0; | ||||
|   } | ||||
|   & .react-period-select__value-container { | ||||
|     padding-left: 0; | ||||
|     padding-right: 0; | ||||
|   } | ||||
|   & .react-period-select__control { | ||||
|     border: 1px solid rgba(0, 0, 0, 0.2); | ||||
|     min-height: 30px; | ||||
|     border-color: rgb(65, 69, 97); | ||||
|     background: #262c49; | ||||
|     box-shadow: 0 0 0 0 rgb(0 0 0 / 15%); | ||||
|     color: #c2c6dc; | ||||
|     padding-right: 12px; | ||||
|     padding-left: 12px; | ||||
|     padding-bottom: 4px; | ||||
|     padding-top: 4px; | ||||
|     width: 100%; | ||||
|     position: relative; | ||||
|     border-radius: 5px; | ||||
|     transition: all 0.3s ease; | ||||
|     font-size: 13px; | ||||
|     line-height: 20px; | ||||
|     padding: 0 12px; | ||||
|   } | ||||
| `; | ||||
|  | ||||
| export const ActionClock = styled(Clock)` | ||||
|   align-self: center; | ||||
|   fill: ${props => props.theme.colors.primary}; | ||||
|   fill: ${(props) => props.theme.colors.primary}; | ||||
|   margin: 0 8px; | ||||
|   flex: 0 0 auto; | ||||
| `; | ||||
|  | ||||
| export const ActionBell = styled(Bell)` | ||||
|   align-self: center; | ||||
|   fill: ${(props) => props.theme.colors.primary}; | ||||
|   margin: 0 8px; | ||||
|   flex: 0 0 auto; | ||||
| `; | ||||
| @@ -222,7 +265,7 @@ export const ActionLabel = styled.div` | ||||
|   line-height: 14px; | ||||
| `; | ||||
|  | ||||
| export const ActionIcon = styled.div` | ||||
| export const ActionIcon = styled.div<{ disabled?: boolean }>` | ||||
|   height: 36px; | ||||
|   min-height: 36px; | ||||
|   min-width: 36px; | ||||
| @@ -232,17 +275,25 @@ export const ActionIcon = styled.div` | ||||
|   cursor: pointer; | ||||
|   margin-right: 8px; | ||||
|   svg { | ||||
|     fill: ${props => props.theme.colors.text.primary}; | ||||
|     fill: ${(props) => props.theme.colors.text.primary}; | ||||
|     transition-duration: 0.2s; | ||||
|     transition-property: background, border, box-shadow, fill; | ||||
|   } | ||||
|   &:hover svg { | ||||
|     fill: ${props => props.theme.colors.text.secondary}; | ||||
|     fill: ${(props) => props.theme.colors.text.secondary}; | ||||
|   } | ||||
|  | ||||
|   ${(props) => | ||||
|     props.disabled && | ||||
|     css` | ||||
|       opacity: 0.8; | ||||
|       cursor: not-allowed; | ||||
|     `} | ||||
|  | ||||
|   align-items: center; | ||||
|   display: inline-flex; | ||||
|   justify-content: center; | ||||
|   position: relative; | ||||
| `; | ||||
|  | ||||
| export const ClearButton = styled.div` | ||||
| @@ -260,8 +311,38 @@ export const ClearButton = styled.div` | ||||
|   justify-content: center; | ||||
|   transition-duration: 0.2s; | ||||
|   transition-property: background, border, box-shadow, color, fill; | ||||
|   color: ${props => props.theme.colors.text.primary}; | ||||
|   color: ${(props) => props.theme.colors.text.primary}; | ||||
|   &:hover { | ||||
|     color: ${props => props.theme.colors.text.secondary}; | ||||
|     color: ${(props) => props.theme.colors.text.secondary}; | ||||
|   } | ||||
| `; | ||||
|  | ||||
| export const ControlWrapper = styled.div` | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   margin-top: 8px; | ||||
| `; | ||||
|  | ||||
| export const RightWrapper = styled.div` | ||||
|   flex: 1 1 50%; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   flex-direction: row-reverse; | ||||
| `; | ||||
|  | ||||
| export const LeftWrapper = styled.div` | ||||
|   flex: 1 1 50%; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
| `; | ||||
|  | ||||
| export const SaveButton = styled(Button)` | ||||
|   padding: 6px 12px; | ||||
|   justify-content: center; | ||||
|   margin-right: 4px; | ||||
| `; | ||||
|  | ||||
| export const RemoveButton = styled.div` | ||||
|   width: 100%; | ||||
|   justify-content: center; | ||||
| `; | ||||
|   | ||||
| @@ -3,16 +3,21 @@ import dayjs from 'dayjs'; | ||||
| import styled from 'styled-components'; | ||||
| import DatePicker from 'react-datepicker'; | ||||
| import _ from 'lodash'; | ||||
| import { colourStyles } from 'shared/components/Select'; | ||||
| import produce from 'immer'; | ||||
| import Select from 'react-select'; | ||||
| import 'react-datepicker/dist/react-datepicker.css'; | ||||
| import { getYear, getMonth } from 'date-fns'; | ||||
| import { useForm, Controller } from 'react-hook-form'; | ||||
| import NOOP from 'shared/utils/noop'; | ||||
| import { Clock, Cross } from 'shared/icons'; | ||||
| import Select from 'react-select/src/Select'; | ||||
| import { Bell, Clock, Cross, Plus, Trash } from 'shared/icons'; | ||||
|  | ||||
| import { | ||||
|   Wrapper, | ||||
|   RemoveDueDate, | ||||
|   SaveButton, | ||||
|   RightWrapper, | ||||
|   LeftWrapper, | ||||
|   DueDateInput, | ||||
|   DueDatePickerWrapper, | ||||
|   ConfirmAddDueDate, | ||||
| @@ -24,11 +29,19 @@ import { | ||||
|   ActionsSeparator, | ||||
|   ActionClock, | ||||
|   ActionLabel, | ||||
|   ControlWrapper, | ||||
|   RemoveButton, | ||||
|   ActionBell, | ||||
| } from './Styles'; | ||||
|  | ||||
| type DueDateManagerProps = { | ||||
|   task: Task; | ||||
|   onDueDateChange: (task: Task, newDueDate: Date, hasTime: boolean) => void; | ||||
|   onDueDateChange: ( | ||||
|     task: Task, | ||||
|     newDueDate: Date, | ||||
|     hasTime: boolean, | ||||
|     notifications: { current: Array<NotificationInternal>; removed: Array<string> }, | ||||
|   ) => void; | ||||
|   onRemoveDueDate: (task: Task) => void; | ||||
|   onCancel: () => void; | ||||
| }; | ||||
| @@ -41,6 +54,39 @@ const FormField = styled.div` | ||||
|   width: 50%; | ||||
|   display: inline-block; | ||||
| `; | ||||
|  | ||||
| const NotificationCount = styled.input``; | ||||
|  | ||||
| const ActionPlus = styled(Plus)` | ||||
|   position: absolute; | ||||
|   fill: ${(props) => props.theme.colors.bg.primary} !important; | ||||
|   stroke: ${(props) => props.theme.colors.bg.primary}; | ||||
| `; | ||||
|  | ||||
| const ActionInput = styled.input` | ||||
|   border: 1px solid rgba(0, 0, 0, 0.2); | ||||
|   margin-left: auto; | ||||
|   margin-right: 4px; | ||||
|   border-color: rgb(65, 69, 97); | ||||
|   background: #262c49; | ||||
|   box-shadow: 0 0 0 0 rgb(0 0 0 / 15%); | ||||
|   color: #c2c6dc; | ||||
|   position: relative; | ||||
|   border-radius: 5px; | ||||
|   transition: all 0.3s ease; | ||||
|   font-size: 13px; | ||||
|   line-height: 20px; | ||||
|   padding: 0 12px; | ||||
|   padding-bottom: 4px; | ||||
|   padding-top: 4px; | ||||
|   width: 100%; | ||||
|   max-width: 48px; | ||||
|   &::-webkit-outer-spin-button, | ||||
|   &::-webkit-inner-spin-button { | ||||
|     -webkit-appearance: none; | ||||
|     margin: 0; | ||||
|   } | ||||
| `; | ||||
| const HeaderSelectLabel = styled.div` | ||||
|   display: inline-block; | ||||
|   position: relative; | ||||
| @@ -131,8 +177,69 @@ const HeaderActions = styled.div` | ||||
|   } | ||||
| `; | ||||
|  | ||||
| const notificationPeriodOptions = [ | ||||
|   { value: 'minute', label: 'Minutes' }, | ||||
|   { value: 'hour', label: 'Hours' }, | ||||
|   { value: 'day', label: 'Days' }, | ||||
|   { value: 'week', label: 'Weeks' }, | ||||
| ]; | ||||
|  | ||||
| type NotificationInternal = { | ||||
|   internalId: string; | ||||
|   externalId: string | null; | ||||
|   period: number; | ||||
|   duration: { value: string; label: string }; | ||||
| }; | ||||
|  | ||||
| type NotificationEntryProps = { | ||||
|   notification: NotificationInternal; | ||||
|   onChange: (period: number, duration: { value: string; label: string }) => void; | ||||
|   onRemove: () => void; | ||||
| }; | ||||
|  | ||||
| const NotificationEntry: React.FC<NotificationEntryProps> = ({ notification, onChange, onRemove }) => { | ||||
|   return ( | ||||
|     <> | ||||
|       <ActionBell width={16} height={16} /> | ||||
|       <ActionLabel>Notification</ActionLabel> | ||||
|       <ActionInput | ||||
|         value={notification.period} | ||||
|         onChange={(e) => { | ||||
|           onChange(parseInt(e.currentTarget.value, 10), notification.duration); | ||||
|         }} | ||||
|         onKeyPress={(e) => { | ||||
|           const isNumber = /^[0-9]$/i.test(e.key); | ||||
|           if (!isNumber && e.key !== 'Backspace') { | ||||
|             e.preventDefault(); | ||||
|           } | ||||
|         }} | ||||
|         dir="ltr" | ||||
|         autoComplete="off" | ||||
|         min="0" | ||||
|         type="number" | ||||
|       /> | ||||
|       <Select | ||||
|         menuPlacement="top" | ||||
|         className="react-period" | ||||
|         classNamePrefix="react-period-select" | ||||
|         styles={colourStyles} | ||||
|         isSearchable={false} | ||||
|         defaultValue={notification.duration} | ||||
|         options={notificationPeriodOptions} | ||||
|         onChange={(e) => { | ||||
|           if (e !== null) { | ||||
|             onChange(notification.period, e); | ||||
|           } | ||||
|         }} | ||||
|       /> | ||||
|       <ActionIcon onClick={() => onRemove()}> | ||||
|         <Cross width={16} height={16} /> | ||||
|       </ActionIcon> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => { | ||||
|   const currentDueDate = task.dueDate ? dayjs(task.dueDate).toDate() : null; | ||||
|   const currentDueDate = task.dueDate.at ? dayjs(task.dueDate.at).toDate() : null; | ||||
|   const { | ||||
|     register, | ||||
|     handleSubmit, | ||||
| @@ -145,28 +252,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, | ||||
|   const [startDate, setStartDate] = useState<Date | null>(currentDueDate); | ||||
|   const [endDate, setEndDate] = useState<Date | null>(currentDueDate); | ||||
|   const [hasTime, enableTime] = useState(task.hasTime ?? false); | ||||
|   const firstRun = useRef<boolean>(true); | ||||
|  | ||||
|   const debouncedFunctionRef = useRef((newDate: Date | null, nowHasTime: boolean) => { | ||||
|     if (!firstRun.current) { | ||||
|       if (newDate) { | ||||
|         onDueDateChange(task, newDate, nowHasTime); | ||||
|       } else { | ||||
|         onRemoveDueDate(task); | ||||
|         enableTime(false); | ||||
|       } | ||||
|     } else { | ||||
|       firstRun.current = false; | ||||
|     } | ||||
|   }); | ||||
|   const debouncedChange = useCallback( | ||||
|     _.debounce((newDate, nowHasTime) => debouncedFunctionRef.current(newDate, nowHasTime), 500), | ||||
|     [], | ||||
|   ); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     debouncedChange(startDate, hasTime); | ||||
|   }, [startDate, hasTime]); | ||||
|   const years = _.range(2010, getYear(new Date()) + 10, 1); | ||||
|   const months = [ | ||||
|     'January', | ||||
| @@ -183,33 +269,41 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, | ||||
|     'December', | ||||
|   ]; | ||||
|  | ||||
|   const onChange = (dates: any) => { | ||||
|     const [start, end] = dates; | ||||
|     setStartDate(start); | ||||
|     setEndDate(end); | ||||
|   }; | ||||
|   const [isRange, setIsRange] = useState(false); | ||||
|  | ||||
|   const [notDuration, setNotDuration] = useState(10); | ||||
|   const [removedNotifications, setRemovedNotifications] = useState<Array<string>>([]); | ||||
|   const [notifications, setNotifications] = useState<Array<NotificationInternal>>( | ||||
|     task.dueDate.notifications | ||||
|       ? task.dueDate.notifications.map((c, idx) => { | ||||
|           const duration = | ||||
|             notificationPeriodOptions.find((o) => o.value === c.duration.toLowerCase()) ?? notificationPeriodOptions[0]; | ||||
|           return { | ||||
|             internalId: `n${idx}`, | ||||
|             externalId: c.id, | ||||
|             period: c.period, | ||||
|             duration, | ||||
|           }; | ||||
|         }) | ||||
|       : [], | ||||
|   ); | ||||
|   return ( | ||||
|     <Wrapper> | ||||
|       <DateRangeInputs> | ||||
|         <DatePicker | ||||
|           selected={startDate} | ||||
|           onChange={(date) => { | ||||
|             if (!Array.isArray(date)) { | ||||
|             if (!Array.isArray(date) && date !== null) { | ||||
|               setStartDate(date); | ||||
|             } | ||||
|           }} | ||||
|           popperClassName="picker-hidden" | ||||
|           dateFormat="yyyy-MM-dd" | ||||
|           disabledKeyboardNavigation | ||||
|           isClearable | ||||
|           placeholderText="Select due date" | ||||
|         /> | ||||
|         {isRange ? ( | ||||
|           <DatePicker | ||||
|             selected={startDate} | ||||
|             isClearable | ||||
|             onChange={(date) => { | ||||
|               if (!Array.isArray(date)) { | ||||
|                 setStartDate(date); | ||||
| @@ -299,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> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -351,10 +351,10 @@ const SimpleLists: React.FC<SimpleProps> = ({ | ||||
|                                                 description="" | ||||
|                                                 labels={task.labels.map((label) => label.projectLabel)} | ||||
|                                                 dueDate={ | ||||
|                                                   task.dueDate | ||||
|                                                   task.dueDate.at | ||||
|                                                     ? { | ||||
|                                                         isPastDue: false, | ||||
|                                                         formattedDate: dayjs(task.dueDate).format('MMM D, YYYY'), | ||||
|                                                         formattedDate: dayjs(task.dueDate.at).format('MMM D, YYYY'), | ||||
|                                                       } | ||||
|                                                     : undefined | ||||
|                                                 } | ||||
|   | ||||
| @@ -24,7 +24,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) { | ||||
|       isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null)); | ||||
|     } | ||||
|     if (task.dueDate) { | ||||
|       const taskDueDate = dayjs(task.dueDate); | ||||
|       const taskDueDate = dayjs(task.dueDate.at); | ||||
|       const today = dayjs(); | ||||
|       let start; | ||||
|       let end; | ||||
| @@ -36,61 +36,31 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) { | ||||
|           isFiltered = shouldFilter(taskDueDate.isSame(today, 'day')); | ||||
|           break; | ||||
|         case DueDateFilterType.TOMORROW: | ||||
|           isFiltered = shouldFilter( | ||||
|             taskDueDate.isBefore( | ||||
|               today | ||||
|                 .clone() | ||||
|                 .add(1, 'day') | ||||
|                 .endOf('day'), | ||||
|             ), | ||||
|           ); | ||||
|           isFiltered = shouldFilter(taskDueDate.isBefore(today.clone().add(1, 'day').endOf('day'))); | ||||
|           break; | ||||
|         case DueDateFilterType.THIS_WEEK: | ||||
|           start = today | ||||
|             .clone() | ||||
|             .weekday(0) | ||||
|             .startOf('day'); | ||||
|           end = today | ||||
|             .clone() | ||||
|             .weekday(6) | ||||
|             .endOf('day'); | ||||
|           start = today.clone().weekday(0).startOf('day'); | ||||
|           end = today.clone().weekday(6).endOf('day'); | ||||
|           isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); | ||||
|           break; | ||||
|         case DueDateFilterType.NEXT_WEEK: | ||||
|           start = today | ||||
|             .clone() | ||||
|             .weekday(0) | ||||
|             .add(7, 'day') | ||||
|             .startOf('day'); | ||||
|           end = today | ||||
|             .clone() | ||||
|             .weekday(6) | ||||
|             .add(7, 'day') | ||||
|             .endOf('day'); | ||||
|           start = today.clone().weekday(0).add(7, 'day').startOf('day'); | ||||
|           end = today.clone().weekday(6).add(7, 'day').endOf('day'); | ||||
|           isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); | ||||
|           break; | ||||
|         case DueDateFilterType.ONE_WEEK: | ||||
|           start = today.clone().startOf('day'); | ||||
|           end = today | ||||
|             .clone() | ||||
|             .add(7, 'day') | ||||
|             .endOf('day'); | ||||
|           end = today.clone().add(7, 'day').endOf('day'); | ||||
|           isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); | ||||
|           break; | ||||
|         case DueDateFilterType.TWO_WEEKS: | ||||
|           start = today.clone().startOf('day'); | ||||
|           end = today | ||||
|             .clone() | ||||
|             .add(14, 'day') | ||||
|             .endOf('day'); | ||||
|           end = today.clone().add(14, 'day').endOf('day'); | ||||
|           isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); | ||||
|           break; | ||||
|         case DueDateFilterType.THREE_WEEKS: | ||||
|           start = today.clone().startOf('day'); | ||||
|           end = today | ||||
|             .clone() | ||||
|             .add(21, 'day') | ||||
|             .endOf('day'); | ||||
|           end = today.clone().add(21, 'day').endOf('day'); | ||||
|           isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); | ||||
|           break; | ||||
|         default: | ||||
| @@ -104,7 +74,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) { | ||||
|     } | ||||
|     for (const member of filters.members) { | ||||
|       if (task.assigned) { | ||||
|         if (task.assigned.findIndex(m => m.id === member.id) !== -1) { | ||||
|         if (task.assigned.findIndex((m) => m.id === member.id) !== -1) { | ||||
|           isFiltered = ShouldFilter.VALID; | ||||
|         } | ||||
|       } | ||||
| @@ -116,7 +86,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) { | ||||
|     } | ||||
|     for (const label of filters.labels) { | ||||
|       if (task.labels) { | ||||
|         if (task.labels.findIndex(m => m.projectLabel.id === label.id) !== -1) { | ||||
|         if (task.labels.findIndex((m) => m.projectLabel.id === label.id) !== -1) { | ||||
|           isFiltered = ShouldFilter.VALID; | ||||
|         } | ||||
|       } | ||||
|   | ||||
| @@ -9,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`; | ||||
|   | ||||
| @@ -332,7 +332,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({ | ||||
|   const saveDescription = () => { | ||||
|     onTaskDescriptionChange(task, taskDescriptionRef.current); | ||||
|   }; | ||||
|   console.log(task.watched); | ||||
|   return ( | ||||
|     <Container> | ||||
|       <LeftSidebar> | ||||
| @@ -351,9 +350,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({ | ||||
|                 } | ||||
|               }} | ||||
|             > | ||||
|               {task.dueDate ? ( | ||||
|               {task.dueDate.at ? ( | ||||
|                 <SidebarButtonText> | ||||
|                   {dayjs(task.dueDate).format(task.hasTime ? 'MMM D [at] h:mm A' : 'MMMM D')} | ||||
|                   {dayjs(task.dueDate.at).format(task.hasTime ? 'MMM D [at] h:mm A' : 'MMMM D')} | ||||
|                 </SidebarButtonText> | ||||
|               ) : ( | ||||
|                 <SidebarButtonText>No due date</SidebarButtonText> | ||||
| @@ -632,6 +631,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({ | ||||
|             {activityStream.map((stream) => | ||||
|               stream.data.type === 'comment' ? ( | ||||
|                 <StreamComment | ||||
|                   key={stream.id} | ||||
|                   onExtraActions={onCommentShowActions} | ||||
|                   onCancelCommentEdit={onCancelCommentEdit} | ||||
|                   onUpdateComment={(message) => onUpdateComment(stream.id, message)} | ||||
| @@ -640,6 +640,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({ | ||||
|                 /> | ||||
|               ) : ( | ||||
|                 <StreamActivity | ||||
|                   key={stream.id} | ||||
|                   activity={task.activity && task.activity.find((activity) => activity.id === stream.id)} | ||||
|                 /> | ||||
|               ), | ||||
|   | ||||
| @@ -30,19 +30,16 @@ function plugin(options) { | ||||
|   } | ||||
|  | ||||
|   function getEmoji(match) { | ||||
|     console.log(match); | ||||
|     const got = emoji.get(match); | ||||
|     if (pad && got !== match) { | ||||
|       return `${got} `; | ||||
|     } | ||||
|  | ||||
|     console.log(got); | ||||
|     return ReactDOMServer.renderToStaticMarkup(<Emoji set="google" emoji={match} size={16} />); | ||||
|   } | ||||
|  | ||||
|   function transformer(tree) { | ||||
|     visit(tree, 'paragraph', function (node) { | ||||
|       console.log(tree); | ||||
|       // node.value = node.value.replace(RE_EMOJI, getEmoji); | ||||
|       // jnode.type = 'html'; | ||||
|       // jnode.tagName = 'div'; | ||||
| @@ -58,7 +55,6 @@ function plugin(options) { | ||||
|       if (emoticonEnable) { | ||||
|         // node.value = node.value.replace(RE_SHORT, getEmojiByShortCode); | ||||
|       } | ||||
|       console.log(node); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -334,7 +334,7 @@ const NavBar: React.FC<NavBarProps> = ({ | ||||
|             <ListUnordered width={20} height={20} /> | ||||
|           </IconContainer> | ||||
|           <IconContainer onClick={onNotificationClick}> | ||||
|             <Bell color="#c2c6dc" size={20} /> | ||||
|             <Bell width={20} height={20} /> | ||||
|             {hasUnread && <NotificationCount />} | ||||
|           </IconContainer> | ||||
|           <IconContainer disabled onClick={NOOP}> | ||||
|   | ||||
| @@ -107,6 +107,17 @@ export type CreateTaskCommentPayload = { | ||||
|   comment: TaskComment; | ||||
| }; | ||||
|  | ||||
| export type CreateTaskDueDateNotification = { | ||||
|   taskID: Scalars['UUID']; | ||||
|   period: Scalars['Int']; | ||||
|   duration: DueDateNotificationDuration; | ||||
| }; | ||||
|  | ||||
| export type CreateTaskDueDateNotificationsResult = { | ||||
|   __typename?: 'CreateTaskDueDateNotificationsResult'; | ||||
|   notifications: Array<DueDateNotification>; | ||||
| }; | ||||
|  | ||||
| export type CreateTeamMember = { | ||||
|   userID: Scalars['UUID']; | ||||
|   teamID: Scalars['UUID']; | ||||
| @@ -200,6 +211,15 @@ export type DeleteTaskCommentPayload = { | ||||
|   commentID: Scalars['UUID']; | ||||
| }; | ||||
|  | ||||
| export type DeleteTaskDueDateNotification = { | ||||
|   id: Scalars['UUID']; | ||||
| }; | ||||
|  | ||||
| export type DeleteTaskDueDateNotificationsResult = { | ||||
|   __typename?: 'DeleteTaskDueDateNotificationsResult'; | ||||
|   notifications: Array<Scalars['UUID']>; | ||||
| }; | ||||
|  | ||||
| export type DeleteTaskGroupInput = { | ||||
|   taskGroupID: Scalars['UUID']; | ||||
| }; | ||||
| @@ -265,6 +285,26 @@ export type DeleteUserAccountPayload = { | ||||
|   userAccount: UserAccount; | ||||
| }; | ||||
|  | ||||
| export type DueDate = { | ||||
|   __typename?: 'DueDate'; | ||||
|   at?: Maybe<Scalars['Time']>; | ||||
|   notifications: Array<DueDateNotification>; | ||||
| }; | ||||
|  | ||||
| export type DueDateNotification = { | ||||
|   __typename?: 'DueDateNotification'; | ||||
|   id: Scalars['ID']; | ||||
|   period: Scalars['Int']; | ||||
|   duration: DueDateNotificationDuration; | ||||
| }; | ||||
|  | ||||
| export enum DueDateNotificationDuration { | ||||
|   Minute = 'MINUTE', | ||||
|   Hour = 'HOUR', | ||||
|   Day = 'DAY', | ||||
|   Week = 'WEEK' | ||||
| } | ||||
|  | ||||
| export type DuplicateTaskGroup = { | ||||
|   projectID: Scalars['UUID']; | ||||
|   taskGroupID: Scalars['UUID']; | ||||
| @@ -393,6 +433,7 @@ export type Mutation = { | ||||
|   createTaskChecklist: TaskChecklist; | ||||
|   createTaskChecklistItem: TaskChecklistItem; | ||||
|   createTaskComment: CreateTaskCommentPayload; | ||||
|   createTaskDueDateNotifications: CreateTaskDueDateNotificationsResult; | ||||
|   createTaskGroup: TaskGroup; | ||||
|   createTeam: Team; | ||||
|   createTeamMember: CreateTeamMemberPayload; | ||||
| @@ -406,6 +447,7 @@ export type Mutation = { | ||||
|   deleteTaskChecklist: DeleteTaskChecklistPayload; | ||||
|   deleteTaskChecklistItem: DeleteTaskChecklistItemPayload; | ||||
|   deleteTaskComment: DeleteTaskCommentPayload; | ||||
|   deleteTaskDueDateNotifications: DeleteTaskDueDateNotificationsResult; | ||||
|   deleteTaskGroup: DeleteTaskGroupPayload; | ||||
|   deleteTaskGroupTasks: DeleteTaskGroupTasksPayload; | ||||
|   deleteTeam: DeleteTeamPayload; | ||||
| @@ -435,6 +477,7 @@ export type Mutation = { | ||||
|   updateTaskComment: UpdateTaskCommentPayload; | ||||
|   updateTaskDescription: Task; | ||||
|   updateTaskDueDate: Task; | ||||
|   updateTaskDueDateNotifications: UpdateTaskDueDateNotificationsResult; | ||||
|   updateTaskGroupLocation: TaskGroup; | ||||
|   updateTaskGroupName: TaskGroup; | ||||
|   updateTaskLocation: UpdateTaskLocationPayload; | ||||
| @@ -486,6 +529,11 @@ export type MutationCreateTaskCommentArgs = { | ||||
| }; | ||||
|  | ||||
|  | ||||
| export type MutationCreateTaskDueDateNotificationsArgs = { | ||||
|   input: Array<CreateTaskDueDateNotification>; | ||||
| }; | ||||
|  | ||||
|  | ||||
| export type MutationCreateTaskGroupArgs = { | ||||
|   input: NewTaskGroup; | ||||
| }; | ||||
| @@ -551,6 +599,11 @@ export type MutationDeleteTaskCommentArgs = { | ||||
| }; | ||||
|  | ||||
|  | ||||
| export type MutationDeleteTaskDueDateNotificationsArgs = { | ||||
|   input: Array<DeleteTaskDueDateNotification>; | ||||
| }; | ||||
|  | ||||
|  | ||||
| export type MutationDeleteTaskGroupArgs = { | ||||
|   input: DeleteTaskGroupInput; | ||||
| }; | ||||
| @@ -696,6 +749,11 @@ export type MutationUpdateTaskDueDateArgs = { | ||||
| }; | ||||
|  | ||||
|  | ||||
| export type MutationUpdateTaskDueDateNotificationsArgs = { | ||||
|   input: Array<UpdateTaskDueDateNotification>; | ||||
| }; | ||||
|  | ||||
|  | ||||
| export type MutationUpdateTaskGroupLocationArgs = { | ||||
|   input: NewTaskGroupLocation; | ||||
| }; | ||||
| @@ -1078,7 +1136,7 @@ export type Task = { | ||||
|   position: Scalars['Float']; | ||||
|   description?: Maybe<Scalars['String']>; | ||||
|   watched: Scalars['Boolean']; | ||||
|   dueDate?: Maybe<Scalars['Time']>; | ||||
|   dueDate: DueDate; | ||||
|   hasTime: Scalars['Boolean']; | ||||
|   complete: Scalars['Boolean']; | ||||
|   completedAt?: Maybe<Scalars['Time']>; | ||||
| @@ -1302,6 +1360,17 @@ export type UpdateTaskDueDate = { | ||||
|   dueDate?: Maybe<Scalars['Time']>; | ||||
| }; | ||||
|  | ||||
| export type UpdateTaskDueDateNotification = { | ||||
|   id: Scalars['UUID']; | ||||
|   period: Scalars['Int']; | ||||
|   duration: DueDateNotificationDuration; | ||||
| }; | ||||
|  | ||||
| export type UpdateTaskDueDateNotificationsResult = { | ||||
|   __typename?: 'UpdateTaskDueDateNotificationsResult'; | ||||
|   notifications: Array<DueDateNotification>; | ||||
| }; | ||||
|  | ||||
| export type UpdateTaskGroupName = { | ||||
|   taskGroupID: Scalars['UUID']; | ||||
|   name: Scalars['String']; | ||||
| @@ -1596,8 +1665,15 @@ export type FindTaskQuery = ( | ||||
|   { __typename?: 'Query' } | ||||
|   & { findTask: ( | ||||
|     { __typename?: 'Task' } | ||||
|     & Pick<Task, 'id' | 'shortId' | 'name' | 'watched' | 'description' | 'dueDate' | 'position' | 'complete' | 'hasTime'> | ||||
|     & { taskGroup: ( | ||||
|     & Pick<Task, 'id' | 'shortId' | 'name' | 'watched' | 'description' | 'position' | 'complete' | 'hasTime'> | ||||
|     & { dueDate: ( | ||||
|       { __typename?: 'DueDate' } | ||||
|       & Pick<DueDate, 'at'> | ||||
|       & { notifications: Array<( | ||||
|         { __typename?: 'DueDateNotification' } | ||||
|         & Pick<DueDateNotification, 'id' | 'period' | 'duration'> | ||||
|       )> } | ||||
|     ), taskGroup: ( | ||||
|       { __typename?: 'TaskGroup' } | ||||
|       & Pick<TaskGroup, 'id' | 'name'> | ||||
|     ), comments: Array<( | ||||
| @@ -1672,8 +1748,11 @@ export type FindTaskQuery = ( | ||||
|  | ||||
| export type TaskFieldsFragment = ( | ||||
|   { __typename?: 'Task' } | ||||
|   & Pick<Task, 'id' | 'shortId' | 'name' | 'description' | 'dueDate' | 'hasTime' | 'complete' | 'watched' | 'completedAt' | 'position'> | ||||
|   & { badges: ( | ||||
|   & Pick<Task, 'id' | 'shortId' | 'name' | 'description' | 'hasTime' | 'complete' | 'watched' | 'completedAt' | 'position'> | ||||
|   & { dueDate: ( | ||||
|     { __typename?: 'DueDate' } | ||||
|     & Pick<DueDate, 'at'> | ||||
|   ), badges: ( | ||||
|     { __typename?: 'TaskBadges' } | ||||
|     & { checklist?: Maybe<( | ||||
|       { __typename?: 'ChecklistBadge' } | ||||
| @@ -1789,10 +1868,13 @@ export type MyTasksQuery = ( | ||||
|     { __typename?: 'MyTasksPayload' } | ||||
|     & { tasks: Array<( | ||||
|       { __typename?: 'Task' } | ||||
|       & Pick<Task, 'id' | 'shortId' | 'name' | 'dueDate' | 'hasTime' | 'complete' | 'completedAt'> | ||||
|       & Pick<Task, 'id' | 'shortId' | 'name' | 'hasTime' | 'complete' | 'completedAt'> | ||||
|       & { taskGroup: ( | ||||
|         { __typename?: 'TaskGroup' } | ||||
|         & Pick<TaskGroup, 'id' | 'name'> | ||||
|       ), dueDate: ( | ||||
|         { __typename?: 'DueDate' } | ||||
|         & Pick<DueDate, 'at'> | ||||
|       ) } | ||||
|     )>, projects: Array<( | ||||
|       { __typename?: 'ProjectTaskMapping' } | ||||
| @@ -2624,6 +2706,9 @@ export type UpdateTaskDueDateMutationVariables = Exact<{ | ||||
|   taskID: Scalars['UUID']; | ||||
|   dueDate?: Maybe<Scalars['Time']>; | ||||
|   hasTime: Scalars['Boolean']; | ||||
|   createNotifications: Array<CreateTaskDueDateNotification> | CreateTaskDueDateNotification; | ||||
|   updateNotifications: Array<UpdateTaskDueDateNotification> | UpdateTaskDueDateNotification; | ||||
|   deleteNotifications: Array<DeleteTaskDueDateNotification> | DeleteTaskDueDateNotification; | ||||
| }>; | ||||
|  | ||||
|  | ||||
| @@ -2631,7 +2716,26 @@ export type UpdateTaskDueDateMutation = ( | ||||
|   { __typename?: 'Mutation' } | ||||
|   & { updateTaskDueDate: ( | ||||
|     { __typename?: 'Task' } | ||||
|     & Pick<Task, 'id' | 'dueDate' | 'hasTime'> | ||||
|     & Pick<Task, 'id' | 'hasTime'> | ||||
|     & { dueDate: ( | ||||
|       { __typename?: 'DueDate' } | ||||
|       & Pick<DueDate, 'at'> | ||||
|     ) } | ||||
|   ), createTaskDueDateNotifications: ( | ||||
|     { __typename?: 'CreateTaskDueDateNotificationsResult' } | ||||
|     & { notifications: Array<( | ||||
|       { __typename?: 'DueDateNotification' } | ||||
|       & Pick<DueDateNotification, 'id' | 'period' | 'duration'> | ||||
|     )> } | ||||
|   ), updateTaskDueDateNotifications: ( | ||||
|     { __typename?: 'UpdateTaskDueDateNotificationsResult' } | ||||
|     & { notifications: Array<( | ||||
|       { __typename?: 'DueDateNotification' } | ||||
|       & Pick<DueDateNotification, 'id' | 'period' | 'duration'> | ||||
|     )> } | ||||
|   ), deleteTaskDueDateNotifications: ( | ||||
|     { __typename?: 'DeleteTaskDueDateNotificationsResult' } | ||||
|     & Pick<DeleteTaskDueDateNotificationsResult, 'notifications'> | ||||
|   ) } | ||||
| ); | ||||
|  | ||||
| @@ -2866,7 +2970,9 @@ export const TaskFieldsFragmentDoc = gql` | ||||
|   shortId | ||||
|   name | ||||
|   description | ||||
|   dueDate | ||||
|   dueDate { | ||||
|     at | ||||
|   } | ||||
|   hasTime | ||||
|   complete | ||||
|   watched | ||||
| @@ -3346,7 +3452,14 @@ export const FindTaskDocument = gql` | ||||
|     name | ||||
|     watched | ||||
|     description | ||||
|     dueDate | ||||
|     dueDate { | ||||
|       at | ||||
|       notifications { | ||||
|         id | ||||
|         period | ||||
|         duration | ||||
|       } | ||||
|     } | ||||
|     position | ||||
|     complete | ||||
|     hasTime | ||||
| @@ -3640,7 +3753,9 @@ export const MyTasksDocument = gql` | ||||
|         name | ||||
|       } | ||||
|       name | ||||
|       dueDate | ||||
|       dueDate { | ||||
|         at | ||||
|       } | ||||
|       hasTime | ||||
|       complete | ||||
|       completedAt | ||||
| @@ -5423,14 +5538,33 @@ export type UpdateTaskDescriptionMutationHookResult = ReturnType<typeof useUpdat | ||||
| export type UpdateTaskDescriptionMutationResult = Apollo.MutationResult<UpdateTaskDescriptionMutation>; | ||||
| export type UpdateTaskDescriptionMutationOptions = Apollo.BaseMutationOptions<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>; | ||||
| export const UpdateTaskDueDateDocument = gql` | ||||
|     mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!) { | ||||
|     mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!, $createNotifications: [CreateTaskDueDateNotification!]!, $updateNotifications: [UpdateTaskDueDateNotification!]!, $deleteNotifications: [DeleteTaskDueDateNotification!]!) { | ||||
|   updateTaskDueDate( | ||||
|     input: {taskID: $taskID, dueDate: $dueDate, hasTime: $hasTime} | ||||
|   ) { | ||||
|     id | ||||
|     dueDate | ||||
|     dueDate { | ||||
|       at | ||||
|     } | ||||
|     hasTime | ||||
|   } | ||||
|   createTaskDueDateNotifications(input: $createNotifications) { | ||||
|     notifications { | ||||
|       id | ||||
|       period | ||||
|       duration | ||||
|     } | ||||
|   } | ||||
|   updateTaskDueDateNotifications(input: $updateNotifications) { | ||||
|     notifications { | ||||
|       id | ||||
|       period | ||||
|       duration | ||||
|     } | ||||
|   } | ||||
|   deleteTaskDueDateNotifications(input: $deleteNotifications) { | ||||
|     notifications | ||||
|   } | ||||
| } | ||||
|     `; | ||||
| export type UpdateTaskDueDateMutationFn = Apollo.MutationFunction<UpdateTaskDueDateMutation, UpdateTaskDueDateMutationVariables>; | ||||
| @@ -5451,6 +5585,9 @@ export type UpdateTaskDueDateMutationFn = Apollo.MutationFunction<UpdateTaskDueD | ||||
|  *      taskID: // value for 'taskID' | ||||
|  *      dueDate: // value for 'dueDate' | ||||
|  *      hasTime: // value for 'hasTime' | ||||
|  *      createNotifications: // value for 'createNotifications' | ||||
|  *      updateNotifications: // value for 'updateNotifications' | ||||
|  *      deleteNotifications: // value for 'deleteNotifications' | ||||
|  *   }, | ||||
|  * }); | ||||
|  */ | ||||
|   | ||||
| @@ -5,7 +5,14 @@ query findTask($taskID: String!) { | ||||
|     name | ||||
|     watched | ||||
|     description | ||||
|     dueDate | ||||
|     dueDate { | ||||
|       at | ||||
|       notifications { | ||||
|         id | ||||
|         period | ||||
|         duration | ||||
|       } | ||||
|     } | ||||
|     position | ||||
|     complete | ||||
|     hasTime | ||||
|   | ||||
| @@ -6,7 +6,9 @@ const TASK_FRAGMENT = gql` | ||||
|     shortId | ||||
|     name | ||||
|     description | ||||
|     dueDate | ||||
|     dueDate { | ||||
|       at | ||||
|     } | ||||
|     hasTime | ||||
|     complete | ||||
|     watched | ||||
|   | ||||
| @@ -12,7 +12,9 @@ query myTasks($status: MyTasksStatus!, $sort: MyTasksSort!) { | ||||
|         name | ||||
|       } | ||||
|       name | ||||
|       dueDate | ||||
|       dueDate { | ||||
|         at | ||||
|       } | ||||
|       hasTime | ||||
|       complete | ||||
|       completedAt | ||||
|   | ||||
| @@ -1,4 +1,8 @@ | ||||
| mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!) { | ||||
| mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!, | ||||
| $createNotifications: [CreateTaskDueDateNotification!]!, | ||||
| $updateNotifications: [UpdateTaskDueDateNotification!]! | ||||
| $deleteNotifications: [DeleteTaskDueDateNotification!]! | ||||
| ) { | ||||
|   updateTaskDueDate ( | ||||
|     input: { | ||||
|       taskID: $taskID | ||||
| @@ -7,7 +11,26 @@ mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!) { | ||||
|     } | ||||
|   ) { | ||||
|     id | ||||
|     dueDate | ||||
|     dueDate { | ||||
|       at | ||||
|     } | ||||
|     hasTime | ||||
|   } | ||||
|   createTaskDueDateNotifications(input: $createNotifications) { | ||||
|     notifications { | ||||
|       id | ||||
|       period | ||||
|       duration | ||||
|     } | ||||
|   } | ||||
|   updateTaskDueDateNotifications(input: $updateNotifications) { | ||||
|     notifications { | ||||
|       id | ||||
|       period | ||||
|       duration | ||||
|     } | ||||
|   } | ||||
|   deleteTaskDueDateNotifications(input: $deleteNotifications) { | ||||
|     notifications | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,6 @@ function parseJSON<T>(value: string | null): T | undefined { | ||||
|   try { | ||||
|     return value === 'undefined' ? undefined : JSON.parse(value ?? ''); | ||||
|   } catch (error) { | ||||
|     console.log('parsing error on', { value }); | ||||
|     return undefined; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,11 @@ | ||||
| import React from 'react'; | ||||
| import Icon, { IconProps } from './Icon'; | ||||
|  | ||||
| type Props = { | ||||
|   size: number | string; | ||||
|   color: string; | ||||
| }; | ||||
|  | ||||
| const Bell = ({ size, color }: Props) => { | ||||
| const Bell: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => { | ||||
|   return ( | ||||
|     <svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 448 512"> | ||||
|     <Icon width={width} height={height} className={className} viewBox="0 0 448 512"> | ||||
|       <path d="M224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64zm215.39-149.71c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71z" /> | ||||
|     </svg> | ||||
|     </Icon> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -46,7 +46,7 @@ export function sortTasks(a: Task, b: Task, taskSorting: TaskSorting) { | ||||
|     if (b.dueDate && !a.dueDate) { | ||||
|       return 1; | ||||
|     } | ||||
|     return dayjs(a.dueDate).diff(dayjs(b.dueDate)); | ||||
|     return dayjs(a.dueDate.at).diff(dayjs(b.dueDate.at)); | ||||
|   } | ||||
|   if (taskSorting.type === TaskSortingType.COMPLETE) { | ||||
|     if (a.complete && !b.complete) { | ||||
|   | ||||
							
								
								
									
										2
									
								
								frontend/src/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								frontend/src/types.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -110,7 +110,7 @@ type Task = { | ||||
|   badges?: TaskBadges; | ||||
|   position: number; | ||||
|   hasTime?: boolean; | ||||
|   dueDate?: string; | ||||
|   dueDate: { at?: string; notifications?: Array<{ id: string; period: number; duration: string }> }; | ||||
|   complete?: boolean; | ||||
|   completedAt?: string | null; | ||||
|   labels: TaskLabel[]; | ||||
|   | ||||
| @@ -187,6 +187,17 @@ type TaskComment struct { | ||||
| 	Message       string       `json:"message"` | ||||
| } | ||||
|  | ||||
| type TaskDueDateReminder struct { | ||||
| 	DueDateReminderID uuid.UUID `json:"due_date_reminder_id"` | ||||
| 	TaskID            uuid.UUID `json:"task_id"` | ||||
| 	Period            int32     `json:"period"` | ||||
| 	Duration          string    `json:"duration"` | ||||
| } | ||||
|  | ||||
| type TaskDueDateReminderDuration struct { | ||||
| 	Code string `json:"code"` | ||||
| } | ||||
|  | ||||
| type TaskGroup struct { | ||||
| 	TaskGroupID uuid.UUID `json:"task_group_id"` | ||||
| 	ProjectID   uuid.UUID `json:"project_id"` | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
| type Querier interface { | ||||
| 	CreateAuthToken(ctx context.Context, arg CreateAuthTokenParams) (AuthToken, error) | ||||
| 	CreateConfirmToken(ctx context.Context, email string) (UserAccountConfirmToken, error) | ||||
| 	CreateDueDateReminder(ctx context.Context, arg CreateDueDateReminderParams) (TaskDueDateReminder, error) | ||||
| 	CreateInvitedProjectMember(ctx context.Context, arg CreateInvitedProjectMemberParams) (ProjectMemberInvited, error) | ||||
| 	CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error) | ||||
| 	CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error) | ||||
| @@ -40,6 +41,7 @@ type Querier interface { | ||||
| 	DeleteAuthTokenByID(ctx context.Context, tokenID uuid.UUID) error | ||||
| 	DeleteAuthTokenByUserID(ctx context.Context, userID uuid.UUID) error | ||||
| 	DeleteConfirmTokenForEmail(ctx context.Context, email string) error | ||||
| 	DeleteDueDateReminder(ctx context.Context, dueDateReminderID uuid.UUID) error | ||||
| 	DeleteExpiredTokens(ctx context.Context) error | ||||
| 	DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) error | ||||
| 	DeleteInvitedUserAccount(ctx context.Context, userAccountInvitedID uuid.UUID) (UserAccountInvited, error) | ||||
| @@ -80,6 +82,7 @@ type Querier interface { | ||||
| 	GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskComment, error) | ||||
| 	GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error) | ||||
| 	GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error) | ||||
| 	GetDueDateRemindersForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskDueDateReminder, error) | ||||
| 	GetInvitedMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]GetInvitedMembersForProjectIDRow, error) | ||||
| 	GetInvitedUserAccounts(ctx context.Context) ([]UserAccountInvited, error) | ||||
| 	GetInvitedUserByEmail(ctx context.Context, email string) (UserAccountInvited, error) | ||||
| @@ -150,6 +153,7 @@ type Querier interface { | ||||
| 	SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error) | ||||
| 	SetUserActiveByEmail(ctx context.Context, email string) (UserAccount, error) | ||||
| 	SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error) | ||||
| 	UpdateDueDateReminder(ctx context.Context, arg UpdateDueDateReminderParams) (TaskDueDateReminder, error) | ||||
| 	UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error) | ||||
| 	UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error) | ||||
| 	UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error) | ||||
|   | ||||
| @@ -116,3 +116,16 @@ SELECT task.* FROM task_assigned | ||||
|  | ||||
| -- name: GetCommentCountForTask :one | ||||
| SELECT COUNT(*) FROM task_comment WHERE task_id = $1; | ||||
|  | ||||
|  | ||||
| -- name: CreateDueDateReminder :one | ||||
| INSERT INTO task_due_date_reminder (task_id, period, duration) VALUES ($1, $2, $3) RETURNING *; | ||||
|  | ||||
| -- name: UpdateDueDateReminder :one | ||||
| UPDATE task_due_date_reminder SET period = $2, duration = $3 WHERE due_date_reminder_id = $1 RETURNING *; | ||||
|  | ||||
| -- name: GetDueDateRemindersForTaskID :many | ||||
| SELECT * FROM task_due_date_reminder WHERE task_id = $1; | ||||
|  | ||||
| -- name: DeleteDueDateReminder :exec | ||||
| DELETE FROM task_due_date_reminder WHERE due_date_reminder_id = $1; | ||||
|   | ||||
| @@ -12,6 +12,28 @@ import ( | ||||
| 	"github.com/lib/pq" | ||||
| ) | ||||
|  | ||||
| const createDueDateReminder = `-- name: CreateDueDateReminder :one | ||||
| INSERT INTO task_due_date_reminder (task_id, period, duration) VALUES ($1, $2, $3) RETURNING due_date_reminder_id, task_id, period, duration | ||||
| ` | ||||
|  | ||||
| type CreateDueDateReminderParams struct { | ||||
| 	TaskID   uuid.UUID `json:"task_id"` | ||||
| 	Period   int32     `json:"period"` | ||||
| 	Duration string    `json:"duration"` | ||||
| } | ||||
|  | ||||
| func (q *Queries) CreateDueDateReminder(ctx context.Context, arg CreateDueDateReminderParams) (TaskDueDateReminder, error) { | ||||
| 	row := q.db.QueryRowContext(ctx, createDueDateReminder, arg.TaskID, arg.Period, arg.Duration) | ||||
| 	var i TaskDueDateReminder | ||||
| 	err := row.Scan( | ||||
| 		&i.DueDateReminderID, | ||||
| 		&i.TaskID, | ||||
| 		&i.Period, | ||||
| 		&i.Duration, | ||||
| 	) | ||||
| 	return i, err | ||||
| } | ||||
|  | ||||
| const createTask = `-- name: CreateTask :one | ||||
| INSERT INTO task (task_group_id, created_at, name, position) | ||||
|   VALUES($1, $2, $3, $4) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id | ||||
| @@ -144,6 +166,15 @@ func (q *Queries) CreateTaskWatcher(ctx context.Context, arg CreateTaskWatcherPa | ||||
| 	return i, err | ||||
| } | ||||
|  | ||||
| const deleteDueDateReminder = `-- name: DeleteDueDateReminder :exec | ||||
| DELETE FROM task_due_date_reminder WHERE due_date_reminder_id = $1 | ||||
| ` | ||||
|  | ||||
| func (q *Queries) DeleteDueDateReminder(ctx context.Context, dueDateReminderID uuid.UUID) error { | ||||
| 	_, err := q.db.ExecContext(ctx, deleteDueDateReminder, dueDateReminderID) | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| const deleteTaskByID = `-- name: DeleteTaskByID :exec | ||||
| DELETE FROM task WHERE task_id = $1 | ||||
| ` | ||||
| @@ -403,6 +434,38 @@ func (q *Queries) GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([ | ||||
| 	return items, nil | ||||
| } | ||||
|  | ||||
| const getDueDateRemindersForTaskID = `-- name: GetDueDateRemindersForTaskID :many | ||||
| SELECT due_date_reminder_id, task_id, period, duration FROM task_due_date_reminder WHERE task_id = $1 | ||||
| ` | ||||
|  | ||||
| func (q *Queries) GetDueDateRemindersForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskDueDateReminder, error) { | ||||
| 	rows, err := q.db.QueryContext(ctx, getDueDateRemindersForTaskID, taskID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
| 	var items []TaskDueDateReminder | ||||
| 	for rows.Next() { | ||||
| 		var i TaskDueDateReminder | ||||
| 		if err := rows.Scan( | ||||
| 			&i.DueDateReminderID, | ||||
| 			&i.TaskID, | ||||
| 			&i.Period, | ||||
| 			&i.Duration, | ||||
| 		); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		items = append(items, i) | ||||
| 	} | ||||
| 	if err := rows.Close(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := rows.Err(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return items, nil | ||||
| } | ||||
|  | ||||
| const getProjectIDForTask = `-- name: GetProjectIDForTask :one | ||||
| SELECT project_id FROM task | ||||
|   INNER JOIN task_group ON task_group.task_group_id = task.task_group_id | ||||
| @@ -651,6 +714,28 @@ func (q *Queries) SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams | ||||
| 	return i, err | ||||
| } | ||||
|  | ||||
| const updateDueDateReminder = `-- name: UpdateDueDateReminder :one | ||||
| UPDATE task_due_date_reminder SET period = $2, duration = $3 WHERE due_date_reminder_id = $1 RETURNING due_date_reminder_id, task_id, period, duration | ||||
| ` | ||||
|  | ||||
| type UpdateDueDateReminderParams struct { | ||||
| 	DueDateReminderID uuid.UUID `json:"due_date_reminder_id"` | ||||
| 	Period            int32     `json:"period"` | ||||
| 	Duration          string    `json:"duration"` | ||||
| } | ||||
|  | ||||
| func (q *Queries) UpdateDueDateReminder(ctx context.Context, arg UpdateDueDateReminderParams) (TaskDueDateReminder, error) { | ||||
| 	row := q.db.QueryRowContext(ctx, updateDueDateReminder, arg.DueDateReminderID, arg.Period, arg.Duration) | ||||
| 	var i TaskDueDateReminder | ||||
| 	err := row.Scan( | ||||
| 		&i.DueDateReminderID, | ||||
| 		&i.TaskID, | ||||
| 		&i.Period, | ||||
| 		&i.Duration, | ||||
| 	) | ||||
| 	return i, err | ||||
| } | ||||
|  | ||||
| const updateTaskComment = `-- name: UpdateTaskComment :one | ||||
| UPDATE task_comment SET message = $2, updated_at = $3 WHERE task_comment_id = $1 RETURNING task_comment_id, task_id, created_at, updated_at, created_by, pinned, message | ||||
| ` | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -60,6 +60,16 @@ type CreateTaskCommentPayload struct { | ||||
| 	Comment *db.TaskComment `json:"comment"` | ||||
| } | ||||
|  | ||||
| type CreateTaskDueDateNotification struct { | ||||
| 	TaskID   uuid.UUID                   `json:"taskID"` | ||||
| 	Period   int                         `json:"period"` | ||||
| 	Duration DueDateNotificationDuration `json:"duration"` | ||||
| } | ||||
|  | ||||
| type CreateTaskDueDateNotificationsResult struct { | ||||
| 	Notifications []DueDateNotification `json:"notifications"` | ||||
| } | ||||
|  | ||||
| type CreateTeamMember struct { | ||||
| 	UserID uuid.UUID `json:"userID"` | ||||
| 	TeamID uuid.UUID `json:"teamID"` | ||||
| @@ -144,6 +154,14 @@ type DeleteTaskCommentPayload struct { | ||||
| 	CommentID uuid.UUID `json:"commentID"` | ||||
| } | ||||
|  | ||||
| type DeleteTaskDueDateNotification struct { | ||||
| 	ID uuid.UUID `json:"id"` | ||||
| } | ||||
|  | ||||
| type DeleteTaskDueDateNotificationsResult struct { | ||||
| 	Notifications []uuid.UUID `json:"notifications"` | ||||
| } | ||||
|  | ||||
| type DeleteTaskGroupInput struct { | ||||
| 	TaskGroupID uuid.UUID `json:"taskGroupID"` | ||||
| } | ||||
| @@ -203,6 +221,17 @@ type DeleteUserAccountPayload struct { | ||||
| 	UserAccount *db.UserAccount `json:"userAccount"` | ||||
| } | ||||
|  | ||||
| type DueDate struct { | ||||
| 	At            *time.Time            `json:"at"` | ||||
| 	Notifications []DueDateNotification `json:"notifications"` | ||||
| } | ||||
|  | ||||
| type DueDateNotification struct { | ||||
| 	ID       uuid.UUID                   `json:"id"` | ||||
| 	Period   int                         `json:"period"` | ||||
| 	Duration DueDateNotificationDuration `json:"duration"` | ||||
| } | ||||
|  | ||||
| type DuplicateTaskGroup struct { | ||||
| 	ProjectID   uuid.UUID `json:"projectID"` | ||||
| 	TaskGroupID uuid.UUID `json:"taskGroupID"` | ||||
| @@ -599,6 +628,16 @@ type UpdateTaskDueDate struct { | ||||
| 	DueDate *time.Time `json:"dueDate"` | ||||
| } | ||||
|  | ||||
| type UpdateTaskDueDateNotification struct { | ||||
| 	ID       uuid.UUID                   `json:"id"` | ||||
| 	Period   int                         `json:"period"` | ||||
| 	Duration DueDateNotificationDuration `json:"duration"` | ||||
| } | ||||
|  | ||||
| type UpdateTaskDueDateNotificationsResult struct { | ||||
| 	Notifications []DueDateNotification `json:"notifications"` | ||||
| } | ||||
|  | ||||
| type UpdateTaskGroupName struct { | ||||
| 	TaskGroupID uuid.UUID `json:"taskGroupID"` | ||||
| 	Name        string    `json:"name"` | ||||
| @@ -821,6 +860,51 @@ func (e ActivityType) MarshalGQL(w io.Writer) { | ||||
| 	fmt.Fprint(w, strconv.Quote(e.String())) | ||||
| } | ||||
|  | ||||
| type DueDateNotificationDuration string | ||||
|  | ||||
| const ( | ||||
| 	DueDateNotificationDurationMinute DueDateNotificationDuration = "MINUTE" | ||||
| 	DueDateNotificationDurationHour   DueDateNotificationDuration = "HOUR" | ||||
| 	DueDateNotificationDurationDay    DueDateNotificationDuration = "DAY" | ||||
| 	DueDateNotificationDurationWeek   DueDateNotificationDuration = "WEEK" | ||||
| ) | ||||
|  | ||||
| var AllDueDateNotificationDuration = []DueDateNotificationDuration{ | ||||
| 	DueDateNotificationDurationMinute, | ||||
| 	DueDateNotificationDurationHour, | ||||
| 	DueDateNotificationDurationDay, | ||||
| 	DueDateNotificationDurationWeek, | ||||
| } | ||||
|  | ||||
| func (e DueDateNotificationDuration) IsValid() bool { | ||||
| 	switch e { | ||||
| 	case DueDateNotificationDurationMinute, DueDateNotificationDurationHour, DueDateNotificationDurationDay, DueDateNotificationDurationWeek: | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (e DueDateNotificationDuration) String() string { | ||||
| 	return string(e) | ||||
| } | ||||
|  | ||||
| func (e *DueDateNotificationDuration) UnmarshalGQL(v interface{}) error { | ||||
| 	str, ok := v.(string) | ||||
| 	if !ok { | ||||
| 		return fmt.Errorf("enums must be strings") | ||||
| 	} | ||||
|  | ||||
| 	*e = DueDateNotificationDuration(str) | ||||
| 	if !e.IsValid() { | ||||
| 		return fmt.Errorf("%s is not a valid DueDateNotificationDuration", str) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (e DueDateNotificationDuration) MarshalGQL(w io.Writer) { | ||||
| 	fmt.Fprint(w, strconv.Quote(e.String())) | ||||
| } | ||||
|  | ||||
| type MyTasksSort string | ||||
|  | ||||
| const ( | ||||
|   | ||||
| @@ -1,3 +1,9 @@ | ||||
| enum DueDateNotificationDuration { | ||||
|   MINUTE | ||||
|   HOUR | ||||
|   DAY | ||||
|   WEEK | ||||
| } | ||||
|  | ||||
| type TaskLabel { | ||||
|   id: ID! | ||||
| @@ -20,6 +26,18 @@ type TaskBadges { | ||||
|   comments: CommentsBadge | ||||
| } | ||||
|  | ||||
| type DueDateNotification { | ||||
|   id: ID! | ||||
|   period: Int! | ||||
|   duration: DueDateNotificationDuration! | ||||
| } | ||||
|  | ||||
| type DueDate { | ||||
|   at: Time | ||||
|   notifications: [DueDateNotification!]! | ||||
| } | ||||
|  | ||||
|  | ||||
| type Task { | ||||
|   id: ID! | ||||
|   shortId: String! | ||||
| @@ -29,7 +47,7 @@ type Task { | ||||
|   position: Float! | ||||
|   description: String | ||||
|   watched: Boolean! | ||||
|   dueDate: Time | ||||
|   dueDate: DueDate! | ||||
|   hasTime: Boolean! | ||||
|   complete: Boolean! | ||||
|   completedAt: Time | ||||
| @@ -371,8 +389,45 @@ extend type Mutation { | ||||
|     Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) | ||||
|   unassignTask(input: UnassignTaskInput): | ||||
|     Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) | ||||
|  | ||||
|  | ||||
|   createTaskDueDateNotifications(input: [CreateTaskDueDateNotification!]!): | ||||
|     CreateTaskDueDateNotificationsResult! | ||||
|   updateTaskDueDateNotifications(input: [UpdateTaskDueDateNotification!]!): | ||||
|     UpdateTaskDueDateNotificationsResult! | ||||
|   deleteTaskDueDateNotifications(input: [DeleteTaskDueDateNotification!]!): | ||||
|     DeleteTaskDueDateNotificationsResult! | ||||
| } | ||||
|  | ||||
| input DeleteTaskDueDateNotification { | ||||
|   id: UUID! | ||||
| } | ||||
|  | ||||
| type DeleteTaskDueDateNotificationsResult { | ||||
|   notifications: [UUID!]! | ||||
| } | ||||
|  | ||||
| input UpdateTaskDueDateNotification { | ||||
|   id: UUID! | ||||
|   period: Int! | ||||
|   duration: DueDateNotificationDuration! | ||||
| } | ||||
|  | ||||
| type UpdateTaskDueDateNotificationsResult { | ||||
|   notifications: [DueDateNotification!]! | ||||
| } | ||||
|  | ||||
| input CreateTaskDueDateNotification { | ||||
|   taskID: UUID! | ||||
|   period: Int! | ||||
|   duration: DueDateNotificationDuration! | ||||
| } | ||||
|  | ||||
| type CreateTaskDueDateNotificationsResult { | ||||
|   notifications: [DueDateNotification!]! | ||||
| } | ||||
|  | ||||
|  | ||||
| input ToggleTaskWatch { | ||||
|   taskID: UUID! | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,9 @@ | ||||
| enum DueDateNotificationDuration { | ||||
|   MINUTE | ||||
|   HOUR | ||||
|   DAY | ||||
|   WEEK | ||||
| } | ||||
|  | ||||
| type TaskLabel { | ||||
|   id: ID! | ||||
| @@ -20,6 +26,18 @@ type TaskBadges { | ||||
|   comments: CommentsBadge | ||||
| } | ||||
|  | ||||
| type DueDateNotification { | ||||
|   id: ID! | ||||
|   period: Int! | ||||
|   duration: DueDateNotificationDuration! | ||||
| } | ||||
|  | ||||
| type DueDate { | ||||
|   at: Time | ||||
|   notifications: [DueDateNotification!]! | ||||
| } | ||||
|  | ||||
|  | ||||
| type Task { | ||||
|   id: ID! | ||||
|   shortId: String! | ||||
| @@ -29,7 +47,7 @@ type Task { | ||||
|   position: Float! | ||||
|   description: String | ||||
|   watched: Boolean! | ||||
|   dueDate: Time | ||||
|   dueDate: DueDate! | ||||
|   hasTime: Boolean! | ||||
|   complete: Boolean! | ||||
|   completedAt: Time | ||||
|   | ||||
| @@ -31,8 +31,45 @@ extend type Mutation { | ||||
|     Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) | ||||
|   unassignTask(input: UnassignTaskInput): | ||||
|     Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) | ||||
|  | ||||
|  | ||||
|   createTaskDueDateNotifications(input: [CreateTaskDueDateNotification!]!): | ||||
|     CreateTaskDueDateNotificationsResult! | ||||
|   updateTaskDueDateNotifications(input: [UpdateTaskDueDateNotification!]!): | ||||
|     UpdateTaskDueDateNotificationsResult! | ||||
|   deleteTaskDueDateNotifications(input: [DeleteTaskDueDateNotification!]!): | ||||
|     DeleteTaskDueDateNotificationsResult! | ||||
| } | ||||
|  | ||||
| input DeleteTaskDueDateNotification { | ||||
|   id: UUID! | ||||
| } | ||||
|  | ||||
| type DeleteTaskDueDateNotificationsResult { | ||||
|   notifications: [UUID!]! | ||||
| } | ||||
|  | ||||
| input UpdateTaskDueDateNotification { | ||||
|   id: UUID! | ||||
|   period: Int! | ||||
|   duration: DueDateNotificationDuration! | ||||
| } | ||||
|  | ||||
| type UpdateTaskDueDateNotificationsResult { | ||||
|   notifications: [DueDateNotification!]! | ||||
| } | ||||
|  | ||||
| input CreateTaskDueDateNotification { | ||||
|   taskID: UUID! | ||||
|   period: Int! | ||||
|   duration: DueDateNotificationDuration! | ||||
| } | ||||
|  | ||||
| type CreateTaskDueDateNotificationsResult { | ||||
|   notifications: [DueDateNotification!]! | ||||
| } | ||||
|  | ||||
|  | ||||
| input ToggleTaskWatch { | ||||
|   taskID: UUID! | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import ( | ||||
| 	"database/sql" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/google/uuid" | ||||
| @@ -501,8 +501,20 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa | ||||
| 	if err != nil { | ||||
| 		return &db.Task{}, err | ||||
| 	} | ||||
| 	isSame := false | ||||
| 	if prevTask.DueDate.Valid && input.DueDate != nil { | ||||
| 		if prevTask.DueDate.Time == *input.DueDate && prevTask.HasTime == input.HasTime { | ||||
| 			isSame = true | ||||
| 		} | ||||
| 	} | ||||
| 	log.WithFields(log.Fields{ | ||||
| 		"isSame": isSame, | ||||
| 		"prev":   prevTask.HasTime, | ||||
| 		"new":    input.HasTime, | ||||
| 	}).Info("chekcing same") | ||||
| 	data := map[string]string{} | ||||
| 	var activityType = TASK_DUE_DATE_ADDED | ||||
| 	data["HasTime"] = strconv.FormatBool(input.HasTime) | ||||
| 	if input.DueDate == nil && prevTask.DueDate.Valid { | ||||
| 		activityType = TASK_DUE_DATE_REMOVED | ||||
| 		data["PrevDueDate"] = prevTask.DueDate.Time.String() | ||||
| @@ -529,13 +541,15 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa | ||||
| 		}) | ||||
| 		createdAt := time.Now().UTC() | ||||
| 		d, _ := json.Marshal(data) | ||||
| 		_, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{ | ||||
| 			TaskID:         task.TaskID, | ||||
| 			Data:           d, | ||||
| 			CausedBy:       userID, | ||||
| 			CreatedAt:      createdAt, | ||||
| 			ActivityTypeID: activityType, | ||||
| 		}) | ||||
| 		if !isSame { | ||||
| 			_, err = r.Repository.CreateTaskActivity(ctx, db.CreateTaskActivityParams{ | ||||
| 				TaskID:         task.TaskID, | ||||
| 				Data:           d, | ||||
| 				CausedBy:       userID, | ||||
| 				CreatedAt:      createdAt, | ||||
| 				ActivityTypeID: activityType, | ||||
| 			}) | ||||
| 		} | ||||
| 	} else { | ||||
| 		task, err = r.Repository.GetTaskByID(ctx, input.TaskID) | ||||
| 	} | ||||
| @@ -670,6 +684,73 @@ func (r *mutationResolver) UnassignTask(ctx context.Context, input *UnassignTask | ||||
| 	return &task, nil | ||||
| } | ||||
|  | ||||
| func (r *mutationResolver) CreateTaskDueDateNotifications(ctx context.Context, input []CreateTaskDueDateNotification) (*CreateTaskDueDateNotificationsResult, error) { | ||||
| 	reminders := []DueDateNotification{} | ||||
| 	for _, in := range input { | ||||
| 		n, err := r.Repository.CreateDueDateReminder(ctx, db.CreateDueDateReminderParams{ | ||||
| 			TaskID:   in.TaskID, | ||||
| 			Period:   int32(in.Period), | ||||
| 			Duration: in.Duration.String(), | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return &CreateTaskDueDateNotificationsResult{}, err | ||||
| 		} | ||||
| 		duration := DueDateNotificationDuration(n.Duration) | ||||
| 		if !duration.IsValid() { | ||||
| 			log.WithField("duration", n.Duration).Error("invalid duration found") | ||||
| 			return &CreateTaskDueDateNotificationsResult{}, errors.New("invalid duration") | ||||
| 		} | ||||
| 		reminders = append(reminders, DueDateNotification{ | ||||
| 			ID:       n.DueDateReminderID, | ||||
| 			Period:   int(n.Period), | ||||
| 			Duration: duration, | ||||
| 		}) | ||||
| 	} | ||||
| 	return &CreateTaskDueDateNotificationsResult{ | ||||
| 		Notifications: reminders, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (r *mutationResolver) UpdateTaskDueDateNotifications(ctx context.Context, input []UpdateTaskDueDateNotification) (*UpdateTaskDueDateNotificationsResult, error) { | ||||
| 	reminders := []DueDateNotification{} | ||||
| 	for _, in := range input { | ||||
| 		n, err := r.Repository.UpdateDueDateReminder(ctx, db.UpdateDueDateReminderParams{ | ||||
| 			DueDateReminderID: in.ID, | ||||
| 			Period:            int32(in.Period), | ||||
| 			Duration:          in.Duration.String(), | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return &UpdateTaskDueDateNotificationsResult{}, err | ||||
| 		} | ||||
| 		duration := DueDateNotificationDuration(n.Duration) | ||||
| 		if !duration.IsValid() { | ||||
| 			log.WithField("duration", n.Duration).Error("invalid duration found") | ||||
| 			return &UpdateTaskDueDateNotificationsResult{}, errors.New("invalid duration") | ||||
| 		} | ||||
| 		reminders = append(reminders, DueDateNotification{ | ||||
| 			ID:       n.DueDateReminderID, | ||||
| 			Period:   int(n.Period), | ||||
| 			Duration: duration, | ||||
| 		}) | ||||
| 	} | ||||
| 	return &UpdateTaskDueDateNotificationsResult{ | ||||
| 		Notifications: reminders, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (r *mutationResolver) DeleteTaskDueDateNotifications(ctx context.Context, input []DeleteTaskDueDateNotification) (*DeleteTaskDueDateNotificationsResult, error) { | ||||
| 	ids := []uuid.UUID{} | ||||
| 	for _, n := range input { | ||||
| 		err := r.Repository.DeleteDueDateReminder(ctx, n.ID) | ||||
| 		if err != nil { | ||||
| 			log.WithError(err).Error("error while deleting task due date notification") | ||||
| 			return &DeleteTaskDueDateNotificationsResult{}, err | ||||
| 		} | ||||
| 		ids = append(ids, n.ID) | ||||
| 	} | ||||
| 	return &DeleteTaskDueDateNotificationsResult{Notifications: ids}, nil | ||||
| } | ||||
|  | ||||
| func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task, error) { | ||||
| 	var taskID uuid.UUID | ||||
| 	var err error | ||||
| @@ -724,11 +805,34 @@ func (r *taskResolver) Watched(ctx context.Context, obj *db.Task) (bool, error) | ||||
| 	return true, nil | ||||
| } | ||||
|  | ||||
| func (r *taskResolver) DueDate(ctx context.Context, obj *db.Task) (*time.Time, error) { | ||||
| 	if obj.DueDate.Valid { | ||||
| 		return &obj.DueDate.Time, nil | ||||
| func (r *taskResolver) DueDate(ctx context.Context, obj *db.Task) (*DueDate, error) { | ||||
| 	nots, err := r.Repository.GetDueDateRemindersForTaskID(ctx, obj.TaskID) | ||||
| 	if err != nil { | ||||
| 		log.WithError(err).Error("error while fetching due date reminders") | ||||
| 		return &DueDate{}, err | ||||
| 	} | ||||
| 	return nil, nil | ||||
| 	reminders := []DueDateNotification{} | ||||
| 	for _, n := range nots { | ||||
| 		duration := DueDateNotificationDuration(n.Duration) | ||||
| 		if !duration.IsValid() { | ||||
| 			log.WithField("duration", n.Duration).Error("invalid duration found") | ||||
| 			return &DueDate{}, errors.New("invalid duration") | ||||
| 		} | ||||
| 		reminders = append(reminders, DueDateNotification{ | ||||
| 			ID:       n.DueDateReminderID, | ||||
| 			Period:   int(n.Period), | ||||
| 			Duration: duration, | ||||
| 		}) | ||||
| 	} | ||||
| 	var time *time.Time | ||||
| 	if obj.DueDate.Valid { | ||||
| 		time = &obj.DueDate.Time | ||||
| 	} | ||||
|  | ||||
| 	return &DueDate{ | ||||
| 		At:            time, | ||||
| 		Notifications: reminders, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (r *taskResolver) CompletedAt(ctx context.Context, obj *db.Task) (*time.Time, error) { | ||||
| @@ -904,7 +1008,10 @@ func (r *taskChecklistItemResolver) ID(ctx context.Context, obj *db.TaskChecklis | ||||
| } | ||||
|  | ||||
| func (r *taskChecklistItemResolver) DueDate(ctx context.Context, obj *db.TaskChecklistItem) (*time.Time, error) { | ||||
| 	panic(fmt.Errorf("not implemented")) | ||||
| 	if obj.DueDate.Valid { | ||||
| 		return &obj.DueDate.Time, nil | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| func (r *taskCommentResolver) ID(ctx context.Context, obj *db.TaskComment) (uuid.UUID, error) { | ||||
|   | ||||
							
								
								
									
										15
									
								
								migrations/0070_add-task_due_date_notification.up.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								migrations/0070_add-task_due_date_notification.up.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| CREATE TABLE task_due_date_reminder_duration ( | ||||
|   code text PRIMARY KEY | ||||
| ); | ||||
|  | ||||
| INSERT INTO task_due_date_reminder_duration VALUES ('MINUTE'); | ||||
| INSERT INTO task_due_date_reminder_duration VALUES ('HOUR'); | ||||
| INSERT INTO task_due_date_reminder_duration VALUES ('DAY'); | ||||
| INSERT INTO task_due_date_reminder_duration VALUES ('WEEK'); | ||||
|  | ||||
| CREATE TABLE task_due_date_reminder ( | ||||
|   due_date_reminder_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), | ||||
|   task_id uuid NOT NULL REFERENCES task(task_id) ON DELETE CASCADE, | ||||
|   period int NOT NULL, | ||||
|   duration text NOT NULL REFERENCES task_due_date_reminder_duration(code) ON DELETE CASCADE | ||||
| ); | ||||
		Reference in New Issue
	
	Block a user