base-js-sw 1.0.9 → 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,65 +1,365 @@
1
1
  import { __experimentalLinkControl as LinkControl } from '@wordpress/blockEditor';
2
+ import { Icon } from '@wordpress/components';
3
+ import { external, update } from '@wordpress/icons';
2
4
  import { safeDecodeURI } from '@wordpress/url';
3
5
  import { useState, useEffect } from 'react';
4
- import './LinkPicker.scss';
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
+
27
+ const linkPickerStyles = `
28
+ .base-js-link-picker {
29
+ width: 100%;
30
+ max-width: 100%;
31
+ min-width: 0;
32
+ overflow-x: hidden;
33
+ }
34
+
35
+ .base-js-link-picker .block-editor-link-control {
36
+ width: 100%;
37
+ max-width: 100% !important;
38
+ min-width: 0 !important;
39
+ }
40
+
41
+ .base-js-link-picker .block-editor-link-control__search-input {
42
+ margin: 0 !important;
43
+ min-width: 0 !important;
44
+ }
45
+
46
+ .base-js-link-picker .components-input-control__container {
47
+ width: 90% !important;
48
+ max-width: 100%;
49
+ }
50
+
51
+ .base-js-link-picker__status {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 8px;
55
+ margin-top: 12px;
56
+ color: #757575;
57
+ font-size: 13px;
58
+ line-height: 1.4;
59
+ width: 100%;
60
+ }
61
+
62
+ .base-js-link-picker__status svg {
63
+ flex: 0 0 auto;
64
+ color: inherit;
65
+ fill: currentColor;
66
+ stroke: currentColor;
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
+ }
98
+ `;
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
+ };
5
153
 
6
154
  export const LinkPicker = ({ setAttributes, at, linkRef = 'link' }) => {
7
- // Get the current link object based on linkRef or fallback to an empty object
8
155
  const linkObj = at[linkRef] || {};
9
156
  const { url, linkTarget, id } = linkObj;
10
157
  const opensInNewTab = linkTarget === '_blank';
11
158
 
12
- // Local state to track temporary URL, ID, and link target
13
159
  const [localUrl, setLocalUrl] = useState(url || '');
14
160
  const [localId, setLocalId] = useState(id || '');
15
161
  const [localLinkTarget, setLocalLinkTarget] = useState(opensInNewTab);
16
162
 
17
- // Effect to update attributes when the local state changes
163
+ const [linkedPostStatus, setLinkedPostStatus] = useState({
164
+ isLoading: false,
165
+ result: null,
166
+ });
167
+
168
+ const isInternalPost = Boolean(localId);
169
+ const hasLinkData = Boolean(localUrl || localId);
170
+
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
+
18
188
  useEffect(() => {
19
189
  setAttributes({
20
190
  [linkRef]: {
21
- url: localUrl ? encodeURI(safeDecodeURI(localUrl)) : '', // Ensure proper encoding for the URL if no ID
22
- linkTarget: localLinkTarget ? '_blank' : undefined,
23
- 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,
24
200
  },
25
201
  });
26
- }, [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]);
27
285
 
28
286
  const handleLinkChange = (updatedValue) => {
29
- console.log(updatedValue)
30
- const { url, opensInNewTab, id, kind } = updatedValue;
287
+ const { url, opensInNewTab, id } = updatedValue;
31
288
 
32
- // Update ID or URL depending on whether it's an internal post or an external link/anchor
33
- if (kind === 'post-type' && id) {
34
- // Internal post, use ID
289
+ const nextUrl = url || '';
290
+ const isPostSelection = Boolean(id);
291
+
292
+ if (isPostSelection) {
35
293
  setLocalId(id);
36
- setLocalUrl(url);
294
+ setLocalUrl(nextUrl);
37
295
  } else {
38
- // External or anchor link, use URL
39
- setLocalUrl(url);
40
- setLocalId(''); // Clear the ID since we are using the URL
296
+ setLocalUrl(nextUrl);
297
+ setLocalId('');
41
298
  }
42
299
 
43
- // Update the new tab option
44
- setLocalLinkTarget(opensInNewTab);
300
+ setLocalLinkTarget(Boolean(opensInNewTab));
45
301
  };
46
302
 
47
303
  return (
48
- <LinkControl
49
- value={{ url: localUrl, opensInNewTab: localLinkTarget, id: localId }}
50
- onChange={(updatedValue) => {
51
- handleLinkChange(updatedValue);
52
- }}
53
- onBlur={() => {
54
- // Save on blur to ensure the value is saved when clicking away
55
- setAttributes({
56
- [linkRef]: {
57
- url: localId ? '' : localUrl ? encodeURI(safeDecodeURI(localUrl)) : '',
58
- linkTarget: localLinkTarget ? '_blank' : undefined,
59
- id: localId ? localId : undefined,
60
- },
61
- });
62
- }}
63
- />
304
+ <div className="base-js-link-picker">
305
+ <style>{linkPickerStyles}</style>
306
+
307
+ <div className="base-js-link-picker__control">
308
+ <LinkControl
309
+ value={{
310
+ url: localUrl,
311
+ opensInNewTab: localLinkTarget,
312
+ id: localId,
313
+ }}
314
+ onChange={(updatedValue) => {
315
+ handleLinkChange(updatedValue);
316
+ }}
317
+ onBlur={() => {
318
+ setAttributes({
319
+ [linkRef]: {
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,
331
+ },
332
+ });
333
+ }}
334
+ />
335
+ </div>
336
+
337
+ {hasLinkData && (
338
+ <div className="base-js-link-picker__status-wrapper">
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…
351
+ </div>
352
+ )}
353
+
354
+ {shouldShowBrokenLinkStatus && (
355
+ <div className="base-js-link-picker__status-detail">
356
+ <BrokenLinkBadge
357
+ message={brokenLinkMessage}
358
+ />
359
+ </div>
360
+ )}
361
+ </div>
362
+ )}
363
+ </div>
64
364
  );
65
- };
365
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "base-js-sw",
3
- "version": "1.0.9",
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",