@griddo/ax 11.15.7 → 11.15.8-rc.0

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 (88) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.atoms.test.tsx +409 -0
  3. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.helpers.test.tsx +418 -0
  4. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.test.tsx +776 -29
  5. package/src/__tests__/components/FieldsBehavior/FieldsBehavior.test.tsx +368 -11
  6. package/src/__tests__/modules/Users/UserList/UserList.test.tsx +6 -6
  7. package/src/components/Fields/IntegrationsField/IntegrationItem/atoms.tsx +14 -5
  8. package/src/components/Fields/IntegrationsField/IntegrationItem/index.tsx +1 -8
  9. package/src/components/Fields/UrlField/index.tsx +5 -4
  10. package/src/components/Fields/Wysiwyg/atoms.tsx +140 -0
  11. package/src/components/Fields/Wysiwyg/config.tsx +190 -9
  12. package/src/components/Fields/Wysiwyg/helpers.tsx +54 -1
  13. package/src/components/Fields/Wysiwyg/index.tsx +210 -61
  14. package/src/components/Fields/Wysiwyg/style.tsx +32 -1
  15. package/src/components/Fields/Wysiwyg/vendors.tsx +0 -1
  16. package/src/components/FieldsBehavior/index.tsx +66 -31
  17. package/src/components/Icon/components/Sync.js +16 -0
  18. package/src/components/Icon/svgs/Sync.svg +3 -0
  19. package/src/components/RestoreModal/index.tsx +1 -3
  20. package/src/components/ScheduleModal/index.tsx +3 -3
  21. package/src/components/TableList/index.tsx +5 -3
  22. package/src/containers/FileDrive/actions.tsx +5 -5
  23. package/src/containers/Forms/actions.tsx +40 -38
  24. package/src/containers/Gallery/actions.tsx +27 -27
  25. package/src/containers/Integrations/actions.tsx +2 -2
  26. package/src/containers/Navigation/Defaults/actions.tsx +1 -1
  27. package/src/containers/PageEditor/actions.tsx +3 -5
  28. package/src/containers/Redirects/actions.tsx +2 -2
  29. package/src/containers/Sites/actions.tsx +15 -8
  30. package/src/containers/StructuredData/actions.tsx +5 -5
  31. package/src/containers/Users/actions.tsx +2 -2
  32. package/src/modules/Categories/CategoriesList/CategoryItem/index.tsx +36 -43
  33. package/src/modules/Categories/CategoriesList/CategoryPanel/index.tsx +15 -11
  34. package/src/modules/Categories/CategoriesList/atoms.tsx +42 -12
  35. package/src/modules/Categories/CategoriesList/index.tsx +8 -30
  36. package/src/modules/Content/PageItem/index.tsx +6 -5
  37. package/src/modules/Content/atoms.tsx +7 -3
  38. package/src/modules/Content/index.tsx +8 -16
  39. package/src/modules/FileDrive/FileModal/DetailPanel/index.tsx +14 -14
  40. package/src/modules/FileDrive/FolderItem/index.tsx +46 -54
  41. package/src/modules/FileDrive/GridItem/index.tsx +32 -37
  42. package/src/modules/FileDrive/ListItem/index.tsx +31 -37
  43. package/src/modules/FileDrive/atoms.tsx +59 -12
  44. package/src/modules/FileDrive/index.tsx +88 -98
  45. package/src/modules/Forms/FormCategoriesList/CategoryItem/index.tsx +42 -34
  46. package/src/modules/Forms/FormCategoriesList/atoms.tsx +17 -6
  47. package/src/modules/Forms/FormCategoriesList/index.tsx +30 -34
  48. package/src/modules/Forms/FormEditor/index.tsx +14 -6
  49. package/src/modules/Forms/FormList/FormItem/index.tsx +5 -1
  50. package/src/modules/Forms/FormList/index.tsx +15 -9
  51. package/src/modules/Forms/atoms.tsx +7 -4
  52. package/src/modules/GlobalEditor/atoms.tsx +77 -0
  53. package/src/modules/GlobalEditor/index.tsx +24 -7
  54. package/src/modules/MediaGallery/FolderItem/index.tsx +36 -52
  55. package/src/modules/MediaGallery/GridItem/index.tsx +27 -38
  56. package/src/modules/MediaGallery/ImageModal/DetailPanel/index.tsx +13 -13
  57. package/src/modules/MediaGallery/ImageModal/index.tsx +2 -16
  58. package/src/modules/MediaGallery/ListItem/index.tsx +25 -37
  59. package/src/modules/MediaGallery/atoms.tsx +84 -15
  60. package/src/modules/MediaGallery/index.tsx +62 -122
  61. package/src/modules/Navigation/Defaults/Item/atoms.tsx +2 -4
  62. package/src/modules/Navigation/Defaults/Item/index.tsx +80 -94
  63. package/src/modules/Navigation/Defaults/atoms.tsx +179 -7
  64. package/src/modules/Navigation/Defaults/index.tsx +51 -18
  65. package/src/modules/Navigation/Defaults/style.tsx +10 -7
  66. package/src/modules/Navigation/Menus/List/Table/Item/atoms.tsx +33 -19
  67. package/src/modules/Navigation/Menus/List/Table/Item/index.tsx +18 -19
  68. package/src/modules/Navigation/Menus/List/Table/index.tsx +15 -13
  69. package/src/modules/Navigation/Menus/List/index.tsx +1 -1
  70. package/src/modules/PageEditor/atoms.tsx +14 -11
  71. package/src/modules/PageEditor/index.tsx +11 -16
  72. package/src/modules/Redirects/RedirectItem/index.tsx +37 -39
  73. package/src/modules/Redirects/atoms.tsx +58 -12
  74. package/src/modules/Redirects/index.tsx +30 -58
  75. package/src/modules/Settings/Integrations/IntegrationItem/CopyModal/index.tsx +1 -1
  76. package/src/modules/Settings/Integrations/IntegrationItem/index.tsx +57 -69
  77. package/src/modules/Settings/Integrations/atoms.tsx +72 -8
  78. package/src/modules/Settings/Integrations/index.tsx +34 -48
  79. package/src/modules/StructuredData/Form/index.tsx +25 -8
  80. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/atoms.tsx +26 -75
  81. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +15 -33
  82. package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/index.tsx +63 -41
  83. package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/style.tsx +13 -8
  84. package/src/modules/StructuredData/StructuredDataList/index.tsx +11 -16
  85. package/src/modules/StructuredData/{StructuredDataList/atoms.tsx → atoms.tsx} +25 -28
  86. package/src/modules/Users/UserList/UserItem/index.tsx +18 -35
  87. package/src/modules/Users/UserList/atoms.tsx +61 -0
  88. package/src/modules/Users/UserList/index.tsx +12 -32
