@elementor/editor-site-navigation 0.19.11 → 0.20.0

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.
Files changed (27) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/index.js +91 -43
  3. package/dist/index.mjs +88 -38
  4. package/package.json +2 -2
  5. package/src/api/post.ts +1 -1
  6. package/src/api/settings.ts +5 -9
  7. package/src/api/user.ts +20 -0
  8. package/src/components/panel/actions-menu/actions/__tests__/delete.test.tsx +8 -0
  9. package/src/components/panel/actions-menu/actions/__tests__/set-home.test.tsx +52 -0
  10. package/src/components/panel/actions-menu/actions/__tests__/view.test.tsx +4 -0
  11. package/src/components/panel/actions-menu/actions/delete.tsx +4 -1
  12. package/src/components/panel/actions-menu/actions/duplicate.tsx +5 -1
  13. package/src/components/panel/actions-menu/actions/rename.tsx +1 -0
  14. package/src/components/panel/actions-menu/actions/set-home.tsx +9 -1
  15. package/src/components/panel/add-new-button.tsx +3 -0
  16. package/src/components/panel/posts-list/__tests__/post-list-item.test.tsx +20 -0
  17. package/src/components/panel/posts-list/__tests__/posts-collapsible-list.test.tsx +12 -6
  18. package/src/components/panel/posts-list/list-items/list-item-view.tsx +50 -29
  19. package/src/components/panel/posts-list/posts-collapsible-list.tsx +1 -4
  20. package/src/components/top-bar/__tests__/add-new-page.test.tsx +36 -0
  21. package/src/components/top-bar/__tests__/recently-edited.test.tsx +68 -0
  22. package/src/components/top-bar/create-post-list-item.tsx +3 -1
  23. package/src/components/top-bar/post-list-item.tsx +1 -0
  24. package/src/hooks/__tests__/use-homepage.test.ts +1 -1
  25. package/src/hooks/__tests__/use-posts.test.ts +1 -1
  26. package/src/hooks/use-user.ts +11 -0
  27. package/src/types.ts +8 -1
