@griddo/ax 11.11.7 → 11.11.8-rc.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 (104) 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 -144
  20. package/src/components/Browser/style.tsx +75 -6
  21. package/src/components/Browser/utils.tsx +13 -0
  22. package/src/components/BrowserContent/index.tsx +2 -2
  23. package/src/components/Button/index.tsx +2 -1
  24. package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/Field/index.tsx +2 -4
  25. package/src/components/Fields/AsyncSelect/style.tsx +13 -0
  26. package/src/components/Fields/FieldGroup/index.tsx +5 -2
  27. package/src/components/Fields/FieldGroup/style.tsx +32 -7
  28. package/src/components/Fields/HeadingField/index.tsx +2 -2
  29. package/src/components/Fields/HiddenField/style.tsx +1 -1
  30. package/src/components/Fields/NumberField/index.tsx +15 -16
  31. package/src/components/Fields/NumberField/style.tsx +2 -0
  32. package/src/components/Fields/ReferenceField/index.tsx +1 -1
  33. package/src/components/Fields/SEOPreview/index.tsx +36 -0
  34. package/src/components/Fields/SEOPreview/style.tsx +24 -0
  35. package/src/components/Fields/Select/index.tsx +5 -1
  36. package/src/components/Fields/Select/style.tsx +56 -0
  37. package/src/components/Fields/SummaryButton/index.tsx +18 -9
  38. package/src/components/Fields/SummaryButton/style.tsx +1 -2
  39. package/src/components/Fields/TagsField/index.tsx +8 -9
  40. package/src/components/Fields/UrlField/index.tsx +26 -27
  41. package/src/components/Fields/index.tsx +2 -0
  42. package/src/components/FloatingNote/index.tsx +35 -0
  43. package/src/components/FloatingNote/style.tsx +26 -0
  44. package/src/components/FloatingPanel/index.tsx +5 -2
  45. package/src/components/FloatingPanel/style.tsx +2 -1
  46. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +85 -0
  47. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/style.tsx +80 -0
  48. package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +57 -0
  49. package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +82 -0
  50. package/src/components/HeadingsPreviewModal/HeadingItem/index.tsx +71 -0
  51. package/src/components/HeadingsPreviewModal/HeadingItem/style.tsx +77 -0
  52. package/src/components/HeadingsPreviewModal/index.tsx +146 -0
  53. package/src/components/HeadingsPreviewModal/style.tsx +82 -0
  54. package/src/components/HeadingsPreviewModal/utils.tsx +257 -0
  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/OcassionalToast/index.tsx +8 -1
  66. package/src/components/OcassionalToast/style.tsx +15 -1
  67. package/src/components/PageFinder/index.tsx +1 -1
  68. package/src/components/ResizePanel/index.tsx +4 -3
  69. package/src/components/ResizePanel/style.tsx +1 -1
  70. package/src/components/SearchField/style.tsx +2 -2
  71. package/src/components/SideModal/index.tsx +2 -1
  72. package/src/components/Tabs/index.tsx +13 -4
  73. package/src/components/Tabs/style.tsx +7 -8
  74. package/src/components/Toast/index.tsx +4 -2
  75. package/src/components/Tooltip/index.tsx +4 -3
  76. package/src/components/index.tsx +8 -2
  77. package/src/forms/fields.tsx +70 -68
  78. package/src/hooks/forms.tsx +22 -1
  79. package/src/hooks/index.tsx +13 -3
  80. package/src/hooks/modals.tsx +103 -15
  81. package/src/hooks/users.tsx +25 -8
  82. package/src/modules/Forms/atoms.tsx +2 -2
  83. package/src/modules/FramePreview/HeadingsOverlay/index.tsx +113 -0
  84. package/src/modules/FramePreview/HeadingsOverlay/style.tsx +24 -0
  85. package/src/modules/FramePreview/index.tsx +55 -16
  86. package/src/modules/FramePreview/style.tsx +34 -2
  87. package/src/modules/FramePreview/utils.tsx +140 -0
  88. package/src/modules/GlobalEditor/Editor/index.tsx +37 -3
  89. package/src/modules/GlobalEditor/PageBrowser/index.tsx +19 -2
  90. package/src/modules/GlobalEditor/Preview/index.tsx +0 -2
  91. package/src/modules/GlobalEditor/Preview/style.tsx +1 -1
  92. package/src/modules/GlobalEditor/index.tsx +119 -57
  93. package/src/modules/PageEditor/Editor/index.tsx +33 -2
  94. package/src/modules/PageEditor/PageBrowser/index.tsx +20 -2
  95. package/src/modules/PageEditor/Preview/index.tsx +0 -2
  96. package/src/modules/PageEditor/Preview/style.tsx +1 -1
  97. package/src/modules/PageEditor/atoms.tsx +1 -1
  98. package/src/modules/PageEditor/index.tsx +130 -66
  99. package/src/modules/PublicPreview/index.tsx +8 -5
  100. package/src/schemas/pages/GlobalPage.ts +87 -70
  101. package/src/schemas/pages/Page.ts +87 -70
  102. package/src/types/index.tsx +12 -0
  103. package/src/components/PageInfoBanner/index.tsx +0 -38
  104. package/src/components/PageInfoBanner/styles.tsx +0 -40
