Skip to content

Commit

Permalink
Merge pull request #332 from 10up/ts/content-picker
Browse files Browse the repository at this point in the history
Migrate `ContentPicker` to TypeScript
  • Loading branch information
fabiankaegy committed Jun 12, 2024
2 parents 3aa1967 + 632cc77 commit 8f1971a
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,21 @@ import { decodeEntities } from '@wordpress/html-entities';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { Post, User, store as coreStore } from '@wordpress/core-data';
import { DragHandle } from '../drag-handle';
import { ContentSearchMode } from '../content-search/types';

type Term = {
count: number;
description: string;
id: number;
link: string;
meta: { [key: string]: unknown };
name: string;
parent: number;
slug: string;
taxonomy: string;
};

const StyledCloseButton = styled('button')`
display: block;
Expand All @@ -22,36 +36,62 @@ const StyledCloseButton = styled('button')`
}
`;

function getType(mode) {
function getEntityKind(mode: ContentSearchMode) {
let type;
switch (mode) {
case 'post':
type = 'postType';
type = 'postType' as const;
break;
case 'user':
type = 'root';
type = 'root' as const;
break;
default:
type = 'taxonomy';
type = 'taxonomy' as const;
break;
}

return type;
}

export type PickedItemType = {
id: number;
type: string;
uuid: string;
title: string;
url: string;
};

interface PickedItemProps {
item: PickedItemType;
isOrderable?: boolean;
handleItemDelete: (deletedItem: PickedItemType) => void;
mode: ContentSearchMode;
id: number | string;
}

const PickedItemContainer = styled.span`
&&& {
align-items: flex-start;
display: flex;
flex-direction: column;
justify-content: space-between;
}
`;

/**
* PickedItem
*
* @param {object} props react props
* @param {object} props.item item to show in the picker
* @param {boolean} props.isOrderable whether or not the picker is sortable
* @param {Function} props.handleItemDelete callback for when the item is deleted
* @param {string} props.mode mode of the picker
* @param {number|string} props.id id of the item
* @param {PickedItemProps} props react props
* @returns {*} React JSX
*/
const PickedItem = ({ item, isOrderable = false, handleItemDelete, mode, id }) => {
const type = getType(mode);
const PickedItem: React.FC<PickedItemProps> = ({
item,
isOrderable = false,
handleItemDelete,
mode,
id,
}) => {
const entityKind = getEntityKind(mode);

const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({
id,
Expand All @@ -61,23 +101,46 @@ const PickedItem = ({ item, isOrderable = false, handleItemDelete, mode, id }) =
// empty, it will return null, which is handled in the effect below.
const preparedItem = useSelect(
(select) => {
const { getEntityRecord, hasFinishedResolution } = select('core');
// @ts-ignore-next-line - The WordPress types are missing the hasFinishedResolution method.
const { getEntityRecord, hasFinishedResolution } = select(coreStore);

const getEntityRecordParameters = [type, item.type, item.id];
const result = getEntityRecord(...getEntityRecordParameters);
const getEntityRecordParameters = [entityKind, item.type, item.id] as const;
const result = getEntityRecord<Post | Term | User>(...getEntityRecordParameters);

if (result) {
const newItem = {
title: mode === 'post' ? result.title.rendered : result.name,
url: result.link,
id: result.id,
};
let newItem: Partial<PickedItemType>;

if (mode === 'post') {
const post = result as Post;
newItem = {
title: post.title.rendered,
url: post.link,
id: post.id,
type: post.type,
};
} else if (mode === 'user') {
const user = result as User;
newItem = {
title: user.name,
url: user.link,
id: user.id,
type: 'user',
};
} else {
const taxonomy = result as Term;
newItem = {
title: taxonomy.name,
url: taxonomy.link,
id: taxonomy.id,
type: taxonomy.taxonomy,
};
}

if (item.uuid) {
newItem.uuid = item.uuid;
}

return newItem;
return newItem as PickedItemType;
}

if (hasFinishedResolution('getEntityRecord', getEntityRecordParameters)) {
Expand All @@ -86,7 +149,7 @@ const PickedItem = ({ item, isOrderable = false, handleItemDelete, mode, id }) =

return undefined;
},
[item.id, type],
[item.id, entityKind],
);

// If `getEntityRecord` did not return an item, pass it to the delete callback.
Expand Down Expand Up @@ -117,14 +180,14 @@ const PickedItem = ({ item, isOrderable = false, handleItemDelete, mode, id }) =
return (
<li className={className} ref={setNodeRef} style={style}>
{isOrderable ? <DragHandle {...attributes} {...listeners} /> : ''}
<span className="block-editor-link-control__search-item-header">
<PickedItemContainer className="block-editor-link-control__search-item-header">
<span className="block-editor-link-control__search-item-title">
{decodeEntities(preparedItem.title)}
</span>
<span aria-hidden className="block-editor-link-control__search-item-info">
{filterURLForDisplay(safeDecodeURI(preparedItem.url)) || ''}
</span>
</span>
</PickedItemContainer>

<StyledCloseButton
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,21 @@ import {
TouchSensor,
useSensor,
useSensors,
DragEndEvent,
} from '@dnd-kit/core';
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import PickedItem from './PickedItem';
import PickedItem, { PickedItemType } from './PickedItem';
import { ContentSearchMode } from '../content-search/types';

const SortableList = ({
interface SortableListProps {
posts: Array<PickedItemType>;
isOrderable: boolean;
handleItemDelete: (post: PickedItemType) => void;
mode: ContentSearchMode;
setPosts: (posts: Array<PickedItemType>) => void;
}

const SortableList: React.FC<SortableListProps> = ({
posts,
isOrderable = false,
handleItemDelete,
Expand All @@ -21,12 +31,12 @@ const SortableList = ({
const items = posts.map((item) => item.uuid);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));

const handleDragEnd = (event) => {
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;

if (active.id !== over.id) {
if (active.id !== over?.id) {
const oldIndex = posts.findIndex((post) => post.uuid === active.id);
const newIndex = posts.findIndex((post) => post.uuid === over.id);
const newIndex = posts.findIndex((post) => post.uuid === over?.id);

setPosts(arrayMove(posts, oldIndex, newIndex));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { ContentSearch } from '../content-search';
import SortableList from './SortableList';
import { StyledComponentContext } from '../styled-components-context';
import { defaultRenderItemType } from '../content-search/SearchItem';
import { ContentSearchMode, QueryFilter, RenderItemComponentProps } from '../content-search/types';
import { NormalizedSuggestion } from '../content-search/utils';
import { PickedItemType } from './PickedItem';

const NAMESPACE = 'tenup-content-picker';

Expand All @@ -32,31 +35,28 @@ const ContentPickerWrapper = styled.div`
width: 100%;
`;

/**
* Content Picker
*
* @param {object} props React props
* @param {string} props.label label for the picker
* @param {boolean} props.hideLabelFromVision whether or not to hide the label from vision
* @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 {?Function} props.queryFilter callback that allows to modify the query
* @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
* @param {boolean} props.fetchInitialResults whether or not to fetch initial results on mount
* @param {Function} props.renderItemType callback to render the item type
* @param {?Function} props.renderItem react component to render the search result item
* @returns {*} React JSX
*/
export const ContentPicker = ({
interface ContentPickerProps {
label?: string;
hideLabelFromVision?: boolean;
mode?: ContentSearchMode;
contentTypes?: string[];
placeholder?: string;
onPickChange?: (ids: any[]) => void;
queryFilter?: QueryFilter;
maxContentItems?: number;
isOrderable?: boolean;
singlePickedLabel?: string;
multiPickedLabel?: string;
content?: any[];
uniqueContentItems?: boolean;
excludeCurrentPost?: boolean;
perPage?: number;
fetchInitialResults?: boolean;
renderItemType?: (props: NormalizedSuggestion) => string;
renderItem?: (props: RenderItemComponentProps) => JSX.Element;
}

export const ContentPicker: React.FC<ContentPickerProps> = ({
label = '',
hideLabelFromVision = true,
mode = 'post',
Expand All @@ -65,7 +65,7 @@ export const ContentPicker = ({
onPickChange = (ids) => {
console.log('Content picker list change', ids); // eslint-disable-line no-console
},
queryFilter = null,
queryFilter = undefined,
maxContentItems = 1,
isOrderable = false,
singlePickedLabel = __('You have selected the following item:', '10up-block-components'),
Expand All @@ -76,7 +76,7 @@ export const ContentPicker = ({
perPage = 20,
fetchInitialResults = false,
renderItemType = defaultRenderItemType,
renderItem = null,
renderItem = undefined,
}) => {
const currentPostId = select('core/editor')?.getCurrentPostId();

Expand All @@ -93,7 +93,7 @@ export const ContentPicker = ({
}
}

const handleSelect = (item) => {
const handleSelect = (item: { id: number; subtype?: string; type: string }) => {
const newItems = [
{
id: item.id,
Expand All @@ -105,7 +105,7 @@ export const ContentPicker = ({
onPickChange(newItems);
};

const onDeleteItem = (deletedItem) => {
const onDeleteItem = (deletedItem: PickedItemType) => {
const newItems = content.filter(({ id, uuid }) => {
if (deletedItem.uuid) {
return uuid !== deletedItem.uuid;
Expand Down
30 changes: 16 additions & 14 deletions components/content-search/SearchItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,23 @@ import { RenderItemComponentProps } from './types';
import { NormalizedSuggestion } from './utils';

const SearchItemWrapper = styled(Button)`
display: flex;
text-align: left;
width: 100%;
justify-content: space-between;
align-items: center;
border-radius: 2px;
box-sizing: border-box;
height: auto !important;
padding: 0.3em 0.7em;
overflow: hidden;
&&& {
display: flex;
text-align: left;
width: 100%;
justify-content: space-between;
align-items: center;
border-radius: 2px;
box-sizing: border-box;
height: auto !important;
padding: 0.3em 0.7em;
overflow: hidden;
&:hover {
/* Add opacity background to support future color changes */
/* Reduce background from #ddd to 0.05 for text contrast */
background-color: rgba(0, 0, 0, 0.05);
&:hover {
/* Add opacity background to support future color changes */
/* Reduce background from #ddd to 0.05 for text contrast */
background-color: rgba(0, 0, 0, 0.05);
}
}
`;

Expand Down
Loading

0 comments on commit 8f1971a

Please sign in to comment.