@@ -36,6 +36,10 @@ describe( '@elementor/editor-site-navigation - PostListItem', () => {
36
36
  status: 'publish',
37
37
  type: 'page',
38
38
  link: 'https://example.local/test-page',
39
+ user_can: {
40
+ edit: true,
41
+ delete: true,
42
+ },
39
43
  };
40
44
 
41
45
  // Act.
@@ -59,6 +63,10 @@ describe( '@elementor/editor-site-navigation - PostListItem', () => {
59
63
  status: 'draft',
60
64
  type: 'page',
61
65
  link: 'https://example.local/test-page',
66
+ user_can: {
67
+ edit: true,
68
+ delete: true,
69
+ },
62
70
  };
63
71
 
64
72
  // Act.
@@ -79,6 +87,10 @@ describe( '@elementor/editor-site-navigation - PostListItem', () => {
79
87
  status: 'publish',
80
88
  type: 'page',
81
89
  link: 'https://example.local/test-Page',
90
+ user_can: {
91
+ edit: true,
92
+ delete: true,
93
+ },
82
94
  };
83
95
 
84
96
  const actions = [ 'View Page', 'Rename', 'Duplicate', 'Delete', 'Set as homepage' ];
@@ -117,6 +129,10 @@ describe( '@elementor/editor-site-navigation - PostListItem', () => {
117
129
  status: 'publish',
118
130
  type: 'page',
119
131
  link: 'https://example.local/test-page',
132
+ user_can: {
133
+ edit: true,
134
+ delete: true,
135
+ },
120
136
  };
121
137
 
122
138
  // Act.
@@ -144,6 +160,10 @@ describe( '@elementor/editor-site-navigation - PostListItem', () => {
144
160
  status: 'publish',
145
161
  type: 'page',
146
162
  link: 'https://example.local/test-page',
163
+ user_can: {
164
+ edit: true,
165
+ delete: true,
166
+ },
147
167
  };
148
168
 
149
169
  renderWithQuery(
@@ -24,8 +24,8 @@ jest.mock( '../../../../hooks/use-posts', () => ( {
24
24
  usePosts: jest.fn( () => ( {
25
25
  isLoading: false,
26
26
  data: [
27
- { id: 1, type: 'page', title: { rendered: 'Home' }, status: 'publish', link: 'www.test.demo' },
28
- { id: 2, type: 'page', title: { rendered: 'About' }, status: 'draft', link: 'www.test.demo' },
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
29
  ],
30
30
  } ) ),
31
31
  } ) );
@@ -34,10 +34,7 @@ jest.mock( '../../../../hooks/use-homepage', () => ( {
34
34
  __esModule: true,
35
35
  useHomepage: jest.fn( () => ( {
36
36
  isLoading: false,
37
- data: {
38
- show_on_front: 'page',
39
- page_on_front: 1,
40
- },
37
+ data: 1,
41
38
  } ) ),
42
39
  } ) );
43
40
 
@@ -46,6 +43,15 @@ jest.mock( '@elementor/editor-documents', () => ( {
46
43
  __useNavigateToDocument: jest.fn(),
47
44
  } ) );
48
45
 
46
+ jest.mock( '../../../../hooks/use-user', () => (
47
+ {
48
+ default: jest.fn( () => ( { isLoading: false, data: { capabilities: {
49
+ edit_pages: true,
50
+ } } } ) ),
51
+ __esModule: true,
52
+ }
53
+ ) );
54
+
49
55
  describe( '@elementor/editor-site-navigation - PostsCollapsibleList', () => {
50
56
  beforeEach( () => {
51
57
  jest.mocked( useActiveDocument ).mockReturnValue( createMockDocument( ( {
@@ -8,6 +8,8 @@ import {
8
8
  ListItemButton,
9
9
  ListItemText,
10
10
  Menu,
11
+ Tooltip,
12
+ Typography,
11
13
  usePopupState,
12
14
  } from '@elementor/ui';
13
15
  import { DotsVerticalIcon, HomeIcon } from '@elementor/icons';
@@ -21,6 +23,23 @@ import SetHome from '../../actions-menu/actions/set-home';
21
23
  import { Post } from '../../../../types';
22
24
  import { __ } from '@wordpress/i18n';
23
25
 
26
+ const DisabledPostTooltip = ( { children, isDisabled }: {children: React.ReactNode, isDisabled: boolean} ) => {
27
+ if ( isDisabled ) {
28
+ const title = <Typography variant="caption">You cannot edit this page.<br />To edit it directly, contact the site owner</Typography>;
29
+
30
+ return <Tooltip
31
+ title={ title }
32
+ placement="bottom"
33
+ arrow={ false }
34
+ >
35
+ { /* @see https://mui.com/material-ui/react-tooltip/#disabled-elements */ }
36
+ { children }
37
+ </Tooltip>;
38
+ }
39
+
40
+ return <>{ children }</>;
41
+ };
42
+
24
43
  export default function ListItemView( { post }: { post: Post } ) {
25
44
  const activeDocument = useActiveDocument();
26
45
  const navigateToDocument = useNavigateToDocument();
@@ -34,40 +53,42 @@ export default function ListItemView( { post }: { post: Post } ) {
34
53
  const isActive = activeDocument?.id === post.id;
35
54
  const status = isActive ? activeDocument?.status.value : post.status;
36
55
  const title = isActive ? activeDocument?.title : post.title.rendered;
56
+ const isDisabled = ! post.user_can.edit;
37
57
 
38
58
  return (
39
59
  <>
40
- <ListItem
41
- disablePadding
42
- secondaryAction={
43
- <IconButton
44
- value
45
- size="small"
46
- { ...bindTrigger( popupState ) }
47
- >
48
- <DotsVerticalIcon fontSize="small" />
49
- </IconButton>
50
- }
51
- >
52
- <ListItemButton
53
- selected={ isActive }
54
- onClick={ () => {
55
- if ( ! isActive ) {
56
- navigateToDocument( post.id );
57
- }
58
- } }
59
- dense
60
+ <DisabledPostTooltip isDisabled={ isDisabled }>
61
+ <ListItem
62
+ disablePadding
63
+ secondaryAction={
64
+ <IconButton
65
+ value
66
+ size="small"
67
+ { ...bindTrigger( popupState ) }
68
+ >
69
+ <DotsVerticalIcon fontSize="small" />
70
+ </IconButton>
71
+ }
60
72
  >
61
- <ListItemText
62
- disableTypography={ true }
73
+ <ListItemButton
74
+ selected={ isActive }
75
+ disabled={ isDisabled }
76
+ onClick={ () => {
77
+ if ( ! isActive ) {
78
+ navigateToDocument( post.id );
79
+ }
80
+ } }
81
+ dense
63
82
  >
64
- <PageTitleAndStatus title={ title } status={ status } />
65
- </ListItemText>
66
- { post.isHome &&
67
- <HomeIcon titleAccess={ __( 'Homepage', 'elementor' ) } color="disabled" />
68
- }
69
- </ListItemButton>
70
- </ListItem>
83
+ <ListItemText disableTypography={ true }>
84
+ <PageTitleAndStatus title={ title } status={ status } />
85
+ </ListItemText>
86
+ { post.isHome &&
87
+ <HomeIcon titleAccess={ __( 'Homepage', 'elementor' ) } color="disabled" />
88
+ }
89
+ </ListItemButton>
90
+ </ListItem>
91
+ </DisabledPostTooltip>
71
92
  <Menu
72
93
  PaperProps={ { sx: { mt: 2, width: 200 } } }
73
94
  MenuListProps={ { dense: true } }
@@ -17,7 +17,7 @@ type Props = {
17
17
  export default function PostsCollapsibleList( { isOpenByDefault = false }: Props ) {
18
18
  const { type, editMode } = usePostListContext();
19
19
  const { data: posts, isLoading: postsLoading, isError: postsError } = usePosts( type );
20
- const { data: homepageSettings } = useHomepage();
20
+ const { data: homepageId } = useHomepage();
21
21
 
22
22
  if ( postsError ) {
23
23
  return <ErrorState />;
@@ -45,9 +45,6 @@ export default function PostsCollapsibleList( { isOpenByDefault = false }: Props
45
45
 
46
46
  const label = `${ postTypesMap[ type ].labels.plural_name } (${ posts.length.toString() })`;
47
47
 
48
- const isHomepageSet = homepageSettings?.show_on_front === 'page' && !! homepageSettings?.page_on_front;
49
- const homepageId = isHomepageSet ? homepageSettings.page_on_front : null;
50
-
51
48
  const mappedPosts = posts.map( ( post ) => {
52
49
  if ( post.id === homepageId ) {
53
50
  return { ...post, isHome: true };
@@ -6,6 +6,7 @@ import { createMockDocument } from 'test-utils';
6
6
  import useRecentPosts from '../../../hooks/use-recent-posts';
7
7
  import useCreatePage from '../../../hooks/use-create-page';
8
8
  import { RecentPost } from '../../../types';
9
+ import useUser from '../../../hooks/use-user';
9
10
 
10
11
  jest.mock( '@elementor/editor-documents', () => ( {
11
12
  __useActiveDocument: jest.fn(),
@@ -23,11 +24,24 @@ jest.mock( '../../../hooks/use-create-page', () => ( {
23
24
  default: jest.fn( () => ( { create: jest.fn(), isLoading: false } ) ),
24
25
  } ) );
25
26
 
27
+ jest.mock( '../../../hooks/use-user', () => (
28
+ {
29
+ default: jest.fn( () => ( { isLoading: false, data: { capabilities: {
30
+ edit_pages: true,
31
+ } } } ) ),
32
+ __esModule: true,
33
+ }
34
+ ) );
35
+
26
36
  describe( '@elementor/recently-edited - Top bar add new page', () => {
27
37
  beforeEach( () => {
28
38
  jest.mocked( useActiveDocument ).mockReturnValue( createMockDocument() );
29
39
  } );
30
40
 
41
+ afterEach( () => {
42
+ jest.clearAllMocks();
43
+ } );
44
+
31
45
  it( 'should render add new page button', () => {
32
46
  // Arrange.
33
47
  const isLoading = false;
@@ -73,4 +87,26 @@ describe( '@elementor/recently-edited - Top bar add new page', () => {
73
87
 
74
88
  expect( navigateToDocument ).toHaveBeenCalledWith( 123 );
75
89
  } );
90
+
91
+ it( 'should be disabled if user does not have edit_posts capability', () => {
92
+ // Arrange.
93
+ jest.mocked( useUser ).mockReturnValue( { isLoading: false, data: { capabilities: {
94
+ edit_pages: false,
95
+ } } } as unknown as ReturnType<typeof useUser> );
96
+
97
+ const isLoading = false;
98
+ const recentPosts: RecentPost[] = [];
99
+
100
+ jest.mocked( useRecentPosts ).mockReturnValue( { isLoading, data: recentPosts } as ReturnType<typeof useRecentPosts> );
101
+
102
+ render( <RecentlyEdited /> );
103
+
104
+ // Act.
105
+ const buttons = screen.getAllByRole( 'button' );
106
+ fireEvent.click( buttons[ 0 ] ); // Opens the recently edited menu
107
+
108
+ // Assert.
109
+ const addNewPage = screen.getByRole( 'menuitem', { name: /Add new page/i }, );
110
+ expect( addNewPage ).toHaveAttribute( 'aria-disabled', 'true' );
111
+ } );
76
112
  } );
@@ -19,6 +19,15 @@ jest.mock( '../../../hooks/use-recent-posts', () => (
19
19
  }
20
20
  ) );
21
21
 
22
+ jest.mock( '../../../hooks/use-user', () => (
23
+ {
24
+ default: jest.fn( () => ( { isLoading: false, data: { capabilities: {
25
+ edit_posts: true,
26
+ } } } ) ),
27
+ __esModule: true,
28
+ }
29
+ ) );
30
+
22
31
  describe( '@elementor/recently-edited - Top bar Recently Edited', () => {
23
32
  it( 'should show the title of the active document without its status when the document is published', async () => {
24
33
  // Arrange.
@@ -155,6 +164,9 @@ describe( '@elementor/recently-edited - Top bar Recently Edited', () => {
155
164
  label: 'Post',
156
165
  },
157
166
  date_modified: 123,
167
+ user_can: {
168
+ edit: true,
169
+ },
158
170
  } ];
159
171
 
160
172
  jest.mocked( useRecentPosts ).mockReturnValue( { isLoading, data: recentPosts } as ReturnType<typeof useRecentPosts> );
@@ -198,6 +210,9 @@ describe( '@elementor/recently-edited - Top bar Recently Edited', () => {
198
210
  label: 'Post',
199
211
  },
200
212
  date_modified: 123,
213
+ user_can: {
214
+ edit: true,
215
+ },
201
216
  },
202
217
  {
203
218
  id: 3,
@@ -209,6 +224,9 @@ describe( '@elementor/recently-edited - Top bar Recently Edited', () => {
209
224
  label: 'Post',
210
225
  },
211
226
  date_modified: 123,
227
+ user_can: {
228
+ edit: true,
229
+ },
212
230
  },
213
231
  {
214
232
  id: 2,
@@ -220,6 +238,9 @@ describe( '@elementor/recently-edited - Top bar Recently Edited', () => {
220
238
  label: 'Post 2',
221
239
  },
222
240
  date_modified: 1234,
241
+ user_can: {
242
+ edit: true,
243
+ },
223
244
  },
224
245
  ];
225
246
 
@@ -241,6 +262,11 @@ describe( '@elementor/recently-edited - Top bar Recently Edited', () => {
241
262
 
242
263
  it( 'should navigate to document on click', () => {
243
264
  // Arrange.
265
+ jest.mocked( useActiveDocument ).mockReturnValue( createMockDocument( {
266
+ id: 1,
267
+ title: 'Test',
268
+ } ) );
269
+
244
270
  const navigateToDocument = jest.fn();
245
271
 
246
272
  jest.mocked( useNavigateToDocument ).mockReturnValue( navigateToDocument );
@@ -257,6 +283,9 @@ describe( '@elementor/recently-edited - Top bar Recently Edited', () => {
257
283
  label: 'Post',
258
284
  },
259
285
  date_modified: 123,
286
+ user_can: {
287
+ edit: true,
288
+ },
260
289
  } ],
261
290
  } as ReturnType<typeof useRecentPosts> );
262
291
 
@@ -272,4 +301,43 @@ describe( '@elementor/recently-edited - Top bar Recently Edited', () => {
272
301
  expect( navigateToDocument ).toHaveBeenCalledTimes( 1 );
273
302
  expect( navigateToDocument ).toHaveBeenCalledWith( 123 );
274
303
  } );
304
+
305
+ it( 'should be disabled when user cant edit post', () => {
306
+ // Arrange.
307
+ jest.mocked( useActiveDocument ).mockReturnValue( createMockDocument( {
308
+ id: 1,
309
+ title: 'Test',
310
+ } ) );
311
+
312
+ const navigateToDocument = jest.fn();
313
+
314
+ jest.mocked( useNavigateToDocument ).mockReturnValue( navigateToDocument );
315
+
316
+ jest.mocked( useRecentPosts ).mockReturnValue( {
317
+ isLoading: false,
318
+ data: [ {
319
+ id: 123,
320
+ title: 'Test post',
321
+ edit_url: 'some_url',
322
+ type: {
323
+ post_type: 'post',
324
+ doc_type: 'wp-post',
325
+ label: 'Post',
326
+ },
327
+ date_modified: 123,
328
+ user_can: {
329
+ edit: false,
330
+ },
331
+ } ],
332
+ } as ReturnType<typeof useRecentPosts> );
333
+
334
+ render( <RecentlyEdited /> );
335
+
336
+ // Open the posts list.
337
+ fireEvent.click( screen.getByRole( 'button' ) );
338
+
339
+ // Assert.
340
+ const listItem = screen.getAllByRole( 'menuitem' )[ 0 ];
341
+ expect( listItem ).toHaveAttribute( 'aria-disabled', 'true' );
342
+ } );
275
343
  } );
@@ -4,6 +4,7 @@ import useCreatePage from '../../hooks/use-create-page';
4
4
  import { PlusIcon } from '@elementor/icons';
5
5
  import { __ } from '@wordpress/i18n';
6
6
  import { __useNavigateToDocument as useNavigateToDocument } from '@elementor/editor-documents';
7
+ import useUser from '../../hooks/use-user';
7
8
 
8
9
  type Props = MenuItemProps & {
9
10
  closePopup: () => void;
@@ -12,12 +13,13 @@ type Props = MenuItemProps & {
12
13
  export function CreatePostListItem( { closePopup, ...props }: Props ) {
13
14
  const { create, isLoading } = useCreatePage();
14
15
  const navigateToDocument = useNavigateToDocument();
16
+ const { data: user } = useUser();
15
17
 
16
18
  return (
17
19
  <MenuItem
20
+ disabled={ isLoading || ! user?.capabilities?.edit_pages }
18
21
  onClick={ async () => {
19
22
  const { id } = await create();
20
-
21
23
  closePopup();
22
24
  await navigateToDocument( id );
23
25
  } }
@@ -16,6 +16,7 @@ export function PostListItem( { post, closePopup, ...props }: Props ) {
16
16
 
17
17
  return (
18
18
  <MenuItem
19
+ disabled={ ! post.user_can.edit }
19
20
  onClick={ async () => {
20
21
  closePopup();
21
22
  await navigateToDocument( post.id );
@@ -26,7 +26,7 @@ describe( '@elementor/site-settings/use-homepage', () => {
26
26
  const { component } = renderHookWithQuery( () => useHomepage() );
27
27
 
28
28
  // Assert.
29
- const expectedPath = `/wp/v2/settings?_fields=${ encodeURIComponent( 'show_on_front,page_on_front' ) }`;
29
+ const expectedPath = '/elementor/v1/site-navigation/homepage';
30
30
 
31
31
  expect( apiFetch ).toHaveBeenCalledWith( {
32
32
  path: expectedPath,
@@ -27,7 +27,7 @@ describe( '@elementor/site-settings/use-posts', () => {
27
27
  const { component } = renderHookWithQuery( () => usePosts( 'page' ) );
28
28
 
29
29
  // Assert.
30
- const expectedPath = `/wp/v2/pages?status=any&per_page=-1&order=asc&_fields=${ encodeURIComponent( 'id,type,title,link,status' ) }`;
30
+ const expectedPath = `/wp/v2/pages?status=any&per_page=-1&order=asc&_fields=${ encodeURIComponent( 'id,type,title,link,status,user_can' ) }`;
31
31
 
32
32
  expect( apiFetch ).toHaveBeenCalledWith( {
33
33
  path: expectedPath,
@@ -0,0 +1,11 @@
1
+ import { useQuery } from '@elementor/query';
2
+ import { getUser } from '../api/user';
3
+
4
+ export const userQueryKey = () => [ 'site-navigation', 'user' ];
5
+
6
+ export default function useUser( ) {
7
+ return useQuery( {
8
+ queryKey: userQueryKey(),
9
+ queryFn: () => getUser(),
10
+ } );
11
+ }
package/src/types.ts CHANGED
@@ -6,7 +6,11 @@ export type Post = {
6
6
  type: string;
7
7
  title: {
8
8
  rendered: string;
9
- }
9
+ };
10
+ user_can: {
11
+ edit: boolean;
12
+ delete: boolean;
13
+ };
10
14
  };
11
15
 
12
16
  export type RecentPost = {
@@ -19,4 +23,7 @@ export type RecentPost = {
19
23
  label: string,
20
24
  },
21
25
  date_modified: number,
26
+ user_can: {
27
+ edit: boolean,
28
+ },
22
29
  }