@@ -1,14 +1,36 @@
1
- import * as React from "react";
2
-
3
- import { ThemeProvider } from "styled-components";
4
- import { render, screen, cleanup } from "@testing-library/react";
5
- import { parseTheme } from "@ax/helpers";
6
1
  import "@testing-library/jest-dom";
7
2
 
8
3
  import FieldsBehavior from "@ax/components/FieldsBehavior";
4
+ import { getValidity } from "@ax/forms";
5
+ import { getFieldByPath, isEmptyContainer, parseTheme, slugify } from "@ax/helpers";
9
6
  import globalTheme from "@ax/themes/theme.json";
10
7
 
11
- afterEach(cleanup);
8
+ import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
9
+ import { ThemeProvider } from "styled-components";
10
+
11
+ // Mock dependencies
12
+ jest.mock("@ax/forms", () => ({
13
+ getValidity: jest.fn(),
14
+ }));
15
+ jest.mock("@ax/helpers", () => ({
16
+ ...jest.requireActual("@ax/helpers"),
17
+ getFieldByPath: jest.fn(() => () => <div data-testid="mock-field" />),
18
+ isEmptyContainer: jest.fn(() => ({ isEmpty: false })),
19
+ slugify: jest.fn((text: string) => (text || "").toLowerCase().replace(/\s+/g, "-")),
20
+ }));
21
+
22
+ const mockGetValidity = getValidity as jest.MockedFunction<typeof getValidity>;
23
+ const _mockGetFieldByPath = getFieldByPath as jest.MockedFunction<typeof getFieldByPath>;
24
+ const mockIsEmptyContainer = isEmptyContainer as jest.MockedFunction<typeof isEmptyContainer>;
25
+ const mockSlugify = slugify as jest.MockedFunction<typeof slugify>;
26
+
27
+ afterEach(() => {
28
+ cleanup();
29
+ jest.clearAllMocks();
30
+ });
31
+
32
+ // Default mock setup for getValidity
33
+ mockGetValidity.mockReturnValue({ isValid: true, errorText: "" });
12
34
 
