Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate ContentPicker to TypeScript #332

Merged
merged 4 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading