Passing error messages for notebooks

This commit is contained in:
Jackson Harper
2023-06-23 17:53:24 +08:00
parent fca3a4c2ea
commit f1e435ff0b
4 changed files with 161 additions and 156 deletions

View File

@ -45,24 +45,15 @@ type NoteSectionProps = {
targetId: string targetId: string
placeHolder: string placeHolder: string
mode: 'edit' | 'preview'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined text: string | undefined
saveText: (text: string, completed: (success: boolean) => void) => void saveText: (text: string) => void
} }
export function ArticleNotes(props: NoteSectionProps): JSX.Element { export function ArticleNotes(props: NoteSectionProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback( const saveText = useCallback(
(text, updateTime) => { (text) => {
props.saveText(text, (success) => { props.saveText(text)
if (success) {
setLastSaved(updateTime)
}
})
}, },
[props] [props]
) )
@ -73,7 +64,6 @@ export function ArticleNotes(props: NoteSectionProps): JSX.Element {
placeHolder={props.placeHolder} placeHolder={props.placeHolder}
text={props.text} text={props.text}
saveText={saveText} saveText={saveText}
lastSaved={lastSaved}
fillBackground={false} fillBackground={false}
/> />
) )
@ -97,14 +87,14 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined) const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback( const saveText = useCallback(
(text, updateTime) => { (text) => {
;(async () => { ;(async () => {
const success = await updateHighlightMutation({ const success = await updateHighlightMutation({
annotation: text, annotation: text,
highlightId: props.highlight?.id, highlightId: props.highlight?.id,
}) })
if (success) { if (success) {
setLastSaved(updateTime) // setLastSaved(updateTime)
props.highlight.annotation = text props.highlight.annotation = text
props.updateHighlight(props.highlight) props.updateHighlight(props.highlight)
} }
@ -119,7 +109,6 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
placeHolder={props.placeHolder} placeHolder={props.placeHolder}
text={props.text} text={props.text}
saveText={saveText} saveText={saveText}
lastSaved={lastSaved}
fillBackground={true} fillBackground={true}
/> />
) )
@ -133,27 +122,22 @@ type MarkdownNote = {
text: string | undefined text: string | undefined
fillBackground: boolean | undefined fillBackground: boolean | undefined
lastSaved: Date | undefined saveText: (text: string) => void
saveText: (text: string, updateTime: Date) => void
} }
export function MarkdownNote(props: MarkdownNote): JSX.Element { export function MarkdownNote(props: MarkdownNote): JSX.Element {
const editorRef = useRef<MdEditor | null>(null) const editorRef = useRef<MdEditor | null>(null)
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const isDark = isDarkTheme() const isDark = isDarkTheme()
const saveRef = useRef(props.saveText) const saveRef = useRef(props.saveText)
useEffect(() => { useEffect(() => {
saveRef.current = props.saveText saveRef.current = props.saveText
}, [props.lastSaved, lastChanged]) }, [props])
const debouncedSave = useMemo< const debouncedSave = useMemo<(text: string) => void>(() => {
(text: string, updateTime: Date) => void const func = (text: string) => {
>(() => { saveRef.current?.(text)
const func = (text: string, updateTime: Date) => {
saveRef.current?.(text, updateTime)
} }
return throttle(func, 3000) return throttle(func, 3000)
}, []) }, [])
@ -167,19 +151,16 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
event.preventDefault() event.preventDefault()
} }
const updateTime = new Date() debouncedSave(data.text)
setLastChanged(updateTime)
localStorage.setItem(`note-${props.targetId}`, JSON.stringify(data))
debouncedSave(data.text, updateTime)
}, },
[props.lastSaved, lastChanged] []
) )
useEffect(() => { useEffect(() => {
const saveMarkdownNote = () => { const saveMarkdownNote = () => {
const md = editorRef.current?.getMdValue() const md = editorRef.current?.getMdValue()
if (md) { if (md) {
props.saveText(md, new Date()) props.saveText(md)
} }
} }
document.addEventListener('saveMarkdownNote', saveMarkdownNote) document.addEventListener('saveMarkdownNote', saveMarkdownNote)
@ -238,38 +219,6 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
renderHTML={(text: string) => mdParser.render(text)} renderHTML={(text: string) => mdParser.render(text)}
onChange={handleEditorChange} onChange={handleEditorChange}
/> />
<HStack
css={{
minHeight: '15px',
width: '100%',
fontSize: '9px',
mt: '5px',
color: '$thTextSubtle',
}}
alignment="start"
distribution="start"
>
{errorSaving && (
<SpanBox
css={{
width: '100%',
fontSize: '9px',
mt: '5px',
}}
>
{errorSaving}
</SpanBox>
)}
{props.lastSaved !== undefined ? (
<>
{lastChanged === props.lastSaved
? 'Saved'
: `Last saved ${formattedShortTime(
props.lastSaved.toISOString()
)}`}
</>
) : null}
</HStack>
</VStack> </VStack>
) )
} }

View File

@ -18,13 +18,6 @@ import throttle from 'lodash/throttle'
import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation' import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation'
import { Highlight } from '../../lib/networking/fragments/highlightFragment' import { Highlight } from '../../lib/networking/fragments/highlightFragment'
import { Button } from '../elements/Button' import { Button } from '../elements/Button'
import {
ModalContent,
ModalOverlay,
ModalRoot,
} from '../elements/ModalPrimitives'
import { CloseButton } from '../elements/CloseButton'
import { StyledText } from '../elements/StyledText'
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import { RcEditorStyles } from './RcEditorStyles' import { RcEditorStyles } from './RcEditorStyles'
import { isDarkTheme } from '../../lib/themeUpdater' import { isDarkTheme } from '../../lib/themeUpdater'
@ -36,46 +29,6 @@ MdEditor.use(Plugins.TabInsert, {
tabMapValue: 1, // note that 1 means a '\t' instead of ' '. tabMapValue: 1, // note that 1 means a '\t' instead of ' '.
}) })
type NoteSectionProps = {
targetId: string
placeHolder: string
mode: 'edit' | 'preview'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined
saveText: (text: string, completed: (success: boolean) => void) => void
}
export function HighlightNoteBox(props: NoteSectionProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback(
(text, updateTime) => {
props.saveText(text, (success) => {
if (success) {
setLastSaved(updateTime)
}
})
},
[props]
)
return (
<MarkdownNote
targetId={props.targetId}
placeHolder={props.placeHolder}
mode={props.mode}
setEditMode={props.setEditMode}
text={props.text}
saveText={saveText}
lastSaved={lastSaved}
fillBackground={false}
/>
)
}
type HighlightViewNoteProps = { type HighlightViewNoteProps = {
targetId: string targetId: string
@ -92,9 +45,10 @@ type HighlightViewNoteProps = {
export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element { export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined) const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const saveText = useCallback( const saveText = useCallback(
(text, updateTime) => { (text, updateTime, interactive) => {
;(async () => { ;(async () => {
const success = await updateHighlightMutation({ const success = await updateHighlightMutation({
annotation: text, annotation: text,
@ -104,13 +58,13 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
setLastSaved(updateTime) setLastSaved(updateTime)
props.highlight.annotation = text props.highlight.annotation = text
props.updateHighlight(props.highlight) props.updateHighlight(props.highlight)
showSuccessToast('Note saved.', { if (interactive) {
position: 'bottom-right', showSuccessToast('Note saved', {
}) position: 'bottom-right',
})
}
} else { } else {
showErrorToast('Error saving note.', { setErrorSaving('Error saving note.')
position: 'bottom-right',
})
} }
})() })()
}, },
@ -126,6 +80,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
text={props.text} text={props.text}
saveText={saveText} saveText={saveText}
lastSaved={lastSaved} lastSaved={lastSaved}
errorSaving={errorSaving}
fillBackground={true} fillBackground={true}
/> />
) )
@ -143,14 +98,47 @@ type MarkdownNote = {
fillBackground: boolean | undefined fillBackground: boolean | undefined
lastSaved: Date | undefined lastSaved: Date | undefined
saveText: (text: string, updateTime: Date) => void errorSaving: string | undefined
saveText: (text: string, updateTime: Date, interactive: boolean) => void
} }
export function MarkdownNote(props: MarkdownNote): JSX.Element { export function MarkdownNote(props: MarkdownNote): JSX.Element {
const editorRef = useRef<MdEditor | null>(null) const editorRef = useRef<MdEditor | null>(null)
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const isDark = isDarkTheme() const isDark = isDarkTheme()
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
const saveRef = useRef(props.saveText)
useEffect(() => {
saveRef.current = props.saveText
}, [props])
const debouncedSave = useMemo<
(text: string, updateTime: Date) => void
>(() => {
const func = (text: string, updateTime: Date) => {
saveRef.current?.(text, updateTime, false)
}
return throttle(func, 3000)
}, [])
const handleEditorChange = useCallback(
(
data: { text: string; html: string },
event?: ChangeEvent<HTMLTextAreaElement> | undefined
) => {
if (event) {
event.preventDefault()
}
const updateTime = new Date()
setLastChanged(updateTime)
debouncedSave(data.text, updateTime)
},
[]
)
return ( return (
<> <>
@ -201,6 +189,7 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
height: '160px', height: '160px',
}} }}
renderHTML={(text: string) => mdParser.render(text)} renderHTML={(text: string) => mdParser.render(text)}
onChange={handleEditorChange}
/> />
<HStack <HStack
css={{ css={{
@ -213,7 +202,7 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
alignment="start" alignment="start"
distribution="start" distribution="start"
> >
{errorSaving && ( {props.errorSaving && (
<SpanBox <SpanBox
css={{ css={{
width: '100%', width: '100%',
@ -222,9 +211,18 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
color: 'red', color: 'red',
}} }}
> >
{errorSaving} {props.errorSaving}
</SpanBox> </SpanBox>
)} )}
{props.lastSaved !== undefined ? (
<>
{lastChanged === props.lastSaved
? 'Saved'
: `Last saved ${formattedShortTime(
props.lastSaved.toISOString()
)}`}
</>
) : null}
<SpanBox <SpanBox
css={{ css={{
fontSize: '9px', fontSize: '9px',
@ -238,7 +236,9 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
onClick={(event) => { onClick={(event) => {
const value = editorRef.current?.getMdValue() const value = editorRef.current?.getMdValue()
if (value) { if (value) {
props.saveText(value, new Date()) const updateTime = new Date()
setLastChanged(updateTime)
props.saveText(value, updateTime, true)
props.setEditMode('preview') props.setEditMode('preview')
} else { } else {
showErrorToast('Error saving note.', { showErrorToast('Error saving note.', {

View File

@ -30,5 +30,11 @@ export const RcEditorStyles = (isDark: boolean, shadow: boolean) => {
border: '1px solid $thBorderSubtle', border: '1px solid $thBorderSubtle',
backgroundColor: isDark ? '#2A2A2A' : 'white', backgroundColor: isDark ? '#2A2A2A' : 'white',
}, },
'.rc-md-editor:focus-within': {
outline: '2px solid $omnivoreCtaYellow',
borderRadius: '5px',
border: 'unset',
boxShadow: 'unset',
},
} }
} }

View File

@ -36,6 +36,8 @@ import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter'
import { Button } from '../../elements/Button' import { Button } from '../../elements/Button'
import { ArticleNotes } from '../../patterns/ArticleNotes' import { ArticleNotes } from '../../patterns/ArticleNotes'
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery' import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
import { formattedShortTime } from '../../../lib/dateFormatting'
import { isDarkTheme } from '../../../lib/themeUpdater'
type NotebookContentProps = { type NotebookContentProps = {
viewer: UserBasicData viewer: UserBasicData
@ -64,6 +66,8 @@ type NoteState = {
} }
export function NotebookContent(props: NotebookContentProps): JSX.Element { export function NotebookContent(props: NotebookContentProps): JSX.Element {
const isDark = isDarkTheme()
const { articleData, mutate } = useGetArticleQuery({ const { articleData, mutate } = useGetArticleQuery({
slug: props.item.slug, slug: props.item.slug,
username: props.viewer.profile.username, username: props.viewer.profile.username,
@ -74,9 +78,6 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>( const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
undefined undefined
) )
const [notesEditMode, setNotesEditMode] = useState<'edit' | 'preview'>(
'preview'
)
const noteState = useRef<NoteState>({ const noteState = useRef<NoteState>({
isCreating: false, isCreating: false,
note: undefined, note: undefined,
@ -87,14 +88,22 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
return uuidv4() return uuidv4()
}, []) }, [])
const updateNote = useCallback((note: Highlight, text: string) => { const updateNote = useCallback(
;(async () => { (note: Highlight, text: string, startTime: Date) => {
const result = await updateHighlightMutation({ ;(async () => {
highlightId: note.id, const result = await updateHighlightMutation({
annotation: text, highlightId: note.id,
}) annotation: text,
})() })
}, []) if (result) {
setLastSaved(startTime)
} else {
setErrorSaving('Error saving')
}
})()
},
[]
)
const createNote = useCallback((text: string) => { const createNote = useCallback((text: string) => {
console.log('creating note: ', newNoteId, noteState.current.isCreating) console.log('creating note: ', newNoteId, noteState.current.isCreating)
@ -112,10 +121,13 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
if (success) { if (success) {
noteState.current.note = success noteState.current.note = success
noteState.current.isCreating = false noteState.current.isCreating = false
} else {
setErrorSaving('Error creating note')
} }
} catch (error) { } catch (error) {
console.error('error creating note: ', error) console.error('error creating note: ', error)
noteState.current.isCreating = false noteState.current.isCreating = false
setErrorSaving('Error creating note')
} }
})() })()
}, []) }, [])
@ -167,10 +179,12 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
}, [highlights]) }, [highlights])
const handleSaveNoteText = useCallback( const handleSaveNoteText = useCallback(
(text, cb: (success: boolean) => void) => { (text) => {
console.log('handleSaveNoteText', noteState.current) const changeTime = new Date()
setLastChanged(changeTime)
if (noteState.current.note) { if (noteState.current.note) {
updateNote(noteState.current.note, text) updateNote(noteState.current.note, text, changeTime)
return return
} }
if (noteState.current.isCreating) { if (noteState.current.isCreating) {
@ -195,6 +209,9 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
const [articleNotesCollapsed, setArticleNotesCollapsed] = useState(false) const [articleNotesCollapsed, setArticleNotesCollapsed] = useState(false)
const [highlightsCollapsed, setHighlightsCollapsed] = useState(false) const [highlightsCollapsed, setHighlightsCollapsed] = useState(false)
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
return ( return (
<VStack <VStack
@ -203,6 +220,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
height: '100%', height: '100%',
width: '100%', width: '100%',
p: '20px', p: '20px',
bg: '$readerMargin',
'@mdDown': { p: '15px' }, '@mdDown': { p: '15px' },
}} }}
> >
@ -212,20 +230,50 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
setCollapsed={setArticleNotesCollapsed} setCollapsed={setArticleNotesCollapsed}
/> />
{!articleNotesCollapsed && ( {!articleNotesCollapsed && (
<HStack <>
alignment="start" <HStack
distribution="start" alignment="start"
css={{ width: '100%', mt: '10px', gap: '10px' }} distribution="start"
> css={{ width: '100%', mt: '10px', gap: '10px' }}
<ArticleNotes >
mode={notesEditMode} <ArticleNotes
targetId={props.item.id} targetId={props.item.id}
setEditMode={setNotesEditMode} text={noteState.current.note?.annotation}
text={noteState.current.note?.annotation} placeHolder="Add notes to this document..."
placeHolder="Add notes to this document..." saveText={handleSaveNoteText}
saveText={handleSaveNoteText} />
/> </HStack>
</HStack> <HStack
css={{
minHeight: '15px',
width: '100%',
fontSize: '9px',
mt: '5px',
color: '$thTextSubtle',
}}
alignment="start"
distribution="start"
>
{errorSaving && (
<SpanBox
css={{
width: '100%',
fontSize: '9px',
mt: '5px',
}}
>
{errorSaving}
</SpanBox>
)}
{lastSaved !== undefined ? (
<>
{lastChanged === lastSaved
? 'Saved'
: `Last saved ${formattedShortTime(lastSaved.toISOString())}`}
</>
) : null}
</HStack>
</>
)} )}
<SpanBox css={{ mt: '10px', mb: '25px' }} /> <SpanBox css={{ mt: '10px', mb: '25px' }} />
@ -250,16 +298,14 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
setShowConfirmDeleteHighlightId setShowConfirmDeleteHighlightId
} }
updateHighlight={() => { updateHighlight={() => {
// dispatchAnnotations({ mutate()
// type: 'UPDATE_HIGHLIGHT',
// updateHighlight: highlight,
// })
}} }}
/> />
))} ))}
{sortedHighlights.length === 0 && ( {sortedHighlights.length === 0 && (
<Box <Box
css={{ css={{
p: '10px',
mt: '15px', mt: '15px',
width: '100%', width: '100%',
fontSize: '9px', fontSize: '9px',
@ -267,6 +313,10 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
mb: '100px', mb: '100px',
bg: isDark ? '#3D3D3D' : '$thBackground',
borderRadius: '6px',
boxShadow: '0px 4px 4px rgba(33, 33, 33, 0.1)',
}} }}
> >
You have not added any highlights to this document. You have not added any highlights to this document.