diff --git a/.eslintrc b/.eslintrc index 46e95b6b..b4d224bc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,6 +3,7 @@ "rules": { "import/no-unresolved": 0, "import/no-extraneous-dependencies": 0, - "react/jsx-props-no-spreading": 0 + "react/jsx-props-no-spreading": 0, + "jsdoc/check-tag-names": 0 } } \ No newline at end of file diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 52852462..def0c3bd 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1,15 +1,7 @@ name: Run E2E Test Suite # Controls when the workflow will run -on: - pull_request: - - push: - branches: - - 'develop' - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: +on: [ push, workflow_dispatch ] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -44,4 +36,4 @@ jobs: env: video=false env: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index cb740275..177ae0e7 100644 --- a/README.md +++ b/README.md @@ -15,428 +15,33 @@ A collection of components built to be used in the block editor. These component 2. Within your block editor code, import the relevant component(s) e.g. `import { ContentPicker } from '@10up/block-components';` 3. We highly recommend you use [10up-toolkit](https://github.com/10up/10up-toolkit) to build your block files as it handles dependency extraction for you. -## ContentPicker +## APIs -A Content Picker component that allows you to pick posts and pages very easily. +- [registerBlockExtension](./api/register-block-extension/) +- [registerIcons](./api/register-icons/) -![Content picker in action](images/content-picker.gif) +## Components -### Usage +- [ClipboardButton](./components/clipboard-button/) +- [ColorSettings](./components//clipboard-button/) +- [ContentPicker](./components//content-picker/) +- [ContentSearch](./components/content-search/) +- [CustomBlockAppender](./components/custom-block-appender/) +- [IconPicker](./components/icon-picker/) +- [InnerBlockSlider](./components/inner-block-slider/) +- [IsAdmin](./components//is-admin/) +- [Optional](./components/optional/) -```js -import { ContentPicker } from '@10up/block-components'; +## Hooks -function MyComponent( props ) { +- [useFilteredList](./hooks/use-filtered-list) +- [useHasSelectedInnerBlock](./hooks/use-has-selected-inner-block/) +- [useIcons](./hooks/use-icons/) +- [useRequestData](./hooks/use-request-data/) - return ( - { console.log(pickedContent) } } - mode="post" - label={ "Please select a Post or Page:" } - contentTypes={ [ 'post', 'page' ] } - /> - ) -} -``` +## Stores -#### Props - -| Name | Type | Default | Description | -| ---------------- | ---------- | --------------------- | ---------------------------------------------------------------------- | -| `onPickChange` | `function` | `undefined` | Callback function the list of picked content gets changed | -| `label` | `string` | `''` | Renders a label for the Search Field. | -| `mode` | `string` | `'post'` | One of: `post`, `user`, `term` | -| `placeholder` | `string` | `''` | Renders placeholder text inside the Search Field. | -| `contentTypes` | `array` | `[ 'post', 'page' ]` | Names of the post types or taxonomies that should get searched | -| `maxContentItems` | `number` | `1` | Max number of items a user can select. -| `isOrderable` | `bool` | `false` | When true, will allow the user to order items. Must be used in conjunction with `maxContentItems > 1` -| `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', uuid: '...',}, {id: 1, uuid: '...', type: 'page'},... ]`. You cannot provide terms and posts to the same picker. `uuid` was added as of version 1.5.0. It is only used as the React component list key in the admin. If it is not included, `id` will be used which will cause errors if you select the same post twice. -| `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`: - -```js -apiFetch( { - path: `wp/v2/search/?search="${keyword}"&subtype="${contentTypes.join(',')}"&type=${mode}` -} )... -``` - -## ContentSearch - -A component that lets you search through posts and pages. This component is used by Content Picker. This component provides only the searching functionality and does not maintain any list of chosen items. - -### Usage - -```js -import { ContentSearch } from '@10up/block-components'; - -function MyComponent( props ) { - - return ( - { console.log(item) } } - mode="post" - label={ "Please select a Post or Page:" } - contentTypes={ [ 'post', 'page' ] } - /> - ) -} -``` - -#### Props - -| Name | Type | Default | Description | -| ---------------- | ---------- | --------------------- | ---------------------------------------------------------------------- | -| `onSelectItem` | `function` | `undefined` | Function called when a searched item is clicke | -| `label` | `string` | `''` | Renders a label for the Search Field. | -| `mode` | `string` | `'post'` | One of: `post`, `user`, `term` | -| `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 - -## ColorSetting - -A component that lets you add a `label`, `help` text and all the existing options from the core `ColorPalette` compopnent. This component can be used by any other Bock Component or a Block. This component calls the `onChange` callback with the value of the selected color and does not add/update any CSS classes. - -### Usage - -```js -import { ColorSetting } from '@10up/block-components'; - -function MyComponent( props ) { - - return ( - setColor( color ) } - /> - ) -} -``` - -#### Props - -| Name | Type | Default | isRequired | Description | -| ---------------- | ---------- | ---------- | --------------------- | ---------------------------------------------------------------------- | -| `label` | `string` | `''` | `No` | If this property is added, a label will be generated using label property as the content. | -| `hideLabelFromVision` | `bool` | `false` | `No` | If true, the label will only be visible to screen readers. | -| `help` | `string` | `''` | `No` | If this property is added, a help text will be generated using help property as the content. | -| `className` | `string` | `''` | `No` | If no className is passed only components-base-control is used. | -| `disableCustomColors` | `bool` | `false` | `No` | Whether to allow custom color or not. | -| `value` | `string` | `''` | `No` | Currently active value. | -| `clearable` | `bool` | `true` | `No` | Whether the palette should have a clearing button or not. -| `colors` | `array` | `[]` | `Yes` | Array with the colors to be shown. -| `onChange` | `function` | `undefined` | `Yes` | Callback called when a color is selected. - -## useHasSelectedInnerBlock - -Determine whether one of the inner blocks currently is selected. - -### Usage - -```js -import { useHasSelectedInnerBlock } from '@10up/block-components'; - -function BlockEdit( props ) { - const hasSelectedInnerBlock = useHasSelectedInnerBlock(props); - - return ( -
- { hasSelectedInnerBlock ? 'InnerBlocks are selected' : 'InnerBlocks are not selected' } -
- ) -} -``` - -## 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

Loading...

; - } - return ( -
-
    - {data && - data.map(({ title: { rendered: postTitle } }) => { - return
  • {postTitle}
  • ; - })} -
- -
- ); -}; -``` - -#### Single piece of data - -```js -const ExampleBockEdit = ({ className }) => { - const [data, isLoading, invalidateRequest ] = useRequestData('postType', 'post', 59); - - if (isLoading) { - return

