LoopInnerBlocksRenderer.jsx
279 lines
| 1 | import { |
| 2 | BlockContextProvider, |
| 3 | useInnerBlocksProps, |
| 4 | __experimentalUseBlockPreview as useBlockPreview, // eslint-disable-line @wordpress/no-unsafe-wp-apis |
| 5 | store as blockEditorStore, |
| 6 | } from '@wordpress/block-editor'; |
| 7 | import { useSelect } from '@wordpress/data'; |
| 8 | import { Spinner } from '@wordpress/components'; |
| 9 | import { memo, useEffect, useMemo, useState } from '@wordpress/element'; |
| 10 | import { applyFilters } from '@wordpress/hooks'; |
| 11 | import apiFetch from '@wordpress/api-fetch'; |
| 12 | |
| 13 | import { BlockAppender } from '@components/index'; |
| 14 | |
| 15 | const DISALLOWED_KEYS = [ 'post_password', 'password' ]; |
| 16 | |
| 17 | function BlockPreview( { blocks, isHidden } ) { |
| 18 | const style = { |
| 19 | display: isHidden ? 'none' : undefined, |
| 20 | }; |
| 21 | |
| 22 | const blockPreviewProps = useBlockPreview( { |
| 23 | blocks, |
| 24 | props: { |
| 25 | style, |
| 26 | className: 'gb-loop-preview', |
| 27 | }, |
| 28 | } ); |
| 29 | |
| 30 | return isHidden ? <div { ...blockPreviewProps } /> : blockPreviewProps.children; |
| 31 | } |
| 32 | |
| 33 | const MemoizedBlockPreview = memo( BlockPreview ); |
| 34 | |
| 35 | function useWpQuery( shouldRequest = true, { query, attributes, selectedBlock, context, queryType } ) { |
| 36 | const { currentPostId, currentPostAuthor } = useSelect( ( select ) => { |
| 37 | const { getCurrentPost } = select( 'core/editor' ); |
| 38 | const currentPost = getCurrentPost ? getCurrentPost() : null; |
| 39 | return { |
| 40 | currentPostId: currentPost?.id, |
| 41 | currentPostAuthor: currentPost?.author, |
| 42 | }; |
| 43 | } ); |
| 44 | |
| 45 | const { |
| 46 | isAdminUser = false, |
| 47 | } = gbPermissions ?? {}; |
| 48 | |
| 49 | const [ data, setData ] = useState( [] ); |
| 50 | const [ isLoading, setIsLoading ] = useState( true ); |
| 51 | |
| 52 | useEffect( () => { |
| 53 | if ( ! shouldRequest ) { |
| 54 | return; |
| 55 | } |
| 56 | |
| 57 | const args = { |
| 58 | ...query, |
| 59 | post_type: query.post_type || 'post', |
| 60 | }; |
| 61 | |
| 62 | /** |
| 63 | * Filter post_status based on user role. |
| 64 | * |
| 65 | * TODO - Expand this in the future to handle custom user roles and other advanced use cases. |
| 66 | */ |
| 67 | if ( ! isAdminUser ) { |
| 68 | if ( Array.isArray( args?.post_status ) ) { |
| 69 | const disallowedStatuses = [ 'private', 'draft', 'trash' ]; |
| 70 | args.post_status = args.post_status.filter( ( status ) => ! disallowedStatuses.includes( status ) ); |
| 71 | } else { |
| 72 | args.post_status = 'publish'; |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | const { queryLoopEditorPostsCap = 50 } = generateBlocksEditor; |
| 77 | |
| 78 | if ( args.posts_per_page > queryLoopEditorPostsCap ) { |
| 79 | args.posts_per_page = queryLoopEditorPostsCap; |
| 80 | } |
| 81 | |
| 82 | async function fetchPosts() { |
| 83 | setIsLoading( true ); |
| 84 | |
| 85 | try { |
| 86 | const response = await apiFetch( { |
| 87 | path: '/generateblocks/v1/get-wp-query', |
| 88 | method: 'POST', |
| 89 | data: { |
| 90 | args, |
| 91 | attributes, |
| 92 | context, |
| 93 | queryType, |
| 94 | block: selectedBlock, |
| 95 | postId: currentPostId, |
| 96 | authorId: currentPostAuthor, |
| 97 | }, |
| 98 | } ); |
| 99 | |
| 100 | const { posts = [] } = response; |
| 101 | setData( posts ); |
| 102 | } catch ( error ) { |
| 103 | console.error( 'Error fetching post record:', error ); // eslint-disable-line no-console |
| 104 | } finally { |
| 105 | setIsLoading( false ); |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | fetchPosts(); |
| 110 | }, [ query, currentPostId, currentPostAuthor ] ); |
| 111 | |
| 112 | const result = { data, isResolvingData: isLoading, hasResolvedData: data?.length > 0 }; |
| 113 | |
| 114 | return shouldRequest ? result : null; |
| 115 | } |
| 116 | |
| 117 | export function LoopInnerBlocksRenderer( props ) { |
| 118 | const { |
| 119 | clientId, |
| 120 | attributes, |
| 121 | isSelected, |
| 122 | } = props; |
| 123 | |
| 124 | const context = applyFilters( 'generateblocks.editor.preview.context', props.context, { props } ); |
| 125 | |
| 126 | const { |
| 127 | 'generateblocks/query': query = {}, |
| 128 | 'generateblocks/queryType': queryType = 'WP_Query', |
| 129 | 'generateblocks/queryId': queryId = null, |
| 130 | } = context; |
| 131 | let dataState = { |
| 132 | data: null, |
| 133 | isResolvingData: true, |
| 134 | hasResolvedData: false, |
| 135 | queryParams: [], |
| 136 | }; |
| 137 | const { getSelectedBlock } = useSelect( blockEditorStore ); |
| 138 | const selectedBlock = getSelectedBlock(); |
| 139 | const wpQuery = useWpQuery( 'WP_Query' === queryType, { query, context, queryType, attributes, selectedBlock } ); |
| 140 | |
| 141 | const otherQuery = applyFilters( 'generateblocks.editor.looper.query', null, { |
| 142 | query, |
| 143 | queryType, |
| 144 | context, |
| 145 | props, |
| 146 | useWpQuery, |
| 147 | selectedBlock, |
| 148 | } ); |
| 149 | |
| 150 | if ( null !== wpQuery ) { |
| 151 | dataState = ( { ...wpQuery } ); |
| 152 | } else if ( null !== otherQuery && null === wpQuery ) { |
| 153 | dataState = ( { ...otherQuery } ); |
| 154 | } else { |
| 155 | dataState = { |
| 156 | data: [], |
| 157 | isResolvingData: false, |
| 158 | hasResolvedData: false, |
| 159 | }; |
| 160 | } |
| 161 | |
| 162 | const { |
| 163 | data, |
| 164 | isResolvingData, |
| 165 | hasResolvedData, |
| 166 | } = dataState; |
| 167 | |
| 168 | const innerBlocks = useSelect( ( select ) => { |
| 169 | return select( 'core/block-editor' )?.getBlocks( clientId ); |
| 170 | }, [] ); |
| 171 | const [ previewId, setPreviewId ] = useState( { |
| 172 | [ queryId ]: null, |
| 173 | } ); |
| 174 | |
| 175 | const innerBlocksProps = useInnerBlocksProps( |
| 176 | {}, |
| 177 | { |
| 178 | renderAppender: () => ! innerBlocks.length ? ( |
| 179 | <BlockAppender |
| 180 | clientId={ clientId } |
| 181 | isSelected={ isSelected } |
| 182 | attributes={ attributes } |
| 183 | /> |
| 184 | ) : false, |
| 185 | blockContext: { |
| 186 | ...context, |
| 187 | 'generateblocks/loopPreviewId': previewId, |
| 188 | }, |
| 189 | } |
| 190 | ); |
| 191 | |
| 192 | const loopItemsContext = useMemo( () => { |
| 193 | if ( hasResolvedData && data?.length ) { |
| 194 | let { posts_per_page: perPage = 10, offset = 0 } = query; |
| 195 | |
| 196 | // Ensure the params are a valid integer for comparison. |
| 197 | perPage = parseInt( perPage, 10 ); |
| 198 | offset = parseInt( offset, 10 ); |
| 199 | |
| 200 | if ( perPage < 0 ) { |
| 201 | perPage = data.length; |
| 202 | } |
| 203 | |
| 204 | const items = 'WP_Query' !== queryType |
| 205 | ? data.slice( |
| 206 | offset > -1 ? offset : 0, |
| 207 | offset > -1 ? offset + perPage : perPage |
| 208 | ) : data; |
| 209 | |
| 210 | const result = items.map( ( item, index ) => { |
| 211 | const { ID = null, id = null, post_type: postType = 'post' } = item; |
| 212 | |
| 213 | // Remove any disallowed or hidden keys |
| 214 | for ( const itemKey in item ) { |
| 215 | if ( DISALLOWED_KEYS.includes( itemKey ) || itemKey.startsWith( '_' ) ) { |
| 216 | delete item[ itemKey ]; |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | return { |
| 221 | postType, |
| 222 | postId: id ? id : ID, |
| 223 | 'generateblocks/loopItem': item, |
| 224 | 'generateblocks/loopIndex': index + 1, // Preview doesn't support pagination so this index is correct. |
| 225 | 'generateblocks/loopPreviewId': previewId, |
| 226 | 'generateblocks/hasLoopItems': true, |
| 227 | 'generateblocks/setLoopPreviewId': setPreviewId, |
| 228 | }; |
| 229 | } ); |
| 230 | |
| 231 | return result; |
| 232 | } |
| 233 | |
| 234 | // If no data found, return limited context for the preview loop item. |
| 235 | return [ { |
| 236 | postId: applyFilters( 'generateblocks.editor.looper.fallback.postId', 0, props ), |
| 237 | postType: applyFilters( 'generateblocks.editor.looper.fallback.postType', 'post', props ), |
| 238 | 'generateblocks/loopItem': { |
| 239 | ID: 0, |
| 240 | }, |
| 241 | 'generateblocks/loopIndex': 1, |
| 242 | 'generateblocks/loopPreviewId': previewId, |
| 243 | 'generateblocks/setLoopPreviewId': setPreviewId, |
| 244 | 'generateblocks/hasLoopItems': false, |
| 245 | } ]; |
| 246 | }, [ data, hasResolvedData, query?.posts_per_page, query?.offset, previewId, queryId ] ); |
| 247 | |
| 248 | if ( isResolvingData ) { |
| 249 | return ( <Spinner /> ); |
| 250 | } |
| 251 | |
| 252 | return loopItemsContext.map( ( loopItemContext, index ) => { |
| 253 | // Include index in case the postId is the same for all loop items. |
| 254 | const contextId = loopItemContext?.postId ?? loopItemContext?.[ 'generateblocks/loopIndex' ] ?? index; |
| 255 | const key = `${ contextId }-${ index }`; |
| 256 | let isActive = 0 === index; |
| 257 | |
| 258 | if ( previewId[ queryId ] ) { |
| 259 | const previewIdInt = parseInt( previewId[ queryId ], 10 ); |
| 260 | |
| 261 | if ( previewIdInt ) { |
| 262 | isActive = contextId ? contextId === previewIdInt : false; |
| 263 | } |
| 264 | } |
| 265 | return ( |
| 266 | <BlockContextProvider |
| 267 | key={ key } |
| 268 | value={ loopItemContext } |
| 269 | > |
| 270 | { isActive && innerBlocksProps.children } |
| 271 | <MemoizedBlockPreview |
| 272 | blocks={ innerBlocks } |
| 273 | isHidden={ isActive } |
| 274 | /> |
| 275 | </BlockContextProvider> |
| 276 | ); |
| 277 | } ); |
| 278 | } |
| 279 |