Skip to content

Commit

Permalink
Merge pull request #178 from 10up/feature/post-level-components
Browse files Browse the repository at this point in the history
Add Post level components to allow easy building of page level blocks
  • Loading branch information
fabiankaegy authored Feb 15, 2023
2 parents 1548d6d + 3682e78 commit 45a4c9d
Show file tree
Hide file tree
Showing 70 changed files with 2,286 additions and 12 deletions.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ A collection of components built to be used in the block editor. These component
- [Repeater](./components/repeater/)
- [RichTextCharacterLimit](./components/rich-text-character-limit)

### Post related Components

These components read/write information from the global post object or a `PostContext`.

- [PostAuthor](./components/post-author/)
- [PostCategoryList](./components/post-category-list/)
- [PostContext](./components/post-context/)
- [PostDate](./components/post-date)
- [PostExcerpt](./components/post-excerpt/)
- [PostFeaturedImage](./components/post-featured-image/)
- [PostPrimaryCategory](./components/post-primary-category/)
- [PostPrimaryTerm](./components/post-primary-term/)
- [PostTermList](./components/post-term-list/)
- [PostTitle](./components/post-title/)
- [PostMeta](./components/post-meta/)

## Hooks

- [useFilteredList](./hooks/use-filtered-list)
Expand All @@ -46,6 +62,22 @@ A collection of components built to be used in the block editor. These component
- [useRequestData](./hooks/use-request-data/)
- [useBlockParentAttributes](./hooks/use-block-parent-attributes/)
- [useScript](./hooks/use-script/)
- [useIsPluginActive](./hooks/use-is-plugin-active/)
- [usePopover](./hooks/use-popover/)

### Post related hooks

These hooks read/write information from the global post object or a `PostContext`.

- [useAllTerms](./hooks/use-all-terms/)
- [useTaxonomy](./hooks/use-taxonomy/)
- [useIsSupportedTaxonomy](./hooks/use-is-supported-taxonomy/)
- [usePost](./hooks/use-post/)
- [usePrimaryTerm](./hooks/use-primary-term/)
- [useSelectedTermIds](./hooks/use-selected-term-ids/)
- [useSelectedTerms](./hooks/use-selected-terms/)
- [useSelectedTermsOfSavedPost](./hooks/use-selected-terms-of-saved-post/)
- [usePostMetaValue](./hooks/use-post-meta-value/)

## Stores

Expand Down
5 changes: 4 additions & 1 deletion api/register-icons/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { dispatch } from '@wordpress/data';
import domReady from '@wordpress/dom-ready';

import { iconStore } from '../../stores';

export function registerIcons(options) {
dispatch(iconStore).registerIconSet(options);
domReady(() => {
dispatch(iconStore).registerIconSet(options);
});
}
3 changes: 3 additions & 0 deletions components/author/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createContext } from '@wordpress/element';

export const AuthorContext = createContext();
144 changes: 144 additions & 0 deletions components/author/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { useContext } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import PropTypes from 'prop-types';
import { AuthorContext } from './context';

/**
* @typedef {object} Author
* @property {object} author
* @property {object<string, string>} author.avatar_urls
* @property {string} author.description
* @property {string} author.email
* @property {string} author.first_name
* @property {number} author.id
* @property {string} author.last_name
* @property {string} author.link
* @property {string} author.name
* @property {string} author.nickname
* @property {string} author.registered_date
* @property {string} author.slug
* @property {string} author.url
*/

export const Name = (props) => {
const { tagName: TagName, ...rest } = props;

/**
* @type {Author}
*/
const { name, link } = useContext(AuthorContext);

const wrapperProps = { ...rest };

if (TagName === 'a' && link) {
wrapperProps.href = link;
}

return <TagName {...wrapperProps}>{name}</TagName>;
};

Name.propTypes = {
tagName: PropTypes.string,
};

Name.defaultProps = {
tagName: 'span',
};

export const FirstName = (props) => {
const { tagName: TagName, ...rest } = props;

/**
* @type {Author}
*/
const { first_name: firstName } = useContext(AuthorContext);

return <TagName {...rest}>{firstName}</TagName>;
};

FirstName.propTypes = {
tagName: PropTypes.string,
};

FirstName.defaultProps = {
tagName: 'span',
};

export const LastName = (props) => {
const { tagName: TagName, ...rest } = props;

/**
* @type {Author}
*/
const { last_name: lastName } = useContext(AuthorContext);

return <TagName {...rest}>{lastName}</TagName>;
};

LastName.propTypes = {
tagName: PropTypes.string,
};

LastName.defaultProps = {
tagName: 'span',
};

function useDefaultAvatar() {
const { avatarURL: defaultAvatarUrl } = useSelect((select) => {
const { getSettings } = select(blockEditorStore);
const { __experimentalDiscussionSettings } = getSettings();
return __experimentalDiscussionSettings;
});
return defaultAvatarUrl;
}

export const Avatar = (props) => {
const { ...rest } = props;

/**
* @type {Author}
*/
const authorDetails = useContext(AuthorContext);

const avatarUrls = authorDetails?.avatar_urls ? Object.values(authorDetails.avatar_urls) : null;
const defaultAvatar = useDefaultAvatar();

const avatarSourceUrl = avatarUrls ? avatarUrls[avatarUrls.length - 1] : defaultAvatar;

return <img src={avatarSourceUrl} {...rest} />;
};

export const Bio = (props) => {
const { tagName: TagName = 'p', ...rest } = props;

/**
* @type {Author}
*/
const { description } = useContext(AuthorContext);

return <TagName {...rest}>{description}</TagName>;
};

