Skip to content

Commit

Permalink
Define ordering (#176)
Browse files Browse the repository at this point in the history
* [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'
  • Loading branch information
bjoerge authored Sep 18, 2017
1 parent 9525798 commit d745a58
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 104 deletions.
4 changes: 4 additions & 0 deletions packages/@sanity/base/sanity.json
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,10 @@
"implements": "part:@sanity/base/sort-alpha-desc-icon",
"path": "components/icons/SortAlphaDesc.js"
},
{
"implements": "part:@sanity/base/sort-icon",
"path": "components/icons/Sort.js"
},
{
"implements": "part:@sanity/base/bars-icon",
"path": "components/icons/Bars.js"
Expand Down
1 change: 1 addition & 0 deletions packages/@sanity/base/src/components/icons/Sort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {default} from 'react-icons/lib/fa/sort'
7 changes: 6 additions & 1 deletion packages/@sanity/base/src/preview/PreviewSubscriber.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default class PreviewSubscriber extends React.PureComponent {
type: PropTypes.object.isRequired,
fields: PropTypes.arrayOf(PropTypes.oneOf(['title', 'description', 'imageUrl'])),
value: PropTypes.any.isRequired,
ordering: PropTypes.object,
children: PropTypes.func
}

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

const viewOptions = this.props.ordering
? {ordering: this.props.ordering}
: {}

const visibilityOn$ = Observable.of(!document.hidden)
.merge(visibilityChange$.map(event => !event.target.hidden))

Expand All @@ -58,7 +63,7 @@ export default class PreviewSubscriber extends React.PureComponent {
.distinctUntilChanged()
.switchMap(isInViewport => {
return isInViewport
? observeForPreview(value, type, fields)
? observeForPreview(value, type, fields, viewOptions)
: Observable.of(null)
})
.subscribe(result => {
Expand Down
5 changes: 3 additions & 2 deletions packages/@sanity/base/src/preview/SanityPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ export default class SanityPreview extends React.PureComponent {
static propTypes = {
layout: PropTypes.string,
value: PropTypes.any,
ordering: PropTypes.object,
type: PropTypes.object.isRequired
}


render() {
const {type, value, layout} = this.props
const {type, value, layout, ordering} = this.props
return (
<PreviewSubscriber type={type} value={value} layout={layout}>
<PreviewSubscriber type={type} value={value} layout={layout} ordering={ordering}>
{RenderPreviewSnapshot}
</PreviewSubscriber>
)
Expand Down
12 changes: 9 additions & 3 deletions packages/@sanity/base/src/preview/observeForPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function is(typeName, type) {
}

// Takes a value and its type and prepares a snapshot for it that can be passed to a preview component
export default function observeForPreview(value, type, fields) {
export default function observeForPreview(value, type, fields, viewOptions) {
if (is('reference', type)) {
// if the value is of type reference, but has no _ref property, we cannot prepare any value for the preview
// and the most sane thing to do is to return `null` for snapshot
Expand All @@ -31,7 +31,13 @@ export default function observeForPreview(value, type, fields) {
const targetFields = fields ? configFields.filter(fieldName => fields.includes(fieldName)) : configFields
const paths = targetFields.map(key => selection[key].split('.'))
return observe(value, paths)
.map(snapshot => ({type: type, snapshot: prepareForPreview(snapshot, type)}))
.map(snapshot => ({
type: type,
snapshot: prepareForPreview(snapshot, type, viewOptions)
}))
}
return Observable.of({type: type, snapshot: invokePrepare(type, value)})
return Observable.of({
type: type,
snapshot: invokePrepare(type, value, viewOptions)
})
}
8 changes: 4 additions & 4 deletions packages/@sanity/base/src/preview/prepareForPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ const reportErrors = debounce(() => {
/* eslint-enable no-console */
}, 1000)

function invokePrepareChecked(type, value) {
function invokePrepareChecked(type, value, viewOptions) {
const prepare = type.preview.prepare
if (!prepare) {
return value
}
try {
return prepare(value)
return prepare(value, viewOptions)
} catch (error) {
if (!COLLECTED_ERRORS[type.name]) {
COLLECTED_ERRORS[type.name] = []
Expand All @@ -57,7 +57,7 @@ function invokePrepareUnchecked(type, value) {

export const invokePrepare = __DEV__ ? invokePrepareChecked : invokePrepareUnchecked

export default function prepareForPreview(rawValue, type) {
export default function prepareForPreview(rawValue, type, viewOptions) {
const selection = type.preview.select
const targetKeys = Object.keys(selection)

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

return invokePrepare(type, remapped)
return invokePrepare(type, remapped, viewOptions)
}
153 changes: 103 additions & 50 deletions packages/@sanity/desk-tool/src/pane/DocumentsPane.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import React from 'react'
import Spinner from 'part:@sanity/components/loading/spinner'
import styles from './styles/DocumentsPane.css'
import {StateLink, IntentLink, withRouterHOC} from 'part:@sanity/base/router'
import {Item} from 'part:@sanity/components/lists/default'
import SortIcon from 'part:@sanity/base/sort-icon'

import ListView from './ListView'
import {partition} from 'lodash'
import {partition, uniqBy} from 'lodash'
import VisibilityOffIcon from 'part:@sanity/base/visibility-off-icon'
import EditIcon from 'part:@sanity/base/edit-icon'
import QueryContainer from 'part:@sanity/base/query-container'
Expand All @@ -21,18 +22,43 @@ import Snackbar from 'part:@sanity/components/snackbar/default'

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

function readListLayoutSettings() {
return JSON.parse(window.localStorage.getItem('desk-tool.listlayout-settings') || '{}')
const LOCALSTORAGE_KEY = 'desk-tool.documents-pane-settings'

function readSettings() {
return JSON.parse(window.localStorage.getItem(LOCALSTORAGE_KEY) || '{}')
}

function writeListLayoutSettings(settings) {
window.localStorage.setItem('desk-tool.listlayout-settings', JSON.stringify(settings))
function writeSettings(settings) {
window.localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(settings))
}

function getDocumentKey(document) {
return getPublishedId(document._id)
}

function toGradientOrderClause(orderBy) {
return orderBy.map(
ordering => [ordering.field, ordering.direction]
.filter(Boolean)
.join(' ')
).join(', ')
}

const ORDER_BY_UPDATED_AT = {
title: 'Last edited',
name: 'updatedAt',
by: [{field: '_updatedAt', direction: 'desc'}]
}

const ORDER_BY_CREATED_AT = {
title: 'Created',
name: 'createdAt',
by: [{field: '_createdAt', direction: 'desc'}]
}

const DEFAULT_SELECTED_ORDERING_OPTION = ORDER_BY_UPDATED_AT
const DEFAULT_ORDERING_OPTIONS = [ORDER_BY_UPDATED_AT, ORDER_BY_CREATED_AT]

function removePublishedWithDrafts(documents) {

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

function writeSettingsForType(type, settings) {
writeSettings(Object.assign(readSettings(), {
[type]: settings
}))
}

export default withRouterHOC(class DocumentsPane extends React.PureComponent {
static propTypes = {
selectedType: PropTypes.string,
selectedDocumentId: PropTypes.string,
schemaType: PropTypes.object,
isCollapsed: PropTypes.bool,
router: PropTypes.shape({
state: PropTypes.shape({
selectType: PropTypes.string,
selectedType: PropTypes.string
})
})
router: PropTypes.object
}

static defaultProps = {
loading: false,
isCollapsed: false,
published: [],
drafts: [],
onSetSorting: NOOP,
onSetListLayout: NOOP
}

state = {
listLayoutSettings: readListLayoutSettings(),
sorting: '_updatedAt desc',
menuIsOpen: false
}

static contextTypes = {
__internalRouter: PropTypes.object
handleSetListLayout = listLayout => {
this.setState(prevState => ({
settings: {
...prevState.settings,
listLayout: listLayout.key
}
}), this.writeSettings)
}


handleSetListLayout = listLayout => {
const {selectedType} = this.props.router.state
const nextSettings = Object.assign(readListLayoutSettings(), {
[selectedType]: listLayout
})
writeListLayoutSettings(nextSettings)
this.setState({listLayoutSettings: nextSettings})
constructor(props) {
super()
const settings = readSettings()
this.state = {
settings: (settings && settings[props.selectedType]) || {
listLayout: 'default',
ordering: DEFAULT_SELECTED_ORDERING_OPTION
},
menuIsOpen: false
}
}

getListLayoutForType(typeName) {
return this.state.listLayoutSettings[typeName] || 'default'
handleSetOrdering = ordering => {
this.setState(prevState => ({
settings: {
...prevState.settings,
ordering: ordering.name
}
}), this.writeSettings)
}

handleSetSorting = sorting => {
this.setState({
sorting: sorting
})
writeSettings() {
writeSettingsForType(this.props.selectedType, this.state.settings)
}

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

handleGoToCreateNew = () => {
const {selectedType} = this.props
getOrderingOptions(selectedType) {
const type = schema.get(selectedType)
const url = this.context.__internalRouter.resolveIntentLink('create', {
type: type.name
})
this.context.__internalRouter.navigateUrl(url)

const optionsWithDefaults = type.orderings
? type.orderings.concat(DEFAULT_ORDERING_OPTIONS)
: DEFAULT_ORDERING_OPTIONS

return uniqBy(optionsWithDefaults, 'name')
.map(option => {
return {
...option,
icon: option.icon || SortIcon,
title: <span>Sort by <b>{option.title}</b></span>
}
})
}

handleGoToCreateNew = () => {
const {selectedType, router} = this.props
router.navigateIntent('create', {type: selectedType})
}

renderDocumentsPaneMenu = () => {
const {selectedType} = this.props
const type = schema.get(selectedType)
return (
<DocumentsPaneMenu
onSetListLayout={this.handleSetListLayout}
onSetSorting={this.handleSetSorting}
onSetOrdering={this.handleSetOrdering}
onGoToCreateNew={this.handleGoToCreateNew}
onMenuClose={this.handleCloseMenu}
onClickOutside={this.handleCloseMenu}
isOpen={this.state.menuIsOpen}
orderingOptions={this.getOrderingOptions(selectedType)}
type={type}
/>
)
}

renderDocumentPaneItem = (item, index, options = {}) => {
const {selectedType, selectedDocumentId} = this.props
const listLayout = this.getListLayoutForType(selectedType)
const {settings} = this.state

const ordering = this.getOrderingOptions(selectedType)
.find(option => option.name === settings.ordering)

const type = schema.get(selectedType)
const linkState = {
selectedDocumentId: getPublishedId(item._id),
Expand All @@ -158,7 +209,8 @@ export default withRouterHOC(class DocumentsPane extends React.PureComponent {
<div className={isSelected ? styles.selectedItem : styles.item}>
<Preview
value={item}
layout={listLayout}
ordering={ordering}
layout={settings.listLayout}
type={type}
/>
<div className={styles.itemStatus}>
Expand Down Expand Up @@ -194,16 +246,17 @@ export default withRouterHOC(class DocumentsPane extends React.PureComponent {

render() {
const {
router,
selectedDocumentId,
schemaType,
isCollapsed
} = this.props

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

const params = {type: schemaType.name, draftsPath: `${DRAFTS_FOLDER}.**`}
const query = `*[_type == $type] | order(${this.state.sorting}) [0...10000] {_id, _type}`

const query = `*[_type == $type] | order(${toGradientOrderClause(currentOrderingOption.by)}) [0...10000] {_id, _type}`
return (
<Pane
{...this.props}
Expand All @@ -218,9 +271,9 @@ export default withRouterHOC(class DocumentsPane extends React.PureComponent {
params={params}
type={schemaType}
selectedId={selectedDocumentId}
listLayout={this.getListLayoutForType(schemaType.name)}
settings={settings}
>
{({result, loading, error, onRetry, type, listLayout}) => {
{({result, loading, error, onRetry, type}) => {
if (error) {
return (
<Snackbar
Expand Down Expand Up @@ -264,7 +317,7 @@ export default withRouterHOC(class DocumentsPane extends React.PureComponent {
items={items}
getItemKey={getDocumentKey}
renderItem={this.renderDocumentPaneItem}
listLayout={listLayout}
listLayout={settings.listLayout}
/>
)}

Expand Down
Loading

0 comments on commit d745a58

Please sign in to comment.