From 13385ddbbb3704acdf0ca4cbf353168d716a0eeb Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Tue, 17 Dec 2019 16:50:12 +0100 Subject: [PATCH] [desk-tool] Load partial document list before fetching full list --- .../desk-tool/src/pane/DocumentsListPane.js | 221 ++++++++++++------ .../desk-tool/src/pane/InfiniteList.js | 21 +- 2 files changed, 163 insertions(+), 79 deletions(-) diff --git a/packages/@sanity/desk-tool/src/pane/DocumentsListPane.js b/packages/@sanity/desk-tool/src/pane/DocumentsListPane.js index a686c44080b..dd8a85af16b 100644 --- a/packages/@sanity/desk-tool/src/pane/DocumentsListPane.js +++ b/packages/@sanity/desk-tool/src/pane/DocumentsListPane.js @@ -2,18 +2,22 @@ import React from 'react' import PropTypes from 'prop-types' import schema from 'part:@sanity/base/schema' import DefaultPane from 'part:@sanity/components/panes/default' -import QueryContainer from 'part:@sanity/base/query-container' +import {getQueryResults} from 'part:@sanity/base/query-container' import Snackbar from 'part:@sanity/components/snackbar/default' import Spinner from 'part:@sanity/components/loading/spinner' import {collate, getPublishedId} from 'part:@sanity/base/util/draft-utils' -import {combineLatest} from 'rxjs' -import {map, tap} from 'rxjs/operators' +import {of, combineLatest} from 'rxjs' +import {map, tap, switchMap, filter as filterEvents} from 'rxjs/operators' +import shallowEquals from 'shallow-equals' import settings from '../settings' import styles from './styles/DocumentsListPane.css' import listStyles from './styles/ListView.css' import InfiniteList from './InfiniteList' import PaneItem from './PaneItem' +const PARTIAL_PAGE_LIMIT = 100 +const FULL_PAGE_LIMIT = 5000 + const DEFAULT_ORDERING = [{field: '_createdAt', direction: 'desc'}] function removePublishedWithDrafts(documents) { @@ -122,7 +126,7 @@ export default class DocumentsListPane extends React.PureComponent { } } - state = {sortOrder: null, layout: null} + state = {queryResult: {}, sortOrder: null, layout: null, isLoadingMore: false} constructor(props) { super() @@ -150,7 +154,7 @@ export default class DocumentsListPane extends React.PureComponent { })), tap(nextState => { if (sync) { - this.state = nextState + this.state = {...this.state, ...nextState} } else { this.setState(nextState) } @@ -161,8 +165,16 @@ export default class DocumentsListPane extends React.PureComponent { sync = false } + componentDidMount() { + this.setupQuery() + } + componentWillUnmount() { this.settingsSubscription.unsubscribe() + + if (this.queryResults$) { + this.queryResults$.unsubscribe() + } } itemIsSelected(item) { @@ -192,14 +204,63 @@ export default class DocumentsListPane extends React.PureComponent { return true } - buildListQuery() { + componentDidUpdate(prevProps) { + // If the filter/params has changed, set up a new query from scratch + if ( + prevProps.options.filter !== this.props.options.filter || + !shallowEquals(prevProps.options.params, this.props.options.params) + ) { + this.setupQuery() + } + } + + setupQuery() { + if (this.queryResults$) { + this.queryResults$.unsubscribe() + } + + const params = this.props.options.params || {} + const initialQuery = this.buildListQuery({fullList: false}) + const fullQuery = this.buildListQuery({fullList: true}) + + // Start by querying for a _partial_ result set which we can render right away + this.queryResults$ = getQueryResults(of({query: initialQuery, params})) + .pipe( + switchMap(queryResult => { + const documents = queryResult && queryResult.result && queryResult.result.documents + const numResults = documents && documents.length + const lessThanFullPage = numResults < PARTIAL_PAGE_LIMIT + + // If this is a progress event, the query failed, or we didn't get a full page worth of items, + // just pass it through! + if (!documents || lessThanFullPage) { + return of(queryResult) + } + + // We've got a partial result set, so set that to state so we can display it + this.setState({queryResult, isLoadingMore: true}) + + // Set up a query to get the entire set of documents available + // (or enough for it not to make sense to scroll to it) + return getQueryResults(of({query: fullQuery, params})).pipe( + // Don't include events that are not "complete", since it'll + // trigger the loading state again even if we have results + filterEvents(({result}) => result) + ) + }) + ) + .subscribe(queryResult => this.setState({queryResult, isLoadingMore: false})) + } + + buildListQuery({fullList}) { const {options} = this.props const {filter, defaultOrdering} = options - const sortState = this.state.sortOrder - const extendedProjection = sortState && sortState.extendedProjection + const {sortOrder} = this.state + const extendedProjection = sortOrder && sortOrder.extendedProjection const projectionFields = ['_id', '_type'] const finalProjection = projectionFields.join(', ') - const sortBy = (sortState && sortState.by) || defaultOrdering || [] + const sortBy = (sortOrder && sortOrder.by) || defaultOrdering || [] + const limit = fullList ? FULL_PAGE_LIMIT : PARTIAL_PAGE_LIMIT const sort = sortBy.length > 0 ? sortBy : DEFAULT_ORDERING if (extendedProjection) { @@ -212,36 +273,101 @@ export default class DocumentsListPane extends React.PureComponent { // Because Studios in the wild rely on the buggy nature of this // do not change this until we have API versioning return [ - `*[${filter}] [0...50000]`, + `*[${filter}] [0...${limit}]`, `{${firstProjection}}`, `order(${toOrderClause(sort)})`, `{${finalProjection}}` ].join(' | ') } - return `*[${filter}] | order(${toOrderClause(sort)}) [0...50000] {${finalProjection}}` + return `*[${filter}] | order(${toOrderClause(sort)}) [0...${limit}] {${finalProjection}}` + } + + renderResults() { + const {queryResult, isLoadingMore} = this.state + const {result} = queryResult + if (!result) { + return null + } + + const {options, defaultLayout} = this.props + const layout = this.state.layout || defaultLayout || 'default' + const filterIsSimpleTypeContraint = isSimpleTypeFilter(options.filter) + const items = removePublishedWithDrafts(result ? result.documents : []) + + if (!items || items.length === 0) { + return ( +
+
+

+ {filterIsSimpleTypeContraint + ? 'No documents of this type found' + : 'No documents matching this filter found'} +

+
+
+ ) + } + + return ( +
+ {items && ( + + )} +
+ ) + } + + renderContent() { + const {defaultLayout} = this.props + const layout = this.state.layout || defaultLayout || 'default' + const {loading, error, onRetry} = this.state.queryResult + + if (error) { + return ( + {error.message}} + /> + ) + } + + if (loading) { + return ( +
+ {loading && } +
+ ) + } + + return this.renderResults() } render() { const { title, - options, className, isCollapsed, isSelected, onCollapse, onExpand, - defaultLayout, menuItems, menuItemGroups, initialValueTemplates } = this.props - const {filter, params} = options - const layout = this.state.layout || defaultLayout || 'default' - const filterIsSimpleTypeContraint = isSimpleTypeFilter(filter) - const hasItems = items => items && items.length > 0 - const query = this.buildListQuery() return ( - - {({result, loading, error, onRetry}) => { - if (error) { - return ( - {error.message}} - /> - ) - } - - if (loading) { - return ( -
- {loading && } -
- ) - } - - if (!result) { - return null - } - - const items = removePublishedWithDrafts(result ? result.documents : []) - - if (!hasItems(items)) { - return ( -
-
-

- {filterIsSimpleTypeContraint - ? 'No documents of this type found' - : 'No documents matching this filter found'} -

-
-
- ) - } - - return ( -
- {items && ( - - )} -
- ) - }} -
+ {this.renderContent()}
) } diff --git a/packages/@sanity/desk-tool/src/pane/InfiniteList.js b/packages/@sanity/desk-tool/src/pane/InfiniteList.js index 679e7fc449c..1a67454c1fb 100644 --- a/packages/@sanity/desk-tool/src/pane/InfiniteList.js +++ b/packages/@sanity/desk-tool/src/pane/InfiniteList.js @@ -8,6 +8,8 @@ export default enhanceWithAvailHeight( static propTypes = { height: PropTypes.number, items: PropTypes.array, // eslint-disable-line react/forbid-prop-types + hasMoreItems: PropTypes.bool, + isLoadingMore: PropTypes.bool, renderItem: PropTypes.func, className: PropTypes.string, getItemKey: PropTypes.func, @@ -16,6 +18,8 @@ export default enhanceWithAvailHeight( } static defaultProps = { + hasMoreItems: false, + isLoadingMore: false, layout: 'default', items: [], height: 250 @@ -54,7 +58,17 @@ export default enhanceWithAvailHeight( } renderItem = ({index, style}) => { - const {renderItem, getItemKey, items} = this.props + const {renderItem, getItemKey, items, isLoadingMore} = this.props + if (index === items.length) { + return ( +
+ {isLoadingMore + ? 'Loading additional documents…' + : 'There are more documents than are currently shown.'} +
+ ) + } + const item = items[index] return (
@@ -64,8 +78,9 @@ export default enhanceWithAvailHeight( } render() { - const {layout, height, items, className, renderItem} = this.props + const {layout, height, items, className, renderItem, hasMoreItems, isLoadingMore} = this.props const {triggerUpdate, itemSize} = this.state + const addExtraItem = hasMoreItems || isLoadingMore if (!items || items.length === 0) { return
@@ -82,7 +97,7 @@ export default enhanceWithAvailHeight( onScroll={this.props.onScroll} className={className || ''} height={height} - itemCount={items.length} + itemCount={addExtraItem ? items.length + 1 : items.length} itemSize={itemSize} renderItem={this.renderItem} overscanCount={50}