@griddo/ax 11.12.0 → 11.12.1-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 (100) hide show
  1. package/config/jest/componentsMock.js +7 -5
  2. package/package.json +2 -2
  3. package/src/__tests__/components/Browser/Browser.test.tsx +438 -87
  4. package/src/__tests__/components/Browser/Browser.utils.test.ts +55 -0
  5. package/src/__tests__/components/ConfigPanel/ConfigPanel.test.tsx +1 -3
  6. package/src/__tests__/components/Fields/Button/Button.test.tsx +29 -27
  7. package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +158 -0
  8. package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorsBanner.test.tsx +90 -0
  9. package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.test.tsx +178 -0
  10. package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +150 -0
  11. package/src/__tests__/components/KeywordsPreviewModal/KeywordItem/KeywordItem.test.tsx +91 -0
  12. package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.test.tsx +122 -0
  13. package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.utils.test.ts +15 -0
  14. package/src/__tests__/components/KeywordsPreviewModal/atoms.test.tsx +101 -0
  15. package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +1 -1
  16. package/src/__tests__/modules/FramePreview/FramePreview.test.tsx +318 -0
  17. package/src/__tests__/modules/FramePreview/FramePreview.utils.test.ts +242 -0
  18. package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +185 -0
  19. package/src/components/Browser/index.tsx +294 -149
  20. package/src/components/Browser/style.tsx +75 -6
  21. package/src/components/Browser/utils.tsx +13 -0
  22. package/src/components/Button/index.tsx +2 -1
  23. package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/Field/index.tsx +2 -4
  24. package/src/components/Fields/AsyncSelect/style.tsx +13 -0
  25. package/src/components/Fields/FieldGroup/index.tsx +5 -2
  26. package/src/components/Fields/FieldGroup/style.tsx +32 -7
  27. package/src/components/Fields/HeadingField/index.tsx +22 -22
  28. package/src/components/Fields/HiddenField/style.tsx +1 -1
  29. package/src/components/Fields/NumberField/index.tsx +15 -16
  30. package/src/components/Fields/NumberField/style.tsx +2 -0
  31. package/src/components/Fields/ReferenceField/index.tsx +1 -1
  32. package/src/components/Fields/SEOPreview/index.tsx +36 -0
  33. package/src/components/Fields/SEOPreview/style.tsx +24 -0
  34. package/src/components/Fields/Select/index.tsx +5 -1
  35. package/src/components/Fields/Select/style.tsx +56 -0
  36. package/src/components/Fields/SummaryButton/index.tsx +18 -9
  37. package/src/components/Fields/SummaryButton/style.tsx +1 -2
  38. package/src/components/Fields/TagsField/index.tsx +8 -9
  39. package/src/components/Fields/UrlField/index.tsx +26 -27
  40. package/src/components/Fields/index.tsx +2 -0
  41. package/src/components/FloatingNote/index.tsx +35 -0
  42. package/src/components/FloatingNote/style.tsx +26 -0
  43. package/src/components/FloatingPanel/index.tsx +5 -2
  44. package/src/components/FloatingPanel/style.tsx +2 -1
  45. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +85 -0
  46. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/style.tsx +80 -0
  47. package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +57 -0
  48. package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +82 -0
  49. package/src/components/HeadingsPreviewModal/HeadingItem/index.tsx +71 -0
  50. package/src/components/HeadingsPreviewModal/HeadingItem/style.tsx +77 -0
  51. package/src/components/HeadingsPreviewModal/index.tsx +148 -0
  52. package/src/components/HeadingsPreviewModal/style.tsx +82 -0
  53. package/src/components/HeadingsPreviewModal/utils.tsx +329 -0
  54. package/src/components/Icon/index.tsx +1 -2
  55. package/src/components/IconAction/index.tsx +1 -1
  56. package/src/components/KeywordsPreviewModal/KeywordItem/index.tsx +46 -0
  57. package/src/components/KeywordsPreviewModal/KeywordItem/style.tsx +64 -0
  58. package/src/components/KeywordsPreviewModal/atoms.tsx +96 -0
  59. package/src/components/KeywordsPreviewModal/index.tsx +99 -0
  60. package/src/components/KeywordsPreviewModal/style.tsx +87 -0
  61. package/src/components/KeywordsPreviewModal/utils.tsx +22 -0
  62. package/src/components/MainWrapper/AppBar/index.tsx +8 -1
  63. package/src/components/MainWrapper/index.tsx +7 -1
  64. package/src/components/Notification/index.tsx +2 -2
  65. package/src/components/PageFinder/index.tsx +1 -1
  66. package/src/components/ResizePanel/index.tsx +4 -3
  67. package/src/components/ResizePanel/style.tsx +1 -1
  68. package/src/components/SearchField/style.tsx +2 -2
  69. package/src/components/SideModal/index.tsx +2 -1
  70. package/src/components/Tabs/index.tsx +13 -4
  71. package/src/components/Tabs/style.tsx +7 -8
  72. package/src/components/Toast/index.tsx +4 -2
  73. package/src/components/Tooltip/index.tsx +4 -3
  74. package/src/components/index.tsx +8 -0
  75. package/src/forms/fields.tsx +70 -68
  76. package/src/hooks/forms.tsx +22 -1
  77. package/src/hooks/index.tsx +13 -3
  78. package/src/hooks/modals.tsx +103 -15
  79. package/src/hooks/users.tsx +25 -8
  80. package/src/modules/Forms/atoms.tsx +2 -2
  81. package/src/modules/FramePreview/HeadingsOverlay/index.tsx +116 -0
  82. package/src/modules/FramePreview/HeadingsOverlay/style.tsx +34 -0
  83. package/src/modules/FramePreview/index.tsx +55 -16
  84. package/src/modules/FramePreview/style.tsx +34 -2
  85. package/src/modules/FramePreview/utils.tsx +140 -0
  86. package/src/modules/GlobalEditor/Editor/index.tsx +37 -3
  87. package/src/modules/GlobalEditor/PageBrowser/index.tsx +19 -2
  88. package/src/modules/GlobalEditor/Preview/index.tsx +0 -2
  89. package/src/modules/GlobalEditor/Preview/style.tsx +1 -1
  90. package/src/modules/GlobalEditor/index.tsx +119 -57
  91. package/src/modules/PageEditor/Editor/index.tsx +33 -2
  92. package/src/modules/PageEditor/PageBrowser/index.tsx +20 -2
  93. package/src/modules/PageEditor/Preview/index.tsx +0 -2
  94. package/src/modules/PageEditor/Preview/style.tsx +1 -1
  95. package/src/modules/PageEditor/atoms.tsx +1 -1
  96. package/src/modules/PageEditor/index.tsx +130 -66
  97. package/src/modules/PublicPreview/index.tsx +5 -2
  98. package/src/schemas/pages/GlobalPage.ts +87 -70
  99. package/src/schemas/pages/Page.ts +87 -70
  100. package/src/types/index.tsx +12 -0
