Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
40 changes: 37 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,45 @@ The `develop` branch is the development branch which means it contains the next

## Local Environment

This repository contains a local environment setup using the `@wordpress/env` package. Before you can start that environment you will need to run `npm ci` in both the repository root and `example` directory. This will install the required dependencies.
This repository contains a local environment setup using the `@wordpress/env` package and uses npm workspaces to manage dependencies for both the root package and the `example` workspace.

Next, run `npm run build` in both the root and `example` directories to build and compile the needed assets or if you want to watch for changes instead, use `npm run start`.
### Installation

Lastly, navigate your terminal to the `example` directory and run `npm run wp-env start` to start the local environment. The environment should be available at [http://localhost:8888](http://localhost:8888) and the credentials to login to the admin are: `admin` `password`.
From the repository root, run:

```bash
npm ci
```

This will install all dependencies for both the root package and the `example` workspace automatically.

### Building

To build the assets, run from the repository root:

```bash
npm run build
```

This will build both the main package and the example workspace. Alternatively, if you want to watch for changes during development, use:

```bash
npm run start
```

You can also build workspaces individually if needed:
- From root: `npm run build` (builds main package)
- From example: `npm run build` (builds example workspace)

### Starting the Local Environment

From the repository root, run:

```bash
npm run start-test-env
```

This will start the WordPress environment and import test media. The environment should be available at [http://localhost:8888](http://localhost:8888) and the credentials to login to the admin are: `admin` `password`.

## Working on a new or existing component

Expand Down
25 changes: 21 additions & 4 deletions components/content-picker/PickedItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { safeHTML } from '@wordpress/dom';
import { safeDecodeURI, filterURLForDisplay } from '@wordpress/url';
import { decodeEntities } from '@wordpress/html-entities';
import { __ } from '@wordpress/i18n';
Expand All @@ -20,8 +21,9 @@ export type PickedItemType = {
type: string;
uuid: string;
title: string;
url: string;
url?: string;
status?: string; // Optional status field for checking trashed posts
info?: string;
};

const PickedItemContainer = styled.div<{
Expand Down Expand Up @@ -131,6 +133,13 @@ const ItemURL = styled.span`
text-overflow: ellipsis;
`;

const ItemInfo = styled.span`
font-size: 0.75rem;
line-height: 1.4;
color: #757575;
margin-top: 4px;
`;

const MoveButton = styled(Button)`
&.components-button.has-icon {
min-width: 20px;
Expand Down Expand Up @@ -188,16 +197,24 @@ export const PickedItemPreview: React.FC<{ item: PickedItemType; isDeleted?: boo
item,
isDeleted = false,
}) => {
const decodedTitle = decodeEntities(item.title);
const { title, url, info } = item;
const decodedTitle = decodeEntities(title);
return (
<>
<ItemTitle isDeleted={isDeleted}>
<Truncate title={decodedTitle} aria-label={decodedTitle}>
{decodedTitle}
</Truncate>
</ItemTitle>
{item.url && !isDeleted && (
<ItemURL>{filterURLForDisplay(safeDecodeURI(item.url)) || ''}</ItemURL>
{url && !isDeleted && (
<ItemURL>{filterURLForDisplay(safeDecodeURI(url)) || ''}</ItemURL>
)}
{info && (
<ItemInfo
dangerouslySetInnerHTML={{
__html: safeHTML(info),
}}
/>
)}
</>
);
Expand Down
126 changes: 74 additions & 52 deletions components/content-picker/SortableList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* External dependencies
*/
import {
DndContext,
closestCenter,
Expand All @@ -11,15 +14,25 @@ import {
defaultDropAnimation,
} from '@dnd-kit/core';
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import styled from '@emotion/styled';

/**
* WordPress dependencies
*/
import { __experimentalTreeGrid as TreeGrid } from '@wordpress/components';
import { useCallback, useState, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { Post, User, store as coreStore } from '@wordpress/core-data';
import styled from '@emotion/styled';

/**
* Internal dependencies
*/
import PickedItem, { PickedItemType } from './PickedItem';
import { DraggableChip } from './DraggableChip';
import { ContentSearchMode } from '../content-search/types';
import { ContentSearchMode, QueryFieldsFilter } from '../content-search/types';
import type { PickedItemFilter } from './index';
import { Term } from './types';

const dropAnimation = {
...defaultDropAnimation,
Expand All @@ -33,20 +46,10 @@ interface SortableListProps {
mode: ContentSearchMode;
setPosts: (posts: Array<PickedItemType>) => void;
PickedItemPreviewComponent?: React.ComponentType<{ item: PickedItemType }>;
queryFieldsFilter?: QueryFieldsFilter;
pickedItemFilter?: PickedItemFilter;
}

type Term = {
count: number;
description: string;
id: number;
link: string;
meta: Record<string, unknown>;
name: string;
parent: number;
slug: string;
taxonomy: string;
};

function getEntityKind(mode: ContentSearchMode) {
let type;
switch (mode) {
Expand Down Expand Up @@ -84,6 +87,8 @@ const SortableList: React.FC<SortableListProps> = ({
mode = 'post',
setPosts,
PickedItemPreviewComponent,
queryFieldsFilter,
pickedItemFilter,
}) => {
const hasMultiplePosts = posts.length > 1;
const [activeId, setActiveId] = useState<string | null>(null);
Expand All @@ -96,19 +101,25 @@ const SortableList: React.FC<SortableListProps> = ({
// @ts-ignore-next-line - The WordPress types are missing the hasFinishedResolution method.
const { getEntityRecord, hasFinishedResolution } = select(coreStore);

let fields = ['link', 'type', 'id'];

if (mode === 'user') {
fields.push('name');
} else if (mode === 'post') {
fields.push('title');
fields.push('url');
fields.push('subtype');
fields.push('status'); // Include status to check for trashed posts
} else {
fields.push('name');
fields.push('taxonomy');
}

if (queryFieldsFilter) {
fields = queryFieldsFilter(fields, mode);
}

return posts.reduce<{ [key: string]: PickedItemType | null }>((acc, item) => {
const fields = ['link', 'type', 'id'];
if (mode === 'user') {
fields.push('name');
} else if (mode === 'post') {
fields.push('title');
fields.push('url');
fields.push('subtype');
fields.push('status'); // Include status to check for trashed posts
} else {
fields.push('name');
fields.push('taxonomy');
}
const getEntityRecordParameters = [
entityKind,
item.type,
Expand All @@ -120,31 +131,42 @@ const SortableList: React.FC<SortableListProps> = ({
if (result) {
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,
status: post.status, // Include status for trashed post detection
};
} 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,
};
switch (mode) {
case 'post': {
const post = result as Post;
newItem = {
title: post.title.rendered,
url: post.link,
id: post.id,
type: post.type,
status: post.status, // Include status for trashed post detection
};
break;
}
case 'user': {
const user = result as User;
newItem = {
title: user.name,
url: user.link,
id: user.id,
type: 'user',
};
break;
}
default: {
const taxonomy = result as Term;
newItem = {
title: taxonomy.name,
url: taxonomy.link,
id: taxonomy.id,
type: taxonomy.taxonomy,
};
break;
}
}

if (pickedItemFilter) {
newItem = pickedItemFilter(newItem, result);
}

if (item.uuid) {
Expand All @@ -159,7 +181,7 @@ const SortableList: React.FC<SortableListProps> = ({
return acc;
}, {});
},
[posts, entityKind],
[posts, entityKind, queryFieldsFilter, pickedItemFilter, mode],
);

const items = posts.map((item) => item.uuid);
Expand Down
25 changes: 24 additions & 1 deletion components/content-picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,27 @@ import { select } from '@wordpress/data';
import { useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { VisuallyHidden } from '@wordpress/components';
import { Post, User } from '@wordpress/core-data';
import { v4 as uuidv4 } from 'uuid';
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 {
ContentSearchMode,
QueryFilter,
QueryFieldsFilter,
RenderItemComponentProps,
SearchResultFilter,
} from '../content-search/types';
import { NormalizedSuggestion } from '../content-search/utils';
import { PickedItemType } from './PickedItem';
import { Term } from './types';

export type PickedItemFilter = (
item: Partial<PickedItemType>,
originalResult: Post | Term | User,
) => Partial<PickedItemType>;

const NAMESPACE = 'tenup-content-picker';

Expand Down Expand Up @@ -48,6 +61,9 @@ export interface ContentPickerProps {
placeholder?: string;
onPickChange?: (ids: any[]) => void;
queryFilter?: QueryFilter;
queryFieldsFilter?: QueryFieldsFilter;
searchResultFilter?: SearchResultFilter;
pickedItemFilter?: PickedItemFilter;
maxContentItems?: number;
isOrderable?: boolean;
singlePickedLabel?: string;
Expand All @@ -73,6 +89,9 @@ export const ContentPicker: React.FC<ContentPickerProps> = ({
console.log('Content picker list change', ids); // eslint-disable-line no-console
},
queryFilter = undefined,
queryFieldsFilter,
searchResultFilter,
pickedItemFilter,
maxContentItems = 1,
isOrderable = false,
singlePickedLabel = __('You have selected the following item:', '10up-block-components'),
Expand Down Expand Up @@ -152,6 +171,8 @@ export const ContentPicker: React.FC<ContentPickerProps> = ({
contentTypes={contentTypes}
mode={mode}
queryFilter={queryFilter}
queryFieldsFilter={queryFieldsFilter}
searchResultFilter={searchResultFilter}
perPage={perPage}
fetchInitialResults={fetchInitialResults}
renderItemType={renderItemType}
Expand Down Expand Up @@ -196,6 +217,8 @@ export const ContentPicker: React.FC<ContentPickerProps> = ({
mode={mode}
setPosts={onPickChange}
PickedItemPreviewComponent={PickedItemPreviewComponent}
queryFieldsFilter={queryFieldsFilter}
pickedItemFilter={pickedItemFilter}
/>
</ul>
</StyleWrapper>
Expand Down
Loading
Loading