Bio.propTypes = {
tagName: PropTypes.string,
};

Bio.defaultProps = {
tagName: 'p',
};

export const Email = (props) => {
const { ...rest } = props;

/**
* @type {Author}
*/
const { email } = useContext(AuthorContext);

return (
<a href={`mailto:${email}`} {...rest}>
{email}
</a>
);
};
11 changes: 9 additions & 2 deletions components/image/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MediaPlaceholder, InspectorControls } from '@wordpress/block-editor';
import { Spinner, FocalPointPicker, PanelBody } from '@wordpress/components';
import { Spinner, FocalPointPicker, PanelBody, Placeholder } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import PropTypes from 'prop-types';

Expand All @@ -12,14 +12,19 @@ const Image = (props) => {
onSelect,
focalPoint = { x: 0.5, y: 0.5 },
onChangeFocalPoint,
canEditImage = true,
...rest
} = props;
const hasImage = !!id;
const { media, isResolvingMedia } = useMedia(id);

const shouldDisplayFocalPointPicker = typeof onChangeFocalPoint === 'function';

if (!hasImage) {
if (!hasImage && !canEditImage) {
return <Placeholder className="block-editor-media-placeholder" withIllustration />;
}

if (!hasImage && canEditImage) {
return <MediaPlaceholder onSelect={onSelect} accept="image" multiple={false} />;
}

Expand Down Expand Up @@ -67,6 +72,7 @@ Image.defaultProps = {
size: 'large',
focalPoint: { x: 0.5, y: 0.5 },
onChangeFocalPoint: undefined,
canEditImage: true,
};

Image.propTypes = {
Expand All @@ -78,4 +84,5 @@ Image.propTypes = {
x: PropTypes.string,
y: PropTypes.string,
}),
canEditImage: PropTypes.bool,
};
3 changes: 2 additions & 1 deletion components/image/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ function BlockEdit(props) {
| `size` | `string` | `large` | Name of the image size to be displayed |
| `focalPoint` | `object` | `{x:0.5,y:0.5}` | Optional focal point object.
| `onChangeFocalPoint` | `function` | `undefined` | Callback that gets called with the new focal point when it changes. (Is required for the FocalPointPicker to appear) |
| `...rest` | `*` | `null` | Any additional attributes you want to pass to the underlying `img` tag |
| `...rest` | `*` | `null` | any additional attributes you want to pass to the underlying `img` tag |
| `canEditImage` | `boolean` | `true` | whether or not the image can be edited by in the context its getting viewed. Controls whether a placeholder or upload controls should be shown when no image is present |
11 changes: 11 additions & 0 deletions components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,16 @@ export { Repeater } from './repeater';
export { Link } from './link';
export { MediaToolbar } from './media-toolbar';
export { Image } from './image';
export { PostContext } from './post-context';
export { PostTitle } from './post-title';
export { PostFeaturedImage } from './post-featured-image';
export { PostMeta } from './post-meta';
export { PostExcerpt } from './post-excerpt';
export { PostAuthor } from './post-author';
export { PostDate, PostDatePicker } from './post-date';
export { PostTermList } from './post-term-list';
export { PostCategoryList } from './post-category-list';
export { PostPrimaryTerm } from './post-primary-term';
export { PostPrimaryCategory } from './post-primary-category';
export { RichTextCharacterLimit, getCharacterCount } from './rich-text-character-limit';
export { CircularProgressBar, Counter } from './counter';
74 changes: 74 additions & 0 deletions components/post-author/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Children } from '@wordpress/element';
import { store as coreStore } from '@wordpress/core-data';
import { Spinner } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import PropTypes from 'prop-types';
import { usePost } from '../../hooks';
import { Name, FirstName, LastName, Avatar, Bio, Email } from '../author';

import { AuthorContext } from '../author/context';

export const PostAuthor = (props) => {
const { children, ...rest } = props;
const { postId, postType } = usePost();

const [author, hasResolved] = useSelect(
(select) => {
const { getEditedEntityRecord, getUser, hasFinishedResolution } = select(coreStore);

const postQuery = ['postType', postType, postId];

const post = getEditedEntityRecord(...postQuery);
const hasResolvedPost = hasFinishedResolution('getEditedEntityRecord', postQuery);

const _authorId = hasResolvedPost ? post?.author : undefined;

const author = getUser(_authorId);
const hasResolvedAuthor = hasFinishedResolution('getUser', [_authorId]);

return [author, hasResolvedAuthor && hasResolvedPost];
},
[postType, postId],
);

const hasRenderCallback = typeof children === 'function';

const hasChildComponents = !hasRenderCallback && Children.count(children);

if (!hasResolved) {
return <Spinner />;
}

if (hasChildComponents) {
return (
<AuthorContext.Provider value={author}>
<div {...rest}>{children}</div>
</AuthorContext.Provider>
);
}

if (hasRenderCallback) {
return children(author);
}

return <Name {...rest} />;
};

PostAuthor.propTypes = {
children: PropTypes.oneOfType([
PropTypes.func,
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]),
};

PostAuthor.defaultProps = {
children: null,
};

PostAuthor.Name = Name;
PostAuthor.FirstName = FirstName;
PostAuthor.LastName = LastName;
PostAuthor.Avatar = Avatar;
PostAuthor.Bio = Bio;
PostAuthor.Email = Email;
Loading

0 comments on commit 45a4c9d

Please sign in to comment.