Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/actions/__tests__/uncategorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,31 @@ describe('normal view', () => {
expectPathToEqual(stateNew, stateNew.cursor, ['a', 'c'])
})

it('does not merge a child into the category being uncategorized', () => {
const steps = [
importText({
text: `
- a
- b
- b
- c
`,
}),
setCursor(['a', 'b']),
uncategorize({}),
]

const stateNew = reducerFlow(steps)(initialState())
const exported = exportContext(stateNew, [HOME_TOKEN], 'text/plain')

expect(exported).toBe(`- ${HOME_TOKEN}
- a
- b
- c`)

expectPathToEqual(stateNew, stateNew.cursor, ['a', 'b'])
})

it('after uncategorize context set cursor to the first visible children.', () => {
const steps = [
newThought('a'),
Expand Down
12 changes: 2 additions & 10 deletions src/actions/uncategorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@ import rootedParentOf from '../selectors/rootedParentOf'
import simplifyPath from '../selectors/simplifyPath'
import { registerActionMetadata } from '../util/actionMetadata.registry'
import appendToPath from '../util/appendToPath'
import createId from '../util/createId'
import head from '../util/head'
import isAttribute from '../util/isAttribute'
import parentOf from '../util/parentOf'
import reducerFlow from '../util/reducerFlow'
import deleteThought from './deleteThought'
import editThought from './editThought'
import sort from './sort'

interface Options {
Expand Down Expand Up @@ -105,14 +103,6 @@ const uncategorize = (state: State, { at }: Options): State => {
}

return reducerFlow([
// first edit the unacategorized thought to a unique value
// otherwise, it could get merged when children are outdented in the next step
editThought({
oldValue: thought.value,
newValue: createId(), // unique value
path: simplePath,
}),

// Sort parent context if sort preference exists and parent does not have a sort preference
// Sort preference must be moved up before sort to prevent conversion to manual sort.
contextHasSortPreference && !parentHasSortPreference
Expand All @@ -135,6 +125,8 @@ const uncategorize = (state: State, { at }: Options): State => {
oldPath: appendToPath(simplePath, child.id),
newPath: appendToPath(parentOf(simplePath), child.id),
newRank: getNewRank(state, child),
// If a child has the same value as the category being deleted, do not merge it into the category.
skipMerge: child.value === thought.value,
})
}),

Expand Down
64 changes: 64 additions & 0 deletions src/commands/__tests__/uncategorize.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,80 @@
import { importTextActionCreator as importText } from '../../actions/importText'
import { undoActionCreator as undo } from '../../actions/undo'
import { executeCommandWithMulticursor } from '../../commands'
import { HOME_TOKEN } from '../../constants'
import { initialize } from '../../initialize'
import exportContext from '../../selectors/exportContext'
import store from '../../stores/app'
import { addMulticursorAtFirstMatchActionCreator as addMulticursor } from '../../test-helpers/addMulticursorAtFirstMatch'
import expectPathToEqual from '../../test-helpers/expectPathToEqual'
import initStore from '../../test-helpers/initStore'
import { setCursorFirstMatchActionCreator as setCursor } from '../../test-helpers/setCursorFirstMatch'
import uncategorizeCommand from '../uncategorize'

beforeEach(initStore)

describe('uncategorize', () => {
it('undoes uncategorize of a duplicate uncle', () => {
store.dispatch([
importText({
text: `
- a
- b
- a
- c
`,
}),
setCursor(['b', 'a']),
])

executeCommandWithMulticursor(uncategorizeCommand, { store })

let state = store.getState()
expect(exportContext(state, [HOME_TOKEN], 'text/plain')).toEqual(`- ${HOME_TOKEN}
- a
- b
- c`)
expectPathToEqual(state, state.cursor, ['b', 'c'])

expect(() => store.dispatch(undo())).not.toThrow()

state = store.getState()
expect(exportContext(state, [HOME_TOKEN], 'text/plain')).toEqual(`- ${HOME_TOKEN}
- a
- b
- a
- c`)
expectPathToEqual(state, state.cursor, ['b', 'a'])
})

it('persists undoing uncategorize of a duplicate uncle without a save error', async () => {
const { cleanup } = await initialize()

try {
store.dispatch([
importText({
text: `
- a
- b
- a
- c
`,
}),
setCursor(['b', 'a']),
])

executeCommandWithMulticursor(uncategorizeCommand, { store })
store.dispatch(undo())

await vi.runOnlyPendingTimersAsync()
await Promise.resolve()

expect(store.getState().error).toBeNull()

Check failure on line 72 in src/commands/__tests__/uncategorize.ts

View workflow job for this annotation

GitHub Actions / TDD — Unit tests

[unit] src/commands/__tests__/uncategorize.ts > uncategorize > persists undoing uncategorize of a duplicate uncle without a save error

AssertionError: expected 'Error saving lexeme: Cannot read prop…' to be null - Expected: null + Received: "Error saving lexeme: Cannot read properties of undefined (reading 'length')" ❯ src/commands/__tests__/uncategorize.ts:72:38
} finally {
cleanup()
}
}, 10000)

describe('multicursor', () => {
it('collapses multiple thoughts', async () => {
store.dispatch([
Expand Down
7 changes: 4 additions & 3 deletions src/redux-enhancers/undoRedoEnhancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ const restorePushQueueFromPatches = (state: State, oldState: State, patch: Patch
const lexemeIndexChanges = patch.filter(p => p?.path.startsWith('/thoughts/lexemeIndex/'))
const thoughtIndexChanges = patch.filter(p => p?.path.startsWith('/thoughts/thoughtIndex/'))

const lexemeIndexUpdates = lexemeIndexChanges.reduce<Index<Lexeme | null>>((acc, op) => {
const lexemeKey = op.path.slice('/thoughts/lexemeIndex/'.length).split('/')[0]
const lexemeIndexUpdates = lexemeIndexChanges.reduce<Index<Lexeme | null>>((acc, { path }) => {
const lexemeKey = path.slice('/thoughts/lexemeIndex/'.length).split('/')[0]
return {
...acc,
[lexemeKey]: op.value || null,
// Patch paths may target nested lexeme properties such as contexts. Persist the full lexeme.
[lexemeKey]: state.thoughts.lexemeIndex[lexemeKey] || null,
}
}, {})
const thoughtIndexUpdates = thoughtIndexChanges.reduce((acc, { path }) => {
Expand Down
Loading