Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
110 changes: 106 additions & 4 deletions next/src/components/core/Query/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,107 @@ const queryContentNodes = gql`
}
`;

const PAGINATION_BLOCKS = new Set([
'core/query-pagination-next',
'core/query-pagination-previous',
'core/query-pagination-numbers',
]);

const buildPageHref = (baseUri: string, page: number) => {
if (page <= 1) return baseUri;
const base = baseUri.endsWith('/') ? baseUri : `${baseUri}/`;
return `${base}page/${page}/`;
};

/**
* Compute the request-time attributes for a single pagination child block,
* mirroring what WordPress core injects when it server-renders these blocks.
*/
const paginationChildAttrs = (
name: string,
attrs: Record<string, unknown>,
currentPage: number,
totalPages: number | null,
baseUri: string
): Record<string, unknown> => {
if (name === 'core/query-pagination-numbers') {
return { currentPage, totalPages, baseUri };
}

const isNext = name === 'core/query-pagination-next';
const rawLabel = typeof attrs.label === 'string' ? attrs.label.trim() : '';
const label = rawLabel || (isNext ? 'Next Page' : 'Previous Page');

let href: string | null = null;
let enabled = false;

if (isNext) {
const canGoNext = totalPages === null ? true : currentPage < totalPages;
if (canGoNext) {
href = buildPageHref(baseUri, currentPage + 1);
enabled = true;
}
} else if (currentPage > 1) {
href = buildPageHref(baseUri, currentPage - 1);
enabled = true;
}

return { href, label, isDisabled: !enabled };
};

/**
* Walk this query's `innerBlocks` and inject the resolved pagination state into
* its pagination children, so they stay in sync at request time (no rebuild
* needed when posts are added). Nested `core/query` loops are left untouched —
* each resolves its own pagination.
*/
const injectPagination = (
blocks: BlockPropsType[],
currentPage: number,
totalPages: number | null,
baseUri: string
): BlockPropsType[] =>
blocks.map((block) => {
if (block.name === 'core/query') return block;

if (PAGINATION_BLOCKS.has(block.name)) {
return {
...block,
attributes: {
...block.attributes,
...paginationChildAttrs(
block.name,
block.attributes,
currentPage,
totalPages,
baseUri
),
},
};
}

return {
...block,
innerBlocks: injectPagination(
block.innerBlocks ?? [],
currentPage,
totalPages,
baseUri
),
};
});

