base-js-sw 1.0.10 → 1.0.12

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,391 @@ 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
 
50
- export const LinkPicker = ({ setAttributes, at, linkRef = 'link' }) => {
51
- // Get the current link object based on linkRef or fallback to an empty object
100
+ const getResultStatus = (result) => {
101
+ return String(
102
+ result?.status ||
103
+ result?.post_status ||
104
+ result?.postStatus ||
105
+ ''
106
+ ).toLowerCase();
107
+ };
108
+
109
+ const getPostId = (value) => {
110
+ if (typeof value === 'number') {
111
+ return Number.isInteger(value) && value > 0 ? value : '';
112
+ }
113
+
114
+ if (typeof value !== 'string') {
115
+ return '';
116
+ }
117
+
118
+ const trimmedValue = value.trim();
119
+
120
+ if (!/^\d+$/.test(trimmedValue)) {
121
+ return '';
122
+ }
123
+
124
+ const numericValue = Number(trimmedValue);
125
+
126
+ return Number.isSafeInteger(numericValue) && numericValue > 0
127
+ ? numericValue
128
+ : '';
129
+ };
130
+
131
+ const isDirectUrlValue = ({ id, kind, type, url }) => {
132
+ const normalizedKind = String(kind || '').toLowerCase();
133
+ const normalizedType = String(type || '').toLowerCase();
134
+
135
+ return (
136
+ normalizedKind === 'url' ||
137
+ normalizedType === 'url' ||
138
+ (typeof id === 'string' && id === url)
139
+ );
140
+ };
141
+
142
+ const getLinkedPostStatusMessage = (result) => {
143
+ const status = getResultStatus(result);
144
+
145
+ if (STATUS_MESSAGES[status]) {
146
+ return STATUS_MESSAGES[status];
147
+ }
148
+
149
+ if (result?.message) {
150
+ return result.message;
151
+ }
152
+
153
+ return 'Linked post needs review.';
154
+ };
155
+
156
+ const BrokenLinkBadge = ({ message }) => {
157
+ return (
158
+ <div className="base-js-link-picker__broken-link">
159
+ <span className="components-badge is-warning has-icon">
160
+ <span className="components-badge__flex-wrapper">
161
+ <svg
162
+ xmlns="http://www.w3.org/2000/svg"
163
+ viewBox="0 0 24 24"
164
+ width="16"
165
+ height="16"
166
+ fill="currentColor"
167
+ className="components-badge__icon"
168
+ aria-hidden="true"
169
+ focusable="false"
170
+ >
171
+ <path
172
+ fillRule="evenodd"
173
+ clipRule="evenodd"
174
+ 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"
175
+ />
176
+ </svg>
177
+
178
+ <span className="components-badge__content">
179
+ {message}
180
+ </span>
181
+ </span>
182
+ </span>
183
+ </div>
184
+ );
185
+ };
186
+
187
+ export const LinkPicker = ({
188
+ setAttributes,
189
+ at,
190
+ linkRef = 'link'
191
+ }) => {
192
+
52
193
  const linkObj = at[linkRef] || {};
53
- const { url, linkTarget, id } = linkObj;
54
- const opensInNewTab = linkTarget === '_blank';
55
-
56
- // Local state to track temporary URL, ID, and link target
57
- const [localUrl, setLocalUrl] = useState(url || '');
58
- const [localId, setLocalId] = useState(id || '');
59
- const [localLinkTarget, setLocalLinkTarget] = useState(opensInNewTab);
60
- const isInternalPost = Boolean(localId);
61
- 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
-
67
- // Effect to update attributes when the local state changes
194
+
195
+ const {
196
+ url,
197
+ linkTarget,
198
+ id
199
+ } = linkObj;
200
+
201
+ const opensInNewTab =
202
+ linkTarget === '_blank';
203
+
204
+ const initialPostId =
205
+ getPostId(id);
206
+
207
+ const [localUrl, setLocalUrl] =
208
+ useState(url || '');
209
+
210
+ const [localId, setLocalId] =
211
+ useState(initialPostId);
212
+
213
+ const [localLinkTarget, setLocalLinkTarget] =
214
+ useState(opensInNewTab);
215
+
216
+ const [linkedPostStatus, setLinkedPostStatus] =
217
+ useState({
218
+ isLoading: false,
219
+ result: null,
220
+ });
221
+
222
+ const postId =
223
+ getPostId(localId);
224
+
225
+ const isInternalPost =
226
+ Boolean(postId);
227
+
228
+ const hasLinkData =
229
+ Boolean(localUrl || postId);
230
+
231
+ const statusLabel =
232
+ isInternalPost
233
+ ? 'Dynamic WordPress link'
234
+ : 'External link';
235
+
236
+ const currentStatus =
237
+ linkedPostStatus.result
238
+ ? getResultStatus(linkedPostStatus.result)
239
+ : '';
240
+
241
+ const brokenLinkMessage =
242
+ linkedPostStatus.result
243
+ ? getLinkedPostStatusMessage(linkedPostStatus.result)
244
+ : '';
245
+
246
+ const shouldShowBrokenLinkStatus =
247
+ isInternalPost
248
+ && linkedPostStatus.result
249
+ && STATUSES.includes(currentStatus);
250
+
68
251
  useEffect(() => {
252
+
253
+ const numericPostId =
254
+ getPostId(localId);
255
+
256
+ const hasValidPostId =
257
+ Boolean(numericPostId);
258
+
259
+ if (!hasValidPostId) {
260
+
261
+ setLinkedPostStatus({
262
+ isLoading: false,
263
+ result: null,
264
+ });
265
+
266
+ return;
267
+
268
+ }
269
+
270
+ if (linkedPostStatusCache[numericPostId]) {
271
+
272
+ setLinkedPostStatus({
273
+ isLoading: false,
274
+ result: linkedPostStatusCache[numericPostId],
275
+ });
276
+
277
+ return;
278
+
279
+ }
280
+
281
+ const apiFetch =
282
+ window.wp?.apiFetch;
283
+
284
+ if (!apiFetch) {
285
+
286
+ setLinkedPostStatus({
287
+ isLoading: false,
288
+ result: {
289
+ status: 'draft',
290
+ message: 'Could not check this post.',
291
+ },
292
+ });
293
+
294
+ return;
295
+
296
+ }
297
+
298
+ let isMounted = true;
299
+
300
+ setLinkedPostStatus({
301
+ isLoading: true,
302
+ result: null,
303
+ });
304
+
305
+ apiFetch({
306
+ path: `/theme/v1/linked-post-validator/post/${numericPostId}`,
307
+ })
308
+ .then((result) => {
309
+
310
+ if (!isMounted) {
311
+ return;
312
+ }
313
+
314
+ linkedPostStatusCache[numericPostId] =
315
+ result;
316
+
317
+ setLinkedPostStatus({
318
+ isLoading: false,
319
+ result,
320
+ });
321
+
322
+ })
323
+ .catch(() => {
324
+
325
+ if (!isMounted) {
326
+ return;
327
+ }
328
+
329
+ const errorResult = {
330
+ status: 'draft',
331
+ message: 'Could not check this post.',
332
+ };
333
+
334
+ setLinkedPostStatus({
335
+ isLoading: false,
336
+ result: errorResult,
337
+ });
338
+
339
+ });
340
+
341
+ return () => {
342
+ isMounted = false;
343
+ };
344
+
345
+ }, [localId]);
346
+
347
+ const handleLinkChange = (updatedValue = {}) => {
348
+
349
+ const {
350
+ url,
351
+ opensInNewTab,
352
+ id,
353
+ kind,
354
+ type,
355
+ } = updatedValue;
356
+
357
+ const nextUrl =
358
+ url || '';
359
+
360
+ /**
361
+ * When manually typing,
362
+ * Gutenberg keeps previous selected id.
363
+ *
364
+ * Manual typing has no kind/type.
365
+ */
366
+ const isManualInput =
367
+ !kind
368
+ && !type;
369
+
370
+ const nextPostId =
371
+ isManualInput
372
+ ? undefined
373
+ : getPostId(id);
374
+
375
+ setLocalUrl(nextUrl);
376
+
377
+ setLocalId(
378
+ nextPostId || ''
379
+ );
380
+
381
+ setLocalLinkTarget(
382
+ Boolean(opensInNewTab)
383
+ );
384
+
69
385
  setAttributes({
70
386
  [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
387
+ url: nextUrl
388
+ ? encodeURI(safeDecodeURI(nextUrl))
389
+ : '',
390
+ linkTarget: opensInNewTab
391
+ ? '_blank'
392
+ : undefined,
393
+ id: nextPostId || undefined,
74
394
  },
75
395
  });
76
- }, [localUrl, localId, localLinkTarget]); // Trigger whenever the URL, ID, or target changes
77
-
78
- const handleLinkChange = (updatedValue) => {
79
- const { url, opensInNewTab, id } = updatedValue;
80
- const nextUrl = url || '';
81
- const isPostSelection = Boolean(id);
82
-
83
- // Update ID or URL depending on whether it's an internal post or an external link/anchor
84
- if (isPostSelection) {
85
- // Internal post, use ID
86
- setLocalId(id);
87
- setLocalUrl(nextUrl);
88
- } else {
89
- // External or anchor link, use URL
90
- setLocalUrl(nextUrl);
91
- setLocalId(''); // Clear the ID since we are using the URL
92
- }
93
396
 
94
- // Update the new tab option
95
- setLocalLinkTarget(Boolean(opensInNewTab));
96
397
  };
97
398
 
98
399
  return (
99
400
  <div className="base-js-link-picker">
401
+
100
402
  <style>{linkPickerStyles}</style>
403
+
101
404
  <div className="base-js-link-picker__control">
405
+
102
406
  <LinkControl
103
- value={{ url: localUrl, opensInNewTab: localLinkTarget, id: localId }}
407
+ value={{
408
+ url: localUrl,
409
+ opensInNewTab: localLinkTarget,
410
+ id: postId || undefined,
411
+ }}
104
412
  onChange={(updatedValue) => {
105
413
  handleLinkChange(updatedValue);
106
414
  }}
107
- onBlur={() => {
108
- // Save on blur to ensure the value is saved when clicking away
109
- setAttributes({
110
- [linkRef]: {
111
- url: localId ? '' : localUrl ? encodeURI(safeDecodeURI(localUrl)) : '',
112
- linkTarget: localLinkTarget ? '_blank' : undefined,
113
- id: localId ? localId : undefined,
114
- },
115
- });
116
- }}
117
415
  />
416
+
118
417
  </div>
418
+
119
419
  {hasLinkData && (
120
420
  <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>
421
+
422
+ <div className="base-js-link-picker__status">
423
+
424
+ <Icon
425
+ icon={isInternalPost ? update : external}
426
+ size={18}
427
+ />
428
+
429
+ <span>{statusLabel}</span>
430
+
431
+ </div>
432
+
433
+ {isInternalPost && linkedPostStatus.isLoading && (
434
+ <div className="base-js-link-picker__status-loading">
435
+ Checking linked post status…
129
436
  </div>
130
- </Tooltip>
437
+ )}
438
+
439
+ {shouldShowBrokenLinkStatus && (
440
+ <div className="base-js-link-picker__status-detail">
441
+
442
+ <BrokenLinkBadge
443
+ message={brokenLinkMessage}
444
+ />
445
+
446
+ </div>
447
+ )}
448
+
131
449
  </div>
132
450
  )}
451
+
133
452
  </div>
134
453
  );
135
- };
454
+
455
+ };
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.12",
4
4
  "description": "Reusable Gutenberg block components for WordPress projects",
5
5
  "main": "index.js",
6
6
  "author": "Shape Works",