Loading...

; - } - return ( -
- - {data &&(
{data.title.rendered}
)} - - -
- ); -}; -``` - -## IsAdmin - -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 -import { IsAdmin } from '@10up/block-components'; - -function MyComponent( props ) { - - return ( - Sorry, you are not allowed to do that

} - > -

Only Administrators can see what you put in here

-
- ) -} -``` - -#### Props - -| Name | Type | Default | Description | -| ---------- | ----------------- | -------- | -------------------------------------------------------------- | -| `fallback` | `ReactElement` | `null` | Element that will be rendered if the user is no admin | -| `children` | `ReactElement(s)` | `'null'` | Child components that will be rendered if the user is an Admin | - -## CustomBlockInserter - -This component is passed to an `InnerBlocks` instance to as it's `renderAppender` to provide a customized button that opens the Block Inserter. - -### Usage - -```js -import { CustomBlockAppender } from '@10up/block-components'; -const MyComponent = ({clientId}) => { - ( - - )} - /> -} -``` - -#### Props - -| Name | Type | Default | Description | -| ---------- | ----------------- | -------- | -------------------------------------------------------------- | -| `rootClientId` | `string` | `''` | Client it of the block | -| `buttonText` | `string` | `''` | Text to display in the button | -| `icon` | `string` | `'plus'` | Icon to display. | -| `..buttonProps` | `object` | `null'` | Any other props passed are spread onto the internal Button component. | - -## InnerBlockSlider - -This component creates a horizontal slider with inner blocks inside of it. - -### Usage - -```js -import { InnerBlockSlider } from '@10up/block-components'; -const MyComponent = ({clientId}) => { - -} -``` - -#### Props - -| Name | Type | Default | Description | -| ---------- | ----------------- | -------- | -------------------------------------------------------------- | -| `allowedBlock` | `string` | `''` | Block type to be allowed inside ofthe slider | -| `slidesPerPage` | `integer` | `1` | Number of slides to show per page | -| `parentBlockId` | `string` | `''` | Client ID of parent block. This is required. | - -## Optional - -A component that takes care of the logic of rendering nodes only when it is selected. - -### Usage - -```js -const BlockEdit = (props) => { - const { attributes, setAttributes, isSelected } = props; - const { title } = attributes; - - const blockProps = useBlockProps(); - - return ( -
- - setAttributes({ title: value }) } /> - -
- ) -} -``` - -The `` node will only render when BlockEdit is selected. - -#### Props - -| Name | Type | Default | Description | -| ---------- | ----------------- | -------- | -------------------------------------------------------------- | -| `value` | `string` | `''` | The value that will be consumed by the children. If the value is falsey the component will only be rendered if the block is selected. | - -## ClipboardButton - -This button component receives a string and copies it to the clipboard on click. - -### Usage - -```js -import { ClipboardButton } from '@10up/block-components'; - -const MyComponent = () => { - return ( -
- { console.log( 'String copied!' ) } } - labels={{copy: 'Copy text', copied: 'Text copied!'}} - disabled={false} - > -
- ); -}; -``` - -#### Props - -| Name | Type | Default | Description | -| ---------- | ----------------- | -------- | -------------------------------------------------------------- | -| `text` | `string` | `''` | The text to be copied to the clipboard | -| `onSuccess` | `function` | `undefined` | Callback function that runs after text is copied to the clipboard | -| `labels` | `object` | `{}` | Prop to assign labels to the button before and after copying the text. Set the properties `copy` and `copied` on the object to replace the default "Copy" and "Copied" text. | -| `disabled` | `bool` | `false` | Prop to enable/disable the button | - -## registerBlockExtension - -The `registerBlockExtension` API is a wrapper to make it easier to add custom settings which produce classnames to any blocks. There are a few problems with using block styles for customisations. For one an editor cannot combine block styles. So you very quickly land in a sittuation where you need to add many block styles just to give an editor the ability to choose exactly the combination of options they want. That leads to a bad user experience though as the previews take up a ton of space and also make the editor slower due to the overhead of the iframes it creates. So in many cases it is nicer to extend a bock with custom settings to achive the same goal. The process of registering your own attributes, modifying the blocks edit function, adding the new classname to the editor listing and also adding it to the frontend is rather cumbersome though. That is where this API comes in. It is a wrapper for the underlying filters that improves the editorial experience and reduces the amount of code that needs to get maintained in order to extend blocks. - -### Usage - -```js -import { registerBlockExtension } from '@10up/block-components'; - -/** - * BlockEdit - * - * a react component that will get mounted in the Editor when the block is - * selected. It is reccomended to use Slots like `BlockControls` or `InspectorControls` - * in here to put settings into the blocks toolbar or sidebar. - * - * @param {object} props block props - * @returns {JSX} - */ -function BlockEdit(props) {...} - -/** - * generateClassNames - * - * a function to generate the new className string that should get added to - * the wrapping element of the block. - * - * @param {object} attributes block attributes - * @returns {string} - */ -function generateClassNames(attributes) {...} - -registerBlockExtension( - 'core/group', - { - extensionName: 'background-patterns', - attributes: { - hasBackgroundPattern: { - type: 'boolean', - default: false, - }, - backgroundPatternShape: { - type: 'string', - default: 'dots', - }, - backgroundPatternColor: { - type: 'string', - default: 'green' - } - }, - classNameGenerator: generateClassNames, - Edit: BlockEdit, - } -); -``` - -### Options - -| Name | Type | Description | -|----------------------------|------------|---------------------------------------------------| -| blockName | `string` | Name of the block the options should get added to | -| options.extensionName | `string` | Unique Identifier of the option added | -| options.attributes | `object` | Block Attributes that should get added to the block | -| options.classNameGenerator | `funciton` | Funciton that gets passed the attributes of the block to generate a class name string | -| options.Edit | `funciton` | BlockEdit component like in `registerBlockType` only without the actual block. So onyl using slots like the `InspectorControls` is advised. | +- [iconStore](./stores/icons) ## Support Level diff --git a/api/index.js b/api/index.js new file mode 100644 index 00000000..65625723 --- /dev/null +++ b/api/index.js @@ -0,0 +1,6 @@ +export { + registerBlockExtension, + // continue to export misspelled version of api for backwards compatibility + registerBlockExtension as registerBlockExtention, +} from './register-block-extension'; +export { registerIcons } from './register-icons'; diff --git a/api/registerBlockExtension.js b/api/register-block-extension/index.js similarity index 100% rename from api/registerBlockExtension.js rename to api/register-block-extension/index.js diff --git a/api/register-block-extension/readme.md b/api/register-block-extension/readme.md new file mode 100644 index 00000000..97e8e5da --- /dev/null +++ b/api/register-block-extension/readme.md @@ -0,0 +1,70 @@ +# registerBlockExtension + +The `registerBlockExtension` API is a wrapper to make it easier to add custom settings which produce classnames to any blocks. There are a few problems with using block styles for customizations. For one an editor cannot combine block styles. So you very quickly land in a situation where you need to add many block styles just to give an editor the ability to choose exactly the combination of options they want. That leads to a bad user experience though as the previews take up a ton of space and also make the editor slower due to the overhead of the iframes it creates. So in many cases it is nicer to extend a bock with custom settings to achieve the same goal. The process of registering your own attributes, modifying the blocks edit function, adding the new classname to the editor listing and also adding it to the frontend is rather cumbersome though. That is where this API comes in. It is a wrapper for the underlying filters that improves the editorial experience and reduces the amount of code that needs to get maintained in order to extend blocks. + +## Usage + +```js +import { registerBlockExtension } from '@10up/block-components'; + +/** + * additional block attributes object + */ +const additionalAttributes = { + hasBackgroundPattern: { + type: 'boolean', + default: false, + }, + backgroundPatternShape: { + type: 'string', + default: 'dots', + }, + backgroundPatternColor: { + type: 'string', + default: 'green' + } +} + +/** + * BlockEdit + * + * a react component that will get mounted in the Editor when the block is + * selected. It is recommended to use Slots like `BlockControls` or `InspectorControls` + * in here to put settings into the blocks toolbar or sidebar. + * + * @param {object} props block props + * @returns {JSX} + */ +function BlockEdit(props) {...} + +/** + * generateClassNames + * + * a function to generate the new className string that should get added to + * the wrapping element of the block. + * + * @param {object} attributes block attributes + * @returns {string} + */ +function generateClassNames(attributes) {...} + +registerBlockExtension( + 'core/group', + { + extensionName: 'background-patterns', + attributes: additionalAttributes, + classNameGenerator: generateClassNames, + Edit: BlockEdit, + } +); +``` + +## Options + +| Name | Type | Description | +|----------------------------|------------|---------------------------------------------------| +| blockName | `string` | Name of the block the options should get added to | +| options.extensionName | `string` | Unique Identifier of the option added | +| options.attributes | `object` | Block Attributes that should get added to the block | +| options.classNameGenerator | `function` | Function that gets passed the attributes of the block to generate a class name string | +| options.Edit | `function` | BlockEdit component like in `registerBlockType` only without the actual block. So only using slots like the `InspectorControls` is advised. | diff --git a/icons/api/register.js b/api/register-icons/index.js similarity index 71% rename from icons/api/register.js rename to api/register-icons/index.js index 8338d8b9..3b2d8ed3 100644 --- a/icons/api/register.js +++ b/api/register-icons/index.js @@ -1,6 +1,6 @@ import { dispatch } from '@wordpress/data'; -import { store as iconStore } from '../store/index'; +import { iconStore } from '../../stores'; export function registerIcons(options) { dispatch(iconStore).registerIconSet(options); diff --git a/components/ClipboardButton/index.js b/components/clipboard-button/index.js similarity index 100% rename from components/ClipboardButton/index.js rename to components/clipboard-button/index.js diff --git a/components/clipboard-button/readme.md b/components/clipboard-button/readme.md new file mode 100644 index 00000000..972c2c00 --- /dev/null +++ b/components/clipboard-button/readme.md @@ -0,0 +1,31 @@ +# ClipboardButton + +This button component receives a string and copies it to the clipboard on click. + +## Usage + +```js +import { ClipboardButton } from '@10up/block-components'; + +const MyComponent = () => { + return ( +
+ { console.log( 'String copied!' ) } } + labels={{copy: 'Copy text', copied: 'Text copied!'}} + disabled={false} + > +
+ ); +}; +``` + +## Props + +| Name | Type | Default | Description | +| ---------- | ----------------- | -------- | -------------------------------------------------------------- | +| `text` | `string` | `''` | The text to be copied to the clipboard | +| `onSuccess` | `function` | `undefined` | Callback function that runs after text is copied to the clipboard | +| `labels` | `object` | `{}` | Prop to assign labels to the button before and after copying the text. Set the properties `copy` and `copied` on the object to replace the default "Copy" and "Copied" text. | +| `disabled` | `bool` | `false` | Prop to enable/disable the button | diff --git a/components/ColorSetting/index.js b/components/color-settings/index.js similarity index 50% rename from components/ColorSetting/index.js rename to components/color-settings/index.js index d2019fa8..ba65daa7 100644 --- a/components/ColorSetting/index.js +++ b/components/color-settings/index.js @@ -25,64 +25,64 @@ import { useInstanceId } from '@wordpress/compose'; * @property {boolean} clearable Whether the palette should have a clearing button or not. * * @param {ColorSettingProps} props ColorSetting Props - * @return {*} React Element + * @returns {*} React Element */ // eslint-disable-next-line import/prefer-default-export export const ColorSetting = (props) => { - const { - label, - help, - className, - hideLabelFromVision, - colors, - value, - onChange, - disableCustomColors, - clearable, - ...rest - } = props; + const { + label, + help, + className, + hideLabelFromVision, + colors, + value, + onChange, + disableCustomColors, + clearable, + ...rest + } = props; - const instanceId = useInstanceId(ColorSetting); - const id = `color-settings-${instanceId}`; + const instanceId = useInstanceId(ColorSetting); + const id = `color-settings-${instanceId}`; - return ( - - - - ); + return ( + + + + ); }; ColorSetting.defaultProps = { - label: '', - hideLabelFromVision: false, - help: '', - className: '', - disableCustomColors: false, - value: '', - clearable: true, + label: '', + hideLabelFromVision: false, + help: '', + className: '', + disableCustomColors: false, + value: '', + clearable: true, }; ColorSetting.propTypes = { - label: PropTypes.string, - hideLabelFromVision: PropTypes.bool, - help: PropTypes.string, - className: PropTypes.string, - disableCustomColors: PropTypes.bool, - value: PropTypes.string, - clearable: PropTypes.bool, - colors: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, + label: PropTypes.string, + hideLabelFromVision: PropTypes.bool, + help: PropTypes.string, + className: PropTypes.string, + disableCustomColors: PropTypes.bool, + value: PropTypes.string, + clearable: PropTypes.bool, + colors: PropTypes.array.isRequired, + onChange: PropTypes.func.isRequired, }; diff --git a/components/color-settings/readme.md b/components/color-settings/readme.md new file mode 100644 index 00000000..717fbc35 --- /dev/null +++ b/components/color-settings/readme.md @@ -0,0 +1,36 @@ +# ColorSetting + +A component that lets you add a `label`, `help` text and all the existing options from the core `ColorPalette` component. This component can be used by any other Bock Component or a Block. This component calls the `onChange` callback with the value of the selected color and does not add/update any CSS classes. + +## Usage + +```js +import { ColorSetting } from '@10up/block-components'; + +function MyComponent( props ) { + + return ( + setColor( color ) } + /> + ) +} +``` + +## Props + +| Name | Type | Default | isRequired | Description | +| ---------------- | ---------- | ---------- | --------------------- | ---------------------------------------------------------------------- | +| `label` | `string` | `''` | `No` | If this property is added, a label will be generated using label property as the content. | +| `hideLabelFromVision` | `bool` | `false` | `No` | If true, the label will only be visible to screen readers. | +| `help` | `string` | `''` | `No` | If this property is added, a help text will be generated using help property as the content. | +| `className` | `string` | `''` | `No` | If no className is passed only components-base-control is used. | +| `disableCustomColors` | `bool` | `false` | `No` | Whether to allow custom color or not. | +| `value` | `string` | `''` | `No` | Currently active value. | +| `clearable` | `bool` | `true` | `No` | Whether the palette should have a clearing button or not. +| `colors` | `array` | `[]` | `Yes` | Array with the colors to be shown. +| `onChange` | `function` | `undefined` | `Yes` | Callback called when a color is selected. diff --git a/components/ContentPicker/PickedItem.js b/components/content-picker/PickedItem.js similarity index 100% rename from components/ContentPicker/PickedItem.js rename to components/content-picker/PickedItem.js diff --git a/components/ContentPicker/SortableList.js b/components/content-picker/SortableList.js similarity index 100% rename from components/ContentPicker/SortableList.js rename to components/content-picker/SortableList.js diff --git a/components/ContentPicker/index.js b/components/content-picker/index.js similarity index 74% rename from components/ContentPicker/index.js rename to components/content-picker/index.js index ef8c80de..1606c4dd 100644 --- a/components/ContentPicker/index.js +++ b/components/content-picker/index.js @@ -2,10 +2,10 @@ import PropTypes from 'prop-types'; import arrayMove from 'array-move'; import styled from '@emotion/styled'; import { select } from '@wordpress/data'; -import { useState, useEffect, useMemo } from '@wordpress/element'; +import { useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { v4 as uuidv4 } from 'uuid'; -import { ContentSearch } from '../ContentSearch'; +import { ContentSearch } from '../content-search'; import SortableList from './SortableList'; const NAMESPACE = 'tenup-content-picker'; @@ -37,19 +37,19 @@ const ContentPickerWrapper = styled('div')` * Content Picker * * @param {object} props React props - * @param props.label - * @param props.mode - * @param props.contentTypes - * @param props.placeholder - * @param props.onPickChange - * @param props.maxContentItems - * @param props.isOrderable - * @param props.singlePickedLabel - * @param props.multiPickedLabel - * @param props.content - * @param props.uniqueContentItems - * @param props.excludeCurrentPost - * @param props.perPage + * @param {string} props.label label for the picker + * @param {string} props.mode mode of the picker + * @param {Array} props.contentTypes array of content types to filter by + * @param {string} props.placeholder placeholder text for the search input + * @param {Function} props.onPickChange callback for when the picker changes + * @param {number} props.maxContentItems max number of items to show in the picker + * @param {boolean} props.isOrderable whether or not the picker is sortable + * @param {string} props.singlePickedLabel label for the single picked item + * @param {string} props.multiPickedLabel label for the multi picked item + * @param {Array} props.content items to show in the picker + * @param {boolean} props.uniqueContentItems whether or not the picker should only show unique items + * @param {boolean} props.excludeCurrentPost whether or not to exclude the current post from the picker + * @param {number} props.perPage number of items to show per page * @returns {*} React JSX */ const ContentPicker = ({ @@ -62,17 +62,11 @@ const ContentPicker = ({ isOrderable, singlePickedLabel, multiPickedLabel, - content: presetContent, + content, uniqueContentItems, excludeCurrentPost, perPage, }) => { - const [content, setContent] = useState([]); - - useEffect(() => { - setContent(presetContent); - }, [presetContent]); - const currentPostId = select('core/editor')?.getCurrentPostId(); /** @@ -88,31 +82,27 @@ const ContentPicker = ({ } } - // Run onPickChange callback when content changes. - useEffect(() => { - onPickChange(content); - }, [content]); - const handleSelect = (item) => { - setContent((previousContent) => [ + const newItems = [ { id: item.id, uuid: uuidv4(), type: 'subtype' in item ? item.subtype : item.type, }, - ...previousContent, - ]); + ...content, + ]; + onPickChange(newItems); }; const onDeleteItem = (deletedItem) => { - setContent((previousContent) => { - return previousContent.filter(({ id, uuid }) => { - if (deletedItem.uuid) { - return uuid !== deletedItem.uuid; - } - return id !== deletedItem.id; - }); + const newItems = content.filter(({ id, uuid }) => { + if (deletedItem.uuid) { + return uuid !== deletedItem.uuid; + } + return id !== deletedItem.id; }); + + onPickChange(newItems); }; const excludeItems = useMemo(() => { @@ -143,7 +133,7 @@ const ContentPicker = ({ label && (
{label} @@ -172,7 +162,7 @@ const ContentPicker = ({ onSortEnd={({ oldIndex, newIndex }) => { const newContent = [...arrayMove(content, oldIndex, newIndex)]; - setContent(newContent); + onPickChange(newContent); }} /> diff --git a/components/content-picker/readme.md b/components/content-picker/readme.md new file mode 100644 index 00000000..23315b01 --- /dev/null +++ b/components/content-picker/readme.md @@ -0,0 +1,48 @@ +# ContentPicker + +A Content Picker component that allows you to pick posts and pages very easily. + +![Content picker in action](images/content-picker.gif) + +## Usage + +```js +import { ContentPicker } from '@10up/block-components'; + +function MyComponent( props ) { + + return ( + { console.log(pickedContent) } } + mode="post" + label={ "Please select a Post or Page:" } + contentTypes={ [ 'post', 'page' ] } + /> + ) +} +``` + +## Props + +| Name | Type | Default | Description | +| ---------------- | ---------- | --------------------- | ---------------------------------------------------------------------- | +| `onPickChange` | `function` | `undefined` | Callback function the list of picked content gets changed | +| `label` | `string` | `''` | Renders a label for the Search Field. | +| `mode` | `string` | `'post'` | One of: `post`, `user`, `term` | +| `placeholder` | `string` | `''` | Renders placeholder text inside the Search Field. | +| `contentTypes` | `array` | `[ 'post', 'page' ]` | Names of the post types or taxonomies that should get searched | +| `maxContentItems` | `number` | `1` | Max number of items a user can select. +| `isOrderable` | `bool` | `false` | When true, will allow the user to order items. Must be used in conjunction with `maxContentItems > 1` +| `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 pre-populate picker with. Must be in the format of: `[{id: 1, type: 'post', uuid: '...',}, {id: 1, uuid: '...', type: 'page'},... ]`. You cannot provide terms and posts to the same picker. `uuid` was added as of version 1.5.0. It is only used as the React component list key in the admin. If it is not included, `id` will be used which will cause errors if you select the same post twice. +| `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`: + +```js +apiFetch( { + path: `wp/v2/search/?search="${keyword}"&subtype="${contentTypes.join(',')}"&type=${mode}` +} )... +``` diff --git a/components/ContentSearch/SearchItem.js b/components/content-search/SearchItem.js similarity index 87% rename from components/ContentSearch/SearchItem.js rename to components/content-search/SearchItem.js index b5c5b8ba..2a8794c3 100644 --- a/components/ContentSearch/SearchItem.js +++ b/components/content-search/SearchItem.js @@ -24,11 +24,12 @@ const ButtonStyled = styled(Button)` * SearchItem * * @param {object} props react props - * @param props.suggestion - * @param props.onClick - * @param props.searchTerm - * @param props.isSelected - * @param props.id + * @param {object} props.suggestion suggestion object + * @param {Array} props.contentTypes array of content types + * @param {Function} props.onClick callback for when the item is clicked + * @param {string} props.searchTerm the search term + * @param {boolean} props.isSelected whether the item is selected + * @param {string} props.id the id of the item * @returns {*} React JSX */ const SearchItem = ({ suggestion, onClick, searchTerm, isSelected, id, contentTypes }) => { diff --git a/components/ContentSearch/index.js b/components/content-search/index.js similarity index 93% rename from components/ContentSearch/index.js rename to components/content-search/index.js index 69626b7e..2214f55e 100644 --- a/components/ContentSearch/index.js +++ b/components/content-search/index.js @@ -1,12 +1,14 @@ +/* eslint-disable guard-for-in */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable react/jsx-no-bind */ import { TextControl, Spinner, NavigableMenu, Button } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; -import { useState, useRef, useEffect, useCallback } from '@wordpress/element'; // eslint-disable-line +import { useState, useRef, useEffect, useCallback } from '@wordpress/element'; import PropTypes from 'prop-types'; import { __ } from '@wordpress/i18n'; +// eslint-disable-next-line no-unused-vars import { jsx, css } from '@emotion/react'; -import { search } from '@wordpress/icons'; import SearchItem from './SearchItem'; -import SortableList from '../ContentPicker/SortableList'; /** @jsx jsx */ const NAMESPACE = 'tenup-content-search'; @@ -14,15 +16,7 @@ const NAMESPACE = 'tenup-content-search'; // Equalize height of list icons to match loader in order to reduce jumping. const listMinHeight = '46px'; -const ContentSearch = ({ - onSelectItem, - placeholder, - label, - contentTypes, - mode, - excludeItems, - perPage, -}) => { +const ContentSearch = ({ onSelectItem, placeholder, label, contentTypes, mode, perPage }) => { const [searchString, setSearchString] = useState(''); const [searchQueries, setSearchQueries] = useState({}); const [selectedItem, setSelectedItem] = useState(null); @@ -36,7 +30,7 @@ const ContentSearch = ({ * update the selected item in state to either the selected item or null if the * selected item does not have a valid id * - * @param {*} item + * @param {*} item item */ function handleOnNavigate(item) { if (item === 0) { @@ -52,7 +46,7 @@ const ContentSearch = ({ * reset the search input & item container * trigger the onSelectItem callback passed in via props * - * @param {*} item + * @param {*} item item */ function handleItemSelection(item) { setSearchString(''); @@ -77,6 +71,7 @@ const ContentSearch = ({ return searchQuery; }, + // eslint-disable-next-line react-hooks/exhaustive-deps [perPage, contentTypes], ); @@ -107,21 +102,6 @@ const ContentSearch = ({ [mode], ); - const filterResults = useCallback( - (results) => { - return results.filter((result) => { - let keep = true; - - if (excludeItems.length) { - keep = excludeItems.every((item) => item.id !== result.id); - } - - return keep; - }); - }, - [excludeItems], - ); - /** * handleSearchStringChange * @@ -129,6 +109,7 @@ const ContentSearch = ({ * search for posts/terms that match and return them to the autocomplete component. * * @param {string} keyword search query string + * @param {string} page page query string */ const handleSearchStringChange = (keyword, page) => { if (keyword.trim() === '') { @@ -217,7 +198,7 @@ const ContentSearch = ({ }); }); }) - .catch((error, code) => { + .catch((error) => { // fetch_error means the request was aborted if (error.code !== 'fetch_error') { setSearchQueries((queries) => { @@ -240,6 +221,7 @@ const ContentSearch = ({ }); } }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchQueries, searchString, currentPage]); let searchResults = null; @@ -391,7 +373,6 @@ ContentSearch.defaultProps = { placeholder: '', perPage: 20, label: '', - excludeItems: [], mode: 'post', onSelectItem: () => { console.log('Select!'); // eslint-disable-line no-console @@ -403,9 +384,8 @@ ContentSearch.propTypes = { mode: PropTypes.string, onSelectItem: PropTypes.func, placeholder: PropTypes.string, - excludeItems: PropTypes.array, label: PropTypes.string, - perPage: PropTypes.number + perPage: PropTypes.number, }; export { ContentSearch }; diff --git a/components/content-search/readme.md b/components/content-search/readme.md new file mode 100644 index 00000000..658991c9 --- /dev/null +++ b/components/content-search/readme.md @@ -0,0 +1,33 @@ +# ContentSearch + +A component that lets you search through posts and pages. This component is used by Content Picker. This component provides only the searching functionality and does not maintain any list of chosen items. + +## Usage + +```js +import { ContentSearch } from '@10up/block-components'; + +function MyComponent( props ) { + + return ( + { console.log(item) } } + mode="post" + label={ "Please select a Post or Page:" } + contentTypes={ [ 'post', 'page' ] } + /> + ) +} +``` + +## Props + +| Name | Type | Default | Description | +| ---------------- | ---------- | --------------------- | ---------------------------------------------------------------------- | +| `onSelectItem` | `function` | `undefined` | Function called when a searched item is clicked | +| `label` | `string` | `''` | Renders a label for the Search Field. | +| `mode` | `string` | `'post'` | One of: `post`, `user`, `term` | +| `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 diff --git a/components/CustomBlockAppender/index.js b/components/custom-block-appender/index.js similarity index 91% rename from components/CustomBlockAppender/index.js rename to components/custom-block-appender/index.js index c1fde8b9..d34c768e 100644 --- a/components/CustomBlockAppender/index.js +++ b/components/custom-block-appender/index.js @@ -7,8 +7,6 @@ import PropTypes from 'prop-types'; /** * WordPress dependencies */ -// eslint-disable-next-line import/no-extraneous-dependencies -import { Fragment } from '@wordpress/element'; import { Inserter } from '@wordpress/block-editor'; import { Button } from '@wordpress/components'; @@ -20,6 +18,7 @@ import { Button } from '@wordpress/components'; * * @param {object} props All props sent to this component. * @param {string} props.rootClientId Client ID of the block where this is being used. + * @param {string} props.className class names to be added to the button. * @param {string} [props.buttonText] Text to display in the Button. * @param {string} [props.icon] The icon to use. * @returns {Function} The component. @@ -65,4 +64,4 @@ CustomBlockAppender.defaultProps = { className: 'custom-block-appender', }; -export default CustomBlockAppender; +export { CustomBlockAppender }; diff --git a/components/custom-block-appender/readme.md b/components/custom-block-appender/readme.md new file mode 100644 index 00000000..9e3f6698 --- /dev/null +++ b/components/custom-block-appender/readme.md @@ -0,0 +1,32 @@ +# CustomBlockAppender + +This component is passed to an `InnerBlocks` instance to as it's `renderAppender` to provide a customized button that opens the Block Inserter. + +## Usage + +```js +import { CustomBlockAppender } from '@10up/block-components'; +const MyComponent = ({clientId}) => { + ( + + )} + /> +} +``` + +## Props + +| Name | Type | Default | Description | +| ---------- | ----------------- | -------- | -------------------------------------------------------------- | +| `rootClientId` | `string` | `''` | Client it of the block | +| `buttonText` | `string` | `''` | Text to display in the button | +| `icon` | `string` | `'plus'` | Icon to display. | +| `..buttonProps` | `object` | `null'` | Any other props passed are spread onto the internal Button component. | diff --git a/components/IconPicker/icon-picker-toolbar-button.js b/components/icon-picker/icon-picker-toolbar-button.js similarity index 79% rename from components/IconPicker/icon-picker-toolbar-button.js rename to components/icon-picker/icon-picker-toolbar-button.js index e1da1c32..18ec1097 100644 --- a/components/IconPicker/icon-picker-toolbar-button.js +++ b/components/icon-picker/icon-picker-toolbar-button.js @@ -21,18 +21,23 @@ const StyledIconPickerDropdown = styled(IconPicker)` * @returns {*} */ export const IconPickerToolbarButton = (props) => { + const { + value: { name, iconSet }, + buttonLabel, + } = props; + return ( ( } + icon={} > - {props?.buttonLabel ?? __('Select Icon')} + {buttonLabel ?? __('Select Icon')} )} renderContent={() => } diff --git a/components/IconPicker/icon-picker.js b/components/icon-picker/icon-picker.js similarity index 97% rename from components/IconPicker/icon-picker.js rename to components/icon-picker/icon-picker.js index 1eaa07a0..0770da57 100644 --- a/components/IconPicker/icon-picker.js +++ b/components/icon-picker/icon-picker.js @@ -10,7 +10,7 @@ import { SearchControl, } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; -import { useState } from '@wordpress/element'; +import { useState, memo } from '@wordpress/element'; import { useIcons } from '../../hooks/use-icons'; import { useFilteredList } from '../../hooks/use-filtered-list'; @@ -105,12 +105,12 @@ const IconGrid = (props) => { {icons.map((icon) => { const isChecked = selectedIcon?.name === icon.name && selectedIcon?.iconSet === icon.iconSet; - const Label = () => ( + const Label = memo(() => ( {icon.label} - ); + )); return ( { const { value, ...rest } = props; - const IconButton = ({ onToggle }) => ( - + const IconButton = useCallback( + ({ onToggle }) => ( + + ), + [value, rest], ); IconButton.propTypes = { diff --git a/components/index.js b/components/index.js new file mode 100644 index 00000000..12f2c431 --- /dev/null +++ b/components/index.js @@ -0,0 +1,9 @@ +export { IsAdmin } from './is-admin'; +export { Optional } from './optional'; +export { InnerBlockSlider } from './inner-block-slider'; +export { IconPicker, Icon, IconPickerToolbarButton, InlineIconPicker } from './icon-picker'; +export { CustomBlockAppender } from './custom-block-appender'; +export { ContentSearch } from './content-search'; +export { ContentPicker } from './content-picker'; +export { ColorSetting } from './color-settings'; +export { ClipboardButton } from './clipboard-button'; diff --git a/components/InnerBlockSlider/icons.js b/components/inner-block-slider/icons.js similarity index 100% rename from components/InnerBlockSlider/icons.js rename to components/inner-block-slider/icons.js diff --git a/components/InnerBlockSlider/index.js b/components/inner-block-slider/index.js similarity index 97% rename from components/InnerBlockSlider/index.js rename to components/inner-block-slider/index.js index dee7af93..7104c761 100644 --- a/components/InnerBlockSlider/index.js +++ b/components/inner-block-slider/index.js @@ -4,6 +4,7 @@ import { createBlock } from '@wordpress/blocks'; import { InnerBlocks } from '@wordpress/block-editor'; import PropTypes from 'prop-types'; /** @jsx jsx */ +// eslint-disable-next-line no-unused-vars import { jsx, css } from '@emotion/react'; import { ChevronLeft, ChevronRight } from './icons'; @@ -72,6 +73,7 @@ const InnerBlockSlider = ({ setCurrentPage(totalPages); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [slideBlocks.length]); const slidesCSS = css` diff --git a/components/inner-block-slider/readme.md b/components/inner-block-slider/readme.md new file mode 100644 index 00000000..de160427 --- /dev/null +++ b/components/inner-block-slider/readme.md @@ -0,0 +1,24 @@ +# InnerBlockSlider + +This component creates a horizontal slider with inner blocks inside of it. + +## Usage + +```js +import { InnerBlockSlider } from '@10up/block-components'; +const MyComponent = ({clientId}) => { + +} +``` + +## Props + +| Name | Type | Default | Description | +| ---------- | ----------------- | -------- | -------------------------------------------------------------- | +| `allowedBlock` | `string` | `''` | Block types to be allowed inside the slider | +| `slidesPerPage` | `integer` | `1` | Number of slides to show per page | +| `parentBlockId` | `string` | `''` | Client ID of parent block. This is required. | diff --git a/components/is-admin.js b/components/is-admin/index.js similarity index 61% rename from components/is-admin.js rename to components/is-admin/index.js index 79d7c2f6..a674bad5 100644 --- a/components/is-admin.js +++ b/components/is-admin/index.js @@ -4,17 +4,18 @@ import { useSelect } from '@wordpress/data'; * IsAdmin * * A wrapper component that checks wether the current user has admin capabilities - * and only returns the child omponents if the user is an admin. You can pass a + * and only returns the child components if the user is an admin. You can pass a * fallback component via the fallback prop. * * @param {object} props react props - * @param props.fallback - * @param props.children + * @param {*} props.fallback fallback component + * @param {*} props.children child components + * @returns {*} */ -export function IsAdmin({ fallback = null, children }) { +export const IsAdmin = ({ fallback = null, children }) => { const hasAdminPermissions = useSelect( (select) => select('core').canUser('read', 'users?roles=1'), [], ); return hasAdminPermissions ? children : fallback; -} +}; diff --git a/components/is-admin/readme.md b/components/is-admin/readme.md new file mode 100644 index 00000000..5c72017c --- /dev/null +++ b/components/is-admin/readme.md @@ -0,0 +1,27 @@ +# IsAdmin + +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 +import { IsAdmin } from '@10up/block-components'; + +function MyComponent( props ) { + + return ( + Sorry, you are not allowed to do that

} + > +

Only Administrators can see what you put in here

+
+ ) +} +``` + +### Props + +| Name | Type | Default | Description | +| ---------- | ----------------- | -------- | -------------------------------------------------------------- | +| `fallback` | `ReactElement` | `null` | Element that will be rendered if the user is no admin | +| `children` | `ReactElement(s)` | `'null'` | Child components that will be rendered if the user is an Admin | diff --git a/components/Optional/index.js b/components/optional/index.js similarity index 100% rename from components/Optional/index.js rename to components/optional/index.js diff --git a/components/optional/readme.md b/components/optional/readme.md new file mode 100644 index 00000000..4da1c403 --- /dev/null +++ b/components/optional/readme.md @@ -0,0 +1,30 @@ +# Optional + +A component that takes care of the logic of rendering nodes only when it is selected. + +## Usage + +```js +const BlockEdit = (props) => { + const { attributes, setAttributes, isSelected } = props; + const { title } = attributes; + + const blockProps = useBlockProps(); + + return ( +
+ + setAttributes({ title: value }) } /> + +
+ ) +} +``` + +The `` node will only render when BlockEdit is selected. + +## Props + +| Name | Type | Default | Description | +| ---------- | ----------------- | -------- | -------------------------------------------------------------- | +| `value` | `string` | `''` | The value that will be consumed by the children. If the value is falsy the component will only be rendered if the block is selected. | diff --git a/example/src/blocks/hello-world/index.js b/example/src/blocks/hello-world/index.js index 8cf7832c..4765224b 100644 --- a/example/src/blocks/hello-world/index.js +++ b/example/src/blocks/hello-world/index.js @@ -18,7 +18,7 @@ registerBlockType( `${ NAMESPACE }/hello-world`, { }, attributes: { selectedPost: { - type: 'object' + type: 'array' } }, transforms: {}, @@ -38,12 +38,12 @@ registerBlockType( `${ NAMESPACE }/hello-world`, { <> - { !!selectedPost && -
selected post
- }
@@ -51,8 +51,12 @@ registerBlockType( `${ NAMESPACE }/hello-world`, { diff --git a/hooks/index.js b/hooks/index.js new file mode 100644 index 00000000..ab8e129f --- /dev/null +++ b/hooks/index.js @@ -0,0 +1,4 @@ +export { useHasSelectedInnerBlock } from './use-has-selected-inner-block'; +export { useRequestData } from './use-request-data'; +export { useIcons, useIcon } from './use-icons'; +export { useFilteredList } from './use-filtered-list'; diff --git a/hooks/use-filtered-list.js b/hooks/use-filtered-list/index.js similarity index 100% rename from hooks/use-filtered-list.js rename to hooks/use-filtered-list/index.js diff --git a/hooks/use-has-selected-inner-block.js b/hooks/use-has-selected-inner-block/index.js similarity index 80% rename from hooks/use-has-selected-inner-block.js rename to hooks/use-has-selected-inner-block/index.js index be1448ea..01bf515d 100644 --- a/hooks/use-has-selected-inner-block.js +++ b/hooks/use-has-selected-inner-block/index.js @@ -4,8 +4,8 @@ import { useSelect } from '@wordpress/data'; * useHasSelectedInnerBlock * Determine whether one of the inner blocks currently is selected * - * @param {object} props - * @param props.clientId + * @param {object} props react props + * @param {string} props.clientId client id of the block * @returns {boolean} wether the block is the ancestor of selected blocks */ export function useHasSelectedInnerBlock({ clientId }) { diff --git a/hooks/use-has-selected-inner-block/readme.md b/hooks/use-has-selected-inner-block/readme.md new file mode 100644 index 00000000..d637e0cb --- /dev/null +++ b/hooks/use-has-selected-inner-block/readme.md @@ -0,0 +1,19 @@ +# useHasSelectedInnerBlock + +Determine whether one of the inner blocks currently is selected. + +## Usage + +```js +import { useHasSelectedInnerBlock } from '@10up/block-components'; + +function BlockEdit( props ) { + const hasSelectedInnerBlock = useHasSelectedInnerBlock(props); + + return ( +
+ { hasSelectedInnerBlock ? 'InnerBlocks are selected' : 'InnerBlocks are not selected' } +
+ ) +} +``` diff --git a/hooks/use-icons.js b/hooks/use-icons/index.js similarity index 95% rename from hooks/use-icons.js rename to hooks/use-icons/index.js index 68f885c8..3c0ee66a 100644 --- a/hooks/use-icons.js +++ b/hooks/use-icons/index.js @@ -1,6 +1,6 @@ import { useSelect } from '@wordpress/data'; import { useState, useEffect } from '@wordpress/element'; -import { store as iconStore } from '../icons/store'; +import { iconStore } from '../../stores'; function transformIcons(iconSet) { return iconSet.icons.map((icon) => ({ ...icon, iconSet: iconSet.name })); diff --git a/hooks/use-request-data.js b/hooks/use-request-data/index.js similarity index 100% rename from hooks/use-request-data.js rename to hooks/use-request-data/index.js diff --git a/hooks/use-request-data/readme.md b/hooks/use-request-data/readme.md new file mode 100644 index 00000000..923e4822 --- /dev/null +++ b/hooks/use-request-data/readme.md @@ -0,0 +1,67 @@ +# 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

Loading...

; + } + return ( +
+
    + {data && + data.map(({ title: { rendered: postTitle } }) => { + return
  • {postTitle}
  • ; + })} +
+ +
+ ); +}; +``` + +## Single piece of data + +```js +const ExampleBockEdit = ({ className }) => { + const [data, isLoading, invalidateRequest ] = useRequestData('postType', 'post', 59); + + if (isLoading) { + return

