@contentful/field-editor-slug 1.2.0 → 1.3.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.
Files changed (39) hide show
  1. package/dist/cjs/SlugEditor.js +139 -0
  2. package/dist/cjs/SlugEditor.test.js +538 -0
  3. package/dist/cjs/SlugEditorField.js +186 -0
  4. package/dist/cjs/TrackingFieldConnector.js +137 -0
  5. package/dist/cjs/index.js +24 -0
  6. package/dist/cjs/services/makeSlug.js +42 -0
  7. package/dist/cjs/services/makeSlug.test.js +14 -0
  8. package/dist/cjs/services/slugify.js +64 -0
  9. package/dist/cjs/services/slugify.test.js +30 -0
  10. package/dist/cjs/styles.js +68 -0
  11. package/dist/esm/SlugEditor.js +90 -0
  12. package/dist/esm/SlugEditor.test.js +490 -0
  13. package/dist/esm/SlugEditorField.js +129 -0
  14. package/dist/esm/TrackingFieldConnector.js +88 -0
  15. package/dist/esm/index.js +3 -0
  16. package/dist/esm/services/makeSlug.js +24 -0
  17. package/dist/esm/services/makeSlug.test.js +10 -0
  18. package/dist/esm/services/slugify.js +49 -0
  19. package/dist/esm/services/slugify.test.js +26 -0
  20. package/dist/esm/styles.js +33 -0
  21. package/dist/{SlugEditor.d.ts → types/SlugEditor.d.ts} +24 -24
  22. package/dist/types/SlugEditor.test.d.ts +1 -0
  23. package/dist/{SlugEditorField.d.ts → types/SlugEditorField.d.ts} +18 -18
  24. package/dist/{TrackingFieldConnector.d.ts → types/TrackingFieldConnector.d.ts} +29 -29
  25. package/dist/{index.d.ts → types/index.d.ts} +3 -3
  26. package/dist/{services → types/services}/makeSlug.d.ts +8 -8
  27. package/dist/types/services/makeSlug.test.d.ts +1 -0
  28. package/dist/{services → types/services}/slugify.d.ts +12 -12
  29. package/dist/types/services/slugify.test.d.ts +1 -0
  30. package/dist/{styles.d.ts → types/styles.d.ts} +6 -6
  31. package/package.json +25 -11
  32. package/CHANGELOG.md +0 -206
  33. package/dist/field-editor-slug.cjs.development.js +0 -463
  34. package/dist/field-editor-slug.cjs.development.js.map +0 -1
  35. package/dist/field-editor-slug.cjs.production.min.js +0 -2
  36. package/dist/field-editor-slug.cjs.production.min.js.map +0 -1
  37. package/dist/field-editor-slug.esm.js +0 -454
  38. package/dist/field-editor-slug.esm.js.map +0 -1
  39. package/dist/index.js +0 -8
