Skip to content

Commit

Permalink
Merge branch 'develop' into trunk
Browse files Browse the repository at this point in the history
  • Loading branch information
tlovett1 committed Sep 24, 2021
2 parents a568653 + 985d0c7 commit 77c0bcb
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 8 deletions.
65 changes: 62 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function MyComponent( props ) {
| `uniqueContentItems` | `bool` | `true` | Prevent duplicate items from being picked.
| `excludeCurrentPost` | `bool` | `true` | Don't allow user to pick the current post. Only applicable on the editor screen.
| `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.

| `perPage` | `number` | `50` | Number of items to show during search
__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.

The `contentTypes` will get used in a Rest Request to the `search` endpoint as the `subtypes`:
Expand Down Expand Up @@ -87,7 +87,7 @@ function MyComponent( props ) {
| `placeholder` | `string` | `''` | Renders placeholder text inside the Search Field. |
| `contentTypes` | `array` | `[ 'post', 'page' ]` | Names of the post types or taxonomies that should get searched |
| `excludeItems` | `array` | `[ { id: 1, type: 'post' ]` | Items to exclude from search |

| `perPage` | `number` | `50` | Number of items to show during search


## useHasSelectedInnerBlock
Expand All @@ -108,10 +108,69 @@ function BlockEdit( props ) {
)
}
```
## useRequestData
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.

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.

Parameters:
* `{string}` entity The entity to retrieve. ie. postType
* `{string}` kind The entity kind to retrieve. ie. posts
* `{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.

Returns:
* `{Array}`
* `{Array} ` Array containing the requested entity kind.
* `{Boolean}` Representing if the request is resolving
* `{Function}` This function will invalidate the resolver and re-run the query.
### Usage

#### Multiple pieces of data.
```js
const ExampleBockEdit = ({ className }) => {
const [data, isLoading, invalidateRequest ] = useRequestData('postType', 'post', { per_page: 5 });

if (isLoading) {
return <h3>Loading...</h3>;
}
return (
<div className={className}>
<ul>
{data &&
data.map(({ title: { rendered: postTitle } }) => {
return <li>{postTitle}</li>;
})}
</ul>
<button type="button" onClick={invalidateRequest}>
Refresh list
</button>
</div>
);
};
```
#### Single piece of data
```js
const ExampleBockEdit = ({ className }) => {
const [data, isLoading, invalidateRequest ] = useRequestData('postType', 'post', 59);

if (isLoading) {
return <h3>Loading...</h3>;
}
return (
<div className={className}>

{data &&( <div>{data.title.rendered}</div>)}

<button type="button" onClick={invalidateRequest}>
Refresh list
</button>
</div>
);
};
```
## IsAdmin

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.
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.

### Usage
```js
Expand Down
4 changes: 4 additions & 0 deletions components/ContentPicker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const ContentPicker = ({
content: presetContent,
uniqueContentItems,
excludeCurrentPost,
perPage
}) => {
const [content, setContent] = useState(presetContent);

Expand Down Expand Up @@ -110,6 +111,7 @@ const ContentPicker = ({
onSelectItem={handleSelect}
contentTypes={contentTypes}
mode={mode}
perPage={perPage}
/>
)}
{Boolean(content?.length) > 0 && (
Expand Down Expand Up @@ -151,6 +153,7 @@ ContentPicker.defaultProps = {
contentTypes: ['post', 'page'],
placeholder: '',
content: [],
perPage: 50,
maxContentItems: 1,
uniqueContentItems: true,
isOrderable: false,
Expand All @@ -172,6 +175,7 @@ ContentPicker.propTypes = {
uniqueContentItems: PropTypes.bool,
excludeCurrentPost: PropTypes.bool,
maxContentItems: PropTypes.number,
perPage: PropTypes.number,
};

export { ContentPicker };
49 changes: 45 additions & 4 deletions components/ContentSearch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import { useState, useRef, useEffect } from '@wordpress/element'; // eslint-disa
import PropTypes from 'prop-types';
import { __ } from '@wordpress/i18n';
import SearchItem from './SearchItem';
/** @jsx jsx */
import { jsx, css } from '@emotion/react';

const NAMESPACE = '10up-content-search';
const NAMESPACE = 'tenup-content-search';

const searchCache = {};

const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, excludeItems }) => {
const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, excludeItems, perPage }) => {
const [searchString, setSearchString] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const abortControllerRef = useRef();

const mounted = useRef(true);

Expand Down Expand Up @@ -72,27 +75,56 @@ const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, e
* @param {string} keyword search query string
*/
const handleSearchStringChange = (keyword) => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}

setSearchString(keyword);

if (keyword.trim() === '') {
setIsLoading(false);
setSearchResults([]);
abortControllerRef.current = null;
return;
}

abortControllerRef.current = new AbortController();

setIsLoading(true);

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

if (searchCache[searchQuery]) {
abortControllerRef.current = null;

setSearchResults(filterResults(searchCache[searchQuery]));
setIsLoading(false);
} else {

apiFetch({
path: searchQuery,
signal: abortControllerRef.current.signal
}).then((results) => {
if (mounted.current === false) {
return;
}

abortControllerRef.current = null;

searchCache[searchQuery] = results;

setSearchResults(filterResults(results));

setIsLoading(false);
}).catch((error, code) => {
// fetch_error means the request was aborted
if (error.code !== 'fetch_error') {
setSearchResults([]);
abortControllerRef.current = null;
setIsLoading(false);
}
});
}
};
Expand All @@ -103,6 +135,12 @@ const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, e
};
}, []);