Loading...

; + } + return ( +
+ + {data &&(
{data.title.rendered}
)} + + +
+ ); +}; +``` diff --git a/index.js b/index.js index aa95de20..b9ae54ab 100644 --- a/index.js +++ b/index.js @@ -1,24 +1,4 @@ -export { ContentPicker } from './components/ContentPicker'; -export { ContentSearch } from './components/ContentSearch'; -export { InnerBlockSlider } from './components/InnerBlockSlider'; -export { ClipboardButton } from './components/ClipboardButton'; -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'; -export { - registerBlockExtension, - // continue to export misspelled version of api for backwards compatibility - registerBlockExtension as registerBlockExtention, -} from './api/registerBlockExtension'; -export { useIcons, useIcon } from './hooks/use-icons'; -export { useFilteredList } from './hooks/use-filtered-list'; -export { - IconPicker, - Icon, - IconPickerToolbarButton, - InlineIconPicker, -} from './components/IconPicker'; -export { registerIcons } from './icons/api/register'; -export { store as iconStore } from './icons/store'; -export { ColorSetting } from './components/ColorSetting'; +export * from './api'; +export * from './stores'; +export * from './hooks'; +export * from './components'; diff --git a/package-lock.json b/package-lock.json index f9fe3ace..03ce8dd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@10up/block-components", - "version": "1.7.1-next.6", + "version": "1.8.1-next.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@10up/block-components", - "version": "1.7.1-next.6", + "version": "1.8.1-next.1", "license": "GPL-2.0-or-later", "dependencies": { "@emotion/react": "^11.1.5", diff --git a/package.json b/package.json index 5b151589..7bd4969d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "1.8.0", + "version": "1.8.1", "description": "10up Components built for the WordPress Block Editor.", "main": "./dist/index.js", "source": "index.js", @@ -11,6 +11,7 @@ "lint": "10up-toolkit lint-js", "test": "10up-toolkit test-unit-jest", "build": "10up-toolkit build", + "start": "10up-toolkit start", "test:e2e": "cypress open" }, "repository": { diff --git a/icons/store/__snapshots__/reducer.test.js.snap b/stores/icons/__snapshots__/reducer.test.js.snap similarity index 100% rename from icons/store/__snapshots__/reducer.test.js.snap rename to stores/icons/__snapshots__/reducer.test.js.snap diff --git a/icons/store/actions.js b/stores/icons/actions.js similarity index 100% rename from icons/store/actions.js rename to stores/icons/actions.js diff --git a/icons/store/index.js b/stores/icons/index.js similarity index 100% rename from icons/store/index.js rename to stores/icons/index.js diff --git a/icons/store/readme.md b/stores/icons/readme.md similarity index 100% rename from icons/store/readme.md rename to stores/icons/readme.md diff --git a/icons/store/reducer.js b/stores/icons/reducer.js similarity index 94% rename from icons/store/reducer.js rename to stores/icons/reducer.js index 0185bf5e..30f53d64 100644 --- a/icons/store/reducer.js +++ b/stores/icons/reducer.js @@ -1,3 +1,4 @@ +/* eslint-disable default-param-last */ /** * Reducer managing the block style variations. * diff --git a/icons/store/reducer.test.js b/stores/icons/reducer.test.js similarity index 100% rename from icons/store/reducer.test.js rename to stores/icons/reducer.test.js diff --git a/icons/store/selectors.js b/stores/icons/selectors.js similarity index 92% rename from icons/store/selectors.js rename to stores/icons/selectors.js index 46274325..315ab997 100644 --- a/icons/store/selectors.js +++ b/stores/icons/selectors.js @@ -1,3 +1,4 @@ +/* eslint-disable no-prototype-builtins */ /** * Returns all icons sets * @@ -16,7 +17,7 @@ export function getIconSets(state) { * @param {object} state Data state. * @param {string} name Name of the Icon Set. * - * @returns {?object} Icon Set. + * @returns {object?} Icon Set. */ export function getIconSet(state, name) { const { iconSets } = state; @@ -44,7 +45,7 @@ export function getIcons(state, name) { * @param {string} name Name of the Icon Set. * @param {string} iconName Name of the iconName. * - * @returns {Icon?} List of Icons. + * @returns {object?} Icon. */ export function getIcon(state, name, iconName) { const { iconSets } = state; diff --git a/icons/store/selectors.test.js b/stores/icons/selectors.test.js similarity index 100% rename from icons/store/selectors.test.js rename to stores/icons/selectors.test.js diff --git a/stores/index.js b/stores/index.js new file mode 100644 index 00000000..1a6423d9 --- /dev/null +++ b/stores/index.js @@ -0,0 +1 @@ +export { store as iconStore } from './icons';