From 0b0697d89cd9cf28ca744676b5ff5418d24c216d Mon Sep 17 00:00:00 2001 From: Esen Dzhailobaev Date: Sat, 4 Jan 2025 03:11:22 +0600 Subject: [PATCH] Third bunch of possible undefined thoughts (#2744) Co-authored-by: Raine Revere --- src/actions/bulletColor.ts | 4 +++- src/actions/formatLetterCase.ts | 10 +++++----- src/actions/formatWithTag.ts | 1 + src/actions/swapNote.ts | 20 ++++++++++--------- src/components/DropHover.tsx | 5 +++-- src/hooks/useDragAndDropSubThought.ts | 6 +++--- src/initialize.ts | 5 ++++- src/redux-middleware/__tests__/pullQueue.ts | 2 +- src/selectors/nextThought.ts | 8 ++++---- src/selectors/parentOfThought.ts | 2 +- src/selectors/prevContext.ts | 4 ++-- src/selectors/prevSibling.ts | 8 ++++++-- src/stores/commandStateStore.ts | 2 +- src/test-helpers/contextToThought.ts | 4 ++-- src/test-helpers/deleteThoughtAtFirstMatch.ts | 4 ++++ src/util/getContextMap.ts | 3 ++- src/util/importJSON.ts | 7 ++++--- src/util/isDivider.ts | 2 +- src/util/moveLexemeThought.ts | 4 ++-- src/util/noteValue.ts | 2 +- src/util/removeDuplicatedContext.ts | 2 +- 21 files changed, 62 insertions(+), 43 deletions(-) diff --git a/src/actions/bulletColor.ts b/src/actions/bulletColor.ts index 9058b81a19a..2b0d2067209 100644 --- a/src/actions/bulletColor.ts +++ b/src/actions/bulletColor.ts @@ -29,7 +29,9 @@ export const bulletColorActionCreator = (payload: Parameters[1]): Thunk => (dispatch, getState) => { const state = getState() - const thought = pathToThought(state, state.cursor!) + if (!state.cursor) return + const thought = pathToThought(state, state.cursor) + if (!thought) return const thoughtText = stripTags(thought.value) const fullySelected = (selection.text()?.length === 0 && thoughtText.length !== 0) || selection.text()?.length === thoughtText.length diff --git a/src/actions/formatLetterCase.ts b/src/actions/formatLetterCase.ts index 8a2a1d71b19..06ce3e343c0 100644 --- a/src/actions/formatLetterCase.ts +++ b/src/actions/formatLetterCase.ts @@ -17,17 +17,17 @@ export const formatLetterCaseActionCreator = if (!cursor) return const thought = pathToThought(state, cursor) - const originalThoughtValue = thought.value + if (!thought) return state - const updatedThoughtValue = applyLetterCase(command, originalThoughtValue) + const oldValue = thought.value + const newValue = applyLetterCase(command, oldValue) const simplePath = simplifyPath(state, cursor) - const offset = selection.offsetThought() dispatch( editThought({ - oldValue: originalThoughtValue, - newValue: updatedThoughtValue, + oldValue, + newValue, path: simplePath, force: true, }), diff --git a/src/actions/formatWithTag.ts b/src/actions/formatWithTag.ts index 337180f205c..b5e0004303e 100644 --- a/src/actions/formatWithTag.ts +++ b/src/actions/formatWithTag.ts @@ -16,6 +16,7 @@ export const formatWithTagActionCreator = const state = getState() if (!state.cursor) return const thought = pathToThought(state, state.cursor) + if (!thought) return const simplePath = thoughtToPath(state, thought.id) suppressFocusStore.update(true) diff --git a/src/actions/swapNote.ts b/src/actions/swapNote.ts index 88e8adce305..7c5039f97e9 100644 --- a/src/actions/swapNote.ts +++ b/src/actions/swapNote.ts @@ -73,15 +73,17 @@ const swapNote = (state: State) => { const newRank = getRankAfter(state, appendToPath(simplePath, noteId)) const note = pathToThought(state, oldPath) - return reducerFlow([ - moveThought({ oldPath, newPath, newRank }), - // delete =note - deleteThought({ - pathParent: cursor, - thoughtId: noteId, - }), - setCursor({ offset: note.value.length, path: newPath }), - ])(state) + return note + ? reducerFlow([ + moveThought({ oldPath, newPath, newRank }), + // delete =note + deleteThought({ + pathParent: cursor, + thoughtId: noteId, + }), + setCursor({ offset: note.value.length, path: newPath }), + ])(state) + : null }, ] : // if the cursor thought does not have a note, swap it with its parent's note (or create a note if one does not exist) diff --git a/src/components/DropHover.tsx b/src/components/DropHover.tsx index a1b97770c8a..9b265933b63 100644 --- a/src/components/DropHover.tsx +++ b/src/components/DropHover.tsx @@ -100,14 +100,15 @@ const DropHoverIfVisible = ({ // const distance = state.cursor ? Math.max(0, Math.min(MAX_DISTANCE_FROM_CURSOR, state.cursor.length - depth!)) : 0 const value = getThoughtById(state, head(simplePath))?.value - + const prevChild = prevChildId && getThoughtById(state, prevChildId) return ( + value !== undefined && (isThoughtHovering || isSubthoughtsHovering) && draggingThoughtValue && // check if it's alphabetically previous to current thought compareReasonable(draggingThoughtValue, value) <= 0 && // check if it's alphabetically next to previous thought if it exists - (!prevChildId || compareReasonable(draggingThoughtValue, getThoughtById(state, prevChildId).value) === 1) + (!prevChild || compareReasonable(draggingThoughtValue, prevChild.value) === 1) ) }) diff --git a/src/hooks/useDragAndDropSubThought.ts b/src/hooks/useDragAndDropSubThought.ts index 8f783b49ed3..372f3b2b97f 100644 --- a/src/hooks/useDragAndDropSubThought.ts +++ b/src/hooks/useDragAndDropSubThought.ts @@ -71,7 +71,7 @@ const canDrop = (props: DroppableSubthoughts, monitor: DropTargetMonitor): boole const distance = state.cursor ? state.cursor.length - thoughtsTo.length - 1 : 0 const isHidden = distance >= visibleDistanceAboveCursor(state) && !isExpandedTop() const isDescendant = isDescendantPath(thoughtsTo, thoughtsFrom) - const divider = isDivider(getThoughtById(state, head(thoughtsTo)).value) + const divider = isDivider(getThoughtById(state, head(thoughtsTo))?.value) const showContexts = thoughtsTo && isContextViewActive(state, thoughtsTo) @@ -115,10 +115,10 @@ const drop = (props: DroppableSubthoughts, monitor: DropTargetMonitor) => { const dropTop = !isExpanded && attributeEquals(state, parentIdTo, '=drop', 'top') // cannot drop on itself - if (equalPath(thoughtsFrom, props.simplePath)) return + if (!thoughtFrom || !thoughtTo || equalPath(thoughtsFrom, props.simplePath)) return // cannot move root or em context or target is divider - if (isDivider(thoughtTo.value) || (isRootOrEM && !sameContext)) { + if (isDivider(thoughtTo?.value) || (isRootOrEM && !sameContext)) { store.dispatch( error({ value: `Cannot move the ${isEM(thoughtsFrom) ? 'em' : 'home'} context to another context.` }), ) diff --git a/src/initialize.ts b/src/initialize.ts index d70ed8de5c7..e48d670a59f 100644 --- a/src/initialize.ts +++ b/src/initialize.ts @@ -205,7 +205,10 @@ const windowEm = { getLexeme: withState(getLexeme), getLexemeContexts: withState((state: State, value: string) => { const contexts = getLexeme(state, value)?.contexts || [] - return contexts.map(id => thoughtToContext(state, getThoughtById(state, id)?.parentId)) + return contexts + .map(id => getThoughtById(state, id)) + .filter(Boolean) + .map(thought => thoughtToContext(state, thought.parentId)) }), getAllChildrenByContext: withState((state: State, context: Context) => getAllChildren(state, contextToThoughtId(state, context) || null), diff --git a/src/redux-middleware/__tests__/pullQueue.ts b/src/redux-middleware/__tests__/pullQueue.ts index 26015bfcce3..9deddca9790 100644 --- a/src/redux-middleware/__tests__/pullQueue.ts +++ b/src/redux-middleware/__tests__/pullQueue.ts @@ -102,7 +102,7 @@ it('do not repopulate deleted thought', async () => { }) const parentEntryChild = contextToThought(store.getState(), ['']) - expect(parentEntryChild).toBe(null) + expect(parentEntryChild).toBe(undefined) }) it('load buffered thoughts', async () => { diff --git a/src/selectors/nextThought.ts b/src/selectors/nextThought.ts index 9e54d3555b2..b987e74c857 100644 --- a/src/selectors/nextThought.ts +++ b/src/selectors/nextThought.ts @@ -18,9 +18,9 @@ import parentOf from '../util/parentOf' const nextContext = (state: State, path: Path) => { // use rootedParentOf(path) instead of thought.parentId since we need to cross the context view const parent = getThoughtById(state, head(rootedParentOf(state, path))) - const contexts = getContextsSortedAndRanked(state, parent.value) + const contexts = parent ? getContextsSortedAndRanked(state, parent.value) : [] // find the thought in the context view - const index = contexts.findIndex(cx => getThoughtById(state, cx.id).parentId === head(path)) + const index = contexts.findIndex(cx => getThoughtById(state, cx.id)?.parentId === head(path)) // get the next context const nextContextId = contexts[index + 1]?.id const nextContext = nextContextId ? getThoughtById(state, nextContextId) : null @@ -33,11 +33,11 @@ const nextContext = (state: State, path: Path) => { /** Gets the first context in a context view. */ const firstContext = (state: State, path: Path): Path | null => { const thought = getThoughtById(state, head(path)) - const contexts = getContextsSortedAndRanked(state, thought.value) + const contexts = thought ? getContextsSortedAndRanked(state, thought.value) : [] // if context view is empty, move to the next thought const firstContext = getThoughtById(state, contexts[0]?.id) - return contexts.length > 1 + return firstContext && contexts.length > 1 ? appendToPath(path, firstContext.parentId) : nextThought(state, path, { ignoreChildren: true }) // eslint-disable-line @typescript-eslint/no-use-before-define } diff --git a/src/selectors/parentOfThought.ts b/src/selectors/parentOfThought.ts index 81037865d15..0f9dbcbea40 100644 --- a/src/selectors/parentOfThought.ts +++ b/src/selectors/parentOfThought.ts @@ -8,7 +8,7 @@ const parentOfThought = (state: State, thoughtId: ThoughtId): Thought | null => const thought = getThoughtById(state, thoughtId) if (!thought) return null const parentThought = getThoughtById(state, thought.parentId) - return parentThought + return parentThought ?? null } export default parentOfThought diff --git a/src/selectors/prevContext.ts b/src/selectors/prevContext.ts index 0ced3f2d199..2f9b6b50fcd 100644 --- a/src/selectors/prevContext.ts +++ b/src/selectors/prevContext.ts @@ -9,9 +9,9 @@ import rootedParentOf from './rootedParentOf' const prevContext = (state: State, path: Path) => { // use rootedParentOf(path) instead of thought.parentId since we need to cross the context view const parent = getThoughtById(state, head(rootedParentOf(state, path))) - const contexts = getContextsSortedAndRanked(state, parent.value) + const contexts = parent ? getContextsSortedAndRanked(state, parent.value) : [] // find the thought in the context view - const index = contexts.findIndex(cx => getThoughtById(state, cx.id).parentId === head(path)) + const index = contexts.findIndex(cx => getThoughtById(state, cx.id)?.parentId === head(path)) const context = contexts[index - 1] return context ? getThoughtById(state, context.parentId) : null } diff --git a/src/selectors/prevSibling.ts b/src/selectors/prevSibling.ts index 3efb7c0539c..526c4049ebf 100644 --- a/src/selectors/prevSibling.ts +++ b/src/selectors/prevSibling.ts @@ -27,11 +27,14 @@ export const prevSibling = ( if (!thought || (!state.showHiddenThoughts && isAttribute(thought.value))) return null const parentPath = rootedParentOf(state, path) + const parent = getThoughtById(state, head(parentPath)) const showContexts = showContextsForced ?? isContextViewActive(state, parentPath) // siblings, including the current thought const siblings = showContexts - ? getContextsSortedAndRanked(state, getThoughtById(state, head(parentPath)).value) + ? parent + ? getContextsSortedAndRanked(state, parent.value) + : [] : getChildrenSorted(state, thought.parentId) // in context view, we need to match the context's parentId, since all context's ids refer to lexeme instances @@ -45,9 +48,10 @@ export const prevSibling = ( } const prev = siblings[index - 1] + if (!prev) return null // in context view, we select then parent since prev again refers to the lexeme instance - return prev ? (showContexts ? getThoughtById(state, prev.parentId) : prev) : null + return showContexts ? (getThoughtById(state, prev.parentId) ?? null) : prev } export default prevSibling diff --git a/src/stores/commandStateStore.ts b/src/stores/commandStateStore.ts index 26ccf032781..c8f98ca8afa 100644 --- a/src/stores/commandStateStore.ts +++ b/src/stores/commandStateStore.ts @@ -36,7 +36,7 @@ export const updateCommandState = () => { const action = selection.isActive() && selection.isOnThought() ? getCommandState(selection.html() ?? '') - : getCommandState(pathToThought(state, state.cursor).value) + : getCommandState(pathToThought(state, state.cursor)?.value ?? '') commandStateStore.update(action) } diff --git a/src/test-helpers/contextToThought.ts b/src/test-helpers/contextToThought.ts index 1b2b6232861..d32398617e8 100644 --- a/src/test-helpers/contextToThought.ts +++ b/src/test-helpers/contextToThought.ts @@ -7,9 +7,9 @@ import getThoughtById from '../selectors/getThoughtById' /** * Converts a Context to a Thought. If more than one thought has the same value in the same context, traveerses the first. */ -const contextToThought = (state: State, context: Context): Thought | null => { +const contextToThought = (state: State, context: Context): Thought | undefined => { const id = contextToThoughtId(state, context) - return id ? getThoughtById(state, id) : null + return id ? getThoughtById(state, id) : undefined } export default contextToThought diff --git a/src/test-helpers/deleteThoughtAtFirstMatch.ts b/src/test-helpers/deleteThoughtAtFirstMatch.ts index ddaff26da8e..f2afa010b76 100644 --- a/src/test-helpers/deleteThoughtAtFirstMatch.ts +++ b/src/test-helpers/deleteThoughtAtFirstMatch.ts @@ -19,6 +19,8 @@ const getThoughtAndParentPath = (state: State, at: string[]): [Thought, Path] => const thought = pathToThought(state, path) + if (!thought) throw new Error(`Thought not found for path: ${path}`) + const pathParent = rootedParentOf(state, path) return [thought, pathParent] @@ -28,6 +30,7 @@ const getThoughtAndParentPath = (state: State, at: string[]): [Thought, Path] => */ const deleteThoughtAtFirstMatch = _.curryRight((state: State, at: string[]) => { const [thought, pathParent] = getThoughtAndParentPath(state, at) + return deleteThought(state, { pathParent, thoughtId: thought.id, @@ -41,6 +44,7 @@ export const deleteThoughtAtFirstMatchActionCreator = (at: Context): Thunk => (dispatch, getState) => { const [thought, pathParent] = getThoughtAndParentPath(getState(), at) + dispatch( deleteThoughtActionCreator({ pathParent, diff --git a/src/util/getContextMap.ts b/src/util/getContextMap.ts index 281a1f18d97..7cc248d02cc 100644 --- a/src/util/getContextMap.ts +++ b/src/util/getContextMap.ts @@ -16,9 +16,10 @@ const getContextMap = (state: State, lexemes: (Lexeme | undefined)[]) => ...acc, ...lexeme.contexts.reduce>((accInner, thoughtId) => { const thought = getThoughtById(state, thoughtId) + if (!thought) return accInner return { ...accInner, - [thought.parentId]: unroot(thoughtToContext(state, thought.parentId)!), + [thought.parentId]: unroot(thoughtToContext(state, thought.parentId)), } }, {}), }), diff --git a/src/util/importJSON.ts b/src/util/importJSON.ts index ca8071fe53e..11bb0f3021d 100644 --- a/src/util/importJSON.ts +++ b/src/util/importJSON.ts @@ -63,6 +63,7 @@ const insertThought = ( }, ) => { const thoughtOld = getThoughtById(state, id) + if (!thoughtOld) return null const childLastUpdated = block.children[0]?.lastUpdated const childCreated = block.children[0]?.created const lastUpdatedInherited = @@ -233,10 +234,10 @@ const importJSON = ( { lastUpdated = timestamp(), updatedBy = clientId, skipRoot = false }: ImportJSONOptions = {}, ) => { const destThought = pathToThought(state, simplePath) - const destEmpty = destThought.value === '' && !anyChild(state, head(simplePath)) + const destEmpty = destThought?.value === '' && !anyChild(state, head(simplePath)) // use getNextRank instead of getRankAfter because if dest is not empty then we need to import thoughts inside it - const rankStart = destEmpty ? destThought.rank : getNextRank(state, head(simplePath)) - const rankIncrement = getRankIncrement(state, blocks, destThought, rankStart) + const rankStart = destEmpty ? destThought?.rank : getNextRank(state, head(simplePath)) + const rankIncrement = destEmpty ? getRankIncrement(state, blocks, destThought, rankStart) : 1 const pathParent = rootedParentOf(state, simplePath) const parentId = head(pathParent) diff --git a/src/util/isDivider.ts b/src/util/isDivider.ts index 52aaca3a2e7..ff2c22cc427 100644 --- a/src/util/isDivider.ts +++ b/src/util/isDivider.ts @@ -1,4 +1,4 @@ /** Returns true if the value starts with multiple dashes and should be interpreted as a divider. */ -const isDivider = (s: string | null) => s !== null && (s.startsWith('---') || s.startsWith('—-')) +const isDivider = (s: string | null | undefined) => s?.startsWith('---') || s?.startsWith('—-') export default isDivider diff --git a/src/util/moveLexemeThought.ts b/src/util/moveLexemeThought.ts index 4f53a0998d1..b5f40678998 100644 --- a/src/util/moveLexemeThought.ts +++ b/src/util/moveLexemeThought.ts @@ -13,9 +13,9 @@ const moveLexemeThought = (state: State, lexeme: Lexeme, oldRank: number, newRan const thought = getThoughtById(state, child) return ( // remove old context - (thought.rank !== oldRank || child !== id) && + (thought?.rank !== oldRank || child !== id) && // remove new context with duplicate rank - (thought.rank !== newRank || child !== id) + (thought?.rank !== newRank || child !== id) ) }), // add new context diff --git a/src/util/noteValue.ts b/src/util/noteValue.ts index 0a4338f6fb3..3e5c9532b44 100644 --- a/src/util/noteValue.ts +++ b/src/util/noteValue.ts @@ -9,7 +9,7 @@ const noteValue = (state: State, id: ThoughtId) => { const noteId = findDescendant(state, id, '=note') if (!noteId) return null const noteThought = getThoughtById(state, noteId) - if (noteThought.pending) return null + if (noteThought?.pending) return null return firstVisibleChild(state, noteId!)?.value ?? null } diff --git a/src/util/removeDuplicatedContext.ts b/src/util/removeDuplicatedContext.ts index 4d3f2e03066..e967a762d55 100644 --- a/src/util/removeDuplicatedContext.ts +++ b/src/util/removeDuplicatedContext.ts @@ -16,7 +16,7 @@ const removeDuplicatedContext = (state: State, lexeme: Lexeme, context: Context) ...lexeme, contexts: (lexeme.contexts || []).filter(child => { const thought = getThoughtById(state, child) - return thought.parentId !== contextToThoughtId(state, context) + return thought?.parentId !== contextToThoughtId(state, context) }), } }