Skip to content

Commit 77c0bcb

Browse files
committed
Merge branch 'develop' into trunk
2 parents a568653 + 985d0c7 commit 77c0bcb

File tree

6 files changed

+154
-8
lines changed

6 files changed

+154
-8
lines changed

README.md

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function MyComponent( props ) {
4646
| `uniqueContentItems` | `bool` | `true` | Prevent duplicate items from being picked.
4747
| `excludeCurrentPost` | `bool` | `true` | Don't allow user to pick the current post. Only applicable on the editor screen.
4848
| `content` | `array` | `[]` | Array of items to prepopulate picker with. Must be in the format of: `[{id: 1, type: 'post'}, {id: 1, type: 'page'},... ]`. You cannot provide terms and posts to the same picker. Can also take the form `[1, 2, ...]` if only one `contentTypes` is provided.
49-
49+
| `perPage` | `number` | `50` | Number of items to show during search
5050
__NOTE:__ Content picker cannot validate that posts you pass it via `content` prop actually exist. If a post does not exist, it will not render as one of the picked items but will still be passed back as picked items if new items are picked/sorted. Therefore, on save you need to validate that all the picked posts/terms actually exist.
5151

5252
The `contentTypes` will get used in a Rest Request to the `search` endpoint as the `subtypes`:
@@ -87,7 +87,7 @@ function MyComponent( props ) {
8787
| `placeholder` | `string` | `''` | Renders placeholder text inside the Search Field. |
8888
| `contentTypes` | `array` | `[ 'post', 'page' ]` | Names of the post types or taxonomies that should get searched |
8989
| `excludeItems` | `array` | `[ { id: 1, type: 'post' ]` | Items to exclude from search |
90-
90+
| `perPage` | `number` | `50` | Number of items to show during search
9191

9292

9393
## useHasSelectedInnerBlock
@@ -108,10 +108,69 @@ function BlockEdit( props ) {
108108
)
109109
}
110110
```
111+
## useRequestData
112+
Custom hook to to make a request using `getEntityRecords` or `getEntityRecord` that provides `data`, `isLoading` and `invalidator` function. The hook determines which selector to use based on the query parameter. If a number is passed, it will use `getEntityRecord` to retrieve a single item. If an object is passed, it will use that as the query for `getEntityRecords` to retrieve multiple pieces of data.
113+
114+
The `invalidator` function, when dispatched, will tell the datastore to invalidate the resolver associated with the request made by getEntityRecords. This will trigger the request to be re-run as if it was being requested for the first time. This is not always needed but is very useful for components that need to update the data after an event. For example, displaying a list of uploaded media after a new item has been uploaded.
115+
116+
Parameters:
117+
* `{string}` entity The entity to retrieve. ie. postType
118+
* `{string}` kind The entity kind to retrieve. ie. posts
119+
* `{Object|Number}` Optional. Query to pass to the geEntityRecords request. Defaults to an empty object. If a number is passed, it is used as the ID of the entity to retrieve via getEntityRecord.
120+
121+
Returns:
122+
* `{Array}`
123+
* `{Array} ` Array containing the requested entity kind.
124+
* `{Boolean}` Representing if the request is resolving
125+
* `{Function}` This function will invalidate the resolver and re-run the query.
126+
### Usage
111127

128+
#### Multiple pieces of data.
129+
```js
130+
const ExampleBockEdit = ({ className }) => {
131+
const [data, isLoading, invalidateRequest ] = useRequestData('postType', 'post', { per_page: 5 });
132+
133+
if (isLoading) {
134+
return <h3>Loading...</h3>;
135+
}
136+
return (
137+
<div className={className}>
138+
<ul>
139+
{data &&
140+
data.map(({ title: { rendered: postTitle } }) => {
141+
return <li>{postTitle}</li>;
142+
})}
143+
</ul>
144+
<button type="button" onClick={invalidateRequest}>
145+
Refresh list
146+
</button>
147+
</div>
148+
);
149+
};
150+
```
151+
#### Single piece of data
152+
```js
153+
const ExampleBockEdit = ({ className }) => {
154+
const [data, isLoading, invalidateRequest ] = useRequestData('postType', 'post', 59);
155+
156+
if (isLoading) {
157+
return <h3>Loading...</h3>;
158+
}
159+
return (
160+
<div className={className}>
161+
162+
{data &&( <div>{data.title.rendered}</div>)}
163+
164+
<button type="button" onClick={invalidateRequest}>
165+
Refresh list
166+
</button>
167+
</div>
168+
);
169+
};
170+
```
112171
## IsAdmin
113172

114-
A wrapper component that only renders child components if the current user has admin capabilities. The usecase for this component is when you have a certain setting that should be restricted to administrators only. For example when you have a block that requires an API token or crenentials you might only want Administrators to edit these. See [10up/maps-block-apple](https://github.com/10up/maps-block-apple/blob/774c6509eabb7ac48dcebea551f32ac7ddc5d246/src/Settings/AuthenticationSettings.js) for a real world example.
173+
A wrapper component that only renders child components if the current user has admin capabilities. The use case for this component is when you have a certain setting that should be restricted to administrators only. For example when you have a block that requires an API token or credentials you might only want Administrators to edit these. See [10up/maps-block-apple](https://github.com/10up/maps-block-apple/blob/774c6509eabb7ac48dcebea551f32ac7ddc5d246/src/Settings/AuthenticationSettings.js) for a real world example.
115174

116175
### Usage
117176
```js

components/ContentPicker/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const ContentPicker = ({
5555
content: presetContent,
5656
uniqueContentItems,
5757
excludeCurrentPost,
58+
perPage
5859
}) => {
5960
const [content, setContent] = useState(presetContent);
6061

@@ -110,6 +111,7 @@ const ContentPicker = ({
110111
onSelectItem={handleSelect}
111112
contentTypes={contentTypes}
112113
mode={mode}
114+
perPage={perPage}
113115
/>
114116
)}
115117
{Boolean(content?.length) > 0 && (
@@ -151,6 +153,7 @@ ContentPicker.defaultProps = {
151153
contentTypes: ['post', 'page'],
152154
placeholder: '',
153155
content: [],
156+
perPage: 50,
154157
maxContentItems: 1,
155158
uniqueContentItems: true,
156159
isOrderable: false,
@@ -172,6 +175,7 @@ ContentPicker.propTypes = {
172175
uniqueContentItems: PropTypes.bool,
173176
excludeCurrentPost: PropTypes.bool,
174177
maxContentItems: PropTypes.number,
178+
perPage: PropTypes.number,
175179
};
176180

177181
export { ContentPicker };

components/ContentSearch/index.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@ import { useState, useRef, useEffect } from '@wordpress/element'; // eslint-disa
44
import PropTypes from 'prop-types';
55
import { __ } from '@wordpress/i18n';
66
import SearchItem from './SearchItem';
7+
/** @jsx jsx */
8+
import { jsx, css } from '@emotion/react';
79

8-
const NAMESPACE = '10up-content-search';
10+
const NAMESPACE = 'tenup-content-search';
911

1012
const searchCache = {};
1113

12-
const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, excludeItems }) => {
14+
const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, excludeItems, perPage }) => {
1315
const [searchString, setSearchString] = useState('');
1416
const [searchResults, setSearchResults] = useState([]);
1517
const [isLoading, setIsLoading] = useState(false);
1618
const [selectedItem, setSelectedItem] = useState(null);
19+
const abortControllerRef = useRef();
1720

1821
const mounted = useRef(true);
1922

@@ -72,27 +75,56 @@ const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, e
7275
* @param {string} keyword search query string
7376
*/
7477
const handleSearchStringChange = (keyword) => {
78+
if (abortControllerRef.current) {
79+
abortControllerRef.current.abort();
80+
}
81+
7582
setSearchString(keyword);
83+
84+
if (keyword.trim() === '') {
85+
setIsLoading(false);
86+
setSearchResults([]);
87+
abortControllerRef.current = null;
88+
return;
89+
}
90+
91+
abortControllerRef.current = new AbortController();
92+
7693
setIsLoading(true);
7794

7895
const searchQuery = `wp/v2/search/?search=${keyword}&subtype=${contentTypes.join(
7996
',',
80-
)}&type=${mode}&_embed`;
97+
)}&type=${mode}&_embed&per_page=50`;
8198

8299
if (searchCache[searchQuery]) {
100+
abortControllerRef.current = null;
101+
83102
setSearchResults(filterResults(searchCache[searchQuery]));
84103
setIsLoading(false);
85104
} else {
105+
86106
apiFetch({
87107
path: searchQuery,
108+
signal: abortControllerRef.current.signal
88109
}).then((results) => {
89110
if (mounted.current === false) {
90111
return;
91112
}
92113

114+
abortControllerRef.current = null;
115+
93116
searchCache[searchQuery] = results;
117+
94118
setSearchResults(filterResults(results));
119+
95120
setIsLoading(false);
121+
}).catch((error, code) => {
122+
// fetch_error means the request was aborted
123+
if (error.code !== 'fetch_error') {
124+
setSearchResults([]);
125+
abortControllerRef.current = null;
126+
setIsLoading(false);
127+
}
96128
});
97129
}
98130
};
@@ -103,6 +135,12 @@ const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, e
103135
};
104136
}, []);
105137

138+
const listCSS = css`
139+
/* stylelint-disable */
140+
max-height: 350px;
141+
overflow-y: scroll;
142+
`;
143+
106144
return (
107145
<NavigableMenu onNavigate={handleOnNavigate} orientation="vertical">
108146
<TextControl
@@ -122,6 +160,7 @@ const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, e
122160
paddingLeft: '0',
123161
listStyle: 'none',
124162
}}
163+
css={listCSS}
125164
>
126165
{isLoading && <Spinner />}
127166
{!isLoading && !hasSearchResults && (
@@ -132,7 +171,7 @@ const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, e
132171
{__('Nothing found.', '10up-block-components')}
133172
</li>
134173
)}
135-
{searchResults.map((item, index) => {
174+
{!isLoading && searchResults.map((item, index) => {
136175
if (!item.title.length) {
137176
return null;
138177
}
@@ -164,6 +203,7 @@ const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, e
164203
ContentSearch.defaultProps = {
165204
contentTypes: ['post', 'page'],
166205
placeholder: '',
206+
perPage: 50,
167207
label: '',
168208
excludeItems: [],
169209
mode: 'post',
@@ -179,6 +219,7 @@ ContentSearch.propTypes = {
179219
placeholder: PropTypes.string,
180220
excludeItems: PropTypes.array,
181221
label: PropTypes.string,
222+
perPage: PropTypes.number
182223
};
183224

184225
export { ContentSearch };

hooks/use-request-data.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* External dependencies
3+
*/
4+
// eslint-disable-next-line import/no-extraneous-dependencies
5+
import isObject from 'lodash/isObject';
6+
7+
/**
8+
* WordPress dependencies
9+
*/
10+
import { useSelect, useDispatch } from '@wordpress/data';
11+
12+
/**
13+
* Hook for retrieving data from the WordPress REST API.
14+
*
15+
* @param {string} entity The entity to retrieve. ie. postType
16+
* @param {string} kind The entity kind to retrieve. ie. posts
17+
* @param {Object | number} [query] Optional. Query to pass to the geEntityRecords request. Defaults to an empty object. If a number is passed, it is used as the ID of the entity to retrieve via getEntityRecord.
18+
* @return {Array} The data returned from the request.
19+
*/
20+
export const useRequestData = (entity, kind, query = {}) => {
21+
const functionToCall = isObject(query) ? 'getEntityRecords' : 'getEntityRecord';
22+
const { invalidateResolution } = useDispatch('core/data');
23+
const { data, isLoading } = useSelect((select) => {
24+
return {
25+
data: select('core')[functionToCall](entity, kind, query),
26+
isLoading: select('core/data').isResolving('core', functionToCall, [
27+
entity,
28+
kind,
29+
query,
30+
]),
31+
};
32+
});
33+
34+
const invalidateResolver = () => {
35+
invalidateResolution('core', functionToCall, [entity, kind, query]);
36+
};
37+
38+
return [data, isLoading, invalidateResolver];
39+
};
40+
41+

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export { ContentSearch } from './components/ContentSearch';
33
export { InnerBlockSlider } from './components/InnerBlockSlider';
44
export { IsAdmin } from './components/is-admin';
55
export { useHasSelectedInnerBlock } from './hooks/use-has-selected-inner-block';
6+
export { useRequestData } from './hooks/use-request-data'
67
export { default as CustomBlockAppender } from './components/CustomBlockAppender';

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publishConfig": {
44
"access": "public"
55
},
6-
"version": "1.3.0",
6+
"version": "1.4.0",
77
"description": "10up Components built for the WordPress Block Editor.",
88
"main": "index.js",
99
"scripts": {

0 commit comments

Comments
 (0)