@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.
- package/package.json +2 -2
- package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.atoms.test.tsx +409 -0
- package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.helpers.test.tsx +418 -0
- package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.test.tsx +776 -29
- package/src/__tests__/components/FieldsBehavior/FieldsBehavior.test.tsx +368 -11
- package/src/__tests__/modules/Users/UserList/UserList.test.tsx +6 -6
- package/src/components/Fields/IntegrationsField/IntegrationItem/atoms.tsx +14 -5
- package/src/components/Fields/IntegrationsField/IntegrationItem/index.tsx +1 -8
- package/src/components/Fields/UrlField/index.tsx +5 -4
- package/src/components/Fields/Wysiwyg/atoms.tsx +140 -0
- package/src/components/Fields/Wysiwyg/config.tsx +190 -9
- package/src/components/Fields/Wysiwyg/helpers.tsx +54 -1
- package/src/components/Fields/Wysiwyg/index.tsx +210 -61
- package/src/components/Fields/Wysiwyg/style.tsx +32 -1
- package/src/components/Fields/Wysiwyg/vendors.tsx +0 -1
- package/src/components/FieldsBehavior/index.tsx +66 -31
- package/src/components/Icon/components/Sync.js +16 -0
- package/src/components/Icon/svgs/Sync.svg +3 -0
- package/src/components/RestoreModal/index.tsx +1 -3
- package/src/components/ScheduleModal/index.tsx +3 -3
- package/src/components/TableList/index.tsx +5 -3
- package/src/containers/FileDrive/actions.tsx +5 -5
- package/src/containers/Forms/actions.tsx +40 -38
- package/src/containers/Gallery/actions.tsx +27 -27
- package/src/containers/Integrations/actions.tsx +2 -2
- package/src/containers/Navigation/Defaults/actions.tsx +1 -1
- package/src/containers/PageEditor/actions.tsx +3 -5
- package/src/containers/Redirects/actions.tsx +2 -2
- package/src/containers/Sites/actions.tsx +15 -8
- package/src/containers/StructuredData/actions.tsx +5 -5
- package/src/containers/Users/actions.tsx +2 -2
- package/src/modules/Categories/CategoriesList/CategoryItem/index.tsx +36 -43
- package/src/modules/Categories/CategoriesList/CategoryPanel/index.tsx +15 -11
- package/src/modules/Categories/CategoriesList/atoms.tsx +42 -12
- package/src/modules/Categories/CategoriesList/index.tsx +8 -30
- package/src/modules/Content/PageItem/index.tsx +6 -5
- package/src/modules/Content/atoms.tsx +7 -3
- package/src/modules/Content/index.tsx +8 -16
- package/src/modules/FileDrive/FileModal/DetailPanel/index.tsx +14 -14
- package/src/modules/FileDrive/FolderItem/index.tsx +46 -54
- package/src/modules/FileDrive/GridItem/index.tsx +32 -37
- package/src/modules/FileDrive/ListItem/index.tsx +31 -37
- package/src/modules/FileDrive/atoms.tsx +59 -12
- package/src/modules/FileDrive/index.tsx +88 -98
- package/src/modules/Forms/FormCategoriesList/CategoryItem/index.tsx +42 -34
- package/src/modules/Forms/FormCategoriesList/atoms.tsx +17 -6
- package/src/modules/Forms/FormCategoriesList/index.tsx +30 -34
- package/src/modules/Forms/FormEditor/index.tsx +14 -6
- package/src/modules/Forms/FormList/FormItem/index.tsx +5 -1
- package/src/modules/Forms/FormList/index.tsx +15 -9
- package/src/modules/Forms/atoms.tsx +7 -4
- package/src/modules/GlobalEditor/atoms.tsx +77 -0
- package/src/modules/GlobalEditor/index.tsx +24 -7
- package/src/modules/MediaGallery/FolderItem/index.tsx +36 -52
- package/src/modules/MediaGallery/GridItem/index.tsx +27 -38
- package/src/modules/MediaGallery/ImageModal/DetailPanel/index.tsx +13 -13
- package/src/modules/MediaGallery/ImageModal/index.tsx +2 -16
- package/src/modules/MediaGallery/ListItem/index.tsx +25 -37
- package/src/modules/MediaGallery/atoms.tsx +84 -15
- package/src/modules/MediaGallery/index.tsx +62 -122
- package/src/modules/Navigation/Defaults/Item/atoms.tsx +2 -4
- package/src/modules/Navigation/Defaults/Item/index.tsx +80 -94
- package/src/modules/Navigation/Defaults/atoms.tsx +179 -7
- package/src/modules/Navigation/Defaults/index.tsx +51 -18
- package/src/modules/Navigation/Defaults/style.tsx +10 -7
- package/src/modules/Navigation/Menus/List/Table/Item/atoms.tsx +33 -19
- package/src/modules/Navigation/Menus/List/Table/Item/index.tsx +18 -19
- package/src/modules/Navigation/Menus/List/Table/index.tsx +15 -13
- package/src/modules/Navigation/Menus/List/index.tsx +1 -1
- package/src/modules/PageEditor/atoms.tsx +14 -11
- package/src/modules/PageEditor/index.tsx +11 -16
- package/src/modules/Redirects/RedirectItem/index.tsx +37 -39
- package/src/modules/Redirects/atoms.tsx +58 -12
- package/src/modules/Redirects/index.tsx +30 -58
- package/src/modules/Settings/Integrations/IntegrationItem/CopyModal/index.tsx +1 -1
- package/src/modules/Settings/Integrations/IntegrationItem/index.tsx +57 -69
- package/src/modules/Settings/Integrations/atoms.tsx +72 -8
- package/src/modules/Settings/Integrations/index.tsx +34 -48
- package/src/modules/StructuredData/Form/index.tsx +25 -8
- package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/atoms.tsx +26 -75
- package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +15 -33
- package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/index.tsx +63 -41
- package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/style.tsx +13 -8
- package/src/modules/StructuredData/StructuredDataList/index.tsx +11 -16
- package/src/modules/StructuredData/{StructuredDataList/atoms.tsx → atoms.tsx} +25 -28
- package/src/modules/Users/UserList/UserItem/index.tsx +18 -35
- package/src/modules/Users/UserList/atoms.tsx +61 -0
- 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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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 = (
|
|
9
|
-
|
|
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 };
|