Skip to content

Commit d745a58

Browse files
authored
Define ordering (#176)
* [test-studio] Setup test cases * [schema] Support 'sorting' on object types + guess default sort config * [base] Pass view options to preview.prepare * [base] Expose sort icon * [desk-tool] Support custom sorting of documents list * [desk-tool] Remove unused state key * [desk-tool] Code nits * [desk-tool] Remove prefixed default sort options and make customizable * [desk-tool] Make icon configurable * [schema] Add missing overridable fields * [chore] More consistent usage of 'ordering'
1 parent 9525798 commit d745a58

File tree

12 files changed

+256
-104
lines changed

12 files changed

+256
-104
lines changed

packages/@sanity/base/sanity.json

+4
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,10 @@
583583
"implements": "part:@sanity/base/sort-alpha-desc-icon",
584584
"path": "components/icons/SortAlphaDesc.js"
585585
},
586+
{
587+
"implements": "part:@sanity/base/sort-icon",
588+
"path": "components/icons/Sort.js"
589+
},
586590
{
587591
"implements": "part:@sanity/base/bars-icon",
588592
"path": "components/icons/Bars.js"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {default} from 'react-icons/lib/fa/sort'

packages/@sanity/base/src/preview/PreviewSubscriber.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default class PreviewSubscriber extends React.PureComponent {
1313
type: PropTypes.object.isRequired,
1414
fields: PropTypes.arrayOf(PropTypes.oneOf(['title', 'description', 'imageUrl'])),
1515
value: PropTypes.any.isRequired,
16+
ordering: PropTypes.object,
1617
children: PropTypes.func
1718
}
1819

@@ -46,6 +47,10 @@ export default class PreviewSubscriber extends React.PureComponent {
4647
subscribe(value, type, fields) {
4748
this.unsubscribe()
4849

50+
const viewOptions = this.props.ordering
51+
? {ordering: this.props.ordering}
52+
: {}
53+
4954
const visibilityOn$ = Observable.of(!document.hidden)
5055
.merge(visibilityChange$.map(event => !event.target.hidden))
5156

@@ -58,7 +63,7 @@ export default class PreviewSubscriber extends React.PureComponent {
5863
.distinctUntilChanged()
5964
.switchMap(isInViewport => {
6065
return isInViewport
61-
? observeForPreview(value, type, fields)
66+
? observeForPreview(value, type, fields, viewOptions)
6267
: Observable.of(null)
6368
})
6469
.subscribe(result => {

packages/@sanity/base/src/preview/SanityPreview.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ export default class SanityPreview extends React.PureComponent {
88
static propTypes = {
99
layout: PropTypes.string,
1010
value: PropTypes.any,
11+
ordering: PropTypes.object,
1112
type: PropTypes.object.isRequired
1213
}
1314

1415

1516
render() {
16-
const {type, value, layout} = this.props
17+
const {type, value, layout, ordering} = this.props
1718
return (
18-
<PreviewSubscriber type={type} value={value} layout={layout}>
19+
<PreviewSubscriber type={type} value={value} layout={layout} ordering={ordering}>
1920
{RenderPreviewSnapshot}
2021
</PreviewSubscriber>
2122
)

packages/@sanity/base/src/preview/observeForPreview.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function is(typeName, type) {
1111
}
1212

1313
// Takes a value and its type and prepares a snapshot for it that can be passed to a preview component
14-
export default function observeForPreview(value, type, fields) {
14+
export default function observeForPreview(value, type, fields, viewOptions) {
1515
if (is('reference', type)) {
1616
// if the value is of type reference, but has no _ref property, we cannot prepare any value for the preview
1717
// and the most sane thing to do is to return `null` for snapshot
@@ -31,7 +31,13 @@ export default function observeForPreview(value, type, fields) {
3131
const targetFields = fields ? configFields.filter(fieldName => fields.includes(fieldName)) : configFields
3232
const paths = targetFields.map(key => selection[key].split('.'))
3333
return observe(value, paths)
34-
.map(snapshot => ({type: type, snapshot: prepareForPreview(snapshot, type)}))
34+
.map(snapshot => ({
35+
type: type,
36+
snapshot: prepareForPreview(snapshot, type, viewOptions)
37+
}))
3538
}
36-
return Observable.of({type: type, snapshot: invokePrepare(type, value)})
39+
return Observable.of({
40+
type: type,
41+
snapshot: invokePrepare(type, value, viewOptions)
42+
})
3743
}

packages/@sanity/base/src/preview/prepareForPreview.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ const reportErrors = debounce(() => {
3434
/* eslint-enable no-console */
3535
}, 1000)
3636

37-
function invokePrepareChecked(type, value) {
37+
function invokePrepareChecked(type, value, viewOptions) {
3838
const prepare = type.preview.prepare
3939
if (!prepare) {
4040
return value
4141
}
4242
try {
43-
return prepare(value)
43+
return prepare(value, viewOptions)
4444
} catch (error) {
4545
if (!COLLECTED_ERRORS[type.name]) {
4646
COLLECTED_ERRORS[type.name] = []
@@ -57,7 +57,7 @@ function invokePrepareUnchecked(type, value) {
5757

5858
export const invokePrepare = __DEV__ ? invokePrepareChecked : invokePrepareUnchecked
5959

60-
export default function prepareForPreview(rawValue, type) {
60+
export default function prepareForPreview(rawValue, type, viewOptions) {
6161
const selection = type.preview.select
6262
const targetKeys = Object.keys(selection)
6363

@@ -66,5 +66,5 @@ export default function prepareForPreview(rawValue, type) {
6666
return acc
6767
}, pick(rawValue, PRESERVE_KEYS))
6868

69-
return invokePrepare(type, remapped)
69+
return invokePrepare(type, remapped, viewOptions)
7070
}

packages/@sanity/desk-tool/src/pane/DocumentsPane.js

+103-50
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import React from 'react'
33
import Spinner from 'part:@sanity/components/loading/spinner'
44
import styles from './styles/DocumentsPane.css'
55
import {StateLink, IntentLink, withRouterHOC} from 'part:@sanity/base/router'
6-
import {Item} from 'part:@sanity/components/lists/default'
6+
import SortIcon from 'part:@sanity/base/sort-icon'
7+
78
import ListView from './ListView'
8-
import {partition} from 'lodash'
9+
import {partition, uniqBy} from 'lodash'
910
import VisibilityOffIcon from 'part:@sanity/base/visibility-off-icon'
1011
import EditIcon from 'part:@sanity/base/edit-icon'
1112
import QueryContainer from 'part:@sanity/base/query-container'
@@ -21,18 +22,43 @@ import Snackbar from 'part:@sanity/components/snackbar/default'
2122

2223
const NOOP = () => {} // eslint-disable-line
2324

24-
function readListLayoutSettings() {
25-
return JSON.parse(window.localStorage.getItem('desk-tool.listlayout-settings') || '{}')
25+
const LOCALSTORAGE_KEY = 'desk-tool.documents-pane-settings'
26+
27+
function readSettings() {
28+
return JSON.parse(window.localStorage.getItem(LOCALSTORAGE_KEY) || '{}')
2629
}
2730

28-
function writeListLayoutSettings(settings) {
29-
window.localStorage.setItem('desk-tool.listlayout-settings', JSON.stringify(settings))
31+
function writeSettings(settings) {
32+
window.localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(settings))
3033
}
3134

3235
function getDocumentKey(document) {
3336
return getPublishedId(document._id)
3437
}
3538

39+
function toGradientOrderClause(orderBy) {
40+
return orderBy.map(
41+
ordering => [ordering.field, ordering.direction]
42+
.filter(Boolean)
43+
.join(' ')
44+
).join(', ')
45+
}
46+
47+
const ORDER_BY_UPDATED_AT = {
48+
title: 'Last edited',
49+
name: 'updatedAt',
50+
by: [{field: '_updatedAt', direction: 'desc'}]
51+
}
52+
53+
const ORDER_BY_CREATED_AT = {
54+
title: 'Created',
55+
name: 'createdAt',
56+
by: [{field: '_createdAt', direction: 'desc'}]
57+
}
58+
59+
const DEFAULT_SELECTED_ORDERING_OPTION = ORDER_BY_UPDATED_AT
60+
const DEFAULT_ORDERING_OPTIONS = [ORDER_BY_UPDATED_AT, ORDER_BY_CREATED_AT]
61+
3662
function removePublishedWithDrafts(documents) {
3763

3864
const [draftIds, publishedIds] = partition(documents.map(doc => doc._id), isDraftId)
@@ -50,57 +76,61 @@ function removePublishedWithDrafts(documents) {
5076
.filter(doc => !(isPublishedId(doc._id) && doc.hasDraft))
5177
}
5278

79+
function writeSettingsForType(type, settings) {
80+
writeSettings(Object.assign(readSettings(), {
81+
[type]: settings
82+
}))
83+
}
84+
5385
export default withRouterHOC(class DocumentsPane extends React.PureComponent {
5486
static propTypes = {
5587
selectedType: PropTypes.string,
5688
selectedDocumentId: PropTypes.string,
5789
schemaType: PropTypes.object,
5890
isCollapsed: PropTypes.bool,
59-
router: PropTypes.shape({
60-
state: PropTypes.shape({
61-
selectType: PropTypes.string,
62-
selectedType: PropTypes.string
63-
})
64-
})
91+
router: PropTypes.object
6592
}
6693

6794
static defaultProps = {
6895
loading: false,
6996
isCollapsed: false,
7097
published: [],
7198
drafts: [],
72-
onSetSorting: NOOP,
7399
onSetListLayout: NOOP
74100
}
75101

76-
state = {
77-
listLayoutSettings: readListLayoutSettings(),
78-
sorting: '_updatedAt desc',
79-
menuIsOpen: false
80-
}
81-
82-
static contextTypes = {
83-
__internalRouter: PropTypes.object
102+
handleSetListLayout = listLayout => {
103+
this.setState(prevState => ({
104+
settings: {
105+
...prevState.settings,
106+
listLayout: listLayout.key
107+
}
108+
}), this.writeSettings)
84109
}
85110

86-
87-
handleSetListLayout = listLayout => {
88-
const {selectedType} = this.props.router.state
89-
const nextSettings = Object.assign(readListLayoutSettings(), {
90-
[selectedType]: listLayout
91-
})
92-
writeListLayoutSettings(nextSettings)
93-
this.setState({listLayoutSettings: nextSettings})
111+
constructor(props) {
112+
super()
113+
const settings = readSettings()
114+
this.state = {
115+
settings: (settings && settings[props.selectedType]) || {
116+
listLayout: 'default',
117+
ordering: DEFAULT_SELECTED_ORDERING_OPTION
118+
},
119+
menuIsOpen: false
120+
}
94121
}
95122

96-
getListLayoutForType(typeName) {
97-
return this.state.listLayoutSettings[typeName] || 'default'
123+
handleSetOrdering = ordering => {
124+
this.setState(prevState => ({
125+
settings: {
126+
...prevState.settings,
127+
ordering: ordering.name
128+
}
129+
}), this.writeSettings)
98130
}
99131

100-
handleSetSorting = sorting => {
101-
this.setState({
102-
sorting: sorting
103-
})
132+
writeSettings() {
133+
writeSettingsForType(this.props.selectedType, this.state.settings)
104134
}
105135

106136
handleToggleMenu = () => {
@@ -115,31 +145,52 @@ export default withRouterHOC(class DocumentsPane extends React.PureComponent {
115145
})
116146
}
117147

118-
handleGoToCreateNew = () => {
119-
const {selectedType} = this.props
148+
getOrderingOptions(selectedType) {
120149
const type = schema.get(selectedType)
121-
const url = this.context.__internalRouter.resolveIntentLink('create', {
122-
type: type.name
123-
})
124-
this.context.__internalRouter.navigateUrl(url)
150+
151+
const optionsWithDefaults = type.orderings
152+
? type.orderings.concat(DEFAULT_ORDERING_OPTIONS)
153+
: DEFAULT_ORDERING_OPTIONS
154+
155+
return uniqBy(optionsWithDefaults, 'name')
156+
.map(option => {
157+
return {
158+
...option,
159+
icon: option.icon || SortIcon,
160+
title: <span>Sort by <b>{option.title}</b></span>
161+
}
162+
})
163+
}
164+
165+
handleGoToCreateNew = () => {
166+
const {selectedType, router} = this.props
167+
router.navigateIntent('create', {type: selectedType})
125168
}
126169

127170
renderDocumentsPaneMenu = () => {
171+
const {selectedType} = this.props
172+
const type = schema.get(selectedType)
128173
return (
129174
<DocumentsPaneMenu
130175
onSetListLayout={this.handleSetListLayout}
131-
onSetSorting={this.handleSetSorting}
176+
onSetOrdering={this.handleSetOrdering}
132177
onGoToCreateNew={this.handleGoToCreateNew}
133178
onMenuClose={this.handleCloseMenu}
134179
onClickOutside={this.handleCloseMenu}
135180
isOpen={this.state.menuIsOpen}
181+
orderingOptions={this.getOrderingOptions(selectedType)}
182+
type={type}
136183
/>
137184
)
138185
}
139186

140187
renderDocumentPaneItem = (item, index, options = {}) => {
141188
const {selectedType, selectedDocumentId} = this.props
142-
const listLayout = this.getListLayoutForType(selectedType)
189+
const {settings} = this.state
190+
191+
const ordering = this.getOrderingOptions(selectedType)
192+
.find(option => option.name === settings.ordering)
193+
143194
const type = schema.get(selectedType)
144195
const linkState = {
145196
selectedDocumentId: getPublishedId(item._id),
@@ -158,7 +209,8 @@ export default withRouterHOC(class DocumentsPane extends React.PureComponent {
158209
<div className={isSelected ? styles.selectedItem : styles.item}>
159210
<Preview
160211
value={item}
161-
layout={listLayout}
212+
ordering={ordering}
213+
layout={settings.listLayout}
162214
type={type}
163215
/>
164216
<div className={styles.itemStatus}>
@@ -194,16 +246,17 @@ export default withRouterHOC(class DocumentsPane extends React.PureComponent {
194246

195247
render() {
196248
const {
197-
router,
198249
selectedDocumentId,
199250
schemaType,
200251
isCollapsed
201252
} = this.props
202253

254+
const {settings} = this.state
255+
const currentOrderingOption = this.getOrderingOptions(schemaType.name)
256+
.find(option => option.name === settings.ordering) || DEFAULT_SELECTED_ORDERING_OPTION
203257

204258
const params = {type: schemaType.name, draftsPath: `${DRAFTS_FOLDER}.**`}
205-
const query = `*[_type == $type] | order(${this.state.sorting}) [0...10000] {_id, _type}`
206-
259+
const query = `*[_type == $type] | order(${toGradientOrderClause(currentOrderingOption.by)}) [0...10000] {_id, _type}`
207260
return (
208261
<Pane
209262
{...this.props}
@@ -218,9 +271,9 @@ export default withRouterHOC(class DocumentsPane extends React.PureComponent {
218271
params={params}
219272
type={schemaType}
220273
selectedId={selectedDocumentId}
221-
listLayout={this.getListLayoutForType(schemaType.name)}
274+
settings={settings}
222275
>
223-
{({result, loading, error, onRetry, type, listLayout}) => {
276+
{({result, loading, error, onRetry, type}) => {
224277
if (error) {
225278
return (
226279
<Snackbar
@@ -264,7 +317,7 @@ export default withRouterHOC(class DocumentsPane extends React.PureComponent {
264317
items={items}
265318
getItemKey={getDocumentKey}
266319
renderItem={this.renderDocumentPaneItem}
267-
listLayout={listLayout}
320+
listLayout={settings.listLayout}
268321
/>
269322
)}
270323

0 commit comments

Comments
 (0)