Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
97 changes: 97 additions & 0 deletions src/commands/__tests__/uncategorize.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,113 @@
import { importTextActionCreator as importText } from '../../actions/importText'
import { undoActionCreator as undo } from '../../actions/undo'
import { executeCommandWithMulticursor } from '../../commands'
import { HOME_TOKEN } from '../../constants'
import db from '../../data-providers/yjs/thoughtspace'
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('pushes complete lexeme updates when undoing uncategorize of a duplicate uncle', async () => {
const updateThoughtsSpy = vi.spyOn(db, 'updateThoughts').mockResolvedValue(undefined)
Comment thread
raineorshine marked this conversation as resolved.
Outdated

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

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

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

const malformedLexemeUpdates = updateThoughtsSpy.mock.calls
.flatMap(([payload]) => Object.values(payload.lexemeIndexUpdates))
.filter(lexeme => lexeme && !Array.isArray(lexeme.contexts))

expect(malformedLexemeUpdates).toEqual([])

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

View workflow job for this annotation

GitHub Actions / TDD — Unit tests

[unit] src/commands/__tests__/uncategorize.ts > uncategorize > pushes complete lexeme updates when undoing uncategorize of a duplicate uncle

AssertionError: expected [ 'AYeNqPqjDYXLF' ] to deeply equal [] - Expected + Received - [] + [ + "AYeNqPqjDYXLF", + ] ❯ src/commands/__tests__/uncategorize.ts:77:38
} finally {
updateThoughtsSpy.mockRestore()
}
})

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().alert?.value).not.toContain('not able to save the last change')
Comment thread
Ruby95515 marked this conversation as resolved.
Outdated
} 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