Skip to content

Commit e62af2d

Browse files
authored
Merge pull request #3 from humanmade/claude/fix-post-pagination-limits-012TsfPWTvCW9aRg2oABs38K
Fix post template max per page to stay within query limits
2 parents 198b9ff + 800ebeb commit e62af2d

File tree

4 files changed

+145
-5
lines changed

4 files changed

+145
-5
lines changed

hm-query-loop.php

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ function filter_block_metadata( $metadata ) {
117117
*/
118118
$query_loop_used_posts = [];
119119

120+
/**
121+
* Track post template per page settings within each query loop.
122+
* Keyed by query ID, stores array of per page values in order.
123+
*
124+
* @var array
125+
*/
126+
$query_loop_post_template_per_pages = [];
127+
120128
/**
121129
* Get displayed post IDs.
122130
*
@@ -237,11 +245,38 @@ function render_block( $block_content, $block ) {
237245
*/
238246
function filter_query_loop_block_query_vars( $query, WP_Block $block ) {
239247
if ( $block->name === 'core/post-template' ) {
248+
global $query_loop_post_template_per_pages;
249+
240250
$attrs = $block->parsed_block['attrs'];
241-
if ( empty( $attrs['hmQueryLoop']['perPage'] ) ) {
242-
return $query;
251+
$query_id = $block->context['queryId'] ?? 0;
252+
253+
// Initialize tracking array for this query loop if not exists
254+
if ( ! isset( $query_loop_post_template_per_pages[ $query_id ] ) ) {
255+
$query_loop_post_template_per_pages[ $query_id ] = [];
243256
}
244-
$attrs['hmQueryLoop']['excludeDisplayedForCurrentLoop'] = $block->context['queryId'];
257+
258+
// Get the query loop's total posts per page
259+
$query_per_page = $query['posts_per_page'] ?? get_option( 'posts_per_page', 10 );
260+
261+
// Calculate total posts used by preceding post templates
262+
$used_posts = array_sum( $query_loop_post_template_per_pages[ $query_id ] );
263+
264+
// Get this post template's per page setting
265+
$post_template_per_page = $attrs['hmQueryLoop']['perPage'] ?? null;
266+
267+
// If no explicit perPage is set, calculate remaining posts
268+
if ( empty( $post_template_per_page ) ) {
269+
$remaining_posts = max( 1, $query_per_page - $used_posts );
270+
$post_template_per_page = $remaining_posts;
271+
272+
// Set it in attrs so it gets tracked
273+
$attrs['hmQueryLoop']['perPage'] = $remaining_posts;
274+
}
275+
276+
// Track this post template's per page value
277+
$query_loop_post_template_per_pages[ $query_id ][] = $post_template_per_page;
278+
279+
$attrs['hmQueryLoop']['excludeDisplayedForCurrentLoop'] = $query_id;
245280
return modify_query_from_block_attrs( $query, $attrs );
246281
}
247282
return modify_query_from_block_attrs( $query, $block->context );

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ addFilter(
220220
*/
221221
const withPostTemplateInspectorControls = createHigherOrderComponent( ( BlockEdit ) => {
222222
return ( props ) => {
223-
const { name, attributes, setAttributes, context } = props;
223+
const { name, attributes, setAttributes, context, clientId } = props;
224224

225225
if ( name !== 'core/post-template' ) {
226226
return <BlockEdit { ...props } />;
@@ -236,6 +236,22 @@ const withPostTemplateInspectorControls = createHigherOrderComponent( ( BlockEdi
236236
// Get the max per page from query block's perPage or site default
237237
const queryPerPage = context?.query?.perPage || window.hmQueryLoopSettings?.postsPerPage || 10;
238238

239+
// Get the list of child post templates for the current query loop
240+
const { postTemplates } = useContext( UsedPostsContext );
241+
242+
// Calculate the total posts used by preceding post templates
243+
let usedPosts = 0;
244+
for ( const postTemplate of postTemplates ) {
245+
if ( postTemplate.clientId === clientId ) {
246+
// Stop when we reach the current post template
247+
break;
248+
}
249+
usedPosts += postTemplate.attributes?.hmQueryLoop?.perPage || 0;
250+
}
251+
252+
// Calculate remaining posts available for this post template
253+
const remainingPosts = Math.max( 1, queryPerPage - usedPosts );
254+
239255
return (
240256
<>
241257
<BlockEdit { ...props } />
@@ -267,7 +283,7 @@ const withPostTemplateInspectorControls = createHigherOrderComponent( ( BlockEdi
267283
} );
268284
} }
269285
min={ 1 }
270-
max={ queryPerPage }
286+
max={ remainingPosts }
271287
/>
272288
</PanelBody>
273289
</InspectorControls>

tests/e2e/multiple-post-templates.spec.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,77 @@ test.describe('Multiple Post Templates', () => {
116116
console.log('Unique posts:', uniqueTitles.length);
117117
expect(uniqueTitles.length).toBe(6);
118118
});
119+
120+
test('should automatically limit third post template to remaining posts', async ({ page, admin, editor, selectBlock }) => {
121+
// Create a new page
122+
await admin.createNewPost({ postType: 'page' });
123+
await page.waitForTimeout(1500);
124+
await page.getByRole('button', { name: 'Close' }).click();
125+
await editor.openDocumentSettingsSidebar();
126+
await page.locator('iframe[name="editor-canvas"]').contentFrame().getByRole('textbox', { name: 'Add title' }).click();
127+
await page.locator('iframe[name="editor-canvas"]').contentFrame().getByRole('textbox', { name: 'Add title' }).fill('test automatic limit');
128+
await page.locator('iframe[name="editor-canvas"]').contentFrame().getByRole('button', { name: 'Add default block' }).click();
129+
await page.locator('iframe[name="editor-canvas"]').contentFrame().getByRole('document', { name: 'Empty block; start writing or' }).fill('/query');
130+
await page.getByRole('option', { name: 'Query Loop' }).click();
131+
await page.locator('iframe[name="editor-canvas"]').contentFrame().getByRole('button', { name: 'Start blank' }).click();
132+
await page.locator('iframe[name="editor-canvas"]').contentFrame().getByRole('button', { name: 'Image, Date, & Title' }).click();
133+
134+
// Configure first post template: 1 post
135+
await page.locator('iframe[name="editor-canvas"]').contentFrame().locator('.components-placeholder__illustration').first().click();
136+
await page.getByRole('button', { name: 'Select parent block: Post' }).click();
137+
await page.getByRole('button', { name: 'Post Template Settings' }).click();
138+
await page.getByRole('spinbutton', { name: 'Posts per template' }).click();
139+
await page.getByRole('spinbutton', { name: 'Posts per template' }).fill('1');
140+
141+
// Duplicate to create second post template
142+
await page.getByRole('toolbar', { name: 'Block tools' }).getByLabel('Options').click();
143+
await page.getByRole('menuitem', { name: /^Duplicate / }).click();
144+
145+
// Configure second post template: 2 posts
146+
await page.getByRole('button', { name: 'Grid view' }).click();
147+
await page.getByRole('button', { name: 'Post Template Settings' }).click();
148+
await page.getByRole('spinbutton', { name: 'Posts per template' }).click();
149+
await page.getByRole('spinbutton', { name: 'Posts per template' }).press('Shift+ArrowLeft');
150+
await page.getByRole('spinbutton', { name: 'Posts per template' }).fill('2');
151+
await page.getByRole('spinbutton', { name: 'Columns' }).click();
152+
await page.getByRole('spinbutton', { name: 'Columns' }).press('Shift+ArrowLeft');
153+
await page.getByRole('spinbutton', { name: 'Columns' }).fill('2');
154+
155+
// Duplicate to create third post template
156+
await page.getByRole('toolbar', { name: 'Block tools' }).getByLabel('Options').click();
157+
await page.getByRole('menuitem', { name: /^Duplicate / }).click();
158+
159+
// Configure third post template: leave Posts per template EMPTY (should auto-calculate to 7)
160+
await page.getByRole('spinbutton', { name: 'Columns' }).click();
161+
await page.getByRole('spinbutton', { name: 'Columns' }).press('Shift+ArrowLeft');
162+
await page.getByRole('spinbutton', { name: 'Columns' }).fill('3');
163+
await page.getByRole('button', { name: 'Post Template Settings' }).click();
164+
165+
// Clear the Posts per template field if it has a value
166+
const postsPerTemplateInput = page.getByRole('spinbutton', { name: 'Posts per template' });
167+
const currentValue = await postsPerTemplateInput.inputValue();
168+
if (currentValue) {
169+
await postsPerTemplateInput.click();
170+
await postsPerTemplateInput.press('Shift+ArrowLeft');
171+
await postsPerTemplateInput.press('Backspace');
172+
}
173+
174+
// Publish and view the page
175+
await page.getByRole('button', { name: 'Publish', exact: true }).click();
176+
await page.getByLabel('Editor publish').getByRole('button', { name: 'Publish', exact: true }).click();
177+
await page.getByLabel('Editor publish').getByRole('link', { name: 'View Page' }).click();
178+
179+
// Get all the post titles on the page
180+
const postTitles = await page.locator('.wp-block-post-template .wp-block-post-title').allTextContents();
181+
console.log('Post titles found:', postTitles);
182+
console.log('Total posts:', postTitles.length);
183+
184+
// Should have exactly 10 posts total (1 + 2 + 7 auto-calculated from default 10 posts per page)
185+
expect(postTitles.length).toBe(10);
186+
187+
// Verify no duplicate posts
188+
const uniqueTitles = [...new Set(postTitles)];
189+
console.log('Unique posts:', uniqueTitles.length);
190+
expect(uniqueTitles.length).toBe(10);
191+
});
119192
});

0 commit comments

Comments
 (0)