const listCSS = css`
/* stylelint-disable */
max-height: 350px;
overflow-y: scroll;
`;

return (
<NavigableMenu onNavigate={handleOnNavigate} orientation="vertical">
<TextControl
Expand All @@ -122,6 +160,7 @@ const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, e
paddingLeft: '0',
listStyle: 'none',
}}
css={listCSS}
>
{isLoading && <Spinner />}
{!isLoading && !hasSearchResults && (
Expand All @@ -132,7 +171,7 @@ const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, e
{__('Nothing found.', '10up-block-components')}
</li>
)}
{searchResults.map((item, index) => {
{!isLoading && searchResults.map((item, index) => {
if (!item.title.length) {
return null;
}
Expand Down Expand Up @@ -164,6 +203,7 @@ const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, e
ContentSearch.defaultProps = {
contentTypes: ['post', 'page'],
placeholder: '',
perPage: 50,
label: '',
excludeItems: [],
mode: 'post',
Expand All @@ -179,6 +219,7 @@ ContentSearch.propTypes = {
placeholder: PropTypes.string,
excludeItems: PropTypes.array,
label: PropTypes.string,
perPage: PropTypes.number
};

export { ContentSearch };
41 changes: 41 additions & 0 deletions hooks/use-request-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* External dependencies
*/
// eslint-disable-next-line import/no-extraneous-dependencies
import isObject from 'lodash/isObject';

/**
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';

/**
* Hook for retrieving data from the WordPress REST API.
*
* @param {string} entity The entity to retrieve. ie. postType
* @param {string} kind The entity kind to retrieve. ie. posts
* @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.
* @return {Array} The data returned from the request.
*/
export const useRequestData = (entity, kind, query = {}) => {
const functionToCall = isObject(query) ? 'getEntityRecords' : 'getEntityRecord';
const { invalidateResolution } = useDispatch('core/data');
const { data, isLoading } = useSelect((select) => {
return {
data: select('core')[functionToCall](entity, kind, query),
isLoading: select('core/data').isResolving('core', functionToCall, [
entity,
kind,
query,
]),
};
});

const invalidateResolver = () => {
invalidateResolution('core', functionToCall, [entity, kind, query]);
};

return [data, isLoading, invalidateResolver];
};


1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export { ContentSearch } from './components/ContentSearch';
export { InnerBlockSlider } from './components/InnerBlockSlider';
export { IsAdmin } from './components/is-admin';
export { useHasSelectedInnerBlock } from './hooks/use-has-selected-inner-block';
export { useRequestData } from './hooks/use-request-data'
export { default as CustomBlockAppender } from './components/CustomBlockAppender';
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "1.3.0",
"version": "1.4.0",
"description": "10up Components built for the WordPress Block Editor.",
"main": "index.js",
"scripts": {
Expand Down

0 comments on commit 77c0bcb

Please sign in to comment.