11/**
22 * WordPress dependencies
33 */
4- import { ToggleControl } from '@wordpress/components' ;
4+ import { ToggleControl , FormTokenField , BaseControl } from '@wordpress/components' ;
55import { useSelect } from '@wordpress/data' ;
66import { useEntityRecord , store as coreDataStore } from '@wordpress/core-data' ;
77import { __ } from '@wordpress/i18n' ;
@@ -21,7 +21,49 @@ export const PostExcludeControls = ( {
2121 setAttributes,
2222 allowedControls,
2323} ) => {
24- const { query : { exclude_current : excludeCurrent } = { } } = attributes ;
24+ const { query : { exclude_current : excludeCurrent , exclude_posts : excludePosts = [ ] , } = { } } = attributes ;
25+
26+ // If the control is not allowed, return null.
27+ if ( ! allowedControls . includes ( 'exclude_current_post' ) && ! allowedControls . includes ( 'exclude_posts' ) ) {
28+ return null ;
29+ }
30+
31+ return (
32+ < >
33+ < h2 > { __ ( 'Exclude Posts' , 'advanced-query-loop' ) } </ h2 >
34+ < ExcludeCurrentPostControl
35+ attributes = { attributes }
36+ setAttributes = { setAttributes }
37+ allowedControls = { allowedControls }
38+ />
39+ < ExcludePostsControl
40+ attributes = { attributes }
41+ setAttributes = { setAttributes }
42+ allowedControls = { allowedControls }
43+ />
44+ </ >
45+ ) ;
46+ } ;
47+
48+ /**
49+ * ExcludeCurrentPostControl is a React functional component used within the context
50+ * of advanced query loop settings. It toggles the exclusion of the current post
51+ * or content associated with the current template from query results.
52+ *
53+ * @param {Object } props The properties passed to the component.
54+ * @param {Object } props.attributes The block attributes.
55+ * @param {Function } props.setAttributes Function to update block attributes.
56+ * @param {Array } props.allowedControls List of control identifiers that are allowed for this block.
57+ *
58+ * @return {Element|null } A `ToggleControl` component if the control is allowed, or `null` if not.
59+ */
60+ const ExcludeCurrentPostControl = ( { attributes, setAttributes, allowedControls } ) => {
61+ const { query : { exclude_current : excludeCurrent , } = { } } = attributes ;
62+
63+ if ( ! allowedControls . includes ( 'exclude_current_post' ) ) {
64+ return null ;
65+ }
66+
2567 const { record : siteOptions } = useEntityRecord ( 'root' , 'site' ) ;
2668 const { currentPost, isAdmin } = useSelect ( ( select ) => {
2769 return {
@@ -33,11 +75,6 @@ export const PostExcludeControls = ( {
3375 } ;
3476 } , [ ] ) ;
3577
36- // If the control is not allowed, return null.
37- if ( ! allowedControls . includes ( 'exclude_current_post' ) ) {
38- return null ;
39- }
40-
4178 if ( ! currentPost ) {
4279 return < div > { __ ( 'Loading…' , 'advanced-query-loop' ) } </ div > ;
4380 }
@@ -62,33 +99,121 @@ export const PostExcludeControls = ( {
6299 } ;
63100
64101 return (
65- < >
66- < h2 > { __ ( 'Exclude Posts' , 'advanced-query-loop' ) } </ h2 >
67- < ToggleControl
68- __nextHasNoMarginBottom
69- label = { __ ( 'Exclude Current Post' , 'advanced-query-loop' ) }
70- checked = { ! ! excludeCurrent }
71- disabled = { isDisabled ( ) }
72- onChange = { ( value ) => {
102+ < ToggleControl
103+ __nextHasNoMarginBottom
104+ label = { __ ( 'Exclude Current Post' , 'advanced-query-loop' ) }
105+ checked = { ! ! excludeCurrent }
106+ disabled = { isDisabled ( ) }
107+ onChange = { ( value ) => {
108+ setAttributes ( {
109+ query : {
110+ ...attributes . query ,
111+ exclude_current : value ? currentPost . id : 0 ,
112+ } ,
113+ } ) ;
114+ } }
115+ help = {
116+ isDisabled ( )
117+ ? __ (
118+ 'This option is disabled for this template as there is no dedicated post to exclude.' ,
119+ 'advanced-query-loop'
120+ )
121+ : __ (
122+ 'Remove the associated post for this template/content from the query results.' ,
123+ 'advanced-query-loop'
124+ )
125+ }
126+ />
127+ ) ;
128+ }
129+
130+ /**
131+ * The ExcludePostsControl component allows users to exclude specific posts
132+ * from queries based on post titles, providing search and selection
133+ * functionality in the form of a token field.
134+ *
135+ * @param {Object } props The component props.
136+ * @param {Object } props.attributes The block attributes.
137+ * @param {Function } props.setAttributes Function to update the block attributes.
138+ * @param {Array } props.allowedControls List of controls allowed for the current context.
139+ *
140+ * @returns {Element|null } Returns the control for selecting excluded posts,
141+ * or null if the 'exclude_posts' control is not allowed.
142+ */
143+ const ExcludePostsControl = ( { attributes, setAttributes, allowedControls } ) => {
144+ const {
145+ query : {
146+ exclude_posts : excludePosts = [ ] ,
147+ multiple_posts : multiplePosts = [ ] ,
148+ postType
149+ } = { }
150+ } = attributes ;
151+
152+ if ( ! allowedControls . includes ( 'exclude_posts' ) ) {
153+ return null ;
154+ }
155+
156+ // Get the posts for all post types used in the query.
157+ const posts = useSelect (
158+ ( select ) => {
159+ const { getEntityRecords } = select ( 'core' ) ;
160+
161+ // Fetch posts for each post type and combine them into one array
162+ return [ ...multiplePosts , postType ] . reduce (
163+ ( accumulator , postType ) => {
164+ // Depending on the number of posts this could take a while, since we can't paginate here
165+ const records = getEntityRecords ( 'postType' , postType , {
166+ per_page : - 1 ,
167+ } ) ;
168+ return [ ...accumulator , ...( records || [ ] ) ] ;
169+ } ,
170+ [ ]
171+ ) ;
172+ } ,
173+ [ postType , multiplePosts ]
174+ ) ;
175+
176+ // For use with flatMap(), as this lets us remove elements during a map()
177+ const idToTitle = ( id ) => {
178+ const post = posts . find ( ( p ) => p . id === id ) ;
179+ return post ? [ post . title . rendered ] : [ ] ;
180+ } ;
181+
182+ const titleToId = ( title ) => {
183+ const post = posts . find ( ( p ) => p . title . rendered === title ) ;
184+ return post ? [ post . id ] : [ ] ;
185+ } ;
186+
187+ if ( ! posts ) {
188+ return < div > { __ ( 'Loading…' , 'advanced-query-loop' ) } </ div > ;
189+ }
190+
191+ return (
192+ < BaseControl
193+ help = { __ (
194+ 'Start typing to search for a post title to exclude, or manually enter one.' ,
195+ 'advanced-query-loop'
196+ ) }
197+ >
198+ < FormTokenField
199+ label = { __ ( 'Posts to Exclude' , 'advanced-query-loop' ) }
200+ value = { excludePosts . flatMap ( ( id ) => idToTitle ( id ) ) }
201+ suggestions = { posts . map ( ( post ) => post . title . rendered ) }
202+ onChange = { ( titles ) => {
203+ // Converts the Titles to Post IDs before saving them
73204 setAttributes ( {
74205 query : {
75206 ...attributes . query ,
76- exclude_current : value ? currentPost . id : 0 ,
207+ exclude_posts :
208+ titles . flatMap ( ( title ) =>
209+ titleToId ( title )
210+ ) || [ ] ,
77211 } ,
78212 } ) ;
79213 } }
80- help = {
81- isDisabled ( )
82- ? __ (
83- 'This option is disabled for this template as there is no dedicated post to exclude.' ,
84- 'advanced-query-loop'
85- )
86- : __ (
87- 'Remove the associated post for this template/content from the query results.' ,
88- 'advanced-query-loop'
89- )
90- }
214+ __experimentalExpandOnFocus
215+ __experimentalShowHowTo = { false }
91216 />
92- </ >
93- ) ;
94- } ;
217+ </ BaseControl >
218+ )
219+ }
0 commit comments