From ded81a39973e12f02fce0dabaf17aa4d29282acd Mon Sep 17 00:00:00 2001 From: James Date: Sun, 26 Jan 2020 21:27:52 -0500 Subject: [PATCH] configures relationship field to dynamically load results from multiple collections --- package.json | 1 + .../forms/field-types/Relationship/index.js | 155 ++++++++++-------- .../views/collections/Edit/index.js | 20 +-- src/mongoose/requestHandlers/query.js | 5 +- 4 files changed, 101 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index c0223d5dfd..de7fd8e90d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dependencies": { "@hapi/joi": "^15.1.1", "accept-language-parser": "^1.5.0", + "async-some": "^1.0.2", "babel-jest": "^24.9.0", "babel-plugin-add-module-exports": "^1.0.2", "body-parser": "^1.19.0", diff --git a/src/client/components/forms/field-types/Relationship/index.js b/src/client/components/forms/field-types/Relationship/index.js index 1f490e8999..b4521426c4 100644 --- a/src/client/components/forms/field-types/Relationship/index.js +++ b/src/client/components/forms/field-types/Relationship/index.js @@ -6,6 +6,7 @@ import useFieldType from '../../useFieldType'; import getSanitizedConfig from '../../../../config/getSanitizedConfig'; import Label from '../../Label'; import Error from '../../Error'; +import some from 'async-some'; import './index.scss'; @@ -16,6 +17,8 @@ const { serverURL, collections } = getSanitizedConfig(); const defaultError = 'Please make a selection.'; const defaultValidate = value => value.length > 0; +const maxResultsPerRequest = 10; + class Relationship extends Component { constructor(props) { super(props); @@ -25,41 +28,55 @@ class Relationship extends Component { this.state = { relations, - results: relations.reduce((acc, relation) => ({ - ...acc, - [relation]: { - docs: [], - totalPages: null, - page: 1, - } - }), {}), + lastFullyLoadedRelation: -1, + lastPageLoaded: 1, options: [], }; } - // Get initial options to populate ReactSelect - // At first, only load the first 10 of the first related model componentDidMount() { - const { relations } = this.state; + this.getNextOptions(); + } + + getNextOptions = () => { + const { relations, lastFullyLoadedRelation, lastPageLoaded } = this.state; const token = cookies.get('token'); - relations.forEach((relation) => { - fetch(`${serverURL}/${relation}`, { - headers: { - Authorization: `Bearer ${token}` - } - }).then((res) => { - res.json().then((json) => { - const updatedResults = this.addResults(json, relation); - const formattedOptions = this.formatOptions(updatedResults); + const relationsToSearch = relations.slice(lastFullyLoadedRelation + 1); - this.setState({ - results: updatedResults, - options: formattedOptions, + if (relationsToSearch.length > 0) { + some(relationsToSearch, async (relation, callback) => { + const response = await fetch(`${serverURL}/${relation}?limit=${maxResultsPerRequest}&page=${lastPageLoaded}`, { + headers: { + Authorization: `Bearer ${token}` + } + }); + + const data = await response.json(); + + if (data.hasNextPage) { + return callback(false, { + data, + relation, }) - }) + } + + callback({ relation, data }); + }, (lastPage, nextPage) => { + if (nextPage) { + const { data, relation } = nextPage; + this.addOptions(data, relation); + } else { + const { data, relation } = lastPage; + this.addOptions(data, relation); + this.setState({ + lastFullyLoadedRelation: relations.indexOf(relation), + lastPageLoaded: 1, + }) + } + }) - }) + } } // This is needed to reduce the selected option to only its value @@ -104,58 +121,62 @@ class Relationship extends Component { return foundValue; } - // Build and maintain a list of all results, keyed by collection type - // Note - this is different than options so that we can easily keep current page and total pages - addResults = (incoming, relation) => { - const { results } = this.state; - - return { - ...results, - [relation]: { - totalPages: incoming.totalPages, - page: incoming.page, - docs: [ - ...results[relation].docs, - ...incoming.docs, - ] - } - } - } - - // Convert results into a ReactSelect-friendly array of options - formatOptions = (results) => { + addOptions = (data, relation) => { const { hasMultipleRelations } = this.props; + const { lastPageLoaded, options } = this.state; - return Object.keys(results).sort().reduce((acc, collectionSlug) => { - const collectionResults = results[collectionSlug].docs; - const collectionConfig = collections.find((collection) => collection.slug === collectionSlug); + if (!hasMultipleRelations) { + this.setState({ + options: [ + ...options, + ...data.docs.map((doc) => ({ + label: doc[collection.useAsTitle], + value: doc.id, + })) + ], + }); + } else { + const allOptionGroups = [...options]; + const collection = collections.find(collection => collection.slug === relation); + const optionsToAddTo = allOptionGroups.find(optionGroup => optionGroup.label === collection.labels.plural); - if (hasMultipleRelations) { - acc.push({ - label: collectionConfig.labels.plural, - options: collectionResults.map((result) => ({ - label: result[collectionConfig.useAsTitle], - value: { - relationTo: collectionConfig.slug, - value: result.id, - }, - })), - }); + const newOptions = data.docs.map((doc) => { + return { + label: doc[collection.useAsTitle], + value: { + relationTo: collection.slug, + value: doc.id, + } + } + }) + + if (optionsToAddTo) { + optionsToAddTo.options = [ + ...optionsToAddTo.options, + ...newOptions, + ]; } else { - collectionResults.map((result) => { - acc.push({ - label: result[collectionConfig.useAsTitle], - value: result.id, - }); + allOptionGroups.push({ + label: collection.labels.plural, + options: newOptions, }); } - return acc; - }, []); + this.setState({ + options: [ + ...allOptionGroups, + ], + }) + } + + this.setState({ + lastPageLoaded: lastPageLoaded + 1, + }); } handleMenuScrollToBottom = () => { - console.log('scrolled'); + const { lastPageLoaded, lastFullyLoadedRelation } = this.state; + this.getNextOptions(); } render() { diff --git a/src/client/components/views/collections/Edit/index.js b/src/client/components/views/collections/Edit/index.js index 37096b2ac8..0dfd15a23c 100644 --- a/src/client/components/views/collections/Edit/index.js +++ b/src/client/components/views/collections/Edit/index.js @@ -27,18 +27,16 @@ const EditView = (props) => { const { params: { id } = {} } = useRouteMatch(); const history = useHistory(); - const handleAjaxResponse = (res) => { - if (!isEditing) { - res.json().then((json) => { - history.push(`${admin}/collections/${collection.slug}/${json.doc.id}`, { - status: { - message: json.message, - type: 'success', - } - }); + const handleAjaxResponse = !isEditing ? (res) => { + res.json().then((json) => { + history.push(`${admin}/collections/${collection.slug}/${json.doc.id}`, { + status: { + message: json.message, + type: 'success', + } }); - } - } + }); + } : null; const [{ data }] = usePayloadAPI( (isEditing ? `${serverURL}/${collection.slug}/${id}` : null), diff --git a/src/mongoose/requestHandlers/query.js b/src/mongoose/requestHandlers/query.js index 9364b39b55..b3ca73a36b 100644 --- a/src/mongoose/requestHandlers/query.js +++ b/src/mongoose/requestHandlers/query.js @@ -3,11 +3,12 @@ const formatErrorResponse = require('../../responses/formatError'); const query = (req, res) => { const paginateQuery = { - limit: req.query.limit, - page: req.query.page, options: {}, }; + if (req.query.page) paginateQuery.page = req.query.page; + if (req.query.limit) paginateQuery.limit = req.query.limit; + if (req.query.depth) { paginateQuery.options.autopopulate = { maxDepth: req.query.depth,