diff --git a/ui/src/gql/timeSpan.ts b/ui/src/gql/timeSpan.ts index 2da310d..2307b79 100644 --- a/ui/src/gql/timeSpan.ts +++ b/ui/src/gql/timeSpan.ts @@ -11,6 +11,7 @@ export const Trackers = gql` value } oldStart + note } } `; @@ -27,6 +28,7 @@ export const TimeSpansInRange = gql` value } oldStart + note } cursor { startId @@ -49,6 +51,7 @@ export const TimeSpans = gql` value } oldStart + note } cursor { startId @@ -60,8 +63,8 @@ export const TimeSpans = gql` `; export const StartTimer = gql` - mutation StartTimer($start: Time!, $tags: [InputTimeSpanTag!]) { - createTimeSpan(start: $start, tags: $tags) { + mutation StartTimer($start: Time!, $tags: [InputTimeSpanTag!], $note: String!) { + createTimeSpan(start: $start, tags: $tags, note: $note) { id start end @@ -70,6 +73,7 @@ export const StartTimer = gql` value } oldStart + note } } `; @@ -85,13 +89,14 @@ export const StopTimer = gql` value } oldStart + note } } `; export const AddTimeSpan = gql` - mutation AddTimeSpan($start: Time!, $end: Time!, $tags: [InputTimeSpanTag!]) { - createTimeSpan(start: $start, end: $end, tags: $tags) { + mutation AddTimeSpan($start: Time!, $end: Time!, $tags: [InputTimeSpanTag!], $note: String!) { + createTimeSpan(start: $start, end: $end, tags: $tags, note: $note) { id start end @@ -100,13 +105,14 @@ export const AddTimeSpan = gql` value } oldStart + note } } `; export const UpdateTimeSpan = gql` - mutation UpdateTimeSpan($id: Int!, $start: Time!, $end: Time, $tags: [InputTimeSpanTag!], $oldStart: Time) { - updateTimeSpan(id: $id, start: $start, end: $end, tags: $tags, oldStart: $oldStart) { + mutation UpdateTimeSpan($id: Int!, $start: Time!, $end: Time, $tags: [InputTimeSpanTag!], $oldStart: Time, $note: String!) { + updateTimeSpan(id: $id, start: $start, end: $end, tags: $tags, oldStart: $oldStart, note: $note) { id start end @@ -115,6 +121,7 @@ export const UpdateTimeSpan = gql` value } oldStart + note } } `; diff --git a/ui/src/timespan/TimeSpan.tsx b/ui/src/timespan/TimeSpan.tsx index 92b5164..277e866 100644 --- a/ui/src/timespan/TimeSpan.tsx +++ b/ui/src/timespan/TimeSpan.tsx @@ -4,7 +4,7 @@ import {TagSelector} from '../tag/TagSelector'; import moment from 'moment'; import Paper from '@material-ui/core/Paper'; import {DateTimeSelector} from '../common/DateTimeSelector'; -import {Button, Typography} from '@material-ui/core'; +import {Button, TextField, Tooltip, Typography} from '@material-ui/core'; import {inUserTz} from './timeutils'; import {useMutation} from '@apollo/react-hooks'; import {StopTimer, StopTimerVariables} from '../gql/__generated__/StopTimer'; @@ -22,6 +22,8 @@ import {Trackers} from '../gql/__generated__/Trackers'; import {addTimeSpanToCache, removeFromTrackersCache} from '../gql/utils'; import {StartTimer, StartTimerVariables} from '../gql/__generated__/StartTimer'; import {RelativeTime, RelativeToNow} from '../common/RelativeTime'; +import ShowNotesIcon from '@material-ui/icons/KeyboardArrowDown'; +import HideNotesIcon from '@material-ui/icons/KeyboardArrowUp'; interface Range { from: moment.Moment; @@ -32,6 +34,7 @@ export interface TimeSpanProps { id: number; range: Range & {oldFrom?: moment.Moment}; initialTags: TagSelectorEntry[]; + note: string; dateSelectorOpen?: React.Dispatch>; rangeChange?: (r: Range) => void; deleted?: () => void; @@ -46,6 +49,7 @@ export const TimeSpan: React.FC = React.memo( range: {from, to, oldFrom}, id, initialTags, + note: initialNote, dateSelectorOpen = () => {}, rangeChange = () => {}, deleted = () => {}, @@ -54,6 +58,9 @@ export const TimeSpan: React.FC = React.memo( elevation = 1, addTagsToTracker, }) => { + const [showNotes, toggleShowingNotes] = React.useState(initialNote !== ''); + const note = React.useRef<{value: string; handle?: number}>({value: initialNote}); + const [selectedEntries, setSelectedEntries] = React.useState(initialTags); const [openMenu, setOpenMenu] = useStateAndDelegateWithDelayOnChange(null, (o) => dateSelectorOpen(!!o) @@ -71,6 +78,10 @@ export const TimeSpan: React.FC = React.memo( refetchQueries: [{query: gqlTimeSpan.Trackers}], }); const [updateTimeSpan] = useMutation(gqlTimeSpan.UpdateTimeSpan); + const noteAwareUpdateTimeSpan = ({variables}: {variables: Omit}) => { + clearTimeout(note.current.handle); + return updateTimeSpan({variables: {...variables, note: note.current.value}}); + }; const [removeTimeSpan] = useMutation(gqlTimeSpan.RemoveTimeSpan, { update: (cache, {data}) => { let oldData: TimeSpans | null = null; @@ -105,6 +116,26 @@ export const TimeSpan: React.FC = React.memo( } }, }); + + const updateNote = (newValue: string) => { + window.clearTimeout(note.current.handle); + const handle = window.setTimeout( + () => + updateTimeSpan({ + variables: { + oldStart: oldFrom, + id, + start: inUserTz(from).format(), + end: to && inUserTz(to).format(), + tags: toInputTags(selectedEntries), + note: newValue, + }, + }), + 200 + ); + note.current = {handle, value: newValue}; + }; + const wasMoved = !isSameDate(from, oldFrom); const showDate = to !== undefined && (!isSameDate(from, to) || wasMoved); return ( @@ -112,146 +143,172 @@ export const TimeSpan: React.FC = React.memo( elevation={elevation} style={{ display: 'flex', - alignItems: 'center', + flexDirection: 'column', padding: '10px', margin: '10px 0', opacity: wasMoved ? 0.5 : 1, }}> -
- { - setSelectedEntries(entries); - updateTimeSpan({ - variables: { - oldStart: oldFrom, - id, - start: inUserTz(from).format(), - end: to && inUserTz(to).format(), - tags: toInputTags(entries), - }, - }); - }} - /> -
- { - if (!newFrom.isValid()) { - return; - } - if (to && moment(newFrom).isAfter(to)) { - const newTo = moment(newFrom).add(15, 'minute'); - updateTimeSpan({ - variables: { - oldStart: oldFrom, - id, - start: inUserTz(newFrom).format(), - end: inUserTz(newTo).format(), - tags: toInputTags(selectedEntries), - }, - }).then(() => rangeChange({from: newFrom, to: newTo})); - } else { - updateTimeSpan({ - variables: { - id, - oldStart: oldFrom, - start: inUserTz(newFrom).format(), - end: to && inUserTz(to).format(), - tags: toInputTags(selectedEntries), - }, - }).then(() => rangeChange({from: newFrom, to})); - } - }} - showDate={showDate} - label="start" - /> - {to !== undefined ? ( +
+ + toggleShowingNotes(!showNotes)}> + {showNotes ? : } + + +
+ { + setSelectedEntries(entries); + noteAwareUpdateTimeSpan({ + variables: { + oldStart: oldFrom, + id, + start: inUserTz(from).format(), + end: to && inUserTz(to).format(), + tags: toInputTags(entries), + }, + }); + }} + /> +
{ - if (!newTo.isValid()) { + selectedDate={from} + onSelectDate={(newFrom) => { + if (!newFrom.isValid()) { return; } - if (moment(newTo).isBefore(from)) { - const newFrom = moment(newTo).subtract(15, 'minute'); - updateTimeSpan({ + if (to && moment(newFrom).isAfter(to)) { + const newTo = moment(newFrom).add(15, 'minute'); + noteAwareUpdateTimeSpan({ variables: { - id, oldStart: oldFrom, + id, start: inUserTz(newFrom).format(), end: inUserTz(newTo).format(), tags: toInputTags(selectedEntries), }, }).then(() => rangeChange({from: newFrom, to: newTo})); } else { - updateTimeSpan({ + noteAwareUpdateTimeSpan({ variables: { id, oldStart: oldFrom, - start: inUserTz(from).format(), - end: inUserTz(newTo).format(), + start: inUserTz(newFrom).format(), + end: to && inUserTz(to).format(), tags: toInputTags(selectedEntries), }, - }).then(() => rangeChange({from, to: newTo})); + }).then(() => rangeChange({from: newFrom, to})); } }} showDate={showDate} - label="end" + label="start" /> - ) : ( - - )} - <> - { - - {to ? : } - - } - ) => setOpenMenu(e.currentTarget)}> - - - setOpenMenu(null)}> - {to ? ( - { - setOpenMenu(null); - startTimer({ - variables: {start: inUserTz(moment()).format(), tags: toInputTags(selectedEntries)}, - }).then(() => continued()); - }}> - Continue - - ) : null} - {addTagsToTracker ? ( + {to !== undefined ? ( + { + if (!newTo.isValid()) { + return; + } + if (moment(newTo).isBefore(from)) { + const newFrom = moment(newTo).subtract(15, 'minute'); + noteAwareUpdateTimeSpan({ + variables: { + id, + oldStart: oldFrom, + start: inUserTz(newFrom).format(), + end: inUserTz(newTo).format(), + tags: toInputTags(selectedEntries), + }, + }).then(() => rangeChange({from: newFrom, to: newTo})); + } else { + noteAwareUpdateTimeSpan({ + variables: { + id, + oldStart: oldFrom, + start: inUserTz(from).format(), + end: inUserTz(newTo).format(), + tags: toInputTags(selectedEntries), + }, + }).then(() => rangeChange({from, to: newTo})); + } + }} + showDate={showDate} + label="end" + /> + ) : ( + + )} + <> + { + + {to ? : } + + } + ) => setOpenMenu(e.currentTarget)}> + + + setOpenMenu(null)}> + {to ? ( + { + setOpenMenu(null); + startTimer({ + variables: { + start: inUserTz(moment()).format(), + tags: toInputTags(selectedEntries), + note: note.current.value, + }, + }).then(() => continued()); + }}> + Continue + + ) : null} + {addTagsToTracker ? ( + { + setOpenMenu(null); + addTagsToTracker(selectedEntries); + }}> + Copy tags + + ) : null} { setOpenMenu(null); - addTagsToTracker(selectedEntries); + removeTimeSpan({variables: {id}}).then(() => deleted()); }}> - Copy tags + Delete - ) : null} - { - setOpenMenu(null); - removeTimeSpan({variables: {id}}).then(() => deleted()); - }}> - Delete - - - + + +
+ {showNotes ? ( +
+ updateNote(e.target.value)} + /> +
+ ) : null} ); } diff --git a/ui/src/timespan/Tracker.tsx b/ui/src/timespan/Tracker.tsx index 32327af..da53879 100644 --- a/ui/src/timespan/Tracker.tsx +++ b/ui/src/timespan/Tracker.tsx @@ -65,12 +65,12 @@ export const Tracker: React.FC = ({selectedEntries, onSelectedEntr (entry: TagSelectorEntry): InputTimeSpanTag => ({key: entry.tag.key, value: entry.value}) ); if (type === Type.Tracker) { - startTimer({variables: {start: inUserTz(moment()).format(), tags}}).then(() => { + startTimer({variables: {start: inUserTz(moment()).format(), tags, note: ''}}).then(() => { setSelectedEntries([]); enqueueSnackbar('tracker started', {variant: 'success'}); }); } else { - addTimeSpan({variables: {start: inUserTz(from).format(), end: inUserTz(to).format(), tags}}).then(() => { + addTimeSpan({variables: {start: inUserTz(from).format(), end: inUserTz(to).format(), tags, note: ''}}).then(() => { setSelectedEntries([]); enqueueSnackbar('time span added', {variant: 'success'}); }); diff --git a/ui/src/timespan/calendar/CalendarPage.tsx b/ui/src/timespan/calendar/CalendarPage.tsx index 7e8a5ad..5a4efc0 100644 --- a/ui/src/timespan/calendar/CalendarPage.tsx +++ b/ui/src/timespan/calendar/CalendarPage.tsx @@ -160,6 +160,7 @@ export const CalendarPage: React.FC = () => { end: moment(data.event.end!).format(), id: parseInt(data.event.id, 10), tags: stripTypename(data.event.extendedProps.ts.tags), + note: data.event.extendedProps.ts.note, }, }); }; @@ -171,6 +172,7 @@ export const CalendarPage: React.FC = () => { end: moment(data.event.end!).format(), id: parseInt(data.event.id, 10), tags: stripTypename(data.event.extendedProps.ts.tags), + note: data.event.extendedProps.ts.note, }, }); }; @@ -180,13 +182,14 @@ export const CalendarPage: React.FC = () => { start: moment(data.start).format(), end: moment(data.end).format(), tags: [], + note: '', }, }); }; const onClick: OptionsInput['eventClick'] = (data) => { data.jsEvent.preventDefault(); if (data.event.id === StartTimerId) { - startTimer({variables: {start: moment().format(), tags: []}}).then(() => { + startTimer({variables: {start: moment().format(), tags: [], note: ''}}).then(() => { setCurrentDate(moment()); }); return; @@ -297,6 +300,7 @@ export const CalendarPage: React.FC = () => { tagsResult.data!.tags!, selected.data!.tags!.map((tag) => ({key: tag.key, value: tag.value})) )} + note={selected.data!.note} dateSelectorOpen={setIgnore} stopped={() => { setSelected({ diff --git a/ui/src/timespan/timespanutils.ts b/ui/src/timespan/timespanutils.ts index 0c51a89..5a39903 100644 --- a/ui/src/timespan/timespanutils.ts +++ b/ui/src/timespan/timespanutils.ts @@ -19,6 +19,7 @@ export const toTimeSpanProps = (timers: Trackers_timers[], tags: Tags_tags[]): T oldFrom: timer.oldStart ? moment(timer.oldStart) : undefined, }, initialTags: tagEntries, + note: timer.note, }; }); };