Skip to content

Commit 85ae130

Browse files
authored
Merge pull request #373 from s3rgiosan/feature/allow-picked-item-customization
Add extensibility filters to ContentPicker and ContentSearch components
2 parents 863134a + 050f230 commit 85ae130

File tree

17 files changed

+11378
-7624
lines changed

17 files changed

+11378
-7624
lines changed

CONTRIBUTING.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,45 @@ The `develop` branch is the development branch which means it contains the next
2828

2929
## Local Environment
3030

31-
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.
31+
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.
3232

33-
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`.
33+
### Installation
3434

35-
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`.
35+
From the repository root, run:
36+
37+
```bash
38+
npm ci
39+
```
40+
41+
This will install all dependencies for both the root package and the `example` workspace automatically.
42+
43+
### Building
44+
45+
To build the assets, run from the repository root:
46+
47+
```bash
48+
npm run build
49+
```
50+
51+
This will build both the main package and the example workspace. Alternatively, if you want to watch for changes during development, use:
52+
53+
```bash
54+
npm run start
55+
```
56+
57+
You can also build workspaces individually if needed:
58+
- From root: `npm run build` (builds main package)
59+
- From example: `npm run build` (builds example workspace)
60+
61+
### Starting the Local Environment
62+
63+
From the repository root, run:
64+
65+
```bash
66+
npm run start-test-env
67+
```
68+
69+
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`.
3670

3771
## Working on a new or existing component
3872

components/content-picker/PickedItem.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import styled from '@emotion/styled';
22
import { useSortable } from '@dnd-kit/sortable';
33
import { CSS } from '@dnd-kit/utilities';
4+
import { safeHTML } from '@wordpress/dom';
45
import { safeDecodeURI, filterURLForDisplay } from '@wordpress/url';
56
import { decodeEntities } from '@wordpress/html-entities';
67
import { __ } from '@wordpress/i18n';
@@ -20,8 +21,9 @@ export type PickedItemType = {
2021
type: string;
2122
uuid: string;
2223
title: string;
23-
url: string;
24+
url?: string;
2425
status?: string; // Optional status field for checking trashed posts
26+
info?: string;
2527
};
2628

2729
const PickedItemContainer = styled.div<{
@@ -131,6 +133,13 @@ const ItemURL = styled.span`
131133
text-overflow: ellipsis;
132134
`;
133135

136+
const ItemInfo = styled.span`
137+
font-size: 0.75rem;
138+
line-height: 1.4;
139+
color: #757575;
140+
margin-top: 4px;
141+
`;
142+
134143
const MoveButton = styled(Button)`
135144
&.components-button.has-icon {
136145
min-width: 20px;
@@ -188,16 +197,24 @@ export const PickedItemPreview: React.FC<{ item: PickedItemType; isDeleted?: boo
188197
item,
189198
isDeleted = false,
190199
}) => {
191-
const decodedTitle = decodeEntities(item.title);
200+
const { title, url, info } = item;
201+
const decodedTitle = decodeEntities(title);
192202
return (
193203
<>
194204
<ItemTitle isDeleted={isDeleted}>
195205
<Truncate title={decodedTitle} aria-label={decodedTitle}>
196206
{decodedTitle}
197207
</Truncate>
198208
</ItemTitle>
199-
{item.url && !isDeleted && (
200-
<ItemURL>{filterURLForDisplay(safeDecodeURI(item.url)) || ''}</ItemURL>
209+
{url && !isDeleted && (
210+
<ItemURL>{filterURLForDisplay(safeDecodeURI(url)) || ''}</ItemURL>
211+
)}
212+
{info && (
213+
<ItemInfo
214+
dangerouslySetInnerHTML={{
215+
__html: safeHTML(info),
216+
}}
217+
/>
201218
)}
202219
</>
203220
);

components/content-picker/SortableList.tsx

Lines changed: 74 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* External dependencies
3+
*/
14
import {
25
DndContext,
36
closestCenter,
@@ -11,15 +14,25 @@ import {
1114
defaultDropAnimation,
1215
} from '@dnd-kit/core';
1316
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
17+
import styled from '@emotion/styled';
18+
19+
/**
20+
* WordPress dependencies
21+
*/
1422
import { __experimentalTreeGrid as TreeGrid } from '@wordpress/components';
1523
import { useCallback, useState, useMemo } from '@wordpress/element';
1624
import { __ } from '@wordpress/i18n';
1725
import { useSelect } from '@wordpress/data';
1826
import { Post, User, store as coreStore } from '@wordpress/core-data';
19-
import styled from '@emotion/styled';
27+
28+
/**
29+
* Internal dependencies
30+
*/
2031
import PickedItem, { PickedItemType } from './PickedItem';
2132
import { DraggableChip } from './DraggableChip';
22-
import { ContentSearchMode } from '../content-search/types';
33+
import { ContentSearchMode, QueryFieldsFilter } from '../content-search/types';
34+
import type { PickedItemFilter } from './index';
35+
import { Term } from './types';
2336

2437
const dropAnimation = {
2538
...defaultDropAnimation,
@@ -33,20 +46,10 @@ interface SortableListProps {
3346
mode: ContentSearchMode;
3447
setPosts: (posts: Array<PickedItemType>) => void;
3548
PickedItemPreviewComponent?: React.ComponentType<{ item: PickedItemType }>;
49+
queryFieldsFilter?: QueryFieldsFilter;
50+
pickedItemFilter?: PickedItemFilter;
3651
}
3752

38-
type Term = {
39-
count: number;
40-
description: string;
41-
id: number;
42-
link: string;
43-
meta: Record<string, unknown>;
44-
name: string;
45-
parent: number;
46-
slug: string;
47-
taxonomy: string;
48-
};
49-
5053
function getEntityKind(mode: ContentSearchMode) {
5154
let type;
5255
switch (mode) {
@@ -84,6 +87,8 @@ const SortableList: React.FC<SortableListProps> = ({
8487
mode = 'post',
8588
setPosts,
8689
PickedItemPreviewComponent,
90+
queryFieldsFilter,
91+
pickedItemFilter,
8792
}) => {
8893
const hasMultiplePosts = posts.length > 1;
8994
const [activeId, setActiveId] = useState<string | null>(null);
@@ -96,19 +101,25 @@ const SortableList: React.FC<SortableListProps> = ({
96101
// @ts-ignore-next-line - The WordPress types are missing the hasFinishedResolution method.
97102
const { getEntityRecord, hasFinishedResolution } = select(coreStore);
98103

104+
let fields = ['link', 'type', 'id'];
105+
106+
if (mode === 'user') {
107+
fields.push('name');
108+
} else if (mode === 'post') {
109+
fields.push('title');
110+
fields.push('url');
111+
fields.push('subtype');
112+
fields.push('status'); // Include status to check for trashed posts
113+
} else {
114+
fields.push('name');
115+
fields.push('taxonomy');
116+
}
117+
118+
if (queryFieldsFilter) {
119+
fields = queryFieldsFilter(fields, mode);
120+
}
121+
99122
return posts.reduce<{ [key: string]: PickedItemType | null }>((acc, item) => {
100-
const fields = ['link', 'type', 'id'];
101-
if (mode === 'user') {
102-
fields.push('name');
103-
} else if (mode === 'post') {
104-
fields.push('title');
105-
fields.push('url');
106-
fields.push('subtype');
107-
fields.push('status'); // Include status to check for trashed posts
108-
} else {
109-
fields.push('name');
110-
fields.push('taxonomy');
111-
}
112123
const getEntityRecordParameters = [
113124
entityKind,
114125
item.type,
@@ -120,31 +131,42 @@ const SortableList: React.FC<SortableListProps> = ({
120131
if (result) {
121132
let newItem: Partial<PickedItemType>;
122133

123-
if (mode === 'post') {
124-
const post = result as Post;
125-
newItem = {
126-
title: post.title.rendered,
127-
url: post.link,
128-
id: post.id,
129-
type: post.type,
130-
status: post.status, // Include status for trashed post detection
131-
};
132-
} else if (mode === 'user') {
133-
const user = result as User;
134-
newItem = {
135-
title: user.name,
136-
url: user.link,
137-
id: user.id,
138-
type: 'user',
139-
};
140-
} else {
141-
const taxonomy = result as Term;
142-
newItem = {
143-
title: taxonomy.name,
144-
url: taxonomy.link,
145-
id: taxonomy.id,
146-
type: taxonomy.taxonomy,
147-
};
134+
switch (mode) {
135+
case 'post': {
136+
const post = result as Post;
137+
newItem = {
138+
title: post.title.rendered,
139+
url: post.link,
140+
id: post.id,
141+
type: post.type,
142+
status: post.status, // Include status for trashed post detection
143+
};
144+
break;
145+
}
146+
case 'user': {
147+
const user = result as User;
148+
newItem = {
149+
title: user.name,
150+
url: user.link,
151+
id: user.id,
152+
type: 'user',
153+
};
154+
break;
155+
}
156+
default: {
157+
const taxonomy = result as Term;
158+
newItem = {
159+
title: taxonomy.name,
160+
url: taxonomy.link,
161+
id: taxonomy.id,
162+
type: taxonomy.taxonomy,
163+
};
164+
break;
165+
}
166+
}
167+
168+
if (pickedItemFilter) {
169+
newItem = pickedItemFilter(newItem, result);
148170
}
149171

150172
if (item.uuid) {
@@ -159,7 +181,7 @@ const SortableList: React.FC<SortableListProps> = ({
159181
return acc;
160182
}, {});
161183
},
162-
[posts, entityKind],
184+
[posts, entityKind, queryFieldsFilter, pickedItemFilter, mode],
163185
);
164186

165187
const items = posts.map((item) => item.uuid);

components/content-picker/index.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,27 @@ import { select } from '@wordpress/data';
33
import { useMemo } from '@wordpress/element';
44
import { __ } from '@wordpress/i18n';
55
import { VisuallyHidden } from '@wordpress/components';
6+
import { Post, User } from '@wordpress/core-data';
67
import { v4 as uuidv4 } from 'uuid';
78
import { ContentSearch } from '../content-search';
89
import SortableList from './SortableList';
910
import { StyledComponentContext } from '../styled-components-context';
1011
import { defaultRenderItemType } from '../content-search/SearchItem';
11-
import { ContentSearchMode, QueryFilter, RenderItemComponentProps } from '../content-search/types';
12+
import {
13+
ContentSearchMode,
14+
QueryFilter,
15+
QueryFieldsFilter,
16+
RenderItemComponentProps,
17+
SearchResultFilter,
18+
} from '../content-search/types';
1219
import { NormalizedSuggestion } from '../content-search/utils';
1320
import { PickedItemType } from './PickedItem';
21+
import { Term } from './types';
22+
23+
export type PickedItemFilter = (
24+
item: Partial<PickedItemType>,
25+
originalResult: Post | Term | User,
26+
) => Partial<PickedItemType>;
1427

1528
const NAMESPACE = 'tenup-content-picker';
1629

@@ -48,6 +61,9 @@ export interface ContentPickerProps {
4861
placeholder?: string;
4962
onPickChange?: (ids: any[]) => void;
5063
queryFilter?: QueryFilter;
64+
queryFieldsFilter?: QueryFieldsFilter;
65+
searchResultFilter?: SearchResultFilter;
66+
pickedItemFilter?: PickedItemFilter;
5167
maxContentItems?: number;
5268
isOrderable?: boolean;
5369
singlePickedLabel?: string;
@@ -73,6 +89,9 @@ export const ContentPicker: React.FC<ContentPickerProps> = ({
7389
console.log('Content picker list change', ids); // eslint-disable-line no-console
7490
},
7591
queryFilter = undefined,
92+
queryFieldsFilter,
93+
searchResultFilter,
94+
pickedItemFilter,
7695
maxContentItems = 1,
7796
isOrderable = false,
7897
singlePickedLabel = __('You have selected the following item:', '10up-block-components'),
@@ -152,6 +171,8 @@ export const ContentPicker: React.FC<ContentPickerProps> = ({
152171
contentTypes={contentTypes}
153172
mode={mode}
154173
queryFilter={queryFilter}
174+
queryFieldsFilter={queryFieldsFilter}
175+
searchResultFilter={searchResultFilter}
155176
perPage={perPage}
156177
fetchInitialResults={fetchInitialResults}
157178
renderItemType={renderItemType}
@@ -196,6 +217,8 @@ export const ContentPicker: React.FC<ContentPickerProps> = ({
196217
mode={mode}
197218
setPosts={onPickChange}
198219
PickedItemPreviewComponent={PickedItemPreviewComponent}
220+
queryFieldsFilter={queryFieldsFilter}
221+
pickedItemFilter={pickedItemFilter}
199222
/>
200223
</ul>
201224
</StyleWrapper>

0 commit comments

Comments
 (0)