Repository navigation
Expand file tree
/
Copy pathdeleteThought.ts
More file actions
291 lines (255 loc) · 11.4 KB
/
Copy pathdeleteThought.ts
File metadata and controls
291 lines (255 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
import _ from 'lodash'
import Index from '../@types/IndexType'
import Lexeme from '../@types/Lexeme'
import Path from '../@types/Path'
import PushBatch from '../@types/PushBatch'
import State from '../@types/State'
import Thought from '../@types/Thought'
import ThoughtId from '../@types/ThoughtId'
import Thunk from '../@types/Thunk'
import updateThoughts from '../actions/updateThoughts'
import { HOME_PATH } from '../constants'
import { clientId } from '../data-providers/thoughtspaceSession'
import { getChildrenRanked } from '../selectors/getChildren'
import { getLexeme } from '../selectors/getLexeme'
import getMovePlacement from '../selectors/getMovePlacement'
import getSortPreference from '../selectors/getSortPreference'
import getSortedRank from '../selectors/getSortedRank'
import getThoughtById from '../selectors/getThoughtById'
import hasLexeme from '../selectors/hasLexeme'
import rootedParentOf from '../selectors/rootedParentOf'
import thoughtToPath from '../selectors/thoughtToPath'
import { registerActionMetadata } from '../util/actionMetadata.registry'
import appendToPath from '../util/appendToPath'
import equalPathHead from '../util/equalPathHead'
import hashPath from '../util/hashPath'
import hashThought from '../util/hashThought'
import head from '../util/head'
import headValue from '../util/headValue'
import isDescendant from '../util/isDescendant'
import isEmptyOrEmojiOnly from '../util/isEmptyOrEmojiOnly'
import keyValueBy from '../util/keyValueBy'
import reducerFlow from '../util/reducerFlow'
import removeContext from '../util/removeContext'
import timestamp from '../util/timestamp'
interface Payload {
pathParent: Path
thoughtId: ThoughtId
/** If both local and remote are false, only deallocate thoughts from State, but do not delete them permanently. This is used by freeThoughts. The parent is marked as pending so that the thought will be automatically restored if it becomes visible again. */
local?: boolean
remote?: boolean
}
interface ThoughtUpdates {
path: Path
thoughtIndex: Index<Thought | null>
lexemeIndex: Index<Lexeme | null>
pendingDeletes?: PushBatch['pendingDeletes']
}
/** Removes a child from a thought and the corresponding Lexeme context. If it was the last instance of the Lexeme, removes it completely from the lexemeIndex. Removes the id from the parent thought event if the thought itself does not exist (See: importFiles > missingChildren). Does not update the cursor. Use deleteThoughtWithCursor or archiveThought for higher-level functions. */
const deleteThought = (state: State, { local = true, pathParent, thoughtId, remote = true }: Payload) => {
const deletedThought = getThoughtById(state, thoughtId) as Thought | undefined
if (!deletedThought) return state
const { value } = deletedThought
// See: Payload.local
const persist = local || remote
// guard against missing lexeme
// while this ideally shouldn't happen, there are some concurrency issues that can cause it to happen, as well as freeThoughts, so we should print an error and just delete the Parent
if (!hasLexeme(state, value)) {
console.warn(`Lexeme not found for thought value: ${value}. Deleting thought anyway.`)
}
const key = deletedThought ? hashThought(value!) : null
const lexeme = deletedThought ? getLexeme(state, value!) : null
const parent = getThoughtById(state, deletedThought ? deletedThought.parentId : head(pathParent))
if (!parent) {
console.error('Parent not found!', thoughtId, deletedThought?.value)
return state
}
const lexemeIndexNew = { ...state.thoughts.lexemeIndex }
const simplePath = thoughtToPath(state, thoughtId)
const path = [...pathParent, thoughtId] as Path
// TODO: Re-enable Recently Edited
// Uncaught TypeError: Cannot perform 'IsArray' on a proxy that has been revoked at Function.isArray (#417)
// let recentlyEdited = state.recentlyEdited
// try {
// recentlyEdited = treeDelete(state, state.recentlyEdited, path)
// } catch (e) {
// console.error('deleteThought: treeDelete immer error')
// console.error(e)
// }
// The new Lexeme is typically the old Lexeme less the deleted context.
// However, during deallocation we should not remove a single context, which would create an invalid Lexeme. Instead, we keep the Lexeme intact with all contexts, only deallocating it when all of its context thoughts have been deallocated.
const lexemeWithoutContext = lexeme ? removeContext(lexeme, thoughtId) : null
const lexemeNew = persist
? lexemeWithoutContext?.contexts.length
? lexemeWithoutContext
: null
: (lexemeWithoutContext?.contexts || []).some(cxid => getThoughtById(state, cxid))
? // ! lexeme must be defined because lexemeWithoutContext exists
lexeme!
: null
// update state so that we do not have to wait for the remote
if (key) {
if (lexemeNew) {
lexemeIndexNew[key] = lexemeNew
} else {
delete lexemeIndexNew[key]
}
}
// disable context view
const contextViewsNew = { ...state.contextViews }
delete contextViewsNew[hashPath(path)]
/** Generates an update object that can be used to delete/update all descendants and delete/update thoughtIndex. */
const recursiveDeletes = (thought: Thought, accumRecursive = {} as ThoughtUpdates): ThoughtUpdates => {
// modify the state to use the lexemeIndex with lexemeNew
// this ensures that contexts are calculated correctly for descendants with duplicate values
const stateNew: State = {
...state,
thoughts: {
...state.thoughts,
lexemeIndex: lexemeIndexNew,
},
}
const children = getChildrenRanked(stateNew, thought.id)
return children.reduce(
(accum, child) => {
const hashedKey = hashThought(child.value)
const lexemeChild = getLexeme(stateNew, child.value)
// The new Lexeme is typically the old Lexeme less the deleted context.
// However, during deallocation we should not remove a single context, which would create an invalid context superscript. Instead, we keep the Lexeme intact with all contexts, only deallocating it when all of its context thoughts have been deallocated.
const lexemeChildWithoutContext = lexemeChild ? removeContext(lexemeChild, child.id) : null
const lexemeChildNew = persist
? lexemeChildWithoutContext?.contexts.length
? lexemeChildWithoutContext
: null
: lexemeChildWithoutContext?.contexts.some(cxid => getThoughtById(state, cxid))
? // !: lexemeChild must be defined because lexemeChildWithoutContext exists
lexemeChild!
: null
// update local lexemeIndex so that we do not have to wait for the remote
if (lexemeChildNew) {
lexemeIndexNew[hashedKey] = lexemeChildNew
} else {
delete lexemeIndexNew[hashedKey]
}
// if pending, append to a special pendingDeletes field so all descendants can be loaded and deleted asynchronously
if (child.pending) {
const thoughtUpdate: ThoughtUpdates = {
...accum,
// do not delete the pending thought yet since the second call to deleteThought needs a starting point
pendingDeletes: [
...(accumRecursive.pendingDeletes || []),
{ path: appendToPath(pathParent, thought.id, child.id), siblingIds: children.map(child => child.id) },
],
}
return thoughtUpdate
}
delete contextViewsNew[hashPath(accum.path)]
// RECURSION
const recursiveResults = recursiveDeletes(child, accum)
return {
...accum,
path: [...accum.path, child.id],
pendingDeletes: _.uniq([
...(accum.pendingDeletes || []),
...(accumRecursive.pendingDeletes || []),
...(recursiveResults.pendingDeletes || []),
]),
lexemeIndex: {
...accum.lexemeIndex,
...recursiveResults.lexemeIndex,
[hashedKey]: lexemeChildNew,
},
thoughtIndex: {
...accum.thoughtIndex,
...recursiveResults.thoughtIndex,
},
}
},
{
path: pathParent,
lexemeIndex: accumRecursive.lexemeIndex,
thoughtIndex: {
...accumRecursive.thoughtIndex,
[thought.id]: null,
},
} as ThoughtUpdates,
)
}
// do not delete descendants when the thought has a duplicate sibling
const descendantUpdatesResult: ThoughtUpdates = deletedThought
? recursiveDeletes(deletedThought)
: { lexemeIndex: {}, thoughtIndex: {}, path: HOME_PATH }
const lexemeIndexUpdates: ThoughtUpdates['lexemeIndex'] = key
? {
...(lexemeNew !== lexeme ? { [key]: lexemeNew } : null),
...descendantUpdatesResult.lexemeIndex,
}
: {}
// Removing a child moves the parent's lastUpdated to now, which is its sort key in a context sorted by Updated, so
// the parent is re-ranked among its siblings to keep their ranks matching the sort condition (#4098). Empty and
// emoji-only thoughts are sorted to their point of creation, so they keep the rank they have.
const parentRank =
persist && getSortPreference(state, parent.parentId).type === 'Updated' && !isEmptyOrEmojiOnly(parent.value)
? getSortedRank(state, parent.parentId, parent.value, { staleId: parent.id })
: parent.rank
const thoughtIndexUpdates = {
// Deleted thought's parent
[parent.id]: {
...parent,
...(persist
? {
childrenMap: keyValueBy(parent.childrenMap || {}, (key, id) => (id !== thoughtId ? { [key]: id } : null)),
lastUpdated: timestamp(),
rank: parentRank,
updatedBy: clientId,
}
: { pending: true }),
} as Thought,
[thoughtId]: null,
// descendants
...descendantUpdatesResult.thoughtIndex,
}
// A new rank is invisible to the persistence layer on its own: sibling order is stored structurally there and only
// changes on a move, which is minted from an explicit placement (#5126).
const movePlacements: Index<ThoughtId | null> =
parentRank !== parent.rank
? { [parent.id]: getMovePlacement(state, parent.parentId, { id: parent.id, rank: parentRank }) }
: {}
const isDeletedThoughtCursor = equalPathHead(simplePath, state.cursor)
const isCursorDescendantOfDeletedThought = !!simplePath && !!state.cursor && isDescendant(simplePath, state.cursor)
// if the deleted thought is the cursor or a descendant of the cursor, we need to calculate a new cursor.
const cursorNew =
isDeletedThoughtCursor || isCursorDescendantOfDeletedThought
? simplePath.length > 1
? rootedParentOf(state, simplePath)
: null
: state.cursor
return reducerFlow([
state => ({
...state,
contextViews: contextViewsNew,
cursor: cursorNew,
editingValue: cursorNew ? headValue(state, cursorNew) : null,
}),
updateThoughts({
thoughtIndexUpdates,
lexemeIndexUpdates,
movePlacements,
// recentlyEdited,
pendingDeletes: descendantUpdatesResult.pendingDeletes,
local,
remote,
overwritePending: !persist,
}),
])(state)
}
/** Action-creator for deleteThought. */
export const deleteThoughtActionCreator =
(payload: Parameters<typeof deleteThought>[1]): Thunk =>
dispatch =>
dispatch({ type: 'deleteThought', ...payload })
export default _.curryRight(deleteThought)
// Register this action's metadata
registerActionMetadata('deleteThought', {
undoable: true,
})