Repository navigation
Expand file tree
/
Copy pathcreateThought.ts
More file actions
138 lines (122 loc) · 5.21 KB
/
Copy pathcreateThought.ts
File metadata and controls
138 lines (122 loc) · 5.21 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
import _ from 'lodash'
import Index from '../@types/IndexType'
import Lexeme from '../@types/Lexeme'
import Path from '../@types/Path'
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 { clientId } from '../data-providers/thoughtspaceSession'
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 { registerActionMetadata } from '../util/actionMetadata.registry'
import { childrenMapKey } from '../util/createChildrenMap'
import createId from '../util/createId'
import hashThought from '../util/hashThought'
import head from '../util/head'
import isEmptyOrEmojiOnly from '../util/isEmptyOrEmojiOnly'
import keyValueBy from '../util/keyValueBy'
import timestamp from '../util/timestamp'
interface Payload {
id?: ThoughtId
/** Callback for when the updates have been synced with IDB. */
idbSynced?: () => void
path: Path
rank: number
splitSource?: ThoughtId
value: string
}
/**
* Creates a new thought with a known context and rank. Does not update the cursor. Use the newThought reducer for a higher level function.
*/
const createThought = (state: State, { path, value, rank, id, idbSynced, splitSource }: Payload) => {
id = id || createId()
const lexemeOld = getLexeme(state, value)
// create Lexeme if it does not exist
const lexemeNew: Lexeme = {
...(lexemeOld || {
created: timestamp(),
lastUpdated: timestamp(),
updatedBy: clientId,
}),
contexts: [...(lexemeOld?.contexts || []), ...(path.length > 0 ? [id] : [])],
}
const parentId = head(path)
const parent = getThoughtById(state, parentId)
if (!parent) {
console.error({ path, value, rank, id, idbSynced, splitSource })
throw new Error(`createThought: Parent thought with id ${parentId} not found`)
}
const thoughtIndexUpdates: Index<Thought> = {}
const newValue = value
// TODO: Why is a duplicate id encountered sometimes?
// const duplicateId = getAllChildren(state, parentId).find(childId => childId === id)
// if (duplicateId) {
// throw new Error(`Parent ${parent.value} (${parentId}) already contains thought ${duplicateId}`)
// }
const thoughtNew: Thought = {
// A new thought has no children yet. A caller that needs children creates them with further createThought calls
// once this thought exists, so the parent's childrenMap never references a thought that is not in the index.
childrenMap: {},
created: timestamp(),
id,
lastUpdated: timestamp(),
parentId: parentId,
rank,
updatedBy: clientId,
value: newValue,
...(splitSource ? { splitSource } : null),
}
// Adding 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 =
getSortPreference(state, parent.parentId).type === 'Updated' && !isEmptyOrEmojiOnly(parent.value)
? getSortedRank(state, parent.parentId, parent.value, { staleId: parentId })
: parent.rank
thoughtIndexUpdates[id] = thoughtNew
thoughtIndexUpdates[parentId] = {
...parent,
id: parentId,
childrenMap: {
// Use this opportunity to delete any children that are missing.
// This was done for the missing children that are created by multiple refreshes during a large import.
// If any abberant behavior is observed, try reverting to the previous implementation in importFiles.
...keyValueBy(parent.childrenMap, (key, childId) => {
const child = getThoughtById(state, childId)
if (!child) {
console.warn(`Sibling ${childId} with missing thought found while creating new thought ${value} (${id})`)
}
return child ? { [key]: childId } : null
}),
[childrenMapKey(parent.childrenMap, thoughtNew)]: id,
},
lastUpdated: timestamp(),
rank: parentRank,
updatedBy: clientId,
}
const lexemeIndexUpdates = {
[hashThought(value)]: lexemeNew,
}
// 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
? { [parentId]: getMovePlacement(state, parent.parentId, { id: parentId, rank: parentRank }) }
: {}
return updateThoughts(state, { lexemeIndexUpdates, thoughtIndexUpdates, idbSynced, movePlacements })
}
/** Action-creator for createThought. */
export const createThoughtActionCreator =
(payload: Parameters<typeof createThought>[1]): Thunk =>
dispatch =>
dispatch({ type: 'createThought', ...payload })
export default _.curryRight(createThought)
// Register this action's metadata
registerActionMetadata('createThought', {
undoable: true,
})