@@ -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";
@@ -84,7 +88,6 @@ import Nav from "./Nav";
84
88
  import Notification from "./Notification";
85
89
  import OcassionalToast from "./OcassionalToast";
86
90
  import PageFinder from "./PageFinder";
87
- import PageInfoBanner from "./PageInfoBanner";
88
91
  import Pagination from "./Pagination";
89
92
  import ProgressBar from "./ProgressBar";
90
93
  import ReorderArrows from "./ReorderArrows";
@@ -165,12 +168,14 @@ export {
165
168
  Flag,
166
169
  FloatingButton,
167
170
  FloatingMenu,
171
+ FloatingNote,
168
172
  FloatingPanel,
169
173
  FormCategorySelect,
170
174
  FormContainer,
171
175
  FormFieldArray,
172
176
  Gallery,
173
177
  GuardModal,
178
+ HeadingsPreviewModal,
174
179
  HeadingField,
175
180
  HiddenField,
176
181
  Icon,
@@ -178,6 +183,7 @@ export {
178
183
  Image,
179
184
  ImageField,
180
185
  InformativeMenu,
186
+ KeywordsPreviewModal,
181
187
  IntegrationsField,
182
188
  LanguageMenu,
183
189
  LastAccessFilter,
@@ -199,7 +205,6 @@ export {
199
205
  NumberField,
200
206
  OcassionalToast,
201
207
  PageFinder,
202
- PageInfoBanner,
203
208
  Pagination,
204
209
  PermissionsFilter,
205
210
  ProgressBar,
@@ -215,6 +220,7 @@ export {
215
220
  SearchField,
216
221
  SearchTagsBar,
217
222
  Select,
223
+ SEOPreview,
218
224
  SharePageModal,
219
225
  SideModal,
220
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,113 @@
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
+ return (
82
+ <S.Box
83
+ key={id}
84
+ $color={color}
85
+ style={{
86
+ top: rect.top + scrollY,
87
+ left: rect.left + scrollX,
88
+ width: rect.width,
89
+ height: rect.height,
90
+ }}
91
+ >
92
+ <S.Label $color={color}>{tag.toUpperCase()}</S.Label>
93
+ </S.Box>
94
+ );
95
+ })}
96
+ </div>,
97
+ document.body,
98
+ );
99
+ };
100
+
101
+ interface HeadingBox {
102
+ id: string;
103
+ tag: string;
104
+ rect: DOMRect;
105
+ scrollX: number;
106
+ scrollY: number;
107
+ }
108
+
109
+ export interface IHeadingsOverlayProps {
110
+ headingFilter: string | null;
111
+ }
112
+
113
+ export default HeadingsOverlay;
@@ -0,0 +1,24 @@
1
+ import styled from "styled-components";
2
+
3
+ const Box = styled.div<{ $color: string }>`
4
+ position: absolute;
5
+ outline: 2px solid ${(p) => p.$color};
6
+ z-index: 1001;
7
+ pointer-events: none;
8
+ `;
9
+
10
+ const Label = styled.span<{ $color: string }>`
11
+ position: absolute;
12
+ background-color: ${(p) => p.$color};
13
+ color: ${(p) => p.theme.colors.textHighEmphasis};
14
+ ${(p) => p.theme.textStyle.uiS};
15
+ padding: ${(p) => `0 ${p.theme.spacing.xxs}`};
16
+ transform: rotate(-90deg);
17
+ top: 1px;
18
+ left: -23px;
19
+ font-family: Source Sans Pro;
20
+ font-style: normal;
21
+ text-decoration: none;
22
+ `;
23
+
24
+ export { Box, Label };