@elementor/editor-site-navigation 0.20.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.21.1](https://github.com/elementor/elementor-packages/compare/@elementor/editor-site-navigation@0.21.0...@elementor/editor-site-navigation@0.21.1) (2024-02-15)
7
+
8
+ **Note:** Version bump only for package @elementor/editor-site-navigation
9
+
10
+
11
+
12
+
13
+
14
+ # [0.21.0](https://github.com/elementor/elementor-packages/compare/@elementor/editor-site-navigation@0.20.0...@elementor/editor-site-navigation@0.21.0) (2024-01-29)
15
+
16
+
17
+ ### Features
18
+
19
+ * **editor-site-navigation:** pagination for pages panel ([#154](https://github.com/elementor/elementor-packages/issues/154)) ([1a1e1f4](https://github.com/elementor/elementor-packages/commit/1a1e1f46006988d10e0a7af292dbbc57644ed16f))
20
+
21
+
22
+
23
+
24
+
6
25
  # [0.20.0](https://github.com/elementor/elementor-packages/compare/@elementor/editor-site-navigation@0.19.11...@elementor/editor-site-navigation@0.20.0) (2024-01-07)
7
26
 
8
27
 
package/dist/index.js CHANGED
@@ -358,17 +358,28 @@ var postTypesMap = {
358
358
  rest_base: "pages"
359
359
  }
360
360
  };
361
- var getRequest2 = (postTypeSlug) => {
361
+ var POST_PER_PAGE = 10;
362
+ var getRequest2 = async (postTypeSlug, page) => {
362
363
  const baseUri = `/wp/v2/${postTypesMap[postTypeSlug].rest_base}`;
363
364
  const keys = ["id", "type", "title", "link", "status", "user_can"];
364
365
  const queryParams = new URLSearchParams({
365
366
  status: "any",
366
- per_page: "-1",
367
367
  order: "asc",
368
+ page: page.toString(),
369
+ per_page: POST_PER_PAGE.toString(),
368
370
  _fields: keys.join(",")
369
371
  });
370
372
  const uri = baseUri + "?" + queryParams.toString();
371
- return (0, import_api_fetch4.default)({ path: uri });
373
+ const result = await (0, import_api_fetch4.default)({ path: uri, parse: false });
374
+ const data = await result.json();
375
+ const totalPages = Number(result.headers.get("x-wp-totalpages"));
376
+ const totalPosts = Number(result.headers.get("x-wp-total"));
377
+ return {
378
+ data,
379
+ totalPages,
380
+ totalPosts,
381
+ currentPage: page
382
+ };
372
383
  };
373
384
  var createRequest = (postTypeSlug, newPost) => {
374
385
  const path = `/wp/v2/${postTypesMap[postTypeSlug].rest_base}`;
@@ -408,11 +419,26 @@ var duplicateRequest = (originalPost) => {
408
419
 
409
420
  // src/hooks/use-posts.ts
410
421
  var postsQueryKey = (postTypeSlug) => ["site-navigation", "posts", postTypeSlug];
422
+ var flattenData = (data) => {
423
+ if (!data) {
424
+ return data;
425
+ }
426
+ const flattened = [];
427
+ data.pages.forEach((page) => {
428
+ flattened.push(...page.data);
429
+ });
430
+ return flattened;
431
+ };
411
432
  function usePosts(postTypeSlug) {
412
- return (0, import_query3.useQuery)({
433
+ const query = (0, import_query3.useInfiniteQuery)({
413
434
  queryKey: postsQueryKey(postTypeSlug),
414
- queryFn: () => getRequest2(postTypeSlug)
435
+ queryFn: ({ pageParam = 1 }) => getRequest2(postTypeSlug, pageParam),
436
+ initialPageParam: 1,
437
+ getNextPageParam: (lastPage) => {
438
+ return lastPage.currentPage < lastPage.totalPages ? lastPage.currentPage + 1 : void 0;
439
+ }
415
440
  });
441
+ return { ...query, data: { posts: flattenData(query.data), total: query.data?.pages[0]?.totalPosts ?? 0 } };
416
442
  }
417
443
 
418
444
  // src/contexts/post-list-context.tsx
@@ -503,7 +529,7 @@ function CollapsibleList({
503
529
  unmountOnExit: true
504
530
  },
505
531
  /* @__PURE__ */ React7.createElement(import_ui6.List, { dense: true }, children)
506
- ), /* @__PURE__ */ React7.createElement(import_ui6.Divider, { sx: { my: 3 } }));
532
+ ), /* @__PURE__ */ React7.createElement(import_ui6.Divider, { sx: { mt: 1 } }));
507
533
  }
508
534
 
509
535
  // src/components/panel/posts-list/post-list-item.tsx
@@ -1157,7 +1183,7 @@ function ErrorState() {
1157
1183
  // src/components/panel/posts-list/posts-collapsible-list.tsx
1158
1184
  function PostsCollapsibleList({ isOpenByDefault = false }) {
1159
1185
  const { type, editMode } = usePostListContext();
1160
- const { data: posts, isLoading: postsLoading, isError: postsError } = usePosts(type);
1186
+ const { data: { posts, total }, isLoading: postsLoading, isError: postsError, fetchNextPage, hasNextPage, isFetchingNextPage } = usePosts(type);
1161
1187
  const { data: homepageId } = useHomepage();
1162
1188
  if (postsError) {
1163
1189
  return /* @__PURE__ */ React23.createElement(ErrorState, null);
@@ -1173,7 +1199,7 @@ function PostsCollapsibleList({ isOpenByDefault = false }) {
1173
1199
  /* @__PURE__ */ React23.createElement(import_ui15.Skeleton, { sx: { my: 4 }, animation: "wave", variant: "rounded", width: "110px", height: "28px" })
1174
1200
  ), /* @__PURE__ */ React23.createElement(import_ui15.Box, null, /* @__PURE__ */ React23.createElement(import_ui15.Skeleton, { sx: { my: 3 }, animation: "wave", variant: "rounded", width: "100%", height: "24px" }), /* @__PURE__ */ React23.createElement(import_ui15.Skeleton, { sx: { my: 3 }, animation: "wave", variant: "rounded", width: "70%", height: "24px" }), /* @__PURE__ */ React23.createElement(import_ui15.Skeleton, { sx: { my: 3 }, animation: "wave", variant: "rounded", width: "70%", height: "24px" }), /* @__PURE__ */ React23.createElement(import_ui15.Skeleton, { sx: { my: 3 }, animation: "wave", variant: "rounded", width: "70%", height: "24px" })));
1175
1201
  }
1176
- const label = `${postTypesMap[type].labels.plural_name} (${posts.length.toString()})`;
1202
+ const label = `${postTypesMap[type].labels.plural_name} (${total.toString()})`;
1177
1203
  const mappedPosts = posts.map((post) => {
1178
1204
  if (post.id === homepageId) {
1179
1205
  return { ...post, isHome: true };
@@ -1211,7 +1237,11 @@ function PostsCollapsibleList({ isOpenByDefault = false }) {
1211
1237
  sortedPosts.map((post) => {
1212
1238
  return /* @__PURE__ */ React23.createElement(PostListItem2, { key: post.id, post });
1213
1239
  }),
1214
- ["duplicate", "create"].includes(editMode.mode) && /* @__PURE__ */ React23.createElement(PostListItem2, null)
1240
+ ["duplicate", "create"].includes(editMode.mode) && /* @__PURE__ */ React23.createElement(PostListItem2, null),
1241
+ hasNextPage && /* @__PURE__ */ React23.createElement(import_ui15.Box, { sx: {
1242
+ display: "flex",
1243
+ justifyContent: "center"
1244
+ } }, /* @__PURE__ */ React23.createElement(import_ui15.Button, { onClick: fetchNextPage, color: "secondary" }, isFetchingNextPage ? /* @__PURE__ */ React23.createElement(import_ui15.CircularProgress, null) : "Load More"))
1215
1245
  )));
1216
1246
  }
1217
1247
 
package/dist/index.mjs CHANGED
@@ -323,10 +323,10 @@ import { __ as __15 } from "@wordpress/i18n";
323
323
  // src/components/panel/posts-list/posts-collapsible-list.tsx
324
324
  import * as React23 from "react";
325
325
  import { PageTypeIcon as PageTypeIcon2 } from "@elementor/icons";
326
- import { Skeleton, Box as Box4, List as List2 } from "@elementor/ui";
326
+ import { Skeleton, Box as Box4, List as List2, Button as Button4, CircularProgress as CircularProgress5 } from "@elementor/ui";
327
327
 
328
328
  // src/hooks/use-posts.ts
329
- import { useQuery as useQuery3 } from "@elementor/query";
329
+ import { useInfiniteQuery } from "@elementor/query";
330
330
 
331
331
  // src/api/post.ts
332
332
  import apiFetch4 from "@wordpress/api-fetch";
@@ -340,17 +340,28 @@ var postTypesMap = {
340
340
  rest_base: "pages"
341
341
  }
342
342
  };
343
- var getRequest2 = (postTypeSlug) => {
343
+ var POST_PER_PAGE = 10;
344
+ var getRequest2 = async (postTypeSlug, page) => {
344
345
  const baseUri = `/wp/v2/${postTypesMap[postTypeSlug].rest_base}`;
345
346
  const keys = ["id", "type", "title", "link", "status", "user_can"];
346
347
  const queryParams = new URLSearchParams({
347
348
  status: "any",
348
- per_page: "-1",
349
349
  order: "asc",
350
+ page: page.toString(),
351
+ per_page: POST_PER_PAGE.toString(),
350
352
  _fields: keys.join(",")
351
353
  });
352
354
  const uri = baseUri + "?" + queryParams.toString();
353
- return apiFetch4({ path: uri });
355
+ const result = await apiFetch4({ path: uri, parse: false });
356
+ const data = await result.json();
357
+ const totalPages = Number(result.headers.get("x-wp-totalpages"));
358
+ const totalPosts = Number(result.headers.get("x-wp-total"));
359
+ return {
360
+ data,
361
+ totalPages,
362
+ totalPosts,
363
+ currentPage: page
364
+ };
354
365
  };
355
366
  var createRequest = (postTypeSlug, newPost) => {
356
367
  const path = `/wp/v2/${postTypesMap[postTypeSlug].rest_base}`;
@@ -390,11 +401,26 @@ var duplicateRequest = (originalPost) => {
390
401
 
391
402
  // src/hooks/use-posts.ts
392
403
  var postsQueryKey = (postTypeSlug) => ["site-navigation", "posts", postTypeSlug];
404
+ var flattenData = (data) => {
405
+ if (!data) {
406
+ return data;
407
+ }
408
+ const flattened = [];
409
+ data.pages.forEach((page) => {
410
+ flattened.push(...page.data);
411
+ });
412
+ return flattened;
413
+ };
393
414
  function usePosts(postTypeSlug) {
394
- return useQuery3({
415
+ const query = useInfiniteQuery({
395
416
  queryKey: postsQueryKey(postTypeSlug),
396
- queryFn: () => getRequest2(postTypeSlug)
417
+ queryFn: ({ pageParam = 1 }) => getRequest2(postTypeSlug, pageParam),
418
+ initialPageParam: 1,
419
+ getNextPageParam: (lastPage) => {
420
+ return lastPage.currentPage < lastPage.totalPages ? lastPage.currentPage + 1 : void 0;
421
+ }
397
422
  });
423
+ return { ...query, data: { posts: flattenData(query.data), total: query.data?.pages[0]?.totalPosts ?? 0 } };
398
424
  }
399
425
 
400
426
  // src/contexts/post-list-context.tsx
@@ -485,7 +511,7 @@ function CollapsibleList({
485
511
  unmountOnExit: true
486
512
  },
487
513
  /* @__PURE__ */ React7.createElement(List, { dense: true }, children)
488
- ), /* @__PURE__ */ React7.createElement(Divider2, { sx: { my: 3 } }));
514
+ ), /* @__PURE__ */ React7.createElement(Divider2, { sx: { mt: 1 } }));
489
515
  }
490
516
 
491
517
  // src/components/panel/posts-list/post-list-item.tsx
@@ -970,10 +996,10 @@ var updateSettings = (settings) => {
970
996
  };
971
997
 
972
998
  // src/hooks/use-homepage.ts
973
- import { useQuery as useQuery4 } from "@elementor/query";
999
+ import { useQuery as useQuery3 } from "@elementor/query";
974
1000
  var settingsQueryKey = () => ["site-navigation", "homepage"];
975
1001
  function useHomepage() {
976
- return useQuery4({
1002
+ return useQuery3({
977
1003
  queryKey: settingsQueryKey(),
978
1004
  queryFn: () => getSettings()
979
1005
  });
@@ -1171,7 +1197,7 @@ function ErrorState() {
1171
1197
  // src/components/panel/posts-list/posts-collapsible-list.tsx
1172
1198
  function PostsCollapsibleList({ isOpenByDefault = false }) {
1173
1199
  const { type, editMode } = usePostListContext();
1174
- const { data: posts, isLoading: postsLoading, isError: postsError } = usePosts(type);
1200
+ const { data: { posts, total }, isLoading: postsLoading, isError: postsError, fetchNextPage, hasNextPage, isFetchingNextPage } = usePosts(type);
1175
1201
  const { data: homepageId } = useHomepage();
1176
1202
  if (postsError) {
1177
1203
  return /* @__PURE__ */ React23.createElement(ErrorState, null);
@@ -1187,7 +1213,7 @@ function PostsCollapsibleList({ isOpenByDefault = false }) {
1187
1213
  /* @__PURE__ */ React23.createElement(Skeleton, { sx: { my: 4 }, animation: "wave", variant: "rounded", width: "110px", height: "28px" })
1188
1214
  ), /* @__PURE__ */ React23.createElement(Box4, null, /* @__PURE__ */ React23.createElement(Skeleton, { sx: { my: 3 }, animation: "wave", variant: "rounded", width: "100%", height: "24px" }), /* @__PURE__ */ React23.createElement(Skeleton, { sx: { my: 3 }, animation: "wave", variant: "rounded", width: "70%", height: "24px" }), /* @__PURE__ */ React23.createElement(Skeleton, { sx: { my: 3 }, animation: "wave", variant: "rounded", width: "70%", height: "24px" }), /* @__PURE__ */ React23.createElement(Skeleton, { sx: { my: 3 }, animation: "wave", variant: "rounded", width: "70%", height: "24px" })));
1189
1215
  }
1190
- const label = `${postTypesMap[type].labels.plural_name} (${posts.length.toString()})`;
1216
+ const label = `${postTypesMap[type].labels.plural_name} (${total.toString()})`;
1191
1217
  const mappedPosts = posts.map((post) => {
1192
1218
  if (post.id === homepageId) {
1193
1219
  return { ...post, isHome: true };
@@ -1225,7 +1251,11 @@ function PostsCollapsibleList({ isOpenByDefault = false }) {
1225
1251
  sortedPosts.map((post) => {
1226
1252
  return /* @__PURE__ */ React23.createElement(PostListItem2, { key: post.id, post });
1227
1253
  }),
1228
- ["duplicate", "create"].includes(editMode.mode) && /* @__PURE__ */ React23.createElement(PostListItem2, null)
1254
+ ["duplicate", "create"].includes(editMode.mode) && /* @__PURE__ */ React23.createElement(PostListItem2, null),
1255
+ hasNextPage && /* @__PURE__ */ React23.createElement(Box4, { sx: {
1256
+ display: "flex",
1257
+ justifyContent: "center"
1258
+ } }, /* @__PURE__ */ React23.createElement(Button4, { onClick: fetchNextPage, color: "secondary" }, isFetchingNextPage ? /* @__PURE__ */ React23.createElement(CircularProgress5, null) : "Load More"))
1229
1259
  )));
1230
1260
  }
1231
1261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-site-navigation",
3
- "version": "0.20.0",
3
+ "version": "0.21.1",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -32,13 +32,13 @@
32
32
  "dev": "tsup --config=../../tsup.dev.ts"
33
33
  },
34
34
  "dependencies": {
35
- "@elementor/editor-app-bar": "^0.9.7",
35
+ "@elementor/editor-app-bar": "^0.10.0",
36
36
  "@elementor/editor-documents": "^0.10.1",
37
- "@elementor/editor-panels": "^0.4.6",
37
+ "@elementor/editor-panels": "^0.4.7",
38
38
  "@elementor/editor-v1-adapters": "^0.6.0",
39
39
  "@elementor/env": "^0.3.2",
40
40
  "@elementor/icons": "^0.7.2",
41
- "@elementor/query": "^0.1.6",
41
+ "@elementor/query": "^0.2.0",
42
42
  "@elementor/ui": "^1.4.61",
43
43
  "@wordpress/api-fetch": "^6.42.0",
44
44
  "@wordpress/i18n": "^4.45.0",
@@ -50,5 +50,5 @@
50
50
  "elementor": {
51
51
  "type": "extension"
52
52
  },
53
- "gitHead": "c80ed9336d2ce4f6c7626e0aa7d8f87cf50d0f42"
53
+ "gitHead": "83fffeb73f3d0c0c8045f58ba2e22c304ef6a8f0"
54
54
  }
package/src/api/post.ts CHANGED
@@ -24,21 +24,49 @@ export const postTypesMap = {
24
24
  },
25
25
  };
26
26
 
27
- export const getRequest = ( postTypeSlug: Slug ) => {
27
+ export const POST_PER_PAGE = 10;
28
+
29
+ type WpPostsResponse = {
30
+ json: () => Promise<Post[]>,
31
+ headers: {
32
+ get: ( key: string ) => string | null,
33
+ },
34
+ }
35
+
36
+ export type PostsResponse = {
37
+ data: Post[],
38
+ totalPages: number,
39
+ totalPosts: number,
40
+ currentPage: number,
41
+ };
42
+
43
+ export const getRequest = async ( postTypeSlug: Slug, page: number ): Promise<PostsResponse> => {
28
44
  const baseUri = `/wp/v2/${ postTypesMap[ postTypeSlug ].rest_base }`;
29
45
 
30
46
  const keys: Array<keyof Post> = [ 'id', 'type', 'title', 'link', 'status', 'user_can' ];
31
47
 
32
48
  const queryParams = new URLSearchParams( {
33
49
  status: 'any',
34
- per_page: '-1',
35
50
  order: 'asc',
51
+ page: page.toString(),
52
+ per_page: POST_PER_PAGE.toString(),
36
53
  _fields: keys.join( ',' ),
37
54
  } );
38
55
 
39
56
  const uri = baseUri + '?' + queryParams.toString();
40
57
 
41
- return apiFetch<Post[]>( { path: uri } );
58
+ const result = await apiFetch<WpPostsResponse>( { path: uri, parse: false } );
59
+ const data = await result.json();
60
+
61
+ const totalPages = Number( result.headers.get( 'x-wp-totalpages' ) );
62
+ const totalPosts = Number( result.headers.get( 'x-wp-total' ) );
63
+
64
+ return {
65
+ data,
66
+ totalPages,
67
+ totalPosts,
68
+ currentPage: page,
69
+ };
42
70
  };
43
71
 
44
72
  export const createRequest = ( postTypeSlug: Slug, newPost: NewPost ) => {
@@ -4,6 +4,7 @@ import PostsCollapsibleList from '../posts-collapsible-list';
4
4
  import { createMockDocument, renderWithQuery } from 'test-utils';
5
5
  import { PostListContextProvider } from '../../../../contexts/post-list-context';
6
6
  import { __useActiveDocument as useActiveDocument } from '@elementor/editor-documents';
7
+ import { usePosts } from '../../../../hooks/use-posts';
7
8
 
8
9
  const mockMutateAsync = jest.fn();
9
10
  jest.mock( '../../../../hooks/use-posts-actions', () => ( {
@@ -23,10 +24,13 @@ jest.mock( '../../../../hooks/use-posts', () => ( {
23
24
  __esModule: true,
24
25
  usePosts: jest.fn( () => ( {
25
26
  isLoading: false,
26
- data: [
27
- { id: 1, type: 'page', title: { rendered: 'Home' }, status: 'publish', link: 'www.test.demo', user_can: { edit: true } },
28
- { id: 2, type: 'page', title: { rendered: 'About' }, status: 'draft', link: 'www.test.demo', user_can: { edit: true } },
29
- ],
27
+ data: {
28
+ posts: [
29
+ { id: 1, type: 'page', title: { rendered: 'Home' }, status: 'publish', link: 'www.test.demo', user_can: { edit: true, delete: true } },
30
+ { id: 2, type: 'page', title: { rendered: 'About' }, status: 'draft', link: 'www.test.demo', user_can: { edit: true, delete: true } },
31
+ ],
32
+ total: 2,
33
+ },
30
34
  } ) ),
31
35
  } ) );
32
36
 
@@ -156,4 +160,42 @@ describe( '@elementor/editor-site-navigation - PostsCollapsibleList', () => {
156
160
  expect( input ).toBeInTheDocument();
157
161
  expect( input ).toHaveValue( 'Home copy' );
158
162
  } );
163
+
164
+ it( 'Should not render load button when there is no next page', () => {
165
+ // Arrange.
166
+ jest.mocked( usePosts ).mockReturnValue( {
167
+ hasNextPage: false,
168
+ isLoading: false,
169
+ data: {
170
+ posts: [
171
+ { id: 1, type: 'page', title: { rendered: 'Home' }, status: 'publish', link: 'www.test.demo', user_can: { edit: true, delete: true } },
172
+ { id: 2, type: 'page', title: { rendered: 'About' }, status: 'draft', link: 'www.test.demo', user_can: { edit: true, delete: true } },
173
+ ],
174
+ total: 2,
175
+ },
176
+ } as ReturnType<typeof usePosts> );
177
+ renderWithQuery( <PostsCollapsibleList isOpenByDefault={ true } /> );
178
+
179
+ // Assert.
180
+ expect( screen.queryByRole( 'button', { name: 'Load More' } ) ).not.toBeInTheDocument();
181
+ } );
182
+
183
+ it( 'Should render load button when there is next page', () => {
184
+ // Arrange.
185
+ jest.mocked( usePosts ).mockReturnValue( {
186
+ hasNextPage: true,
187
+ isLoading: false,
188
+ data: {
189
+ posts: [
190
+ { id: 1, type: 'page', title: { rendered: 'Home' }, status: 'publish', link: 'www.test.demo', user_can: { edit: true, delete: true } },
191
+ { id: 2, type: 'page', title: { rendered: 'About' }, status: 'draft', link: 'www.test.demo', user_can: { edit: true, delete: true } },
192
+ ],
193
+ total: 2,
194
+ },
195
+ } as ReturnType<typeof usePosts> );
196
+ renderWithQuery( <PostsCollapsibleList isOpenByDefault={ true } /> );
197
+
198
+ // Assert.
199
+ expect( screen.getByRole( 'button', { name: 'Load More' } ) ).toBeInTheDocument();
200
+ } );
159
201
  } );
@@ -70,7 +70,7 @@ export default function CollapsibleList(
70
70
  { children }
71
71
  </List>
72
72
  </Collapse>
73
- <Divider sx={ { my: 3 } } />
73
+ <Divider sx={ { mt: 1 } } />
74
74
  </>
75
75
  );
76
76
  }
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { PageTypeIcon } from '@elementor/icons';
3
- import { Skeleton, Box, List } from '@elementor/ui';
3
+ import { Skeleton, Box, List, Button, CircularProgress } from '@elementor/ui';
4
4
  import { usePosts } from '../../../hooks/use-posts';
5
5
  import { usePostListContext } from '../../../contexts/post-list-context';
6
6
  import { postTypesMap } from '../../../api/post';
@@ -16,7 +16,7 @@ type Props = {
16
16
 
17
17
  export default function PostsCollapsibleList( { isOpenByDefault = false }: Props ) {
18
18
  const { type, editMode } = usePostListContext();
19
- const { data: posts, isLoading: postsLoading, isError: postsError } = usePosts( type );
19
+ const { data: { posts, total }, isLoading: postsLoading, isError: postsError, fetchNextPage, hasNextPage, isFetchingNextPage } = usePosts( type );
20
20
  const { data: homepageId } = useHomepage();
21
21
 
22
22
  if ( postsError ) {
@@ -43,7 +43,7 @@ export default function PostsCollapsibleList( { isOpenByDefault = false }: Props
43
43
  );
44
44
  }
45
45
 
46
- const label = `${ postTypesMap[ type ].labels.plural_name } (${ posts.length.toString() })`;
46
+ const label = `${ postTypesMap[ type ].labels.plural_name } (${ total.toString() })`;
47
47
 
48
48
  const mappedPosts = posts.map( ( post ) => {
49
49
  if ( post.id === homepageId ) {
@@ -91,6 +91,16 @@ export default function PostsCollapsibleList( { isOpenByDefault = false }: Props
91
91
  [ 'duplicate', 'create' ].includes( editMode.mode ) &&
92
92
  <PostListItem />
93
93
  }
94
+ { hasNextPage &&
95
+ <Box sx={ {
96
+ display: 'flex',
97
+ justifyContent: 'center',
98
+ } }>
99
+ <Button onClick={ fetchNextPage } color="secondary">
100
+ { isFetchingNextPage ? <CircularProgress /> : 'Load More' }
101
+ </Button>
102
+ </Box>
103
+ }
94
104
  </CollapsibleList>
95
105
  </List>
96
106
  </>
@@ -2,42 +2,76 @@ import { waitFor } from '@testing-library/react';
2
2
  import apiFetch from '@wordpress/api-fetch';
3
3
  import { renderHookWithQuery } from 'test-utils';
4
4
  import { usePosts } from '../use-posts';
5
+ import { POST_PER_PAGE } from '../../api/post';
5
6
 
6
- jest.mock( '@wordpress/api-fetch' );
7
+ const MOCK_POST_PER_PAGE = 2;
7
8
 
8
- describe( '@elementor/site-settings/use-posts', () => {
9
- beforeEach( () => {
10
- jest.mocked( apiFetch ).mockImplementation( () => Promise.resolve( [] ) );
11
- } );
9
+ jest.mock( '@wordpress/api-fetch', () => ( {
10
+ default: jest.fn( ( param ) => {
11
+ const pages = [
12
+ { id: 1, type: 'page', title: { rendered: 'Home' }, status: 'draft', link: 'www.test.demo' },
13
+ { id: 2, type: 'page', title: { rendered: 'About' }, status: 'publish', link: 'www.test.demo' },
14
+ { id: 3, type: 'page', title: { rendered: 'Services' }, status: 'publish', link: 'www.test.demo', isHome: true },
15
+ ];
16
+
17
+ const url = new URL( 'http:/example.com' + param.path );
18
+
19
+ const page = Number( url.searchParams.get( 'page' ) );
12
20
 
21
+ const startIndex = ( page - 1 ) * MOCK_POST_PER_PAGE;
22
+ const endIndex = startIndex + MOCK_POST_PER_PAGE;
23
+ const paginatedPages = pages.slice( startIndex, endIndex );
24
+
25
+ return {
26
+ json: () => Promise.resolve( paginatedPages ),
27
+ headers: {
28
+ get: ( header: string ) => {
29
+ switch ( header ) {
30
+ case 'x-wp-totalpages':
31
+ return Math.ceil( pages.length / MOCK_POST_PER_PAGE );
32
+ case 'x-wp-total':
33
+ return pages.length;
34
+ default:
35
+ return null;
36
+ }
37
+ },
38
+ },
39
+ };
40
+ } ),
41
+ __esModule: true,
42
+ } ) );
43
+
44
+ describe( '@elementor/site-settings/use-posts', () => {
13
45
  afterEach( () => {
14
46
  jest.clearAllMocks();
15
47
  } );
16
48
 
17
49
  it( 'usePosts hook should return posts list by type', async () => {
18
- // Arrange.
50
+ //Arrange
19
51
  const pages = [
20
52
  { id: 1, type: 'page', title: { rendered: 'Home' }, status: 'draft', link: 'www.test.demo' },
21
53
  { id: 2, type: 'page', title: { rendered: 'About' }, status: 'publish', link: 'www.test.demo' },
22
54
  { id: 3, type: 'page', title: { rendered: 'Services' }, status: 'publish', link: 'www.test.demo', isHome: true },
23
55
  ];
24
- jest.mocked( apiFetch ).mockImplementation( () => Promise.resolve( pages ) );
25
56
 
26
57
  // Act.
27
58
  const { component } = renderHookWithQuery( () => usePosts( 'page' ) );
28
59
 
29
60
  // Assert.
30
- const expectedPath = `/wp/v2/pages?status=any&per_page=-1&order=asc&_fields=${ encodeURIComponent( 'id,type,title,link,status,user_can' ) }`;
61
+ const expectedPath = `/wp/v2/pages?status=any&order=asc&page=1&per_page=${ POST_PER_PAGE }&_fields=${ encodeURIComponent( 'id,type,title,link,status,user_can' ) }`;
31
62
 
32
63
  expect( apiFetch ).toHaveBeenCalledWith( {
64
+ parse: false,
33
65
  path: expectedPath,
34
66
  } );
35
67
  expect( apiFetch ).toHaveBeenCalledTimes( 1 );
36
68
 
37
69
  await waitFor( () => {
38
- return component.result.current.isSuccess;
70
+ expect( component.result.current.isLoading ).toBeFalsy();
39
71
  } );
40
72
 
41
- expect( component.result.current.data ).toBe( pages );
73
+ expect( component.result.current.data.posts ).toContainEqual( pages[ 0 ] );
74
+ expect( component.result.current.data.posts ).toContainEqual( pages[ 1 ] );
75
+ expect( component.result.current.data.posts ).not.toContainEqual( pages[ 2 ] );
42
76
  } );
43
77
  } );
@@ -1,11 +1,32 @@
1
- import { useQuery } from '@elementor/query';
2
- import { getRequest, Slug } from '../api/post';
1
+ import { useInfiniteQuery } from '@elementor/query';
2
+ import { getRequest, PostsResponse, Slug } from '../api/post';
3
+ import { Post } from '../types';
3
4
 
4
5
  export const postsQueryKey = ( postTypeSlug: string ) => [ 'site-navigation', 'posts', postTypeSlug ];
5
6
 
7
+ const flattenData = ( data?: {pages: PostsResponse[]} ) => {
8
+ if ( ! data ) {
9
+ return data;
10
+ }
11
+
12
+ const flattened: Post[] = [];
13
+
14
+ data.pages.forEach( ( page ) => {
15
+ flattened.push( ...page.data );
16
+ } );
17
+
18
+ return flattened;
19
+ };
20
+
6
21
  export function usePosts( postTypeSlug: Slug ) {
7
- return useQuery( {
22
+ const query = useInfiniteQuery( {
8
23
  queryKey: postsQueryKey( postTypeSlug ),
9
- queryFn: () => getRequest( postTypeSlug ),
24
+ queryFn: ( { pageParam = 1 } ) => getRequest( postTypeSlug, pageParam ),
25
+ initialPageParam: 1,
26
+ getNextPageParam: ( lastPage ) => {
27
+ return lastPage.currentPage < lastPage.totalPages ? lastPage.currentPage + 1 : undefined;
28
+ },
10
29
  } );
30
+
31
+ return { ...query, data: { posts: flattenData( query.data ), total: query.data?.pages[ 0 ]?.totalPosts ?? 0 } };
11
32
  }