@@ -1,4 +1,4 @@
1
- import React, { useRef, useEffect } from "react";
1
+ import { useRef, useEffect } from "react";
2
2
  import { useLocation } from "react-router-dom";
3
3
 
4
4
  import { createPortal } from "react-dom";
@@ -14,7 +14,9 @@ const Toast = (props: IToastProps): JSX.Element => {
14
14
  const isEditor = pathname.includes("/editor");
15
15
 
16
16
  let temp: NodeJS.Timeout;
17
- const setTemp = (time: number) => (temp = setTimeout(() => setIsVisible(false), time));
17
+ const setTemp = (time: number) => {
18
+ temp = setTimeout(() => setIsVisible(false), time);
19
+ };
18
20
 
19
21
  const close = () => {
20
22
  if (toast.current) {
@@ -1,4 +1,3 @@
1
- import type { ReactNode } from "react";
2
1
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
2
 
4
3
  import { useHandleClickOutside } from "@ax/hooks";
@@ -6,7 +5,7 @@ import { useHandleClickOutside } from "@ax/hooks";
6
5
  import * as S from "./style";
7
6
 
8
7
  const Tooltip = (props: ITooltipProps) => {
9
- const { content, children, hideOnClick = true, bottom, left, expanded, top } = props;
8
+ const { content, children, hideOnClick = true, bottom, left, expanded, top, className } = props;
10
9
 
11
10
  const initialState: IState = {
12
11
  active: false,
@@ -100,6 +99,7 @@ const Tooltip = (props: ITooltipProps) => {
100
99
  onMouseLeave={hideTip}
101
100
  onMouseDown={handleClick}
102
101
  expanded={expanded}
102
+ className={className}
103
103
  >
104
104
  <div ref={childrenRef}>{children}</div>
105
105
  <S.Tip
@@ -127,13 +127,14 @@ interface IState {
127
127
  }
128
128
 
129
129
  export interface ITooltipProps {
130
- content?: string | boolean | JSX.Element[] | JSX.Element;
130
+ content?: React.ReactNode;
131
131
  children: any;
132
132
  hideOnClick?: boolean;
133
133
  bottom?: boolean;
134
134
  left?: number;
135
135
  expanded?: boolean;
136
136
  top?: number;
137
+ className?: string;
137
138
  }
138
139
 
139
140
  export default Tooltip;
@@ -45,6 +45,7 @@ import {
45
45
  RadioGroup,
46
46
  ReferenceField,
47
47
  RichText,
48
+ SEOPreview,
48
49
  Select,
49
50
  SummaryButton,
50
51
  TextArea,
@@ -64,13 +65,16 @@ import FilterTagsBar from "./FilterTagsBar";
64
65
  import Flag from "./Flag";
65
66
  import FloatingButton from "./FloatingButton";
66
67
  import FloatingMenu from "./FloatingMenu";
68
+ import FloatingNote from "./FloatingNote";
67
69
  import FloatingPanel from "./FloatingPanel";
68
70
  import Gallery from "./Gallery";
69
71
  import GuardModal from "./GuardModal";
72
+ import HeadingsPreviewModal from "./HeadingsPreviewModal";
70
73
  import Icon from "./Icon";
71
74
  import IconAction from "./IconAction";
72
75
  import Image from "./Image";
73
76
  import InformativeMenu from "./InformativeMenu";
77
+ import KeywordsPreviewModal from "./KeywordsPreviewModal";
74
78
  import LanguageMenu from "./LanguageMenu";
75
79
  import { ListItem, ListTitle } from "./Lists";
76
80
  import Loader from "./Loader";
@@ -164,12 +168,14 @@ export {
164
168
  Flag,
165
169
  FloatingButton,
166
170
  FloatingMenu,
171
+ FloatingNote,
167
172
  FloatingPanel,
168
173
  FormCategorySelect,
169
174
  FormContainer,
170
175
  FormFieldArray,
171
176
  Gallery,
172
177
  GuardModal,
178
+ HeadingsPreviewModal,
173
179
  HeadingField,
174
180
  HiddenField,
175
181
  Icon,
@@ -177,6 +183,7 @@ export {
177
183
  Image,
178
184
  ImageField,
179
185
  InformativeMenu,
186
+ KeywordsPreviewModal,
180
187
  IntegrationsField,
181
188
  LanguageMenu,
182
189
  LastAccessFilter,
@@ -213,6 +220,7 @@ export {
213
220
  SearchField,
214
221
  SearchTagsBar,
215
222
  Select,
223
+ SEOPreview,
216
224
  SharePageModal,
217
225
  SideModal,
218
226
  SiteFilter,
@@ -1,6 +1,5 @@
1
- import React from "react";
2
- import { FieldContainer, FieldsBehavior } from "@ax/components";
3
- import { IErrorItem, IPage, ISite } from "@ax/types";
1
+ import { FieldContainer, FieldGroup, FieldsBehavior } from "@ax/components";
2
+ import type { IErrorItem, IPage, ISite } from "@ax/types";
4
3
 
5
4
  const getInnerFields = (
6
5
  innerFields: any[],
@@ -16,48 +15,54 @@ const getInnerFields = (
16
15
  ) => {
17
16
  let fieldArr: any[] = [];
18
17
 
19
- return (
20
- innerFields &&
21
- innerFields.map((singleFieldProps: any) => {
22
- const { key } = singleFieldProps;
23
- const error = errors && errors.find((err: any) => err.editorID === selectedContent.editorID && err.key === key);
18
+ return innerFields?.map((singleFieldProps: any) => {
19
+ const { key } = singleFieldProps;
20
+ const error = errors?.find((err: any) => err.editorID === selectedContent.editorID && err.key === key);
24
21
 
25
- if (singleFieldProps.type === "ConditionalField" || singleFieldProps.type === "ArrayFieldGroup") {
26
- fieldArr = getInnerFields(
27
- singleFieldProps.fields,
28
- innerActions,
29
- selectedContent,
30
- isTemplateActivated,
31
- theme,
32
- moduleCopy,
33
- parentDisabled,
34
- site,
35
- errors,
36
- deleteError,
37
- );
38
- }
22
+ const isGroup = singleFieldProps.type === "FieldGroup";
23
+ const isCollapsed = isGroup && singleFieldProps.collapsed;
24
+ const isConditional = singleFieldProps.type === "ConditionalField";
25
+ const isArrayGroup = singleFieldProps.type === "ArrayFieldGroup";
39
26
 
40
- return (
41
- <FieldContainer
42
- key={key}
43
- objKey={key}
44
- innerFields={fieldArr}
45
- field={singleFieldProps}
46
- actions={innerActions}
47
- selectedContent={selectedContent}
48
- updateValue={innerActions.updateValue}
49
- goTo={innerActions.goTo}
50
- site={site}
51
- {...singleFieldProps}
52
- disabled={!isTemplateActivated || singleFieldProps.disabled || parentDisabled}
53
- error={error}
54
- deleteError={deleteError}
55
- theme={theme}
56
- moduleCopy={moduleCopy}
57
- />
27
+ if (isGroup || isConditional || isArrayGroup) {
28
+ fieldArr = getInnerFields(
29
+ singleFieldProps.fields,
30
+ innerActions,
31
+ selectedContent,
32
+ isTemplateActivated,
33
+ theme,
34
+ moduleCopy,
35
+ parentDisabled,
36
+ site,
37
+ errors,
38
+ deleteError,
58
39
  );
59
- })
60
- );
40
+ }
41
+
42
+ return isGroup ? (
43
+ <FieldGroup key={key} title={singleFieldProps.title} collapsed={isCollapsed} solid={singleFieldProps.solid}>
44
+ {fieldArr}
45
+ </FieldGroup>
46
+ ) : (
47
+ <FieldContainer
48
+ key={key}
49
+ objKey={key}
50
+ innerFields={fieldArr}
51
+ field={singleFieldProps}
52
+ actions={innerActions}
53
+ selectedContent={selectedContent}
54
+ updateValue={innerActions.updateValue}
55
+ goTo={innerActions.goTo}
56
+ site={site}
57
+ {...singleFieldProps}
58
+ disabled={!isTemplateActivated || singleFieldProps.disabled || parentDisabled}
59
+ error={error}
60
+ deleteError={deleteError}
61
+ theme={theme}
62
+ moduleCopy={moduleCopy}
63
+ />
64
+ );
65
+ });
61
66
  };
62
67
 
63
68
  const getStructuredDataInnerFields = (
@@ -69,38 +74,35 @@ const getStructuredDataInnerFields = (
69
74
  ) => {
70
75
  let fieldArr: any[] = [];
71
76
 
72
- return (
73
- innerFields &&
74
- innerFields.map((singleFieldProps: any) => {
75
- const { key, type, fields } = singleFieldProps;
77
+ return innerFields?.map((singleFieldProps: any) => {
78
+ const { key, type, fields } = singleFieldProps;
76
79
 
77
- const handleChange = (newValue: any) => {
78
- updateValue({ [key]: newValue });
79
- };
80
+ const handleChange = (newValue: any) => {
81
+ updateValue({ [key]: newValue });
82
+ };
80
83
 
81
- const value = content && content[key];
84
+ const value = content?.[key];
82
85
 
83
- if (type === "ConditionalField" || type === "ArrayFieldGroup") {
84
- fieldArr = getStructuredDataInnerFields(fields, content, updateValue, theme, errors);
85
- }
86
+ if (type === "ConditionalField" || type === "ArrayFieldGroup") {
87
+ fieldArr = getStructuredDataInnerFields(fields, content, updateValue, theme, errors);
88
+ }
86
89
 
87
- const error = errors.find((err: any) => err.key === key);
90
+ const error = errors.find((err: any) => err.key === key);
88
91
 
89
- const fieldProps = {
90
- value,
91
- objKey: key,
92
- fieldType: type,
93
- innerFields: fieldArr,
94
- field: singleFieldProps,
95
- onChange: handleChange,
96
- ...singleFieldProps,
97
- theme,
98
- error,
99
- };
92
+ const fieldProps = {
93
+ value,
94
+ objKey: key,
95
+ fieldType: type,
96
+ innerFields: fieldArr,
97
+ field: singleFieldProps,
98
+ onChange: handleChange,
99
+ ...singleFieldProps,
100
+ theme,
101
+ error,
102
+ };
100
103
 
101
- return <FieldsBehavior key={key} {...fieldProps} />;
102
- })
103
- );
104
+ return <FieldsBehavior key={key} {...fieldProps} />;
105
+ });
104
106
  };
105
107
 
106
108
  export { getInnerFields, getStructuredDataInnerFields };
@@ -26,6 +26,27 @@ const useDebounce = (value: any) => {
26
26
  return debouncedValue;
27
27
  };
28
28
 
29
+ const useDebouncedCallback = <T extends unknown[]>(callback: (...args: T) => void, delay: number) => {
30
+ const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
31
+ const callbackRef = useRef(callback);
32
+
33
+ useEffect(() => {
34
+ callbackRef.current = callback;
35
+ }, [callback]);
36
+
37
+ return useCallback(
38
+ (...args: T) => {
39
+ if (delay === 0) {
40
+ callbackRef.current(...args);
41
+ return;
42
+ }
43
+ clearTimeout(timeoutRef.current);
44
+ timeoutRef.current = setTimeout(() => callbackRef.current(...args), delay);
45
+ },
46
+ [delay],
47
+ );
48
+ };
49
+
29
50
  const useEqualStructured = (component: any) => {
30
51
  return memo(component, (prevProps: any, newProps: any) => {
31
52
  const { fieldKey } = prevProps;
@@ -234,4 +255,4 @@ const useShouldBeSaved = (form: Record<string, unknown> | IUser | FormContent, d
234
255
  return { isDirty, setIsDirty };
235
256
  };
236
257
 
237
- export { useDebounce, useEqualStructured, useIsDirty, usePrevious, useShouldBeSaved };
258
+ export { useDebounce, useDebouncedCallback, useEqualStructured, usePrevious, useIsDirty, useShouldBeSaved };
@@ -1,12 +1,19 @@
1
1
  import { type IBulkSelectedItems, useBulkSelection } from "./bulk";
2
2
  import { useAdaptiveText, useCategoryColors, useEmptyState } from "./content";
3
- import { useDebounce, useEqualStructured, useIsDirty, usePrevious, useShouldBeSaved } from "./forms";
3
+ import {
4
+ useDebounce,
5
+ useDebouncedCallback,
6
+ useEqualStructured,
7
+ useIsDirty,
8
+ usePrevious,
9
+ useShouldBeSaved,
10
+ } from "./forms";
4
11
  import { useOnMessageReceivedFromIframe, useOnMessageReceivedFromOutside } from "./iframe";
5
12
  import { useURLSearchParam } from "./location";
6
- import { useHandleClickOutside, useModal, useToast } from "./modals";
13
+ import { useHandleClickOutside, useModal, useModals, useToast } from "./modals";
7
14
  import { useNetworkStatus } from "./network";
8
15
  import { useResizable } from "./resize";
9
- import { useGlobalPermission, usePermission } from "./users";
16
+ import { useGlobalPermission, usePermission, usePermissions } from "./users";
10
17
  import { useWindowSize } from "./window";
11
18
 
12
19
  export {
@@ -14,16 +21,19 @@ export {
14
21
  useBulkSelection,
15
22
  useCategoryColors,
16
23
  useDebounce,
24
+ useDebouncedCallback,
17
25
  useEmptyState,
18
26
  useEqualStructured,
19
27
  useGlobalPermission,
20
28
  useHandleClickOutside,
21
29
  useIsDirty,
22
30
  useModal,
31
+ useModals,
23
32
  useNetworkStatus,
24
33
  useOnMessageReceivedFromIframe,
25
34
  useOnMessageReceivedFromOutside,
26
35
  usePermission,
36
+ usePermissions,
27
37
  usePrevious,
28
38
  useResizable,
29
39
  useShouldBeSaved,
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from "react";
1
+ import { useState, useEffect, useRef, useCallback } from "react";
2
2
 
3
3
  const useModal = (initialState?: boolean, bodyBlock = true) => {
4
4
  const [isOpen, setIsOpen] = useState(initialState || false);
@@ -6,11 +6,26 @@ const useModal = (initialState?: boolean, bodyBlock = true) => {
6
6
  setIsOpen(!isOpen);
7
7
  };
8
8
 
9
- if (isOpen && bodyBlock) {
10
- document.body.classList.add("modal-open");
11
- } else {
12
- document.body.classList.remove("modal-open");
13
- }
9
+ useEffect(() => {
10
+ if (isOpen && bodyBlock) {
11
+ if (!document.body.classList.contains("modal-open")) {
12
+ document.body.classList.add("modal-open");
13
+ }
14
+ return () => {
15
+ // Solo eliminar si no hay otros modales abiertos
16
+ const modals = document.querySelectorAll('[data-testid="modal-wrapper"]');
17
+ if (modals.length <= 1) {
18
+ document.body.classList.remove("modal-open");
19
+ }
20
+ };
21
+ } else if (!bodyBlock || !isOpen) {
22
+ // Solo eliminar si no hay modales abiertos
23
+ const modals = document.querySelectorAll('[data-testid="modal-wrapper"]');
24
+ if (modals.length === 0) {
25
+ document.body.classList.remove("modal-open");
26
+ }
27
+ }
28
+ }, [isOpen, bodyBlock]);
14
29
 
15
30
  return {
16
31
  isOpen,
@@ -18,6 +33,66 @@ const useModal = (initialState?: boolean, bodyBlock = true) => {
18
33
  };
19
34
  };
20
35
 
36
+ const useModals = <T extends string>(modalKeys: readonly T[], bodyBlock = true) => {
37
+ const [openModals, setOpenModals] = useState<Record<T, boolean>>(() =>
38
+ modalKeys.reduce(
39
+ (acc, key) => {
40
+ acc[key] = false;
41
+ return acc;
42
+ },
43
+ {} as Record<T, boolean>,
44
+ ),
45
+ );
46
+
47
+ const toggleModal = useCallback((modalKey: T) => {
48
+ setOpenModals((prev) => ({ ...prev, [modalKey]: !prev[modalKey] }));
49
+ }, []);
50
+
51
+ const openModal = useCallback((modalKey: T) => {
52
+ setOpenModals((prev) => ({ ...prev, [modalKey]: true }));
53
+ }, []);
54
+
55
+ const closeModal = useCallback((modalKey: T) => {
56
+ setOpenModals((prev) => ({ ...prev, [modalKey]: false }));
57
+ }, []);
58
+
59
+ const isOpen = useCallback(
60
+ (modalKey: T) => {
61
+ return openModals[modalKey] || false;
62
+ },
63
+ [openModals],
64
+ );
65
+
66
+ useEffect(() => {
67
+ const hasOpenModals = Object.values(openModals).some(Boolean);
68
+
69
+ if (hasOpenModals && bodyBlock) {
70
+ if (!document.body.classList.contains("modal-open")) {
71
+ document.body.classList.add("modal-open");
72
+ }
73
+ return () => {
74
+ // Solo eliminar si no hay otros modales abiertos
75
+ const modals = document.querySelectorAll('[data-testid="modal-wrapper"]');
76
+ if (modals.length <= 1) {
77
+ document.body.classList.remove("modal-open");
78
+ }
79
+ };
80
+ }
81
+ // Solo eliminar si no hay modales abiertos
82
+ const modals = document.querySelectorAll('[data-testid="modal-wrapper"]');
83
+ if (modals.length === 0) {
84
+ document.body.classList.remove("modal-open");
85
+ }
86
+ }, [openModals, bodyBlock]);
87
+
88
+ return {
89
+ toggleModal,
90
+ openModal,
91
+ closeModal,
92
+ isOpen,
93
+ };
94
+ };
95
+
21
96
  const useHandleClickOutside = (isOpen: boolean, handleClickOutside: (e: MouseEvent) => void) => {
22
97
  useEffect(() => {
23
98
  if (isOpen) {
@@ -35,19 +110,32 @@ const useHandleClickOutside = (isOpen: boolean, handleClickOutside: (e: MouseEve
35
110
  const useToast = () => {
36
111
  const [isVisible, setIsVisible] = useState(false);
37
112
  const [state, setState] = useState<any>(null);
113
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
38
114
 
39
- let temp: any;
40
- const setTemp = () => (temp = setTimeout(() => setIsVisible(false), 6000));
41
- const stopTemp = () => clearTimeout(temp);
115
+ const toggleToast = useCallback((newState?: any) => {
116
+ if (timeoutRef.current) {
117
+ clearTimeout(timeoutRef.current);
118
+ }
42
119
 
43
- const toggleToast = (state?: unknown) => {
120
+ if (newState) {
121
+ setState(newState);
122
+ }
44
123
  setIsVisible(true);
45
- setTemp();
46
- stopTemp();
47
- state && setState(state);
48
- };
124
+
125
+ timeoutRef.current = setTimeout(() => {
126
+ setIsVisible(false);
127
+ }, 6000);
128
+ }, []);
129
+
130
+ useEffect(() => {
131
+ return () => {
132
+ if (timeoutRef.current) {
133
+ clearTimeout(timeoutRef.current);
134
+ }
135
+ };
136
+ }, []);
49
137
 
50
138
  return { isVisible, setIsVisible, toggleToast, state };
51
139
  };
52
140
 
53
- export { useModal, useHandleClickOutside, useToast };
141
+ export { useModal, useModals, useHandleClickOutside, useToast };
@@ -1,11 +1,11 @@
1
+ import { useMemo } from "react";
1
2
  import { useSelector } from "react-redux";
2
- import { IRootState } from "@ax/types";
3
+
4
+ import type { IRootState } from "@ax/types";
3
5
 
4
6
  const usePermission = (permission: string | string[] | undefined): boolean => {
5
7
  const userPermissions = useSelector((state: IRootState) => state.users.currentPermissions);
6
- const isSuperAdmin = useSelector(
7
- (state: IRootState) => state.users.currentUser && state.users.currentUser.isSuperAdmin,
8
- );
8
+ const isSuperAdmin = useSelector((state: IRootState) => state.users.currentUser?.isSuperAdmin);
9
9
 
10
10
  const isAllowedTo = (permissions: string[]) =>
11
11
  userPermissions && permissions.some((permission: string) => userPermissions.includes(permission));
@@ -19,11 +19,28 @@ const usePermission = (permission: string | string[] | undefined): boolean => {
19
19
  return isAllowedTo(arrayPermission);
20
20
  };
21
21
 
22
+ const usePermissions = <T extends Record<string, string | string[]>>(permissions: T): Record<keyof T, boolean> => {
23
+ const userPermissions = useSelector((state: IRootState) => state.users.currentPermissions);
24
+ const isSuperAdmin = useSelector((state: IRootState) => state.users.currentUser?.isSuperAdmin);
25
+
26
+ return useMemo(() => {
27
+ const isAllowedTo = (permission: string | string[]) => {
28
+ if (isSuperAdmin) return true;
29
+ const arrayPermission = Array.isArray(permission) ? permission : [permission];
30
+ return userPermissions && arrayPermission.some((perm: string) => userPermissions.includes(perm));
31
+ };
32
+
33
+ const result: Record<string, boolean> = {};
34
+ for (const key of Object.keys(permissions)) {
35
+ result[key] = isAllowedTo(permissions[key]);
36
+ }
37
+ return result as Record<keyof T, boolean>;
38
+ }, [permissions, userPermissions, isSuperAdmin]);
39
+ };
40
+
22
41
  const useGlobalPermission = (permission: string | string[] | undefined): boolean => {
23
42
  const userPermissions = useSelector((state: IRootState) => state.users.globalPermissions);
24
- const isSuperAdmin = useSelector(
25
- (state: IRootState) => state.users.currentUser && state.users.currentUser.isSuperAdmin,
26
- );
43
+ const isSuperAdmin = useSelector((state: IRootState) => state.users.currentUser?.isSuperAdmin);
27
44
 
28
45
  const isAllowedTo = (permissions: string[]) =>
29
46
  userPermissions && permissions.some((permission: string) => userPermissions.includes(permission));
@@ -37,4 +54,4 @@ const useGlobalPermission = (permission: string | string[] | undefined): boolean
37
54
  return isAllowedTo(arrayPermission);
38
55
  };
39
56
 
40
- export { usePermission, useGlobalPermission };
57
+ export { usePermission, usePermissions, useGlobalPermission };
@@ -1,6 +1,6 @@
1
- import React, { Dispatch, SetStateAction } from "react";
1
+ import type { Dispatch, SetStateAction } from "react";
2
2
 
3
- import { IModal } from "@ax/types";
3
+ import type { IModal } from "@ax/types";
4
4
  import { Modal, FieldsBehavior, AsyncSelect } from "@ax/components";
5
5
 
6
6
  import * as S from "./style";
@@ -0,0 +1,116 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
+
4
+ import * as S from "./style";
5
+
6
+ const headColors: Record<string, string> = {
7
+ h1: "#FFF06D",
8
+ h2: "#FFB8F8",
9
+ h3: "#73F8C8",
10
+ h4: "#9BEDFF",
11
+ h5: "#C6C1FF",
12
+ h6: "#FFCE95",
13
+ };
14
+
15
+ const isEffectivelyVisible = (el: HTMLElement): boolean => {
16
+ let node: HTMLElement | null = el;
17
+ while (node && node !== document.body) {
18
+ const style = window.getComputedStyle(node);
19
+ if (style.display === "none" || style.visibility === "hidden") return false;
20
+ if (parseFloat(style.opacity) === 0) return false;
21
+ node = node.parentElement;
22
+ }
23
+ return true;
24
+ };
25
+
26
+ const HeadingsOverlay = ({ headingFilter }: IHeadingsOverlayProps) => {
27
+ const [boxes, setBoxes] = useState<HeadingBox[]>([]);
28
+ const rafRef = useRef<number>(0);
29
+
30
+ useEffect(() => {
31
+ const update = () => {
32
+ cancelAnimationFrame(rafRef.current);
33
+ rafRef.current = requestAnimationFrame(() => {
34
+ const selector = headingFilter || "h1, h2, h3, h4, h5, h6";
35
+ const headings = Array.from(document.querySelectorAll<HTMLElement>(selector));
36
+ const scrollX = window.scrollX;
37
+ const scrollY = window.scrollY;
38
+ const boxes: HeadingBox[] = [];
39
+ for (let i = 0; i < headings.length; i++) {
40
+ const el = headings[i];
41
+ const rect = el.getBoundingClientRect();
42
+ if (rect.width === 0 && rect.height === 0) continue;
43
+ if (!isEffectivelyVisible(el)) continue;
44
+ boxes.push({
45
+ id: el.dataset.griddoid || `heading-${i}`,
46
+ tag: el.tagName.toLowerCase(),
47
+ rect,
48
+ scrollX,
49
+ scrollY,
50
+ });
51
+ }
52
+ setBoxes(boxes);
53
+ });
54
+ };
55
+
56
+ update();
57
+
58
+ window.addEventListener("resize", update);
59
+ window.addEventListener("scroll", update, true);
60
+ document.addEventListener("animationend", update, true);
61
+ document.addEventListener("transitionend", update, true);
62
+
63
+ const observer = new MutationObserver(update);
64
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true });
65
+
66
+ return () => {
67
+ cancelAnimationFrame(rafRef.current);
68
+ window.removeEventListener("resize", update);
69
+ window.removeEventListener("scroll", update, true);
70
+ document.removeEventListener("animationend", update, true);
71
+ document.removeEventListener("transitionend", update, true);
72
+ observer.disconnect();
73
+ };
74
+ }, [headingFilter]);
75
+
76
+ return createPortal(
77
+ <div data-testid="headings-overlay">
78
+ {boxes.map(({ id, tag, rect, scrollX, scrollY }) => {
79
+ const color = headColors[tag];
80
+ if (!color) return null;
81
+ const labelAbove = rect.left < 30;
82
+ return (
83
+ <S.Box
84
+ key={id}
85
+ $color={color}
86
+ style={{
87
+ top: rect.top + scrollY,
88
+ left: rect.left + scrollX,
89
+ width: rect.width,
90
+ height: rect.height,
91
+ }}
92
+ >
93
+ <S.Label $color={color} $above={labelAbove}>
94
+ {tag.toUpperCase()}
95
+ </S.Label>
96
+ </S.Box>
97
+ );
98
+ })}
99
+ </div>,
100
+ document.body,
101
+ );
102
+ };
103
+
104
+ interface HeadingBox {
105
+ id: string;
106
+ tag: string;
107
+ rect: DOMRect;
108
+ scrollX: number;
109
+ scrollY: number;
110
+ }
111
+
112
+ export interface IHeadingsOverlayProps {
113
+ headingFilter: string | null;
114
+ }
115
+
116
+ export default HeadingsOverlay;