base-js-sw 1.0.10 → 1.0.11

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.
@@ -1,9 +1,29 @@
1
1
  import { __experimentalLinkControl as LinkControl } from '@wordpress/blockEditor';
2
- import { Icon, Tooltip } from '@wordpress/components';
2
+ import { Icon } from '@wordpress/components';
3
3
  import { external, update } from '@wordpress/icons';
4
4
  import { safeDecodeURI } from '@wordpress/url';
5
5
  import { useState, useEffect } from 'react';
6
6
 
7
+ const linkedPostStatusCache = {};
8
+
9
+ const STATUSES = [
10
+ 'draft',
11
+ 'private',
12
+ 'trash',
13
+ 'pending',
14
+ 'future',
15
+ 'auto-draft',
16
+ ];
17
+
18
+ const STATUS_MESSAGES = {
19
+ draft: 'Post is a draft.',
20
+ private: 'Post is private.',
21
+ trash: 'Post is in the trash.',
22
+ pending: 'Post is pending review.',
23
+ future: 'Post is scheduled.',
24
+ 'auto-draft': 'Post is an auto draft.',
25
+ };
26
+
7
27
  const linkPickerStyles = `
8
28
  .base-js-link-picker {
9
29
  width: 100%;
@@ -34,9 +54,9 @@ const linkPickerStyles = `
34
54
  gap: 8px;
35
55
  margin-top: 12px;
36
56
  color: #757575;
37
- cursor: help;
38
57
  font-size: 13px;
39
58
  line-height: 1.4;
59
+ width: 100%;
40
60
  }
41
61
 