13
35
  describe("FieldsBehavior component rendering", () => {
14
36
  it("should render the component", () => {
@@ -26,9 +48,10 @@ describe("FieldsBehavior component rendering", () => {
26
48
  filled: true,
27
49
  title: "Cool title",
28
50
  type: "TextField",
29
- mandatory: "true",
51
+ mandatory: true,
30
52
  isTitle: true,
31
53
  theme: "default-theme",
54
+ value: undefined,
32
55
  };
33
56
 
34
57
  render(
@@ -53,15 +76,26 @@ describe("FieldsBehavior component rendering", () => {
53
76
  placeholder: "https://",
54
77
  condition: "url",
55
78
  },
56
- error: true,
79
+ error: {
80
+ type: "validation",
81
+ message: "This field is required",
82
+ validator: { required: true },
83
+ editorID: null,
84
+ component: null,
85
+ name: "subjDocumentUrl",
86
+ key: "subjDocumentUrl",
87
+ tab: "default",
88
+ template: false,
89
+ },
57
90
  helptext: "ayuda",
58
91
  filled: true,
59
92
  title: "Cool title",
60
93
  type: "TextField",
61
- mandatory: "true",
94
+ mandatory: true,
62
95
  isTitle: true,
63
96
  theme: "default-theme",
64
97
  whiteList: ["BasicContent", "CardCollection"],
98
+ value: undefined,
65
99
  };
66
100
 
67
101
  render(
@@ -90,11 +124,12 @@ describe("FieldsBehavior component rendering", () => {
90
124
  },
91
125
  title: "Cool title",
92
126
  type: "TextField",
93
- mandatory: "true",
127
+ mandatory: true,
94
128
  isTitle: true,
95
129
  theme: "default-theme",
96
130
  advanced: true,
97
131
  whiteList: [],
132
+ value: undefined,
98
133
  };
99
134
 
100
135
  render(
@@ -126,10 +161,11 @@ describe("FieldsBehavior component rendering", () => {
126
161
  },
127
162
  title: "Cool title",
128
163
  type: "TextField",
129
- mandatory: "true",
164
+ mandatory: true,
130
165
  isTitle: true,
131
166
  theme: "default-theme",
132
167
  hideable: true,
168
+ value: undefined,
133
169
  };
134
170
 
135
171
  render(
@@ -147,3 +183,324 @@ describe("FieldsBehavior component rendering", () => {
147
183
  expect(tipComponent).toBeTruthy();
148
184
  });
149
185
  });
186
+
187
+ describe("FieldsBehavior validation", () => {
188
+ const baseProps = {
189
+ objKey: "testField",
190
+ fieldType: "TextField",
191
+ title: "Test Field",
192
+ value: "test value",
193
+ validators: { required: true },
194
+ helptext: "Help text",
195
+ };
196
+
197
+ beforeEach(() => {
198
+ mockGetValidity.mockReturnValue({ isValid: true, errorText: "" });
199
+ });
200
+
201
+ it("should display helptext initially", () => {
202
+ render(
203
+ <ThemeProvider theme={parseTheme(globalTheme)}>
204
+ <FieldsBehavior {...baseProps} />
205
+ </ThemeProvider>,
206
+ );
207
+
208
+ expect(screen.queryByText("Help text")).toBeTruthy();
209
+ });
210
+
211
+ it("should validate on error prop change", () => {
212
+ const deleteError = jest.fn();
213
+ const error = {
214
+ type: "validation",
215
+ message: "Required",
216
+ validator: { required: true },
217
+ editorID: null,
218
+ component: null,
219
+ name: "testField",
220
+ key: "testField",
221
+ tab: "default",
222
+ template: false,
223
+ };
224
+
225
+ mockGetValidity.mockReturnValue({ isValid: false, errorText: "Required" });
226
+
227
+ const { rerender } = render(
228
+ <ThemeProvider theme={parseTheme(globalTheme)}>
229
+ <FieldsBehavior {...baseProps} error={error} deleteError={deleteError} />
230
+ </ThemeProvider>,
231
+ );
232
+
233
+ // After rerender without error, should clear
234
+ mockGetValidity.mockReturnValue({ isValid: true, errorText: "" });
235
+ rerender(
236
+ <ThemeProvider theme={parseTheme(globalTheme)}>
237
+ <FieldsBehavior {...baseProps} error={undefined} deleteError={deleteError} />
238
+ </ThemeProvider>,
239
+ );
240
+
241
+ expect(screen.getByTestId("fields-behavior-wrapper")).toBeTruthy();
242
+ });
243
+
244
+ it("should accept validators prop", () => {
245
+ const customValidators = { minLength: 5, maxLength: 100 };
246
+
247
+ const { container } = render(
248
+ <ThemeProvider theme={parseTheme(globalTheme)}>
249
+ <FieldsBehavior {...baseProps} validators={customValidators} />
250
+ </ThemeProvider>,
251
+ );
252
+
253
+ expect(container).toBeTruthy();
254
+ });
255
+ });
256
+
257
+ describe("FieldsBehavior advanced options", () => {
258
+ const baseProps = {
259
+ objKey: "testField",
260
+ fieldType: "TextField",
261
+ title: "Test Field",
262
+ advanced: true,
263
+ value: "test",
264
+ };
265
+
266
+ it("should toggle advanced options on settings icon click", async () => {
267
+ render(
268
+ <ThemeProvider theme={parseTheme(globalTheme)}>
269
+ <FieldsBehavior {...baseProps} />
270
+ </ThemeProvider>,
271
+ );
272
+
273
+ const settingsButton = screen.getByRole("button", { hidden: true });
274
+ expect(settingsButton).toBeTruthy();
275
+
276
+ // The showAdvanced state should be passed to Field component
277
+ // which is mocked and won't show the actual behavior
278
+ fireEvent.click(settingsButton);
279
+
280
+ expect(settingsButton).toBeTruthy();
281
+ });
282
+
283
+ it("should not show settings icon when advanced is false", () => {
284
+ render(
285
+ <ThemeProvider theme={parseTheme(globalTheme)}>
286
+ <FieldsBehavior {...baseProps} advanced={false} />
287
+ </ThemeProvider>,
288
+ );
289
+
290
+ const tooltips = screen.queryAllByTestId("tooltip-component");
291
+ const hasSettingsIcon = tooltips.some((tooltip) => tooltip.querySelector('[data-icon="settings"]') !== null);
292
+
293
+ expect(hasSettingsIcon).toBeFalsy();
294
+ });
295
+ });
296
+
297
+ describe("FieldsBehavior slug synchronization", () => {
298
+ it("should render sync icon only for page slug field", () => {
299
+ // Test with page slug
300
+ const pageSlugProps = {
301
+ objKey: "slug",
302
+ fieldType: "TextField",
303
+ editorID: 0,
304
+ title: "Slug",
305
+ value: "current-slug",
306
+ selectedContent: { title: "Current Page" },
307
+ };
308
+
309
+ const { rerender } = render(
310
+ <ThemeProvider theme={parseTheme(globalTheme)}>
311
+ <FieldsBehavior {...pageSlugProps} />
312
+ </ThemeProvider>,
313
+ );
314
+
315
+ const tooltips = screen.queryAllByTestId("tooltip-component");
316
+ expect(tooltips.length).toBeGreaterThan(0);
317
+
318
+ // Test without page slug (different objKey)
319
+ rerender(
320
+ <ThemeProvider theme={parseTheme(globalTheme)}>
321
+ <FieldsBehavior {...pageSlugProps} objKey="otherField" />
322
+ </ThemeProvider>,
323
+ );
324
+
325
+ expect(screen.getByTestId("fields-behavior-wrapper")).toBeTruthy();
326
+ });
327
+
328
+ it("should have correct sync behavior for page slug", () => {
329
+ const updateValue = jest.fn();
330
+ mockSlugify.mockReturnValue("new-title");
331
+
332
+ const props = {
333
+ objKey: "slug",
334
+ fieldType: "TextField",
335
+ editorID: 0,
336
+ title: "Slug",
337
+ value: "old-slug",
338
+ selectedContent: { title: "New Title" },
339
+ updateValue,
340
+ };
341
+
342
+ render(
343
+ <ThemeProvider theme={parseTheme(globalTheme)}>
344
+ <FieldsBehavior {...props} />
345
+ </ThemeProvider>,
346
+ );
347
+
348
+ expect(screen.getByTestId("fields-behavior-wrapper")).toBeTruthy();
349
+ });
350
+ });
351
+
352
+ describe("FieldsBehavior container state", () => {
353
+ it("should show title when container is not empty", () => {
354
+ mockIsEmptyContainer.mockReturnValue({
355
+ isEmpty: false,
356
+ containedComponent: { containerText: "Component", component: "TestComponent", componentID: 1 },
357
+ });
358
+
359
+ const defaultProps = {
360
+ objKey: "testContainer",
361
+ fieldType: "ComponentContainer",
362
+ title: "Container Title",
363
+ value: { someField: "value" },
364
+ };
365
+
366
+ render(
367
+ <ThemeProvider theme={parseTheme(globalTheme)}>
368
+ <FieldsBehavior {...defaultProps} />
369
+ </ThemeProvider>,
370
+ );
371
+
372
+ expect(screen.getByText("Container Title")).toBeTruthy();
373
+ });
374
+
375
+ it("should hide hide button when container is empty", () => {
376
+ mockIsEmptyContainer.mockReturnValue({
377
+ isEmpty: true,
378
+ containedComponent: { containerText: "", component: "", componentID: 0 },
379
+ });
380
+
381
+ const defaultProps = {
382
+ objKey: "testContainer",
383
+ fieldType: "ComponentContainer",
384
+ title: "Container Title",
385
+ value: { someField: "value" },
386
+ hideable: true,
387
+ };
388
+
389
+ render(
390
+ <ThemeProvider theme={parseTheme(globalTheme)}>
391
+ <FieldsBehavior {...defaultProps} />
392
+ </ThemeProvider>,
393
+ );
394
+
395
+ const tooltips = screen.queryAllByTestId("tooltip-component");
396
+ const hasHideIcon = tooltips.some((tooltip) => tooltip.querySelector('[data-icon="delete"]') !== null);
397
+
398
+ expect(hasHideIcon).toBeFalsy();
399
+ });
400
+ });
401
+
402
+ describe("FieldsBehavior optional props", () => {
403
+ const baseProps = {
404
+ objKey: "testField",
405
+ fieldType: "TextField",
406
+ title: "Test Field",
407
+ value: "test",
408
+ };
409
+
410
+ it("should render without hideField callback", () => {
411
+ render(
412
+ <ThemeProvider theme={parseTheme(globalTheme)}>
413
+ <FieldsBehavior {...baseProps} hideable={true} />
414
+ </ThemeProvider>,
415
+ );
416
+
417
+ expect(screen.getByTestId("fields-behavior-wrapper")).toBeTruthy();
418
+ });
419
+
420
+ it("should render without updateValue callback", () => {
421
+ render(
422
+ <ThemeProvider theme={parseTheme(globalTheme)}>
423
+ <FieldsBehavior {...baseProps} objKey="slug" editorID={0} selectedContent={{ title: "Test" }} />
424
+ </ThemeProvider>,
425
+ );
426
+
427
+ expect(screen.getByTestId("fields-behavior-wrapper")).toBeTruthy();
428
+ });
429
+
430
+ it("should render without deleteError callback", async () => {
431
+ mockGetValidity.mockReturnValue({ isValid: false, errorText: "Error" });
432
+
433
+ render(
434
+ <ThemeProvider theme={parseTheme(globalTheme)}>
435
+ <FieldsBehavior
436
+ {...baseProps}
437
+ error={{
438
+ type: "validation",
439
+ message: "Error",
440
+ validator: { required: true },
441
+ editorID: null,
442
+ component: null,
443
+ name: "testField",
444
+ key: "testField",
445
+ tab: "default",
446
+ template: false,
447
+ }}
448
+ validators={{ required: true }}
449
+ />
450
+ </ThemeProvider>,
451
+ );
452
+
453
+ await waitFor(() => {
454
+ expect(screen.getByText("Error")).toBeTruthy();
455
+ });
456
+ });
457
+
458
+ it("should handle missing selectedContent gracefully", () => {
459
+ render(
460
+ <ThemeProvider theme={parseTheme(globalTheme)}>
461
+ <FieldsBehavior {...baseProps} objKey="slug" editorID={0} selectedContent={undefined} />
462
+ </ThemeProvider>,
463
+ );
464
+
465
+ expect(screen.getByTestId("fields-behavior-wrapper")).toBeTruthy();
466
+ });
467
+ });
468
+
469
+ describe("FieldsBehavior mandatory field indicator", () => {
470
+ it("should display asterisk for mandatory fields", () => {
471
+ const defaultProps = {
472
+ objKey: "testField",
473
+ fieldType: "TextField",
474
+ title: "Required Field",
475
+ mandatory: true,
476
+ value: "test",
477
+ };
478
+
479
+ render(
480
+ <ThemeProvider theme={parseTheme(globalTheme)}>
481
+ <FieldsBehavior {...defaultProps} />
482
+ </ThemeProvider>,
483
+ );
484
+
485
+ expect(screen.getByText("*")).toBeTruthy();
486
+ });
487
+
488
+ it("should not display asterisk for optional fields", () => {
489
+ const defaultProps = {
490
+ objKey: "testField",
491
+ fieldType: "TextField",
492
+ title: "Optional Field",
493
+ mandatory: false,
494
+ value: "test",
495
+ };
496
+
497
+ render(
498
+ <ThemeProvider theme={parseTheme(globalTheme)}>
499
+ <FieldsBehavior {...defaultProps} />
500
+ </ThemeProvider>,
501
+ );
502
+
503
+ const asterisk = screen.queryByText("*");
504
+ expect(asterisk).toBeFalsy();
505
+ });
506
+ });
@@ -224,20 +224,20 @@ describe("UserList module events", () => {
224
224
  render(Component, { store });
225
225
  });
226
226
 
227
- expect(screen.queryByTestId("user-delete-bulk-modal")).toBeFalsy();
227
+ expect(screen.queryByTestId("modal-delete-content")).toBeFalsy();
228
228
 
229
229
  expect(screen.queryAllByTestId("user-item").length).toBe(3);
230
230
 
231
231
  await user.click(screen.getByLabelText("Prueba Prueba"));
232
232
  await user.click(screen.getByText("delete"));
233
233
 
234
- expect(screen.getByTestId("user-delete-bulk-modal")).toBeTruthy();
234
+ expect(screen.getByTestId("modal-delete-content")).toBeTruthy();
235
235
 
236
236
  await act(async () => {
237
237
  fireEvent.click(screen.getByText("Delete"));
238
238
  });
239
239
 
240
- expect(screen.queryByTestId("user-delete-bulk-modal")).toBeFalsy();
240
+ expect(screen.queryByTestId("modal-delete-content")).toBeFalsy();
241
241
  expect(screen.queryAllByTestId("user-item").length).toBe(2);
242
242
 
243
243
  expect(store.getActions()).toContainEqual({
@@ -284,20 +284,20 @@ describe("UserList module events", () => {
284
284
  render(Component, { store });
285
285
  });
286
286
 
287
- expect(screen.queryByTestId("user-delete-bulk-modal")).toBeFalsy();
287
+ expect(screen.queryByTestId("modal-delete-content")).toBeFalsy();
288
288
 
289
289
  expect(screen.queryAllByTestId("user-item").length).toBe(2);
290
290
 
291
291
  await user.click(screen.getByLabelText("Admin"));
292
292
  await user.click(screen.getByText("Remove from this site"));
293
293
 
294
- expect(screen.getByTestId("user-delete-bulk-modal")).toBeTruthy();
294
+ expect(screen.getByTestId("modal-delete-content")).toBeTruthy();
295
295
 
296
296
  await act(async () => {
297
297
  fireEvent.click(screen.getByText("Remove"));
298
298
  });
299
299
 
300
- expect(screen.queryByTestId("user-delete-bulk-modal")).toBeFalsy();
300
+ expect(screen.queryByTestId("modal-delete-content")).toBeFalsy();
301
301
  expect(screen.queryAllByTestId("user-item").length).toBe(1);
302
302
  });
303
303
  });
@@ -1,12 +1,20 @@
1
- import React from "react";
2
-
3
- import { IModal } from "@ax/types";
4
1
  import { Modal } from "@ax/components";
2
+ import type { IModal } from "@ax/types";
5
3
 
6
4
  import * as S from "./style";
7
5
 
8
- const DeleteModal = (props: IModal & { integrationName: string | undefined }): JSX.Element => {
9
- const { isOpen, toggleModal, mainModalAction, secondaryModalAction, integrationName } = props;
6
+ const DeleteModal = (
7
+ props: IModal & { integrationName: string | undefined; onDelete(): void; isDeleting?: boolean },
8
+ ): JSX.Element => {
9
+ const { isOpen, toggleModal, integrationName, onDelete, isDeleting = false } = props;
10
+
11
+ const mainModalAction = {
12
+ title: isDeleting ? "Removing" : "Remove",
13
+ onClick: onDelete,
14
+ disabled: isDeleting,
15
+ };
16
+
17
+ const secondaryModalAction = { title: "Cancel", onClick: toggleModal };
10
18
 
11
19
  return (
12
20
  <Modal
@@ -16,6 +24,7 @@ const DeleteModal = (props: IModal & { integrationName: string | undefined }): J
16
24
  secondaryAction={secondaryModalAction}
17
25
  mainAction={mainModalAction}
18
26
  size="S"
27
+ height="auto"
19
28
  >
20
29
  <S.ModalContent>
21
30
  Are you sure you want to remove <strong>{integrationName}</strong> from this page?
@@ -58,12 +58,6 @@ const IntegrationItem = (props: IProps): JSX.Element => {
58
58
 
59
59
  const integrationName = isCustom ? "Custom Code" : integration.name;
60
60
 
61
- const mainDeleteModalAction = {
62
- title: "Remove",
63
- onClick: removeItem,
64
- };
65
- const secondaryDeleteModalAction = { title: "Cancel", onClick: toggleModalDelete };
66
-
67
61
  return (
68
62
  <>
69
63
  <S.Component disabled={disabled} data-testid="integration-item">
@@ -109,9 +103,8 @@ const IntegrationItem = (props: IProps): JSX.Element => {
109
103
  <DeleteModal
110
104
  isOpen={isOpenDelete}
111
105
  toggleModal={toggleModalDelete}
112
- secondaryModalAction={secondaryDeleteModalAction}
113
- mainModalAction={mainDeleteModalAction}
114
106
  integrationName={integration.name}
107
+ onDelete={removeItem}
115
108
  />
116
109
  {isVisibleCopy && <Toast message="1 Custom Code copied to clipboard" setIsVisible={setIsVisibleCopy} />}
117
110
  </>
@@ -1,10 +1,11 @@
1
1
  import { memo, useEffect, useState } from "react";
2
2
 
3
- import { IconAction, TextField, FloatingPanel, FieldsBehavior, PageFinder } from "@ax/components";
4
- import { useModal } from "@ax/hooks";
5
- import { isReqOk } from "@ax/helpers";
6
- import type { IPage, IUrlField, Field, ISelectOption } from "@ax/types";
7
3
  import { pages as pagesApi } from "@ax/api";
4
+ import { FieldsBehavior, FloatingPanel, IconAction, PageFinder, TextField } from "@ax/components";
5
+ import { isReqOk } from "@ax/helpers";
6
+ import { useModal } from "@ax/hooks";
7
+ import type { Field, IPage, ISelectOption, IUrlField } from "@ax/types";
8
+
8
9
  import { findAnchorsFromPage, findAnchorsFromTab, findTabsFromPage } from "./utils";
9
10
 
10
11
  import * as S from "./style";
@@ -0,0 +1,140 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
+
4
+ import { Button, FieldsBehavior } from "@ax/components";
5
+ import type { IUrlField } from "@ax/types";
6
+
7
+ import * as S from "./style";
8
+
9
+ const POPOVER_PADDING = 16;
10
+ const POPOVER_WIDTH = 320;
11
+
12
+ const LinkPopover = (props: ILinkPopoverProps) => {
13
+ const { x: initialX, y: initialY, selectedText = "", linkData, onInsert, onClose } = props;
14
+ const [linkText, setLinkText] = useState<string>(selectedText);
15
+ const [urlField, setUrlField] = useState<IUrlField | null>(null);
16
+ const [linkOptions, setLinkOptions] = useState<string[]>([]);
17
+ const [position, setPosition] = useState({ x: initialX, y: initialY });
18
+ const popoverRef = useRef<HTMLDivElement>(null);
19
+
20
+ useEffect(() => {
21
+ if (!popoverRef.current) return;
22
+
23
+ setTimeout(() => {
24
+ const popoverHeight = popoverRef.current?.offsetHeight || 0;
25
+ let finalX = initialX;
26
+ let finalY = initialY;
27
+
28
+ // Check if popover goes off screen to the right
29
+ if (initialX + POPOVER_WIDTH + POPOVER_PADDING > window.innerWidth) {
30
+ finalX = window.innerWidth - POPOVER_WIDTH - POPOVER_PADDING;
31
+ }
32
+
33
+ // Check if popover goes off screen to the left
34
+ if (finalX < POPOVER_PADDING) {
35
+ finalX = POPOVER_PADDING;
36
+ }
37
+
38
+ // Check if popover goes off screen at the bottom
39
+ if (initialY + popoverHeight + POPOVER_PADDING > window.innerHeight) {
40
+ finalY = initialY - popoverHeight - 10;
41
+ }
42
+
43
+ // Check if popover goes off screen at the top
44
+ if (finalY < POPOVER_PADDING) {
45
+ finalY = POPOVER_PADDING;
46
+ }
47
+
48
+ setPosition({ x: finalX, y: finalY });
49
+ }, 0);
50
+ }, [initialX, initialY]);
51
+
52
+ useEffect(() => {
53
+ if (linkData?.isEditing) {
54
+ const urlFieldValue: IUrlField = {
55
+ title: selectedText,
56
+ };
57
+
58
+ if (linkData.linkPageId) {
59
+ urlFieldValue.linkTo = linkData.linkPageId;
60
+ urlFieldValue.linkToURL = linkData.url;
61
+ } else {
62
+ urlFieldValue.href = linkData.url;
63
+ }
64
+
65
+ setUrlField(urlFieldValue);
66
+ setLinkOptions([...(linkData.newTab ? ["newTab"] : []), ...(linkData.noFollow ? ["noFollow"] : [])]);
67
+ }
68
+ }, [linkData, selectedText]);
69
+
70
+ const handleInsert = () => {
71
+ const hasUrl = urlField?.href || urlField?.linkToURL;
72
+ if (hasUrl) {
73
+ const finalUrl = urlField.href || urlField.linkToURL || "";
74
+ const finalText = linkText || urlField.title || finalUrl;
75
+ const newTab = linkOptions.includes("newTab");
76
+ const noFollow = linkOptions.includes("noFollow");
77
+ const linkTo = urlField.linkToURL ? urlField.linkTo : undefined;
78
+ onInsert(finalUrl, finalText, { newTab, noFollow, linkTo });
79
+ }
80
+ };
81
+
82
+ const handleOverlayClick = () => {
83
+ onClose();
84
+ };
85
+
86
+ return createPortal(
87
+ <>
88
+ <S.PopoverOverlay onClick={handleOverlayClick} />
89
+ <S.PopoverContainer ref={popoverRef} x={position.x} y={position.y}>
90
+ {!linkData?.isImageLink && (
91
+ <FieldsBehavior
92
+ title="Text"
93
+ name="text"
94
+ fieldType="TextField"
95
+ value={linkText}
96
+ onChange={setLinkText}
97
+ placeholder="Type link text"
98
+ />
99
+ )}
100
+ <FieldsBehavior
101
+ title="URL"
102
+ name="url"
103
+ fieldType="UrlField"
104
+ value={urlField}
105
+ onChange={setUrlField}
106
+ showAdvanced={false}
107
+ mandatory={true}
108
+ />
109
+ <FieldsBehavior
110
+ title=""
111
+ name="linkOptions"
112
+ fieldType="CheckGroup"
113
+ value={linkOptions}
114
+ onChange={setLinkOptions}
115
+ options={[
116
+ { name: "newTab", title: "Open in new tab", value: "newTab" },
117
+ { name: "noFollow", title: "Add nofollow", value: "noFollow" },
118
+ ]}
119
+ />
120
+ <S.PopoverButtonWrapper>
121
+ <Button buttonStyle="text" onClick={handleInsert} disabled={!urlField?.href && !urlField?.linkToURL}>
122
+ INSERT
123
+ </Button>
124
+ </S.PopoverButtonWrapper>
125
+ </S.PopoverContainer>
126
+ </>,
127
+ document.body,
128
+ );
129
+ };
130
+
131
+ interface ILinkPopoverProps {
132
+ x: number;
133
+ y: number;
134
+ selectedText?: string;
135
+ linkData?: { url: string; newTab: boolean; noFollow: boolean; isEditing: boolean; linkPageId?: number | null; isImageLink?: boolean } | null;
136
+ onInsert: (url: string, text: string, options?: { newTab: boolean; noFollow: boolean; linkTo?: number | null }) => void;
137
+ onClose: () => void;
138
+ }
139
+
140
+ export { LinkPopover };