-
Notifications
You must be signed in to change notification settings - Fork 395
/
DeskTool.js
331 lines (279 loc) · 9.45 KB
/
DeskTool.js
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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
import React from 'react'
import PropTypes from 'prop-types'
import {isEqual} from 'lodash'
import {throwError, interval, defer} from 'rxjs'
import {map, switchMap, distinctUntilChanged, debounce} from 'rxjs/operators'
import shallowEquals from 'shallow-equals'
import {withRouterHOC} from 'part:@sanity/base/router'
import {resolvePanes} from './utils/resolvePanes'
import styles from './styles/DeskTool.css'
import DeskToolPanes from './DeskToolPanes'
import StructureError from './components/StructureError'
import serializeStructure from './utils/serializeStructure'
import defaultStructure from './defaultStructure'
import {LOADING_PANE} from './index'
const EMPTY_PANE_KEYS = []
const hasLoading = panes => panes.some(item => item === LOADING_PANE)
const isStructure = structure => {
return (
structure &&
(typeof structure === 'function' ||
typeof structure.serialize !== 'function' ||
typeof structure.then !== 'function' ||
typeof structure.subscribe !== 'function' ||
typeof structure.type !== 'string')
)
}
let prevStructureError = null
if (__DEV__) {
if (module.hot && module.hot.data) {
prevStructureError = module.hot.data.prevError
}
}
// We are lazy-requiring/resolving the structure inside of a function in order to catch errors
// on the root-level of the module. Any loading errors will be caught and emitted as errors
// eslint-disable-next-line complexity
const loadStructure = () => {
let structure
try {
const mod = require('part:@sanity/desk-tool/structure?') || defaultStructure
structure = mod && mod.__esModule ? mod.default : mod
// On invalid modules, when HMR kicks in, we sometimes get an empty object back when the
// source has changed without fixing the problem. In this case, keep showing the error
if (
__DEV__ &&
prevStructureError &&
structure &&
structure.constructor.name === 'Object' &&
Object.keys(structure).length === 0
) {
return throwError(prevStructureError)
}
prevStructureError = null
} catch (err) {
prevStructureError = err
return throwError(err)
}
if (!isStructure(structure)) {
return throwError(
new Error(
`Structure needs to export a function, an observable, a promise or a stucture builder, got ${typeof structure}`
)
)
}
// Defer to catch immediately thrown errors on serialization
return defer(() => serializeStructure(structure))
}
const maybeSerialize = structure =>
structure && typeof structure.serialize === 'function'
? structure.serialize({path: []})
: structure
export default withRouterHOC(
// eslint-disable-next-line react/prefer-stateless-function
class DeskTool extends React.Component {
static propTypes = {
router: PropTypes.shape({
navigate: PropTypes.func.isRequired,
state: PropTypes.shape({
panes: PropTypes.arrayOf(
PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
params: PropTypes.object
})
)
),
legacyEditDocumentId: PropTypes.string,
type: PropTypes.string,
action: PropTypes.string
})
}).isRequired,
onPaneChange: PropTypes.func.isRequired
}
state = {isResolving: true, panes: null}
constructor(props) {
super(props)
props.onPaneChange([])
}
setResolvedPanes = panes => {
const router = this.props.router
const paneSegments = router.state.panes || []
this.setState({panes, isResolving: false})
if (panes.length < paneSegments.length) {
router.navigate(
{...router.state, panes: paneSegments.slice(0, panes.length)},
{replace: true}
)
}
}
setResolveError = error => {
prevStructureError = error
// Log error for proper stacktraces
console.error(error) // eslint-disable-line no-console
this.setState({error, isResolving: false})
}
derivePanes(props, fromIndex = [0, 0]) {
if (this.paneDeriver) {
this.paneDeriver.unsubscribe()
}
this.setState({isResolving: true})
this.paneDeriver = loadStructure()
.pipe(
distinctUntilChanged(),
map(maybeSerialize),
switchMap(structure =>
resolvePanes(structure, props.router.state.panes || [], this.state.panes, fromIndex)
),
debounce(panes => interval(hasLoading(panes) ? 50 : 0))
)
.subscribe(this.setResolvedPanes, this.setResolveError)
}
calcPanesEquality = (prev = [], next = []) => {
if (prev === next) {
return {ids: true, params: true}
}
if (prev.length !== next.length) {
return {ids: false, params: false}
}
let paramsDiffer = false
const idsEqual = prev.every((prevGroup, index) => {
const nextGroup = next[index]
if (prevGroup.length !== nextGroup.length) {
return false
}
return prevGroup.every((prevPane, paneIndex) => {
const nextPane = nextGroup[paneIndex]
paramsDiffer =
paramsDiffer ||
!isEqual(nextPane.params, prevPane.params) ||
!isEqual(nextPane.payload, prevPane.payload)
return nextPane.id === prevPane.id
})
})
return {ids: idsEqual, params: !paramsDiffer}
}
panesAreEqual = (prev, next) => this.calcPanesEquality(prev, next).ids
shouldDerivePanes = prevProps => {
const nextRouterState = this.props.router.state
const prevRouterState = prevProps.router.state
return (
!this.panesAreEqual(prevRouterState.panes, nextRouterState.panes) ||
nextRouterState.legacyEditDocumentId !== prevRouterState.legacyEditDocumentId ||
nextRouterState.type !== prevRouterState.type ||
nextRouterState.action !== prevRouterState.action
)
}
componentDidUpdate(prevProps, prevState) {
if (
prevProps.onPaneChange !== this.props.onPaneChange ||
prevState.panes !== this.state.panes
) {
this.props.onPaneChange(this.state.panes || [])
}
if (this.shouldDerivePanes(prevProps)) {
const prevPanes = prevProps.router.state.panes || []
const nextPanes = this.props.router.state.panes || []
const diffAt = getPaneDiffIndex(nextPanes, prevPanes)
if (diffAt) {
this.derivePanes(this.props, diffAt)
}
}
}
shouldComponentUpdate(nextProps, nextState) {
const prevPanes = this.props.router.state.panes || []
const nextPanes = nextProps.router.state.panes || []
const panesEqual = this.calcPanesEquality(prevPanes, nextPanes)
const {router: oldRouter, ...oldProps} = this.props
const {router: newRouter, ...newProps} = nextProps
const {panes: oldPanes, ...oldState} = this.state
const {panes: newPanes, ...newState} = nextState
const shouldUpdate =
!panesEqual.params ||
!panesEqual.ids ||
!shallowEquals(oldProps, newProps) ||
!isEqual(oldPanes, newPanes) ||
!shallowEquals(oldState, newState)
return shouldUpdate
}
maybeHandleOldUrl() {
const {navigate} = this.props.router
const {panes, action, legacyEditDocumentId} = this.props.router.state
if (action === 'edit' && legacyEditDocumentId) {
navigate({panes: panes.concat([{id: legacyEditDocumentId}])}, {replace: true})
}
}
componentDidMount() {
this.maybeHandleOldUrl()
this.derivePanes(this.props)
this.props.onPaneChange(this.state.panes || [])
}
componentWillUnmount() {
if (this.paneDeriver) {
this.paneDeriver.unsubscribe()
}
}
render() {
const {router} = this.props
const {panes, error} = this.state
if (error) {
return <StructureError error={error} />
}
const keys =
(router.state.panes || []).reduce(
(ids, group) => ids.concat(group.map(sibling => sibling.id)),
[]
) || EMPTY_PANE_KEYS
const groupIndexes = (router.state.panes || []).reduce(
(ids, group) => ids.concat(group.map((sibling, groupIndex) => groupIndex)),
[]
)
return (
<div className={styles.deskTool}>
{panes && (
<DeskToolPanes
router={router}
panes={this.state.panes}
keys={keys}
groupIndexes={groupIndexes}
autoCollapse
/>
)}
</div>
)
}
}
)
function getPaneDiffIndex(nextPanes, prevPanes) {
for (let index = 0; index < nextPanes.length; index++) {
const nextGroup = nextPanes[index]
const prevGroup = prevPanes[index]
// Whole group is now invalid
if (!prevGroup) {
return [index, 0]
}
/* eslint-disable max-depth */
// Iterate over siblings
for (let splitIndex = 0; splitIndex < nextGroup.length; splitIndex++) {
const nextSibling = nextGroup[splitIndex]
const prevSibling = prevGroup[splitIndex]
// Didn't have a sibling here previously, diff from here!
if (!prevSibling) {
return [index, splitIndex]
}
// Does the ID differ from the previous?
if (nextSibling.id !== prevSibling.id) {
return [index, splitIndex]
}
}
/* eslint-enable max-depth */
}
// "No diff"
return undefined
}
if (__DEV__) {
if (module.hot) {
module.hot.dispose(data => {
data.prevError = prevStructureError
})
}
}