42
62
  .base-js-link-picker__status svg {
@@ -45,91 +65,301 @@ const linkPickerStyles = `
45
65
  fill: currentColor;
46
66
  stroke: currentColor;
47
67
  }
68
+
69
+ .base-js-link-picker__status-detail {
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 8px;
73
+ margin-top: 8px;
74
+ width: 100%;
75
+ }
76
+
77
+ .base-js-link-picker__status-detail .components-badge {
78
+ max-width: 100%;
79
+ width: 100%;
80
+ }
81
+
82
+ .base-js-link-picker__status-detail .base-js-link-picker__broken-link {
83
+ max-width: 100%;
84
+ width: 100%;
85
+ }
86
+
87
+ .base-js-link-picker__broken-link .components-badge {
88
+ max-width: 100%;
89
+ width: 100%;
90
+ }
91
+
92
+ .base-js-link-picker__status-loading {
93
+ margin-top: 8px;
94
+ font-size: 13px;
95
+ line-height: 1.4;
96
+ color: #757575;
97
+ }
48
98
  `;
49
99
 
100
+ const getResultStatus = (result) => {
101
+ return String(
102
+ result?.status ||
103
+ result?.post_status ||
104
+ result?.postStatus ||
105
+ ''
106
+ ).toLowerCase();
107
+ };
108
+
109
+ const getLinkedPostStatusMessage = (result) => {
110
+ const status = getResultStatus(result);
111
+
112
+ if (STATUS_MESSAGES[status]) {
113
+ return STATUS_MESSAGES[status];
114
+ }
115
+
116
+ if (result?.message) {
117
+ return result.message;
118
+ }
119
+
120
+ return 'Linked post needs review.';
121
+ };
122
+
123
+ const BrokenLinkBadge = ({ message }) => {
124
+ return (
125
+ <div className="base-js-link-picker__broken-link">
126
+ <span className="components-badge is-warning has-icon">
127
+ <span className="components-badge__flex-wrapper">
128
+ <svg
129
+ xmlns="http://www.w3.org/2000/svg"
130
+ viewBox="0 0 24 24"
131
+ width="16"
132
+ height="16"
133
+ fill="currentColor"
134
+ className="components-badge__icon"
135
+ aria-hidden="true"
136
+ focusable="false"
137
+ >
138
+ <path
139
+ fillRule="evenodd"
140
+ clipRule="evenodd"
141
+ d="M5.5 12a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0ZM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 12v-1.5h1.5V16h-1.5Zm0-8v5h1.5V8h-1.5Z"
142
+ />
143
+ </svg>
144
+
145
+ <span className="components-badge__content">
146
+ {message}
147
+ </span>
148
+ </span>
149
+ </span>
150
+ </div>
151
+ );
152
+ };
153
+
50
154
  export const LinkPicker = ({ setAttributes, at, linkRef = 'link' }) => {
51
- // Get the current link object based on linkRef or fallback to an empty object
52
155
  const linkObj = at[linkRef] || {};
53
156
  const { url, linkTarget, id } = linkObj;
54
157
  const opensInNewTab = linkTarget === '_blank';
55
158
 
56
- // Local state to track temporary URL, ID, and link target
57
159
  const [localUrl, setLocalUrl] = useState(url || '');
58
160
  const [localId, setLocalId] = useState(id || '');
59
161
  const [localLinkTarget, setLocalLinkTarget] = useState(opensInNewTab);
162
+
163
+ const [linkedPostStatus, setLinkedPostStatus] = useState({
164
+ isLoading: false,
165
+ result: null,
166
+ });
167
+
60
168
  const isInternalPost = Boolean(localId);
61
169
  const hasLinkData = Boolean(localUrl || localId);
62
- const statusLabel = isInternalPost ? 'Dynamic WordPress link' : 'External link';
63
- const statusTooltip = isInternalPost
64
- ? 'This is a dynamic link to a page within the website. If the page’s URL changes it will update automatically'
65
- : 'This is a static link and does not auto-update.';
66
170
 
67
- // Effect to update attributes when the local state changes
171
+ const statusLabel = isInternalPost
172
+ ? 'Dynamic WordPress link'
173
+ : 'External link';
174
+
175
+ const currentStatus = linkedPostStatus.result
176
+ ? getResultStatus(linkedPostStatus.result)
177
+ : '';
178
+
179
+ const brokenLinkMessage = linkedPostStatus.result
180
+ ? getLinkedPostStatusMessage(linkedPostStatus.result)
181
+ : '';
182
+
183
+ const shouldShowBrokenLinkStatus =
184
+ isInternalPost &&
185
+ linkedPostStatus.result &&
186
+ STATUSES.includes(currentStatus);
187
+
68
188
  useEffect(() => {
69
189
  setAttributes({
70
190
  [linkRef]: {
71
- url: localUrl ? encodeURI(safeDecodeURI(localUrl)) : '', // Ensure proper encoding for the URL if no ID
72
- linkTarget: localLinkTarget ? '_blank' : undefined,
73
- id: localId ? localId : undefined, // Set ID if available, else undefined
191
+ url: localUrl
192
+ ? encodeURI(safeDecodeURI(localUrl))
193
+ : '',
194
+ linkTarget: localLinkTarget
195
+ ? '_blank'
196
+ : undefined,
197
+ id: localId
198
+ ? localId
199
+ : undefined,
74
200
  },
75
201
  });
76
- }, [localUrl, localId, localLinkTarget]); // Trigger whenever the URL, ID, or target changes
202
+ }, [localUrl, localId, localLinkTarget]);
203
+
204
+ useEffect(() => {
205
+ const numericPostId = parseInt(localId, 10);
206
+
207
+ const hasValidPostId =
208
+ Number.isInteger(numericPostId) &&
209
+ numericPostId > 0;
210
+
211
+ if (!hasValidPostId) {
212
+ setLinkedPostStatus({
213
+ isLoading: false,
214
+ result: null,
215
+ });
216
+
217
+ return;
218
+ }
219
+
220
+ if (linkedPostStatusCache[numericPostId]) {
221
+ setLinkedPostStatus({
222
+ isLoading: false,
223
+ result: linkedPostStatusCache[numericPostId],
224
+ });
225
+
226
+ return;
227
+ }
228
+
229
+ const apiFetch = window.wp?.apiFetch;
230
+
231
+ if (!apiFetch) {
232
+ setLinkedPostStatus({
233
+ isLoading: false,
234
+ result: {
235
+ status: 'draft',
236
+ message: 'Could not check this post.',
237
+ },
238
+ });
239
+
240
+ return;
241
+ }
242
+
243
+ let isMounted = true;
244
+
245
+ setLinkedPostStatus({
246
+ isLoading: true,
247
+ result: null,
248
+ });
249
+
250
+ apiFetch({
251
+ path: `/theme/v1/linked-post-validator/post/${numericPostId}`,
252
+ })
253
+ .then((result) => {
254
+ if (!isMounted) {
255
+ return;
256
+ }
257
+
258
+ linkedPostStatusCache[numericPostId] = result;
259
+
260
+ setLinkedPostStatus({
261
+ isLoading: false,
262
+ result,
263
+ });
264
+ })
265
+ .catch(() => {
266
+ if (!isMounted) {
267
+ return;
268
+ }
269
+
270
+ const errorResult = {
271
+ status: 'draft',
272
+ message: 'Could not check this post.',
273
+ };
274
+
275
+ setLinkedPostStatus({
276
+ isLoading: false,
277
+ result: errorResult,
278
+ });
279
+ });
280
+
281
+ return () => {
282
+ isMounted = false;
283
+ };
284
+ }, [localId]);
77
285
 