export const getData = async (
fetcher: FetchApiFuncType,
attrs: QueryAttributes | null = null,
lang: string | null = null
lang: string | null = null,
context: BlockDataContext = {}
) => {
const perPageRaw = attrs?.query?.perPage;
const perPage = Math.min(100, Math.max(1, toPositiveInt(perPageRaw) ?? 10));

const offsetRaw = attrs?.query?.offset;
const offset = Math.max(0, toPositiveInt(offsetRaw) ?? 0);
const page = context?.page && context.page > 0 ? context.page : 1;
const offset = (page - 1) * perPage;

const order = toOrderEnum(attrs?.query?.order);
const orderby = toOrderByEnum(attrs?.query?.orderBy);
Expand Down Expand Up @@ -157,7 +248,7 @@ export const getData = async (
typeof total === 'number' && total >= 0
? Math.ceil(total / perPage)
: null;
const currentPage = Math.floor(offset / perPage) + 1;
const currentPage = page;

const nodes = (data?.contentNodes?.nodes ?? []).map((node: unknown) => ({
excerpt: '',
Expand All @@ -167,6 +258,16 @@ export const getData = async (
...(node as Record<string, unknown>),
}));

const baseUri = context?.baseUri ?? '/';
const innerBlocks = Array.isArray(context?.innerBlocks)
? injectPagination(
context.innerBlocks,
currentPage,
totalPages,
baseUri
)
: undefined;

return {
data: {
posts: {
Expand All @@ -180,5 +281,6 @@ export const getData = async (
total,
totalPages,
},
...(innerBlocks !== undefined ? { innerBlocks } : {}),
};
};
72 changes: 72 additions & 0 deletions next/src/components/core/QueryPaginationNumbers/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Link from 'next/link';

/**
* Mirrors WordPress `paginate_links()` `end_size` (how many pages are always
* shown at the very start and end of the list).
*/
const END_SIZE = 1;

const buildHref = (baseUri: string, page: number) => {
if (page <= 1) return baseUri;
const base = baseUri.endsWith('/') ? baseUri : `${baseUri}/`;
return `${base}page/${page}/`;
};

/**
* Numbered pagination for a `core/query` loop. `currentPage`, `totalPages` and
* `baseUri` are injected at request time by the parent query's data layer (see
* `core/Query/data.ts`), matching the next/previous blocks. Rendering replicates
* `paginate_links()` with `prev_next` disabled:
* the first/last `END_SIZE` pages plus a `midSize` window around the current
* page are shown, and skipped ranges collapse into `…` dots.
*/
export default function QueryPaginationNumbers({
midSize = 2,
currentPage,
totalPages,
baseUri = '/',
}: QueryPaginationNumbersProps) {
if (!currentPage || !totalPages || totalPages < 2) return null;

const elements: React.ReactNode[] = [];
let dots = false;

for (let page = 1; page <= totalPages; page++) {
if (page === currentPage) {
elements.push(
<span
key={page}
aria-current="page"
className="page-numbers current"
>
{page}
</span>
);
dots = true;
} else if (
page <= END_SIZE ||
(page >= currentPage - midSize && page <= currentPage + midSize) ||
page > totalPages - END_SIZE
) {
elements.push(
<Link
key={page}
className="page-numbers"
href={buildHref(baseUri, page)}
>
{page}
</Link>
);
dots = true;
} else if (dots) {
elements.push(
<span key={`dots-${page}`} className="page-numbers dots">
</span>
);
dots = false;
}
}

return <div className="wp-block-query-pagination-numbers">{elements}</div>;
}
12 changes: 12 additions & 0 deletions next/src/components/core/QueryPaginationNumbers/typings.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
interface QueryPaginationNumbersAttributes extends BlockAttributes {
midSize?: number;
}

interface QueryPaginationNumbersProps extends QueryPaginationNumbersAttributes {
/** Injected at request time by the parent `core/query` data layer. */
currentPage?: number;
/** Injected by the parent query; `null`/undefined when the total is unknown. */
totalPages?: number | null;
/** Base uri of the paginated node (e.g. `/blog/`), injected by the parent query. */
baseUri?: string;
}
2 changes: 2 additions & 0 deletions next/src/components/global/Blocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const blocksList: Record<string, () => Promise<BlockModule>> = {
'core/query': () => import('../core/Query'),
'core/query-pagination': () => import('../core/QueryPagination'),
'core/query-pagination-next': () => import('../core/QueryPaginationNext'),
'core/query-pagination-numbers': () =>
import('../core/QueryPaginationNumbers'),
'core/query-pagination-previous': () =>
import('../core/QueryPaginationPrevious'),
'core/query-title': () => import('../core/QueryTitle'),
Expand Down
7 changes: 6 additions & 1 deletion next/src/lib/format-blocks-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import getBlockFinalComponentProps from '@/lib/get-block-final-component-props';

export default async function formatBlocksJSON(
blocksJSON: string,
options?: { skipGetData?: boolean; lang?: string | null }
options?: {
skipGetData?: boolean;
lang?: string | null;
page?: number;
baseUri?: string;
}
) {
/**
* Replace ocurrences of WP upload URIs with relative url
Expand Down
47 changes: 37 additions & 10 deletions next/src/lib/get-block-final-component-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ export default function getBlockFinalComponentProps(
attributes: object;
innerBlocks: Array<BlockPropsType>;
},
options?: { skipGetData?: boolean; lang?: string | null }
options?: {
skipGetData?: boolean;
lang?: string | null;
page?: number;
baseUri?: string;
}
): Promise<BlockPropsType> {
return new Promise(async (res) => {
const props: BlockPropsType = {
Expand All @@ -48,9 +53,9 @@ export default function getBlockFinalComponentProps(
};

Promise.allSettled([
getAttributes(name, attributes, options),
getAttributes(name, attributes, innerBlocks, options),
getInnerBlocks(innerBlocks, options),
]).then(([attrsResult, blksResult]) => {
]).then(async ([attrsResult, blksResult]) => {
if (attrsResult.status === 'fulfilled') {
const { attrs, innerBlocks: dataInnerBlocks } =
attrsResult.value as {
Expand All @@ -59,10 +64,16 @@ export default function getBlockFinalComponentProps(
};
props.attributes = attrs ?? {};

// If getData returned innerBlocks, use those (fresh) and skip the static ones.
// (this is a fix made for core/navigation for example, which has links as innerBlocks, but we want them to be always up to date, even if it's part of the FSE template)
// If getData returned innerBlocks, use those (fresh) instead of the
// static ones — but still run them back through this same enrichment
// pipeline, so any dynamic block nested inside (e.g. a `core/navigation`
// nested inside a submenu, or a pagination block inside a `core/query`)
// gets its own getData resolved too.
if (dataInnerBlocks !== undefined) {
props.innerBlocks = dataInnerBlocks;
props.innerBlocks = (await getInnerBlocks(
dataInnerBlocks,
options
).catch(() => [])) as BlockPropsType['innerBlocks'];
} else if (blksResult.status === 'fulfilled') {
props.innerBlocks =
(blksResult.value as BlockPropsType['innerBlocks']) ??
Expand All @@ -88,7 +99,13 @@ export default function getBlockFinalComponentProps(
const getAttributes = (
name: string,
attributes: object,
options?: { skipGetData?: boolean; lang?: string | null }
innerBlocks: Array<BlockPropsType>,
options?: {
skipGetData?: boolean;
lang?: string | null;
page?: number;
baseUri?: string;
}
) =>
new Promise(async (res) => {
if (
Expand All @@ -104,7 +121,8 @@ const getAttributes = (
getData?: (
fetcher: FetchApiFuncType,
attrs: object,
lang?: string | null
lang?: string | null,
context?: BlockDataContext
) => Promise<object>;
}
).getData;
Expand All @@ -114,7 +132,11 @@ const getAttributes = (
return;
}

getData(fetchAPI, attributes, options?.lang ?? null).then(
getData(fetchAPI, attributes, options?.lang ?? null, {
page: options?.page,
baseUri: options?.baseUri,
innerBlocks,
}).then(
(data = {}) => {
const { innerBlocks, ...restData } = data as {
innerBlocks?: BlockPropsType[];
Expand All @@ -130,7 +152,12 @@ const getAttributes = (

const getInnerBlocks = (
blocks: Array<BlockPropsType>,
options?: { skipGetData?: boolean; lang?: string | null }
options?: {
skipGetData?: boolean;
lang?: string | null;
page?: number;
baseUri?: string;
}
) =>
new Promise((res, rej) => {
if (!(blocks?.length > 0)) rej([]);
Expand Down
15 changes: 8 additions & 7 deletions next/src/lib/get-node-by-uri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,6 @@ export default async function getNodeByURI(
const response = await fetchAPI(query, {
variables,
auth,
headers: {
'X-Query-Page': String(routePage && routePage > 0 ? routePage : 1),
},
});

const { node: rawNode, seo, generalSettings } = response;
Expand Down Expand Up @@ -119,12 +116,14 @@ export default async function getNodeByURI(
previewDraft
? (node.preview?.node?.blocksJSON ?? '')
: (node?.blocksJSON ?? ''),
{ lang }
{ lang, page: routePage, baseUri: uri }
),
getTemplateData(node),
enrichTemplateBlocks(
getTemplateBlocks(node?.fseTemplate?.slug),
lang
lang,
routePage,
uri
),
])
.then(([bProm, tProm, tbProm]) => ({
Expand Down Expand Up @@ -339,13 +338,15 @@ const getTemplateBlocks = (templateSlug: string): BlockPropsType[] => {
*/
const enrichTemplateBlocks = (
blocks: BlockPropsType[],
lang: string | null = null
lang: string | null = null,
page = 1,
baseUri: string | undefined = undefined
): Promise<BlockPropsType[]> =>
blocks.length === 0
? Promise.resolve([])
: Promise.allSettled(
blocks.map((block) =>
getBlockFinalComponentProps(block, { lang })
getBlockFinalComponentProps(block, { lang, page, baseUri })
)
).then(
(results) =>
Expand Down
Loading