@elementor/editor-site-navigation 0.3.0 → 0.4.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,29 @@
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.4.1](https://github.com/elementor/elementor-packages/compare/@elementor/editor-site-navigation@0.4.0...@elementor/editor-site-navigation@0.4.1) (2023-05-23)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **site-navigation:** render special chars [ED-10809] ([#42](https://github.com/elementor/elementor-packages/issues/42)) ([a276cf3](https://github.com/elementor/elementor-packages/commit/a276cf36e4f428da3b109f0bcd4e83ca3501e793))
12
+
13
+
14
+
15
+
16
+
17
+ # [0.4.0](https://github.com/elementor/elementor-packages/compare/@elementor/editor-site-navigation@0.3.0...@elementor/editor-site-navigation@0.4.0) (2023-05-21)
18
+
19
+
20
+ ### Features
21
+
22
+ * **editor-site-navigation:** change post without refresh [ED-10713] ([#39](https://github.com/elementor/elementor-packages/issues/39)) ([b72e82f](https://github.com/elementor/elementor-packages/commit/b72e82f9adb9c1237300cbf603f33f268f9f0400))
23
+ * **locations:** change api to support props for slots [ED-10730] ([#38](https://github.com/elementor/elementor-packages/issues/38)) ([44bec3c](https://github.com/elementor/elementor-packages/commit/44bec3cda1020037ba7105c6f05ce4baa8e3b376))
24
+
25
+
26
+
27
+
28
+
6
29
  # 0.3.0 (2023-05-16)
7
30
 
8
31
 
package/dist/index.js CHANGED
@@ -52,10 +52,10 @@ function getIconsMap() {
52
52
  }
53
53
 
54
54
  // src/components/top-bar/recently-edited.tsx
55
- var React4 = __toESM(require("react"));
56
- var import_ui4 = require("@elementor/ui");
55
+ var React5 = __toESM(require("react"));
56
+ var import_ui5 = require("@elementor/ui");
57
57
  var import_icons4 = require("@elementor/icons");
58
- var import_editor_documents = require("@elementor/editor-documents");
58
+ var import_editor_documents3 = require("@elementor/editor-documents");
59
59
 
60
60
  // src/components/top-bar/indicator.tsx
61
61
  var React = __toESM(require("react"));
@@ -79,10 +79,40 @@ function Tooltip(props) {
79
79
  );
80
80
  }
81
81
 
82
- // src/components/top-bar/posts-list.tsx
83
- var import_ui3 = require("@elementor/ui");
84
- var React3 = __toESM(require("react"));
85
- var import_i18n = require("@wordpress/i18n");
82
+ // src/hooks/use-recent-posts.ts
83
+ var import_react = require("react");
84
+ var import_api_fetch = __toESM(require("@wordpress/api-fetch"));
85
+ var import_url = require("@wordpress/url");
86
+ var endpointPath = "/elementor/v1/site-navigation/recent-posts";
87
+ function useRecentPosts(documentId) {
88
+ const [recentPosts, setRecentPosts] = (0, import_react.useState)([]);
89
+ const [isLoading, setIsLoading] = (0, import_react.useState)(false);
90
+ (0, import_react.useEffect)(() => {
91
+ if (documentId) {
92
+ setIsLoading(true);
93
+ fetchRecentlyEditedPosts(documentId).then((posts) => {
94
+ setRecentPosts(posts);
95
+ setIsLoading(false);
96
+ });
97
+ }
98
+ }, [documentId]);
99
+ return {
100
+ isLoading,
101
+ recentPosts
102
+ };
103
+ }
104
+ async function fetchRecentlyEditedPosts(documentId) {
105
+ const queryParams = {
106
+ posts_per_page: 5,
107
+ post__not_in: documentId
108
+ };
109
+ return await (0, import_api_fetch.default)({
110
+ path: (0, import_url.addQueryArgs)(endpointPath, queryParams)
111
+ }).then((response) => response).catch(() => []);
112
+ }
113
+
114
+ // src/components/top-bar/recently-edited.tsx
115
+ var import_i18n2 = require("@wordpress/i18n");
86
116
 
87
117
  // src/components/top-bar/chip-doc-type.tsx
88
118
  var import_icons2 = require("@elementor/icons");
@@ -104,132 +134,114 @@ function DocTypeChip({ postType, docType, label }) {
104
134
  );
105
135
  }
106
136
 
137
+ // src/components/top-bar/post-list-item.tsx
138
+ var import_ui3 = require("@elementor/ui");
139
+ var React3 = __toESM(require("react"));
140
+ var import_editor_documents = require("@elementor/editor-documents");
141
+
142
+ // src/hooks/use-reverse-html-entities.ts
143
+ var import_react2 = require("react");
144
+ function useReverseHtmlEntities(escapedHTML = "") {
145
+ return (0, import_react2.useMemo)(() => {
146
+ const textarea = document.createElement("textarea");
147
+ textarea.innerHTML = escapedHTML;
148
+ const { value } = textarea;
149
+ textarea.remove();
150
+ return value;
151
+ }, [escapedHTML]);
152
+ }
153
+
154
+ // src/components/top-bar/post-list-item.tsx
155
+ function PostListItem({ post, closePopup }) {
156
+ const navigateToDocument = (0, import_editor_documents.useNavigateToDocument)();
157
+ const postTitle = useReverseHtmlEntities(post.title);
158
+ return /* @__PURE__ */ React3.createElement(import_ui3.MenuItem, { dense: true, sx: { width: "100%" }, onClick: () => {
159
+ closePopup();
160
+ navigateToDocument(post.id);
161
+ } }, postTitle, /* @__PURE__ */ React3.createElement(DocTypeChip, { postType: post.type.post_type, docType: post.type.doc_type, label: post.type.label }));
162
+ }
163
+
164
+ // src/components/top-bar/create-post-list-item.tsx
165
+ var import_ui4 = require("@elementor/ui");
166
+ var React4 = __toESM(require("react"));
167
+
107
168
  // src/hooks/use-create-page.ts
108
- var import_api_fetch = __toESM(require("@wordpress/api-fetch"));
109
- var import_react = require("react");
110
- var endpointPath = "/elementor/v1/site-navigation/add-new-post";
111
- function useCreatePage({ onCreated: onCreated2 }) {
112
- const [isLoading, setIsLoading] = (0, import_react.useState)(false);
169
+ var import_api_fetch2 = __toESM(require("@wordpress/api-fetch"));
170
+ var import_react3 = require("react");
171
+ var endpointPath2 = "/elementor/v1/site-navigation/add-new-post";
172
+ function useCreatePage() {
173
+ const [isLoading, setIsLoading] = (0, import_react3.useState)(false);
113
174
  return {
114
175
  create: () => {
115
176
  setIsLoading(true);
116
- addNewPage().then((newPost) => newPost).then((newPost) => {
117
- setIsLoading(false);
118
- onCreated2(newPost.edit_url);
119
- }).catch(() => {
120
- setIsLoading(false);
121
- });
177
+ return addNewPage().then((newPost) => newPost).finally(() => setIsLoading(false));
122
178
  },
123
179
  isLoading
124
180
  };
125
181
  }
126
182
  async function addNewPage() {
127
- return await (0, import_api_fetch.default)({
128
- path: endpointPath,
183
+ return await (0, import_api_fetch2.default)({
184
+ path: endpointPath2,
129
185
  method: "POST",
130
186
  data: { post_type: "page" }
131
187
  });
132
188
  }
133
189
 
134
- // src/components/top-bar/posts-list.tsx
190
+ // src/components/top-bar/create-post-list-item.tsx
135
191
  var import_icons3 = require("@elementor/icons");
136
- function PostsList({ recentPosts }) {
137
- const { create, isLoading } = useCreatePage({ onCreated });
138
- return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(import_ui3.ListSubheader, { sx: { fontSize: 12, fontStyle: "italic", pl: 4 }, component: "div", id: "nested-list-subheader" }, (0, import_i18n.__)("Recent", "elementor")), recentPosts.length ? recentPosts.map(({ title, edit_url: editUrl, type, id }) => /* @__PURE__ */ React3.createElement(
139
- import_ui3.MenuItem,
140
- {
141
- dense: true,
142
- key: id,
143
- component: "a",
144
- href: editUrl
145
- },
146
- title,
147
- /* @__PURE__ */ React3.createElement(DocTypeChip, { postType: type.post_type, docType: type.doc_type, label: type.label })
148
- )) : /* @__PURE__ */ React3.createElement(import_ui3.Typography, { variant: "caption", sx: { color: "grey.500", fontStyle: "italic", p: 4 }, component: "div", "aria-label": void 0 }, (0, import_i18n.__)("There are no other pages or templates on this site yet.", "elementor")), /* @__PURE__ */ React3.createElement(import_ui3.Divider, null), /* @__PURE__ */ React3.createElement(
149
- import_ui3.MenuItem,
150
- {
151
- dense: true,
152
- size: "small",
153
- color: "inherit",
154
- component: "div",
155
- onClick: create
156
- },
157
- /* @__PURE__ */ React3.createElement(import_ui3.ListItemIcon, null, isLoading ? /* @__PURE__ */ React3.createElement(import_ui3.CircularProgress, null) : /* @__PURE__ */ React3.createElement(import_icons3.PlusIcon, null)),
158
- (0, import_i18n.__)("Add new page", "elementor")
159
- ));
160
- }
161
- function onCreated(url) {
162
- window.location.href = url;
163
- }
164
-
165
- // src/hooks/use-recent-posts.ts
166
- var import_react2 = require("react");
167
- var import_api_fetch2 = __toESM(require("@wordpress/api-fetch"));
168
- var import_url = require("@wordpress/url");
169
- var endpointPath2 = "/elementor/v1/site-navigation/recent-posts";
170
- function useRecentPosts(documentId) {
171
- const [recentPosts, setRecentPosts] = (0, import_react2.useState)([]);
172
- const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
173
- (0, import_react2.useEffect)(() => {
174
- if (documentId) {
175
- setIsLoading(true);
176
- fetchRecentlyEditedPosts(documentId).then((posts) => {
177
- setRecentPosts(posts);
178
- setIsLoading(false);
179
- });
180
- }
181
- }, [documentId]);
182
- return {
183
- isLoading,
184
- recentPosts
185
- };
186
- }
187
- async function fetchRecentlyEditedPosts(documentId) {
188
- const queryParams = {
189
- posts_per_page: 5,
190
- post__not_in: documentId
191
- };
192
- return await (0, import_api_fetch2.default)({
193
- path: (0, import_url.addQueryArgs)(endpointPath2, queryParams)
194
- }).then((response) => response).catch(() => []);
192
+ var import_i18n = require("@wordpress/i18n");
193
+ var import_editor_documents2 = require("@elementor/editor-documents");
194
+ function CreatePostListItem({ closePopup }) {
195
+ const { create, isLoading } = useCreatePage();
196
+ const navigateToDocument = (0, import_editor_documents2.useNavigateToDocument)();
197
+ return /* @__PURE__ */ React4.createElement(import_ui4.MenuItem, { dense: true, size: "small", color: "inherit", component: "div", onClick: async () => {
198
+ const { id } = await create();
199
+ closePopup();
200
+ navigateToDocument(id);
201
+ } }, /* @__PURE__ */ React4.createElement(import_ui4.ListItemIcon, null, isLoading ? /* @__PURE__ */ React4.createElement(import_ui4.CircularProgress, null) : /* @__PURE__ */ React4.createElement(import_icons3.PlusIcon, null)), (0, import_i18n.__)("Add new page", "elementor"));
195
202
  }
196
203
 
197
204
  // src/components/top-bar/recently-edited.tsx
198
205
  function RecentlyEdited() {
199
- const activeDocument = (0, import_editor_documents.useActiveDocument)();
200
- const hostDocument = (0, import_editor_documents.useHostDocument)();
201
- const document = activeDocument && activeDocument.type.value !== "kit" ? activeDocument : hostDocument;
202
- const { recentPosts } = useRecentPosts(document?.id);
203
- const popupState = (0, import_ui4.usePopupState)({
206
+ const activeDocument = (0, import_editor_documents3.useActiveDocument)();
207
+ const hostDocument = (0, import_editor_documents3.useHostDocument)();
208
+ const document2 = activeDocument && activeDocument.type.value !== "kit" ? activeDocument : hostDocument;
209
+ const { recentPosts } = useRecentPosts(document2?.id);
210
+ const popupState = (0, import_ui5.usePopupState)({
204
211
  variant: "popover",
205
212
  popupId: "elementor-v2-top-bar-recently-edited"
206
213
  });
207
- if (!document) {
214
+ const documentTitle = useReverseHtmlEntities(document2?.title);
215
+ if (!document2) {
208
216
  return null;
209
217
  }
210
- return /* @__PURE__ */ React4.createElement(import_ui4.Box, { sx: { cursor: "default" } }, /* @__PURE__ */ React4.createElement(
211
- import_ui4.Button,
218
+ return /* @__PURE__ */ React5.createElement(import_ui5.Box, { sx: { cursor: "default" } }, /* @__PURE__ */ React5.createElement(
219
+ import_ui5.Button,
212
220
  {
213
221
  color: "inherit",
214
222
  size: "small",
215
- endIcon: /* @__PURE__ */ React4.createElement(import_icons4.ChevronDownIcon, { fontSize: "small" }),
216
- ...(0, import_ui4.bindTrigger)(popupState)
223
+ endIcon: /* @__PURE__ */ React5.createElement(import_icons4.ChevronDownIcon, { fontSize: "small" }),
224
+ ...(0, import_ui5.bindTrigger)(popupState)
217
225
  },
218
- /* @__PURE__ */ React4.createElement(
226
+ /* @__PURE__ */ React5.createElement(
219
227
  Indicator,
220
228
  {
221
- title: document.title,
222
- status: document.status
229
+ title: documentTitle,
230
+ status: document2.status
223
231
  }
224
232
  )
225
- ), /* @__PURE__ */ React4.createElement(
226
- import_ui4.Menu,
233
+ ), /* @__PURE__ */ React5.createElement(
234
+ import_ui5.Menu,
227
235
  {
228
236
  MenuListProps: { component: "div" },
229
237
  PaperProps: { sx: { mt: 4, minWidth: 314 } },
230
- ...(0, import_ui4.bindMenu)(popupState)
238
+ ...(0, import_ui5.bindMenu)(popupState)
231
239
  },
232
- /* @__PURE__ */ React4.createElement(PostsList, { recentPosts })
240
+ /* @__PURE__ */ React5.createElement(import_ui5.ListSubheader, { sx: { fontSize: 12, fontStyle: "italic", pl: 4 }, component: "div", id: "nested-list-subheader" }, (0, import_i18n2.__)("Recent", "elementor")),
241
+ recentPosts.map((post) => /* @__PURE__ */ React5.createElement(PostListItem, { key: post.id, post, closePopup: popupState.close })),
242
+ recentPosts.length === 0 && /* @__PURE__ */ React5.createElement(import_ui5.Typography, { variant: "caption", sx: { color: "grey.500", fontStyle: "italic", p: 4 }, component: "div", "aria-label": void 0 }, (0, import_i18n2.__)("There are no other pages or templates on this site yet.", "elementor")),
243
+ /* @__PURE__ */ React5.createElement(import_ui5.Divider, null),
244
+ /* @__PURE__ */ React5.createElement(CreatePostListItem, { closePopup: popupState.close })
233
245
  ));
234
246
  }
235
247
 
@@ -240,7 +252,7 @@ function init() {
240
252
  }
241
253
  function registerTopBarMenuItems() {
242
254
  (0, import_editor_app_bar.injectIntoPageIndication)({
243
- name: "document-recently-edited",
255
+ id: "document-recently-edited",
244
256
  filler: RecentlyEdited
245
257
  });
246
258
  }
package/dist/index.mjs CHANGED
@@ -22,14 +22,17 @@ function getIconsMap() {
22
22
  }
23
23
 
24
24
  // src/components/top-bar/recently-edited.tsx
25
- import * as React4 from "react";
25
+ import * as React5 from "react";
26
26
  import {
27
27
  bindMenu,
28
28
  usePopupState,
29
29
  bindTrigger,
30
30
  Menu,
31
31
  Button,
32
- Box
32
+ Box,
33
+ ListSubheader,
34
+ Typography as Typography2,
35
+ Divider
33
36
  } from "@elementor/ui";
34
37
  import { ChevronDownIcon } from "@elementor/icons";
35
38
  import { useActiveDocument, useHostDocument } from "@elementor/editor-documents";
@@ -56,17 +59,40 @@ function Tooltip(props) {
56
59
  );
57
60
  }
58
61
 
59
- // src/components/top-bar/posts-list.tsx
60
- import {
61
- Divider,
62
- MenuItem,
63
- ListItemIcon,
64
- ListSubheader,
65
- Typography as Typography2,
66
- CircularProgress
67
- } from "@elementor/ui";
68
- import * as React3 from "react";
69
- import { __ } from "@wordpress/i18n";
62
+ // src/hooks/use-recent-posts.ts
63
+ import { useEffect, useState } from "react";
64
+ import apiFetch from "@wordpress/api-fetch";
65
+ import { addQueryArgs } from "@wordpress/url";
66
+ var endpointPath = "/elementor/v1/site-navigation/recent-posts";
67
+ function useRecentPosts(documentId) {
68
+ const [recentPosts, setRecentPosts] = useState([]);
69
+ const [isLoading, setIsLoading] = useState(false);
70
+ useEffect(() => {
71
+ if (documentId) {
72
+ setIsLoading(true);
73
+ fetchRecentlyEditedPosts(documentId).then((posts) => {
74
+ setRecentPosts(posts);
75
+ setIsLoading(false);
76
+ });
77
+ }
78
+ }, [documentId]);
79
+ return {
80
+ isLoading,
81
+ recentPosts
82
+ };
83
+ }
84
+ async function fetchRecentlyEditedPosts(documentId) {
85
+ const queryParams = {
86
+ posts_per_page: 5,
87
+ post__not_in: documentId
88
+ };
89
+ return await apiFetch({
90
+ path: addQueryArgs(endpointPath, queryParams)
91
+ }).then((response) => response).catch(() => []);
92
+ }
93
+
94
+ // src/components/top-bar/recently-edited.tsx
95
+ import { __ as __2 } from "@wordpress/i18n";
70
96
 
71
97
  // src/components/top-bar/chip-doc-type.tsx
72
98
  import {
@@ -90,132 +116,114 @@ function DocTypeChip({ postType, docType, label }) {
90
116
  );
91
117
  }
92
118
 
119
+ // src/components/top-bar/post-list-item.tsx
120
+ import { MenuItem } from "@elementor/ui";
121
+ import * as React3 from "react";
122
+ import { useNavigateToDocument } from "@elementor/editor-documents";
123
+
124
+ // src/hooks/use-reverse-html-entities.ts
125
+ import { useMemo } from "react";
126
+ function useReverseHtmlEntities(escapedHTML = "") {
127
+ return useMemo(() => {
128
+ const textarea = document.createElement("textarea");
129
+ textarea.innerHTML = escapedHTML;
130
+ const { value } = textarea;
131
+ textarea.remove();
132
+ return value;
133
+ }, [escapedHTML]);
134
+ }
135
+
136
+ // src/components/top-bar/post-list-item.tsx
137
+ function PostListItem({ post, closePopup }) {
138
+ const navigateToDocument = useNavigateToDocument();
139
+ const postTitle = useReverseHtmlEntities(post.title);
140
+ return /* @__PURE__ */ React3.createElement(MenuItem, { dense: true, sx: { width: "100%" }, onClick: () => {
141
+ closePopup();
142
+ navigateToDocument(post.id);
143
+ } }, postTitle, /* @__PURE__ */ React3.createElement(DocTypeChip, { postType: post.type.post_type, docType: post.type.doc_type, label: post.type.label }));
144
+ }
145
+
146
+ // src/components/top-bar/create-post-list-item.tsx
147
+ import { CircularProgress, ListItemIcon, MenuItem as MenuItem2 } from "@elementor/ui";
148
+ import * as React4 from "react";
149
+
93
150
  // src/hooks/use-create-page.ts
94
- import apiFetch from "@wordpress/api-fetch";
95
- import { useState } from "react";
96
- var endpointPath = "/elementor/v1/site-navigation/add-new-post";
97
- function useCreatePage({ onCreated: onCreated2 }) {
98
- const [isLoading, setIsLoading] = useState(false);
151
+ import apiFetch2 from "@wordpress/api-fetch";
152
+ import { useState as useState2 } from "react";
153
+ var endpointPath2 = "/elementor/v1/site-navigation/add-new-post";
154
+ function useCreatePage() {
155
+ const [isLoading, setIsLoading] = useState2(false);
99
156
  return {
100
157
  create: () => {
101
158
  setIsLoading(true);
102
- addNewPage().then((newPost) => newPost).then((newPost) => {
103
- setIsLoading(false);
104
- onCreated2(newPost.edit_url);
105
- }).catch(() => {
106
- setIsLoading(false);
107
- });
159
+ return addNewPage().then((newPost) => newPost).finally(() => setIsLoading(false));
108
160
  },
109
161
  isLoading
110
162
  };
111
163
  }
112
164
  async function addNewPage() {
113
- return await apiFetch({
114
- path: endpointPath,
165
+ return await apiFetch2({
166
+ path: endpointPath2,
115
167
  method: "POST",
116
168
  data: { post_type: "page" }
117
169
  });
118
170
  }
119
171
 
120
- // src/components/top-bar/posts-list.tsx
172
+ // src/components/top-bar/create-post-list-item.tsx
121
173
  import { PlusIcon } from "@elementor/icons";
122
- function PostsList({ recentPosts }) {
123
- const { create, isLoading } = useCreatePage({ onCreated });
124
- return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(ListSubheader, { sx: { fontSize: 12, fontStyle: "italic", pl: 4 }, component: "div", id: "nested-list-subheader" }, __("Recent", "elementor")), recentPosts.length ? recentPosts.map(({ title, edit_url: editUrl, type, id }) => /* @__PURE__ */ React3.createElement(
125
- MenuItem,
126
- {
127
- dense: true,
128
- key: id,
129
- component: "a",
130
- href: editUrl
131
- },
132
- title,
133
- /* @__PURE__ */ React3.createElement(DocTypeChip, { postType: type.post_type, docType: type.doc_type, label: type.label })
134
- )) : /* @__PURE__ */ React3.createElement(Typography2, { variant: "caption", sx: { color: "grey.500", fontStyle: "italic", p: 4 }, component: "div", "aria-label": void 0 }, __("There are no other pages or templates on this site yet.", "elementor")), /* @__PURE__ */ React3.createElement(Divider, null), /* @__PURE__ */ React3.createElement(
135
- MenuItem,
136
- {
137
- dense: true,
138
- size: "small",
139
- color: "inherit",
140
- component: "div",
141
- onClick: create
142
- },
143
- /* @__PURE__ */ React3.createElement(ListItemIcon, null, isLoading ? /* @__PURE__ */ React3.createElement(CircularProgress, null) : /* @__PURE__ */ React3.createElement(PlusIcon, null)),
144
- __("Add new page", "elementor")
145
- ));
146
- }
147
- function onCreated(url) {
148
- window.location.href = url;
149
- }
150
-
151
- // src/hooks/use-recent-posts.ts
152
- import { useEffect, useState as useState2 } from "react";
153
- import apiFetch2 from "@wordpress/api-fetch";
154
- import { addQueryArgs } from "@wordpress/url";
155
- var endpointPath2 = "/elementor/v1/site-navigation/recent-posts";
156
- function useRecentPosts(documentId) {
157
- const [recentPosts, setRecentPosts] = useState2([]);
158
- const [isLoading, setIsLoading] = useState2(false);
159
- useEffect(() => {
160
- if (documentId) {
161
- setIsLoading(true);
162
- fetchRecentlyEditedPosts(documentId).then((posts) => {
163
- setRecentPosts(posts);
164
- setIsLoading(false);
165
- });
166
- }
167
- }, [documentId]);
168
- return {
169
- isLoading,
170
- recentPosts
171
- };
172
- }
173
- async function fetchRecentlyEditedPosts(documentId) {
174
- const queryParams = {
175
- posts_per_page: 5,
176
- post__not_in: documentId
177
- };
178
- return await apiFetch2({
179
- path: addQueryArgs(endpointPath2, queryParams)
180
- }).then((response) => response).catch(() => []);
174
+ import { __ } from "@wordpress/i18n";
175
+ import { useNavigateToDocument as useNavigateToDocument2 } from "@elementor/editor-documents";
176
+ function CreatePostListItem({ closePopup }) {
177
+ const { create, isLoading } = useCreatePage();
178
+ const navigateToDocument = useNavigateToDocument2();
179
+ return /* @__PURE__ */ React4.createElement(MenuItem2, { dense: true, size: "small", color: "inherit", component: "div", onClick: async () => {
180
+ const { id } = await create();
181
+ closePopup();
182
+ navigateToDocument(id);
183
+ } }, /* @__PURE__ */ React4.createElement(ListItemIcon, null, isLoading ? /* @__PURE__ */ React4.createElement(CircularProgress, null) : /* @__PURE__ */ React4.createElement(PlusIcon, null)), __("Add new page", "elementor"));
181
184
  }
182
185
 
183
186
  // src/components/top-bar/recently-edited.tsx
184
187
  function RecentlyEdited() {
185
188
  const activeDocument = useActiveDocument();
186
189
  const hostDocument = useHostDocument();
187
- const document = activeDocument && activeDocument.type.value !== "kit" ? activeDocument : hostDocument;
188
- const { recentPosts } = useRecentPosts(document?.id);
190
+ const document2 = activeDocument && activeDocument.type.value !== "kit" ? activeDocument : hostDocument;
191
+ const { recentPosts } = useRecentPosts(document2?.id);
189
192
  const popupState = usePopupState({
190
193
  variant: "popover",
191
194
  popupId: "elementor-v2-top-bar-recently-edited"
192
195
  });
193
- if (!document) {
196
+ const documentTitle = useReverseHtmlEntities(document2?.title);
197
+ if (!document2) {
194
198
  return null;
195
199
  }
196
- return /* @__PURE__ */ React4.createElement(Box, { sx: { cursor: "default" } }, /* @__PURE__ */ React4.createElement(
200
+ return /* @__PURE__ */ React5.createElement(Box, { sx: { cursor: "default" } }, /* @__PURE__ */ React5.createElement(
197
201
  Button,
198
202
  {
199
203
  color: "inherit",
200
204
  size: "small",
201
- endIcon: /* @__PURE__ */ React4.createElement(ChevronDownIcon, { fontSize: "small" }),
205
+ endIcon: /* @__PURE__ */ React5.createElement(ChevronDownIcon, { fontSize: "small" }),
202
206
  ...bindTrigger(popupState)
203
207
  },
204
- /* @__PURE__ */ React4.createElement(
208
+ /* @__PURE__ */ React5.createElement(
205
209
  Indicator,
206
210
  {
207
- title: document.title,
208
- status: document.status
211
+ title: documentTitle,
212
+ status: document2.status
209
213
  }
210
214
  )
211
- ), /* @__PURE__ */ React4.createElement(
215
+ ), /* @__PURE__ */ React5.createElement(
212
216
  Menu,
213
217
  {
214
218
  MenuListProps: { component: "div" },
215
219
  PaperProps: { sx: { mt: 4, minWidth: 314 } },
216
220
  ...bindMenu(popupState)
217
221
  },
218
- /* @__PURE__ */ React4.createElement(PostsList, { recentPosts })
222
+ /* @__PURE__ */ React5.createElement(ListSubheader, { sx: { fontSize: 12, fontStyle: "italic", pl: 4 }, component: "div", id: "nested-list-subheader" }, __2("Recent", "elementor")),
223
+ recentPosts.map((post) => /* @__PURE__ */ React5.createElement(PostListItem, { key: post.id, post, closePopup: popupState.close })),
224
+ recentPosts.length === 0 && /* @__PURE__ */ React5.createElement(Typography2, { variant: "caption", sx: { color: "grey.500", fontStyle: "italic", p: 4 }, component: "div", "aria-label": void 0 }, __2("There are no other pages or templates on this site yet.", "elementor")),
225
+ /* @__PURE__ */ React5.createElement(Divider, null),
226
+ /* @__PURE__ */ React5.createElement(CreatePostListItem, { closePopup: popupState.close })
219
227
  ));
220
228
  }
221
229
 
@@ -226,7 +234,7 @@ function init() {
226
234
  }
227
235
  function registerTopBarMenuItems() {
228
236
  injectIntoPageIndication({
229
- name: "document-recently-edited",
237
+ id: "document-recently-edited",
230
238
  filler: RecentlyEdited
231
239
  });
232
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-site-navigation",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -32,8 +32,8 @@
32
32
  "dev": "tsup src/index.ts --format esm --clean"
33
33
  },
34
34
  "dependencies": {
35
- "@elementor/editor-app-bar": "^0.3.0",
36
- "@elementor/editor-documents": "^0.3.0",
35
+ "@elementor/editor-app-bar": "^0.4.0",
36
+ "@elementor/editor-documents": "^0.4.0",
37
37
  "@elementor/icons": "^0.2.1",
38
38
  "@elementor/ui": "^1.4.50",
39
39
  "@wordpress/api-fetch": "^6.27.0",
@@ -46,5 +46,5 @@
46
46
  "elementor": {
47
47
  "type": "extension"
48
48
  },
49
- "gitHead": "ccd6099031737d52202ba98812e21d16fac4bef2"
49
+ "gitHead": "6efee223232dba6482843cca5a00ebecd19d3890"
50
50
  }
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
- import { render } from '@testing-library/react';
3
- import { useHostDocument, useActiveDocument } from '@elementor/editor-documents';
2
+ import { render, waitFor } from '@testing-library/react';
3
+ import { useHostDocument, useActiveDocument, useNavigateToDocument } from '@elementor/editor-documents';
4
4
  import RecentlyEdited from '../recently-edited';
5
5
  import { createMockDocument } from 'test-utils';
6
6
  import useRecentPosts, { Post } from '../../../hooks/use-recent-posts';
@@ -9,6 +9,7 @@ import useCreatePage from '../../../hooks/use-create-page';
9
9
  jest.mock( '@elementor/editor-documents', () => ( {
10
10
  useActiveDocument: jest.fn(),
11
11
  useHostDocument: jest.fn(),
12
+ useNavigateToDocument: jest.fn(),
12
13
  } ) );
13
14
 
14
15
  jest.mock( '../../../hooks/use-recent-posts', () => (
@@ -55,16 +56,18 @@ describe( '@elementor/recently-edited - Top bar add new page', () => {
55
56
  expect( label ).toBeInTheDocument();
56
57
  } );
57
58
 
58
- it( 'should trigger create page hook on click', () => {
59
+ it( 'should trigger create page hook on click', async () => {
59
60
  // Arrange.
60
61
  mockActiveDocument();
61
62
 
62
63
  const isLoading = false;
63
64
  const recentPosts: Post[] = [];
64
- const create = jest.fn();
65
+ const create = jest.fn().mockReturnValue( Promise.resolve( { id: 123 } ) );
66
+ const navigateToDocument = jest.fn();
65
67
 
66
68
  jest.mocked( useRecentPosts ).mockReturnValue( { isLoading, recentPosts } );
67
69
  jest.mocked( useCreatePage ).mockReturnValue( { isLoading, create } );
70
+ jest.mocked( useNavigateToDocument ).mockReturnValue( navigateToDocument );
68
71
 
69
72
  const { getByText, getAllByRole } = render( <RecentlyEdited /> );
70
73
 
@@ -76,7 +79,12 @@ describe( '@elementor/recently-edited - Top bar add new page', () => {
76
79
  addNewPage.click();
77
80
 
78
81
  // Assert.
79
- expect( create ).toHaveBeenCalledTimes( 1 );
82
+ await waitFor( () => {
83
+ expect( create ).toHaveBeenCalledTimes( 1 );
84
+
85
+ expect( navigateToDocument ).toHaveBeenCalledTimes( 1 );
86
+ expect( navigateToDocument ).toHaveBeenCalledWith( 123 );
87
+ } );
80
88
  } );
81
89
  } );
82
90
 
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { render } from '@testing-library/react';
3
- import { useHostDocument, useActiveDocument } from '@elementor/editor-documents';
3
+ import { useHostDocument, useActiveDocument, useNavigateToDocument } from '@elementor/editor-documents';
4
4
  import RecentlyEdited from '../recently-edited';
5
5
  import { createMockDocument } from 'test-utils';
6
6
  import useRecentPosts, { Post } from '../../../hooks/use-recent-posts';
@@ -8,6 +8,7 @@ import useRecentPosts, { Post } from '../../../hooks/use-recent-posts';
8
8
  jest.mock( '@elementor/editor-documents', () => ( {
9
9
  useActiveDocument: jest.fn(),
10
10
  useHostDocument: jest.fn(),
11
+ useNavigateToDocument: jest.fn(),
11
12
  } ) );
12
13
 
13
14
  jest.mock( '../../../hooks/use-recent-posts', () => (
@@ -175,4 +176,93 @@ describe( '@elementor/recently-edited - Top bar Recently Edited', () => {
175
176
 
176
177
  expect( getByText( 'Test post' ) ).toBeInTheDocument();
177
178
  } );
179
+
180
+ it( 'should render titles with HTML entities', () => {
181
+ // Arrange.
182
+ jest.mocked( useActiveDocument ).mockImplementation( () =>
183
+ createMockDocument( {
184
+ id: 1,
185
+ title: 'Header title with special char &#165;',
186
+ type: {
187
+ value: 'header',
188
+ label: 'Header',
189
+ },
190
+ } )
191
+ );
192
+
193
+ const isLoading = false;
194
+ const recentPosts: Post[] = [
195
+ {
196
+ id: 1,
197
+ title: 'Post title with <h1>HTML</h1>',
198
+ edit_url: 'some_url',
199
+ type: {
200
+ post_type: 'post',
201
+ doc_type: 'wp-post',
202
+ label: 'Post',
203
+ },
204
+ date_modified: 123,
205
+ },
206
+ {
207
+ id: 2,
208
+ title: 'Post title with &lt;HTML entities&gt;',
209
+ edit_url: 'some_url_2',
210
+ type: {
211
+ post_type: 'post',
212
+ doc_type: 'wp-post',
213
+ label: 'Post 2',
214
+ },
215
+ date_modified: 1234,
216
+ },
217
+ ];
218
+
219
+ jest.mocked( useRecentPosts ).mockReturnValue( { isLoading, recentPosts } );
220
+
221
+ // Act.
222
+ const { getByText, getByRole } = render( <RecentlyEdited /> );
223
+
224
+ // Assert - the document title should be rendered with the HTML entity.
225
+ expect( getByText( 'Header title with special char ¥' ) ).toBeInTheDocument();
226
+
227
+ // Open the posts list.
228
+ getByRole( 'button' ).click();
229
+
230
+ // Assert - the post title should be rendered with the HTML entity.
231
+ expect( getByText( 'Post title with <h1>HTML</h1>' ) ).toBeInTheDocument();
232
+ expect( getByText( 'Post title with <HTML entities>' ) ).toBeInTheDocument();
233
+ } );
234
+
235
+ it( 'should navigate to document on click', () => {
236
+ // Arrange.
237
+ const navigateToDocument = jest.fn();
238
+
239
+ jest.mocked( useNavigateToDocument ).mockReturnValue( navigateToDocument );
240
+
241
+ jest.mocked( useRecentPosts ).mockReturnValue( {
242
+ isLoading: false,
243
+ recentPosts: [ {
244
+ id: 123,
245
+ title: 'Test post',
246
+ edit_url: 'some_url',
247
+ type: {
248
+ post_type: 'post',
249
+ doc_type: 'wp-post',
250
+ label: 'Post',
251
+ },
252
+ date_modified: 123,
253
+ } ],
254
+ } );
255
+
256
+ const { getByText, getByRole } = render( <RecentlyEdited /> );
257
+
258
+ // Open the posts list.
259
+ getByRole( 'button' ).click();
260
+
261
+ // Act.
262
+ getByText( 'Test post' ).click();
263
+
264
+ // Assert.
265
+ expect( navigateToDocument ).toHaveBeenCalledTimes( 1 );
266
+ expect( navigateToDocument ).toHaveBeenCalledWith( 123 );
267
+ } );
178
268
  } );
@@ -0,0 +1,26 @@
1
+ import { CircularProgress, ListItemIcon, MenuItem } from '@elementor/ui';
2
+ import * as React from 'react';
3
+ import useCreatePage from '../../hooks/use-create-page';
4
+ import { PlusIcon } from '@elementor/icons';
5
+ import { __ } from '@wordpress/i18n';
6
+ import { useNavigateToDocument } from '@elementor/editor-documents';
7
+
8
+ export function CreatePostListItem( { closePopup }: { closePopup: () => void } ) {
9
+ const { create, isLoading } = useCreatePage();
10
+ const navigateToDocument = useNavigateToDocument();
11
+
12
+ return (
13
+ <MenuItem dense size="small" color="inherit" component="div" onClick={ async () => {
14
+ const { id } = await create();
15
+
16
+ closePopup();
17
+ navigateToDocument( id );
18
+ } }>
19
+ <ListItemIcon>
20
+ { isLoading ? <CircularProgress /> : <PlusIcon /> }
21
+ </ListItemIcon>
22
+
23
+ { __( 'Add new page', 'elementor' ) }
24
+ </MenuItem>
25
+ );
26
+ }
@@ -0,0 +1,26 @@
1
+ import DocTypeChip from './chip-doc-type';
2
+ import { MenuItem, MenuItemProps } from '@elementor/ui';
3
+ import * as React from 'react';
4
+ import { Post } from '../../hooks/use-recent-posts';
5
+ import { useNavigateToDocument } from '@elementor/editor-documents';
6
+ import { useReverseHtmlEntities } from '../../hooks/use-reverse-html-entities';
7
+
8
+ type Props = MenuItemProps & {
9
+ post: Post;
10
+ closePopup: () => void;
11
+ }
12
+
13
+ export function PostListItem( { post, closePopup }: Props ) {
14
+ const navigateToDocument = useNavigateToDocument();
15
+ const postTitle = useReverseHtmlEntities( post.title );
16
+
17
+ return (
18
+ <MenuItem dense sx={ { width: '100%' } } onClick={ () => {
19
+ closePopup();
20
+ navigateToDocument( post.id );
21
+ } }>
22
+ { postTitle }
23
+ <DocTypeChip postType={ post.type.post_type } docType={ post.type.doc_type } label={ post.type.label } />
24
+ </MenuItem>
25
+ );
26
+ }
@@ -6,12 +6,18 @@ import {
6
6
  Menu,
7
7
  Button,
8
8
  Box,
9
+ ListSubheader,
10
+ Typography,
11
+ Divider,
9
12
  } from '@elementor/ui';
10
13
  import { ChevronDownIcon } from '@elementor/icons';
11
14
  import { useActiveDocument, useHostDocument } from '@elementor/editor-documents';
12
15
  import Indicator from './indicator';
13
- import PostsList from './posts-list';
14
16
  import useRecentPosts from '../../hooks/use-recent-posts';
17
+ import { __ } from '@wordpress/i18n';
18
+ import { PostListItem } from './post-list-item';
19
+ import { CreatePostListItem } from './create-post-list-item';
20
+ import { useReverseHtmlEntities } from '../../hooks/use-reverse-html-entities';
15
21
 
16
22
  export default function RecentlyEdited() {
17
23
  const activeDocument = useActiveDocument();
@@ -27,6 +33,8 @@ export default function RecentlyEdited() {
27
33
  popupId: 'elementor-v2-top-bar-recently-edited',
28
34
  } );
29
35
 
36
+ const documentTitle = useReverseHtmlEntities( document?.title );
37
+
30
38
  if ( ! document ) {
31
39
  return null;
32
40
  }
@@ -40,7 +48,7 @@ export default function RecentlyEdited() {
40
48
  { ...bindTrigger( popupState ) }
41
49
  >
42
50
  <Indicator
43
- title={ document.title }
51
+ title={ documentTitle }
44
52
  status={ document.status }
45
53
  />
46
54
  </Button>
@@ -50,7 +58,23 @@ export default function RecentlyEdited() {
50
58
  PaperProps={ { sx: { mt: 4, minWidth: 314 } } }
51
59
  { ...bindMenu( popupState ) }
52
60
  >
53
- <PostsList recentPosts={ recentPosts } />
61
+ <ListSubheader sx={ { fontSize: 12, fontStyle: 'italic', pl: 4 } } component="div" id="nested-list-subheader">
62
+ { __( 'Recent', 'elementor' ) }
63
+ </ListSubheader>
64
+
65
+ { recentPosts.map( ( post ) => (
66
+ <PostListItem key={ post.id } post={ post } closePopup={ popupState.close } />
67
+ ) ) }
68
+
69
+ { recentPosts.length === 0 && (
70
+ <Typography variant="caption" sx={ { color: 'grey.500', fontStyle: 'italic', p: 4 } } component="div" aria-label={ undefined }>
71
+ { __( 'There are no other pages or templates on this site yet.', 'elementor' ) }
72
+ </Typography>
73
+ ) }
74
+
75
+ <Divider />
76
+
77
+ <CreatePostListItem closePopup={ popupState.close } />
54
78
  </Menu>
55
79
  </Box>
56
80
  );
@@ -17,8 +17,7 @@ describe( '@elementor/recently-edited/use-page', () => {
17
17
 
18
18
  it( 'should run useCreatePage hook', async () => {
19
19
  // Arrange.
20
- const onCreated = jest.fn();
21
- const { result } = renderHook( () => useCreatePage( { onCreated } ) );
20
+ const { result } = renderHook( useCreatePage );
22
21
  const newPost = {
23
22
  id: 1,
24
23
  edit_url: 'editurl.com',
@@ -6,28 +6,18 @@ export interface NewPost {
6
6
  edit_url: string,
7
7
  }
8
8
 
9
- export type Args = {
10
- onCreated: ( url : string ) => void;
11
- }
12
-
13
9
  export const endpointPath = '/elementor/v1/site-navigation/add-new-post';
14
10
 
15
- export default function useCreatePage( { onCreated }: Args ) {
11
+ export default function useCreatePage() {
16
12
  const [ isLoading, setIsLoading ] = useState( false );
17
13
 
18
14
  return {
19
15
  create: () => {
20
16
  setIsLoading( true );
21
17
 
22
- addNewPage()
18
+ return addNewPage()
23
19
  .then( ( newPost ) => newPost as NewPost )
24
- .then( ( newPost ) => {
25
- setIsLoading( false );
26
- onCreated( newPost.edit_url );
27
- } )
28
- .catch( () => {
29
- setIsLoading( false );
30
- } );
20
+ .finally( () => setIsLoading( false ) );
31
21
  },
32
22
  isLoading,
33
23
  };
@@ -0,0 +1,14 @@
1
+ import { useMemo } from 'react';
2
+
3
+ export function useReverseHtmlEntities( escapedHTML = '' ) {
4
+ return useMemo( () => {
5
+ const textarea = document.createElement( 'textarea' );
6
+ textarea.innerHTML = escapedHTML;
7
+
8
+ const { value } = textarea;
9
+
10
+ textarea.remove();
11
+
12
+ return value;
13
+ }, [ escapedHTML ] );
14
+ }
package/src/init.ts CHANGED
@@ -7,7 +7,7 @@ export default function init() {
7
7
 
8
8
  function registerTopBarMenuItems() {
9
9
  injectIntoPageIndication( {
10
- name: 'document-recently-edited',
10
+ id: 'document-recently-edited',
11
11
  filler: RecentlyEdited,
12
12
  } );
13
13
  }
@@ -1,67 +0,0 @@
1
- import {
2
- Divider,
3
- MenuItem,
4
- ListItemIcon,
5
- ListSubheader,
6
- Typography,
7
- CircularProgress,
8
- } from '@elementor/ui';
9
-
10
- import * as React from 'react';
11
- import { __ } from '@wordpress/i18n';
12
- import DocTypeChip, { Props } from './chip-doc-type';
13
- import { Post } from '../../hooks/use-recent-posts';
14
- import useCreatePage from '../../hooks/use-create-page';
15
- import { PlusIcon } from '@elementor/icons';
16
-
17
- export type RecentPostsProps = {
18
- recentPosts: Post[];
19
- };
20
-
21
- export default function PostsList( { recentPosts }: RecentPostsProps ) {
22
- const { create, isLoading } = useCreatePage( { onCreated } );
23
-
24
- return (
25
- <>
26
- <ListSubheader sx={ { fontSize: 12, fontStyle: 'italic', pl: 4 } } component="div" id="nested-list-subheader">
27
- { __( 'Recent', 'elementor' ) }
28
- </ListSubheader>
29
-
30
- { recentPosts.length
31
- ? ( recentPosts.map( ( { title, edit_url: editUrl, type, id } ) => (
32
- <MenuItem
33
- dense
34
- key={ id }
35
- component="a"
36
- href={ editUrl }
37
- >
38
- { title }
39
- <DocTypeChip postType={ type.post_type } docType={ type.doc_type as Props['docType'] } label={ type.label } />
40
- </MenuItem>
41
- ) ) ) : (
42
- <Typography variant="caption" sx={ { color: 'grey.500', fontStyle: 'italic', p: 4 } } component="div" aria-label={ undefined }>
43
- { __( 'There are no other pages or templates on this site yet.', 'elementor' ) }
44
- </Typography>
45
- )
46
- }
47
- <Divider />
48
- <MenuItem
49
- dense
50
- size="small"
51
- color="inherit"
52
- component="div"
53
- onClick={ create }
54
- >
55
- <ListItemIcon>
56
- { isLoading ? <CircularProgress /> : <PlusIcon /> }
57
- </ListItemIcon>
58
-
59
- { __( 'Add new page', 'elementor' ) }
60
- </MenuItem>
61
- </>
62
- );
63
- }
64
-
65
- function onCreated( url: string ) {
66
- window.location.href = url;
67
- }