78
286
  const handleLinkChange = (updatedValue) => {
79
287
  const { url, opensInNewTab, id } = updatedValue;
288
+
80
289
  const nextUrl = url || '';
81
290
  const isPostSelection = Boolean(id);
82
291
 
83
- // Update ID or URL depending on whether it's an internal post or an external link/anchor
84
292
  if (isPostSelection) {
85
- // Internal post, use ID
86
293
  setLocalId(id);
87
294
  setLocalUrl(nextUrl);
88
295
  } else {
89
- // External or anchor link, use URL
90
296
  setLocalUrl(nextUrl);
91
- setLocalId(''); // Clear the ID since we are using the URL
297
+ setLocalId('');
92
298
  }
93
299
 
94
- // Update the new tab option
95
300
  setLocalLinkTarget(Boolean(opensInNewTab));
96
301
  };
97
302
 
98
303
  return (
99
304
  <div className="base-js-link-picker">
100
305
  <style>{linkPickerStyles}</style>
306
+
101
307
  <div className="base-js-link-picker__control">
102
308
  <LinkControl
103
- value={{ url: localUrl, opensInNewTab: localLinkTarget, id: localId }}
309
+ value={{
310
+ url: localUrl,
311
+ opensInNewTab: localLinkTarget,
312
+ id: localId,
313
+ }}
104
314
  onChange={(updatedValue) => {
105
315
  handleLinkChange(updatedValue);
106
316
  }}
107
317
  onBlur={() => {
108
- // Save on blur to ensure the value is saved when clicking away
109
318
  setAttributes({
110
319
  [linkRef]: {
111
- url: localId ? '' : localUrl ? encodeURI(safeDecodeURI(localUrl)) : '',
112
- linkTarget: localLinkTarget ? '_blank' : undefined,
113
- id: localId ? localId : undefined,
320
+ url: localId
321
+ ? ''
322
+ : localUrl
323
+ ? encodeURI(safeDecodeURI(localUrl))
324
+ : '',
325
+ linkTarget: localLinkTarget
326
+ ? '_blank'
327
+ : undefined,
328
+ id: localId
329
+ ? localId
330
+ : undefined,
114
331
  },
115
332
  });
116
333
  }}
117
334
  />
118
335
  </div>
336
+
119
337
  {hasLinkData && (
120
338
  <div className="base-js-link-picker__status-wrapper">
121
- <Tooltip text={statusTooltip}>
122
- <div
123
- className="base-js-link-picker__status"
124
- tabIndex={0}
125
- aria-label={statusTooltip}
126
- >
127
- <Icon icon={isInternalPost ? update : external} size={18} />
128
- <span>{statusLabel}</span>
339
+ <div className="base-js-link-picker__status">
340
+ <Icon
341
+ icon={isInternalPost ? update : external}
342
+ size={18}
343
+ />
344
+
345
+ <span>{statusLabel}</span>
346
+ </div>
347
+
348
+ {isInternalPost && linkedPostStatus.isLoading && (
349
+ <div className="base-js-link-picker__status-loading">
350
+ Checking linked post status…
129
351
  </div>
130
- </Tooltip>
352
+ )}
353
+
354
+ {shouldShowBrokenLinkStatus && (
355
+ <div className="base-js-link-picker__status-detail">
356
+ <BrokenLinkBadge
357
+ message={brokenLinkMessage}
358
+ />
359
+ </div>
360
+ )}
131
361
  </div>
132
362
  )}
133
363
  </div>
134
364
  );
135
- };
365
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "base-js-sw",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Reusable Gutenberg block components for WordPress projects",
5
5
  "main": "index.js",
6
6
  "author": "Shape Works",