@@ -0,0 +1,490 @@
1
+ import * as React from 'react';
2
+ import { createFakeFieldAPI, createFakeLocalesAPI } from '@contentful/field-editor-test-utils';
3
+ import { cleanup, configure, fireEvent, render, waitFor } from '@testing-library/react';
4
+ import identity from 'lodash/identity';
5
+ import '@testing-library/jest-dom/extend-expect';
6
+ import { SlugEditor } from './SlugEditor';
7
+ configure({
8
+ testIdAttribute: 'data-test-id'
9
+ });
10
+ jest.mock('lodash/throttle', ()=>({
11
+ default: identity
12
+ }), {
13
+ virtual: true
14
+ });
15
+ jest.mock('use-debounce', ()=>({
16
+ useDebounce: (text)=>[
17
+ text
18
+ ]
19
+ }));
20
+ function createMocks(initialValues = {}) {
21
+ const [field] = createFakeFieldAPI((field)=>({
22
+ ...field,
23
+ id: 'slug-id',
24
+ onValueChanged: jest.fn().mockImplementation(field.onValueChanged),
25
+ setValue: jest.fn().mockImplementation(field.setValue)
26
+ }), initialValues.field || '');
27
+ const [titleField] = createFakeFieldAPI((field)=>({
28
+ ...field,
29
+ id: 'title-id',
30
+ setValue: jest.fn().mockImplementation(field.setValue),
31
+ getValue: jest.fn().mockImplementation(field.getValue),
32
+ onValueChanged: jest.fn().mockImplementation(field.onValueChanged)
33
+ }), initialValues.titleField || '');
34
+ const [descriptionField] = createFakeFieldAPI((field)=>({
35
+ ...field,
36
+ id: 'description-id',
37
+ setValue: jest.fn().mockImplementation(field.setValue),
38
+ getValue: jest.fn().mockImplementation(field.getValue),
39
+ onValueChanged: jest.fn().mockImplementation(field.onValueChanged)
40
+ }), initialValues.descriptionField || '');
41
+ const sdk = {
42
+ locales: createFakeLocalesAPI(),
43
+ space: {
44
+ getEntries: jest.fn().mockResolvedValue({
45
+ total: 0
46
+ })
47
+ },
48
+ entry: {
49
+ getSys: jest.fn().mockReturnValue({
50
+ id: 'entry-id',
51
+ publishedVersion: undefined,
52
+ createdAt: '2020-01-24T15:33:47.906Z',
53
+ contentType: {
54
+ sys: {
55
+ id: 'content-type-id'
56
+ }
57
+ }
58
+ }),
59
+ onSysChanged: jest.fn(),
60
+ fields: {
61
+ 'title-id': titleField,
62
+ 'entry-id': field,
63
+ 'description-id': descriptionField
64
+ }
65
+ },
66
+ contentType: {
67
+ displayField: 'title-id'
68
+ }
69
+ };
70
+ return {
71
+ field,
72
+ titleField,
73
+ descriptionField,
74
+ sdk
75
+ };
76
+ }
77
+ describe('SlugEditor', ()=>{
78
+ afterEach(cleanup);
79
+ describe('should not subscribe to title changes', ()=>{
80
+ it('when entry is published', async ()=>{
81
+ const { field , titleField , sdk } = createMocks();
82
+ sdk.entry.getSys.mockReturnValue({
83
+ publishedVersion: 2
84
+ });
85
+ render(React.createElement(SlugEditor, {
86
+ field: field,
87
+ baseSdk: sdk,
88
+ isInitiallyDisabled: false
89
+ }));
90
+ await waitFor(()=>{
91
+ expect(field.setValue).not.toHaveBeenCalled();
92
+ expect(titleField.onValueChanged).toHaveBeenCalledWith('en-US', expect.any(Function));
93
+ expect(sdk.space.getEntries).not.toHaveBeenCalled();
94
+ expect(sdk.entry.fields['title-id'].getValue).toHaveBeenCalledTimes(1);
95
+ expect(sdk.entry.getSys).toHaveBeenCalledTimes(2);
96
+ });
97
+ });
98
+ it('when title and slug are the same field', async ()=>{
99
+ const { field , titleField , sdk } = createMocks();
100
+ sdk.contentType.displayField = 'entry-id';
101
+ render(React.createElement(SlugEditor, {
102
+ field: field,
103
+ baseSdk: sdk,
104
+ isInitiallyDisabled: false
105
+ }));
106
+ await waitFor(()=>{
107
+ expect(titleField.onValueChanged).not.toHaveBeenCalled();
108
+ expect(field.setValue).not.toHaveBeenCalled();
109
+ });
110
+ });
111
+ it('when a saved slug is different from a title at the render', async ()=>{
112
+ const { field , titleField , sdk } = createMocks({
113
+ titleField: 'Hello world!',
114
+ field: 'something-different'
115
+ });
116
+ render(React.createElement(SlugEditor, {
117
+ field: field,
118
+ baseSdk: sdk,
119
+ isInitiallyDisabled: false
120
+ }));
121
+ await waitFor(()=>{
122
+ expect(titleField.onValueChanged).toHaveBeenCalledWith('en-US', expect.any(Function));
123
+ expect(field.setValue).not.toHaveBeenCalled();
124
+ });
125
+ });
126
+ });
127
+ describe('should check for uniqueness', ()=>{
128
+ it('if it is published', async ()=>{
129
+ const { field , titleField , sdk } = createMocks({
130
+ titleField: 'Slug value',
131
+ field: 'slug-value'
132
+ });
133
+ sdk.entry.getSys.mockReturnValue({
134
+ id: 'entry-id',
135
+ publishedVersion: 2,
136
+ contentType: {
137
+ sys: {
138
+ id: 'content-type-id'
139
+ }
140
+ }
141
+ });
142
+ sdk.space.getEntries.mockResolvedValue({
143
+ total: 0
144
+ });
145
+ const { queryByTestId , queryByText } = render(React.createElement(SlugEditor, {
146
+ field: field,
147
+ baseSdk: sdk,
148
+ isInitiallyDisabled: false
149
+ }));
150
+ await waitFor(()=>{
151
+ expect(titleField.onValueChanged).toHaveBeenCalledWith('en-US', expect.any(Function));
152
+ expect(sdk.space.getEntries).toHaveBeenLastCalledWith({
153
+ content_type: 'content-type-id',
154
+ 'fields.slug-id.en-US': 'slug-value',
155
+ limit: 0,
156
+ 'sys.id[ne]': 'entry-id',
157
+ 'sys.publishedAt[exists]': true
158
+ });
159
+ expect(sdk.space.getEntries).toHaveBeenCalledTimes(1);
160
+ expect(queryByTestId('slug-editor-spinner')).not.toBeInTheDocument();
161
+ expect(queryByText('This slug has already been published in another entry')).not.toBeInTheDocument();
162
+ });
163
+ });
164
+ it('if it is not published', async ()=>{
165
+ const { field , titleField , sdk } = createMocks({
166
+ titleField: 'Slug value',
167
+ field: 'slug-value'
168
+ });
169
+ sdk.entry.getSys.mockReturnValue({
170
+ id: 'entry-id',
171
+ publishedVersion: undefined,
172
+ contentType: {
173
+ sys: {
174
+ id: 'content-type-id'
175
+ }
176
+ }
177
+ });
178
+ sdk.space.getEntries.mockResolvedValue({
179
+ total: 2
180
+ });
181
+ const { queryByTestId , queryByText , getByTestId } = render(React.createElement(SlugEditor, {
182
+ field: field,
183
+ baseSdk: sdk,
184
+ isInitiallyDisabled: false
185
+ }));
186
+ await waitFor(()=>{
187
+ expect(titleField.onValueChanged).toHaveBeenCalledWith('en-US', expect.any(Function));
188
+ expect(sdk.space.getEntries).toHaveBeenLastCalledWith({
189
+ content_type: 'content-type-id',
190
+ 'fields.slug-id.en-US': 'slug-value',
191
+ limit: 0,
192
+ 'sys.id[ne]': 'entry-id',
193
+ 'sys.publishedAt[exists]': true
194
+ });
195
+ expect(sdk.space.getEntries).toHaveBeenCalledTimes(1);
196
+ expect(queryByTestId('slug-editor-spinner')).not.toBeInTheDocument();
197
+ expect(queryByText('This slug has already been published in another entry')).toBeInTheDocument();
198
+ expect(getByTestId('cf-ui-text-input')).toHaveValue('slug-value');
199
+ });
200
+ sdk.space.getEntries.mockResolvedValue({
201
+ total: 0
202
+ });
203
+ fireEvent.change(getByTestId('cf-ui-text-input'), {
204
+ target: {
205
+ value: '123'
206
+ }
207
+ });
208
+ await waitFor(()=>{
209
+ expect(field.setValue).toHaveBeenCalledTimes(1);
210
+ expect(field.setValue).toHaveBeenCalledWith('123');
211
+ expect(sdk.space.getEntries).toHaveBeenCalledTimes(2);
212
+ expect(sdk.space.getEntries).toHaveBeenLastCalledWith({
213
+ content_type: 'content-type-id',
214
+ 'fields.slug-id.en-US': '123',
215
+ limit: 0,
216
+ 'sys.id[ne]': 'entry-id',
217
+ 'sys.publishedAt[exists]': true
218
+ });
219
+ expect(queryByText('This slug has already been published in another entry')).not.toBeInTheDocument();
220
+ });
221
+ });
222
+ });
223
+ describe('should react to title changes', ()=>{
224
+ it('when field is disabled', async ()=>{
225
+ const { field , titleField , sdk } = createMocks({
226
+ field: '',
227
+ titleField: ''
228
+ });
229
+ render(React.createElement(SlugEditor, {
230
+ field: field,
231
+ baseSdk: sdk,
232
+ isInitiallyDisabled: true
233
+ }));
234
+ await waitFor(()=>{
235
+ expect(field.setValue).toHaveBeenCalled();
236
+ expect(titleField.onValueChanged).toHaveBeenCalledWith('en-US', expect.any(Function));
237
+ expect(sdk.space.getEntries).toHaveBeenCalled();
238
+ expect(sdk.entry.fields['title-id'].getValue).toHaveBeenCalledTimes(1);
239
+ expect(sdk.entry.getSys).toHaveBeenCalledTimes(2);
240
+ });
241
+ });
242
+ it('should generate unique value with date if title is empty', async ()=>{
243
+ const { field , titleField , sdk } = createMocks({
244
+ field: '',
245
+ titleField: ''
246
+ });
247
+ render(React.createElement(SlugEditor, {
248
+ field: field,
249
+ baseSdk: sdk,
250
+ isInitiallyDisabled: false
251
+ }));
252
+ await waitFor(()=>{
253
+ expect(titleField.onValueChanged).toHaveBeenCalledWith('en-US', expect.any(Function));
254
+ expect(field.setValue).toHaveBeenCalledTimes(1);
255
+ expect(field.setValue).toHaveBeenLastCalledWith('untitled-entry-2020-01-24-at-15-33-47');
256
+ });
257
+ await sdk.entry.fields['title-id'].setValue('Hello world!');
258
+ await waitFor(()=>{
259
+ expect(field.setValue).toHaveBeenCalledTimes(2);
260
+ expect(field.setValue).toHaveBeenLastCalledWith('hello-world');
261
+ expect(sdk.space.getEntries).toHaveBeenCalledTimes(2);
262
+ });
263
+ await sdk.entry.fields['title-id'].setValue('фраза написанная по русски');
264
+ await waitFor(()=>{
265
+ expect(field.setValue).toHaveBeenCalledTimes(3);
266
+ expect(field.setValue).toHaveBeenLastCalledWith('fraza-napisannaya-po-russki');
267
+ expect(sdk.space.getEntries).toHaveBeenCalledTimes(3);
268
+ });
269
+ });
270
+ it('should generate value from title if it is not empty', async ()=>{
271
+ const { field , titleField , sdk } = createMocks({
272
+ field: '',
273
+ titleField: 'This is initial title value'
274
+ });
275
+ render(React.createElement(SlugEditor, {
276
+ field: field,
277
+ baseSdk: sdk,
278
+ isInitiallyDisabled: false
279
+ }));
280
+ await waitFor(()=>{
281
+ expect(titleField.onValueChanged).toHaveBeenCalledWith('en-US', expect.any(Function));
282
+ expect(field.setValue).toHaveBeenCalledTimes(1);
283
+ expect(field.setValue).toHaveBeenLastCalledWith('this-is-initial-title-value');
284
+ });
285
+ await sdk.entry.fields['title-id'].setValue('Hello world!');
286
+ await waitFor(()=>{
287
+ expect(field.setValue).toHaveBeenCalledTimes(2);
288
+ expect(field.setValue).toHaveBeenLastCalledWith('hello-world');
289
+ expect(sdk.space.getEntries).toHaveBeenCalledTimes(2);
290
+ });
291
+ });
292
+ it('should stop tracking value after user intentionally changes slug value', async ()=>{
293
+ const { field , titleField , sdk } = createMocks({
294
+ field: '',
295
+ titleField: ''
296
+ });
297
+ const { getByTestId } = render(React.createElement(SlugEditor, {
298
+ field: field,
299
+ baseSdk: sdk,
300
+ isInitiallyDisabled: false
301
+ }));
302
+ await waitFor(async ()=>{
303
+ await sdk.entry.fields['title-id'].setValue('Hello world!');
304
+ });
305
+ await waitFor(()=>{
306
+ expect(titleField.onValueChanged).toHaveBeenCalledWith('en-US', expect.any(Function));
307
+ expect(field.setValue).toHaveBeenCalledTimes(2);
308
+ expect(field.setValue).toHaveBeenCalledWith('untitled-entry-2020-01-24-at-15-33-47');
309
+ expect(field.setValue).toHaveBeenLastCalledWith('hello-world');
310
+ expect(sdk.space.getEntries).toHaveBeenCalledTimes(2);
311
+ });
312
+ fireEvent.change(getByTestId('cf-ui-text-input'), {
313
+ target: {
314
+ value: 'new-custom-slug'
315
+ }
316
+ });
317
+ await waitFor(()=>{
318
+ expect(field.setValue).toHaveBeenCalledTimes(3);
319
+ expect(field.setValue).toHaveBeenLastCalledWith('new-custom-slug');
320
+ });
321
+ await sdk.entry.fields['title-id'].setValue('I decided to update my title');
322
+ await waitFor(()=>{
323
+ expect(field.setValue).toHaveBeenCalledTimes(3);
324
+ });
325
+ await sdk.entry.fields['title-id'].setValue('I decided to update my title again');
326
+ await waitFor(()=>{
327
+ expect(field.setValue).toHaveBeenCalledTimes(3);
328
+ });
329
+ });
330
+ it('should start tracking again after potential slug equals real one', async ()=>{
331
+ const { field , sdk } = createMocks({
332
+ field: '',
333
+ titleField: ''
334
+ });
335
+ const { getByTestId } = render(React.createElement(SlugEditor, {
336
+ field: field,
337
+ baseSdk: sdk,
338
+ isInitiallyDisabled: false
339
+ }));
340
+ await waitFor(async ()=>{
341
+ await sdk.entry.fields['title-id'].setValue('ABC DEF');
342
+ });
343
+ await waitFor(()=>{
344
+ expect(field.setValue).toHaveBeenLastCalledWith('abc-def');
345
+ expect(field.setValue).toHaveBeenCalledTimes(2);
346
+ });
347
+ fireEvent.change(getByTestId('cf-ui-text-input'), {
348
+ target: {
349
+ value: 'abc'
350
+ }
351
+ });
352
+ await sdk.entry.fields['title-id'].setValue('ABC D');
353
+ await waitFor(()=>{
354
+ expect(field.setValue).toHaveBeenLastCalledWith('abc');
355
+ expect(field.setValue).toHaveBeenCalledTimes(3);
356
+ });
357
+ await waitFor(async ()=>{
358
+ await sdk.entry.fields['title-id'].setValue('ABC');
359
+ await sdk.entry.fields['title-id'].setValue('ABC ABC');
360
+ });
361
+ await waitFor(()=>{
362
+ expect(field.setValue).toHaveBeenLastCalledWith('abc-abc');
363
+ expect(field.setValue).toHaveBeenCalledTimes(4);
364
+ });
365
+ });
366
+ });
367
+ describe('for non default locales', ()=>{
368
+ it('locale is not optional and has no fallback then it should track default locale changes & current locale changes', async ()=>{
369
+ const { sdk , field , titleField } = createMocks();
370
+ field.locale = 'ru-RU';
371
+ field.required = false;
372
+ sdk.locales.available = [
373
+ 'de-DE',
374
+ 'ru-RU'
375
+ ];
376
+ sdk.locales.default = 'de-DE';
377
+ sdk.locales.optional = {
378
+ 'de-DE': false,
379
+ 'ru-RU': false
380
+ };
381
+ sdk.locales.fallbacks = {
382
+ 'de-DE': undefined,
383
+ 'ru-RU': undefined
384
+ };
385
+ render(React.createElement(SlugEditor, {
386
+ field: field,
387
+ baseSdk: sdk,
388
+ isInitiallyDisabled: false
389
+ }));
390
+ await waitFor(()=>{
391
+ expect(field.setValue).toHaveBeenCalledWith('untitled-entry-2020-01-24-at-15-33-47');
392
+ expect(titleField.onValueChanged).toHaveBeenCalledWith('ru-RU', expect.any(Function));
393
+ expect(titleField.onValueChanged).toHaveBeenCalledWith('de-DE', expect.any(Function));
394
+ });
395
+ });
396
+ it('locale is optional and has a fallback then it should track only current locale changes', async ()=>{
397
+ const { sdk , field , titleField } = createMocks();
398
+ field.locale = 'ru-RU';
399
+ field.required = false;
400
+ sdk.locales.available = [
401
+ 'de-DE',
402
+ 'ru-RU'
403
+ ];
404
+ sdk.locales.default = 'de-DE';
405
+ sdk.locales.optional = {
406
+ 'de-DE': false,
407
+ 'ru-RU': true
408
+ };
409
+ sdk.locales.fallbacks = {
410
+ 'de-DE': undefined,
411
+ 'ru-RU': 'de-DE'
412
+ };
413
+ render(React.createElement(SlugEditor, {
414
+ field: field,
415
+ baseSdk: sdk,
416
+ isInitiallyDisabled: false
417
+ }));
418
+ await waitFor(()=>{
419
+ expect(field.setValue).not.toHaveBeenCalled();
420
+ expect(titleField.onValueChanged).toHaveBeenCalledWith('ru-RU', expect.any(Function));
421
+ expect(titleField.onValueChanged).not.toHaveBeenCalledWith('de-DE', expect.any(Function));
422
+ });
423
+ });
424
+ });
425
+ it('slug suggestion is limited to 75 symbols', async ()=>{
426
+ const { field , sdk } = createMocks({
427
+ field: '',
428
+ titleField: ''
429
+ });
430
+ render(React.createElement(SlugEditor, {
431
+ field: field,
432
+ baseSdk: sdk,
433
+ isInitiallyDisabled: false
434
+ }));
435
+ await waitFor(async ()=>{
436
+ await sdk.entry.fields['title-id'].setValue('a'.repeat(80));
437
+ });
438
+ await waitFor(()=>{
439
+ const expectedSlug = 'a'.repeat(75);
440
+ expect(field.setValue).toHaveBeenLastCalledWith(expectedSlug);
441
+ });
442
+ });
443
+ it('slug suggestion does not contain cut-off words', async ()=>{
444
+ const { field , sdk } = createMocks({
445
+ field: '',
446
+ titleField: ''
447
+ });
448
+ render(React.createElement(SlugEditor, {
449
+ field: field,
450
+ baseSdk: sdk,
451
+ isInitiallyDisabled: false
452
+ }));
453
+ await waitFor(async ()=>{
454
+ await sdk.entry.fields['title-id'].setValue(`one two three ${'a'.repeat(80)}`);
455
+ });
456
+ await waitFor(()=>{
457
+ const expectedSlug = 'one-two-three';
458
+ expect(field.setValue).toHaveBeenLastCalledWith(expectedSlug);
459
+ });
460
+ });
461
+ it('should subscribe for changes in custom field id', async ()=>{
462
+ const { field , titleField , descriptionField , sdk } = createMocks({
463
+ field: '',
464
+ titleField: 'This is initial title value',
465
+ descriptionField: 'This is initial description value'
466
+ });
467
+ render(React.createElement(SlugEditor, {
468
+ field: field,
469
+ baseSdk: sdk,
470
+ isInitiallyDisabled: false,
471
+ parameters: {
472
+ instance: {
473
+ trackingFieldId: 'description-id'
474
+ }
475
+ }
476
+ }));
477
+ await waitFor(()=>{
478
+ expect(titleField.onValueChanged).not.toHaveBeenCalled();
479
+ expect(descriptionField.onValueChanged).toHaveBeenCalledWith('en-US', expect.any(Function));
480
+ expect(field.setValue).toHaveBeenCalledTimes(1);
481
+ expect(field.setValue).toHaveBeenLastCalledWith('this-is-initial-description-value');
482
+ });
483
+ await sdk.entry.fields['description-id'].setValue('Hello world!');
484
+ await waitFor(()=>{
485
+ expect(field.setValue).toHaveBeenCalledTimes(2);
486
+ expect(field.setValue).toHaveBeenLastCalledWith('hello-world');
487
+ expect(sdk.space.getEntries).toHaveBeenCalledTimes(2);
488
+ });
489
+ });
490
+ });
@@ -0,0 +1,129 @@
1
+ import * as React from 'react';
2
+ import { Spinner, TextInput, ValidationMessage } from '@contentful/f36-components';
3
+ import { LinkIcon } from '@contentful/f36-icons';
4
+ import { useDebounce } from 'use-debounce';
5
+ import { makeSlug } from './services/makeSlug';
6
+ import * as styles from './styles';
7
+ function useSlugUpdater(props, check) {
8
+ const { value , setValue , createdAt , locale , titleValue , isOptionalLocaleWithFallback } = props;
9
+ React.useEffect(()=>{
10
+ if (check === false) {
11
+ return;
12
+ }
13
+ const newSlug = makeSlug(titleValue, {
14
+ isOptionalLocaleWithFallback,
15
+ locale,
16
+ createdAt
17
+ });
18
+ if (newSlug !== value) {
19
+ setValue(newSlug);
20
+ }
21
+ }, [
22
+ value,
23
+ titleValue,
24
+ isOptionalLocaleWithFallback,
25
+ check,
26
+ createdAt,
27
+ locale,
28
+ setValue
29
+ ]);
30
+ }
31
+ function useUniqueChecker(props) {
32
+ const { performUniqueCheck } = props;
33
+ const [status, setStatus] = React.useState(props.value ? 'checking' : 'unique');
34
+ const [debouncedValue] = useDebounce(props.value, 1000);
35
+ React.useEffect(()=>{
36
+ if (!debouncedValue) {
37
+ setStatus('unique');
38
+ return;
39
+ }
40
+ setStatus('checking');
41
+ performUniqueCheck(debouncedValue).then((unique)=>{
42
+ setStatus(unique ? 'unique' : 'duplicate');
43
+ }).catch(()=>{
44
+ setStatus('checking');
45
+ });
46
+ }, [
47
+ debouncedValue,
48
+ performUniqueCheck
49
+ ]);
50
+ return status;
51
+ }
52
+ export function SlugEditorFieldStatic(props) {
53
+ const { hasError , isDisabled , value , setValue , onChange , onBlur } = props;
54
+ const status = useUniqueChecker(props);
55
+ return React.createElement("div", {
56
+ className: styles.inputContainer
57
+ }, React.createElement(LinkIcon, {
58
+ className: styles.icon
59
+ }), React.createElement(TextInput, {
60
+ className: styles.input,
61
+ isInvalid: hasError || status === 'duplicate',
62
+ isDisabled: isDisabled,
63
+ value: value || '',
64
+ onChange: (e)=>{
65
+ setValue(e.target.value);
66
+ if (onChange) {
67
+ onChange();
68
+ }
69
+ },
70
+ onBlur: ()=>{
71
+ if (onBlur) {
72
+ onBlur();
73
+ }
74
+ }
75
+ }), status === 'checking' && React.createElement("div", {
76
+ className: styles.spinnerContainer
77
+ }, React.createElement(Spinner, {
78
+ testId: "slug-editor-spinner"
79
+ })), status === 'duplicate' && React.createElement(ValidationMessage, {
80
+ testId: "slug-editor-duplicate-error",
81
+ className: styles.uniqueValidationError
82
+ }, "This slug has already been published in another entry"));
83
+ }
84
+ export function SlugEditorField(props) {
85
+ const { titleValue , isOptionalLocaleWithFallback , locale , createdAt , value } = props;
86
+ const areEqual = React.useCallback(()=>{
87
+ const potentialSlug = makeSlug(titleValue, {
88
+ isOptionalLocaleWithFallback: isOptionalLocaleWithFallback,
89
+ locale: locale,
90
+ createdAt: createdAt
91
+ });
92
+ return value === potentialSlug;
93
+ }, [
94
+ titleValue,
95
+ isOptionalLocaleWithFallback,
96
+ locale,
97
+ createdAt,
98
+ value
99
+ ]);
100
+ const [check, setCheck] = React.useState(()=>{
101
+ if (props.value) {
102
+ if (!props.titleValue) {
103
+ return false;
104
+ }
105
+ return areEqual();
106
+ }
107
+ return true;
108
+ });
109
+ React.useEffect(()=>{
110
+ if (areEqual()) {
111
+ setCheck(true);
112
+ }
113
+ }, [
114
+ props.titleValue,
115
+ areEqual
116
+ ]);
117
+ useSlugUpdater(props, check);
118
+ return React.createElement(SlugEditorFieldStatic, {
119
+ ...props,
120
+ onChange: ()=>{
121
+ setCheck(false);
122
+ },
123
+ onBlur: ()=>{
124
+ if (areEqual()) {
125
+ setCheck(true);
126
+ }
127
+ }
128
+ });
129
+ }