@griddo/ax 11.14.2-rc.0 → 11.14.2

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 (148) hide show
  1. package/config/jest/reactEasyCropMock.js +15 -0
  2. package/config/jest/reactTimezoneMock.js +13 -0
  3. package/package.json +221 -219
  4. package/public/img/welcome.svg +127 -0
  5. package/src/__tests__/components/Browser/Browser.test.tsx +27 -51
  6. package/src/__tests__/components/CategoryCell/CategoryCell.test.tsx +10 -5
  7. package/src/__tests__/components/ElementsTooltip/ElementsTooltip.test.tsx +27 -14
  8. package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +2 -0
  9. package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +138 -1
  10. package/src/__tests__/components/ImageDragAndDrop/CropStep/CropStep.test.tsx +84 -0
  11. package/src/__tests__/components/ImageDragAndDrop/ImageDragAndDrop.test.tsx +173 -0
  12. package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.test.tsx +3 -4
  13. package/src/__tests__/components/ProfileImage/ProfileImage.test.tsx +120 -0
  14. package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +8 -0
  15. package/src/__tests__/components/UserRolesAndSites/RoleItem/RoleItem.test.tsx +190 -0
  16. package/src/__tests__/components/UserRolesAndSites/UserRolesAndSites.test.tsx +471 -0
  17. package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +15 -2
  18. package/src/__tests__/modules/Sites/Sites.test.tsx +68 -224
  19. package/src/__tests__/modules/Sites/SitesList/ListView/BulkHeader/BulkHeader.test.tsx +21 -17
  20. package/src/__tests__/modules/Sites/SitesList/SitesList.test.tsx +65 -565
  21. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/DataStep/DataStep.test.tsx +109 -0
  22. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/FinalStep/FinalStep.test.tsx +157 -0
  23. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/CropView.test.tsx +51 -0
  24. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/ImageStep.test.tsx +70 -0
  25. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/UploadView.test.tsx +92 -0
  26. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/TimezoneStep/TimezoneStep.test.tsx +94 -0
  27. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/WelcomeModal.test.tsx +78 -0
  28. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/WelcomeStep/WelcomeStep.test.tsx +39 -0
  29. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/utils.test.ts +55 -0
  30. package/src/api/sites.tsx +4 -4
  31. package/src/components/Avatar/index.tsx +26 -5
  32. package/src/components/Avatar/style.tsx +20 -10
  33. package/src/components/Browser/index.tsx +7 -1
  34. package/src/components/ConfigPanel/index.tsx +11 -7
  35. package/src/components/ElementsTooltip/index.tsx +96 -34
  36. package/src/components/ElementsTooltip/style.tsx +12 -1
  37. package/src/components/Fields/FileField/index.tsx +16 -18
  38. package/src/components/Fields/HeadingField/index.tsx +1 -1
  39. package/src/components/Fields/ImageField/index.tsx +9 -38
  40. package/src/components/Fields/ImageField/style.tsx +12 -1
  41. package/src/components/Fields/ToggleField/index.tsx +1 -1
  42. package/src/components/Fields/Wysiwyg/index.tsx +25 -20
  43. package/src/components/FileGallery/GalleryPanel/index.tsx +15 -7
  44. package/src/components/FileGallery/index.tsx +33 -28
  45. package/src/components/Gallery/GalleryPanel/index.tsx +5 -16
  46. package/src/components/Gallery/index.tsx +0 -2
  47. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +11 -2
  48. package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +21 -3
  49. package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +2 -2
  50. package/src/components/HeadingsPreviewModal/index.tsx +13 -3
  51. package/src/components/HeadingsPreviewModal/style.tsx +18 -0
  52. package/src/components/HeadingsPreviewModal/utils.tsx +31 -3
  53. package/src/components/Image/index.tsx +2 -2
  54. package/src/components/ImageDragAndDrop/CropStep/index.tsx +95 -0
  55. package/src/components/ImageDragAndDrop/CropStep/style.tsx +101 -0
  56. package/src/{modules/MediaGallery → components}/ImageDragAndDrop/index.tsx +103 -40
  57. package/src/{modules/MediaGallery → components}/ImageDragAndDrop/style.tsx +14 -2
  58. package/src/components/KeywordsPreviewModal/atoms.tsx +2 -2
  59. package/src/components/KeywordsPreviewModal/index.tsx +6 -6
  60. package/src/components/KeywordsPreviewModal/utils.tsx +2 -2
  61. package/src/components/ProfileImage/index.tsx +55 -0
  62. package/src/components/ProfileImage/style.tsx +58 -0
  63. package/src/components/ResizePanel/ResizeHandle/index.tsx +44 -6
  64. package/src/components/ResizePanel/ResizeHandle/style.tsx +7 -0
  65. package/src/components/ResizePanel/index.tsx +25 -4
  66. package/src/components/Tabs/style.tsx +1 -1
  67. package/src/components/Tag/index.tsx +0 -1
  68. package/src/components/UserRolesAndSites/RoleItem/index.tsx +42 -0
  69. package/src/components/UserRolesAndSites/RoleItem/style.tsx +29 -0
  70. package/src/components/UserRolesAndSites/index.tsx +102 -0
  71. package/src/components/UserRolesAndSites/style.tsx +67 -0
  72. package/src/components/index.tsx +6 -0
  73. package/src/constants/index.ts +13 -1
  74. package/src/containers/App/actions.tsx +8 -1
  75. package/src/containers/Sites/actions.tsx +26 -0
  76. package/src/containers/Sites/constants.tsx +1 -0
  77. package/src/containers/Sites/interfaces.tsx +6 -0
  78. package/src/containers/Sites/reducer.tsx +5 -1
  79. package/src/containers/Users/reducer.tsx +6 -5
  80. package/src/guards/routeLeaving/index.tsx +9 -11
  81. package/src/helpers/images.tsx +50 -3
  82. package/src/helpers/index.tsx +2 -1
  83. package/src/hooks/forms.tsx +45 -48
  84. package/src/hooks/index.tsx +2 -1
  85. package/src/hooks/modals.tsx +4 -3
  86. package/src/hooks/window.ts +50 -2
  87. package/src/modules/ActivityLog/ItemLogUser/UserItem/index.tsx +1 -1
  88. package/src/modules/App/Routing/Logout/index.tsx +3 -5
  89. package/src/modules/App/Routing/NavMenu/NavItem/index.tsx +73 -52
  90. package/src/modules/App/Routing/NavMenu/NavItem/style.tsx +21 -7
  91. package/src/modules/App/Routing/NavMenu/index.tsx +59 -54
  92. package/src/modules/App/Routing/NavMenu/style.tsx +13 -11
  93. package/src/modules/CreatePass/index.tsx +1 -1
  94. package/src/modules/FileDrive/FileDragAndDrop/index.tsx +11 -8
  95. package/src/modules/FileDrive/FileModal/index.tsx +8 -9
  96. package/src/modules/FileDrive/index.tsx +1 -18
  97. package/src/modules/Forms/FormEditor/index.tsx +1 -1
  98. package/src/modules/FramePreview/HeadingsOverlay/index.tsx +22 -11
  99. package/src/modules/FramePreview/HeadingsOverlay/style.tsx +1 -1
  100. package/src/modules/MediaGallery/ImageModal/index.tsx +1 -5
  101. package/src/modules/MediaGallery/index.tsx +1 -3
  102. package/src/modules/Settings/Globals/constants.tsx +942 -106
  103. package/src/modules/Sites/SitesList/AllSitesHeader/index.tsx +33 -0
  104. package/src/modules/Sites/SitesList/AllSitesHeader/style.tsx +35 -0
  105. package/src/modules/Sites/SitesList/GridView/GridHeaderFilter/index.tsx +5 -5
  106. package/src/modules/Sites/SitesList/GridView/GridSiteItem/index.tsx +23 -119
  107. package/src/modules/Sites/SitesList/ListView/BulkHeader/TableHeader/index.tsx +4 -4
  108. package/src/modules/Sites/SitesList/ListView/BulkHeader/index.tsx +4 -3
  109. package/src/modules/Sites/SitesList/ListView/ListSiteItem/index.tsx +23 -120
  110. package/src/modules/Sites/SitesList/{RecentSiteItem → RecentSites/RecentSiteItem}/index.tsx +4 -5
  111. package/src/modules/Sites/SitesList/RecentSites/index.tsx +49 -0
  112. package/src/modules/Sites/SitesList/RecentSites/style.tsx +92 -0
  113. package/src/modules/Sites/SitesList/SiteModal/index.tsx +8 -7
  114. package/src/modules/Sites/SitesList/WelcomeModal/DataStep/index.tsx +72 -0
  115. package/src/modules/Sites/SitesList/WelcomeModal/DataStep/style.tsx +59 -0
  116. package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/constants.tsx +78 -0
  117. package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/index.tsx +78 -0
  118. package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/style.tsx +141 -0
  119. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/index.tsx +93 -0
  120. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/style.tsx +77 -0
  121. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/index.tsx +100 -0
  122. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/style.tsx +94 -0
  123. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/index.tsx +44 -0
  124. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/style.tsx +31 -0
  125. package/src/modules/Sites/SitesList/WelcomeModal/TimezoneStep/index.tsx +51 -0
  126. package/src/modules/Sites/SitesList/WelcomeModal/TimezoneStep/style.tsx +52 -0
  127. package/src/modules/Sites/SitesList/WelcomeModal/WelcomeStep/index.tsx +40 -0
  128. package/src/modules/Sites/SitesList/WelcomeModal/WelcomeStep/style.tsx +53 -0
  129. package/src/modules/Sites/SitesList/WelcomeModal/index.tsx +215 -0
  130. package/src/modules/Sites/SitesList/WelcomeModal/style.tsx +12 -0
  131. package/src/modules/Sites/SitesList/WelcomeModal/utils.ts +26 -0
  132. package/src/modules/Sites/SitesList/atoms.tsx +4 -4
  133. package/src/modules/Sites/SitesList/hooks.tsx +149 -16
  134. package/src/modules/Sites/SitesList/index.tsx +127 -125
  135. package/src/modules/Sites/SitesList/style.tsx +1 -117
  136. package/src/modules/Sites/SitesList/utils.tsx +9 -2
  137. package/src/modules/Sites/index.tsx +19 -8
  138. package/src/modules/Users/Profile/index.tsx +169 -31
  139. package/src/modules/Users/Profile/style.tsx +81 -1
  140. package/src/modules/Users/Roles/RoleItem/index.tsx +2 -2
  141. package/src/modules/Users/UserCreate/SiteItem/index.tsx +11 -14
  142. package/src/modules/Users/UserForm/atoms.tsx +3 -3
  143. package/src/modules/Users/UserForm/index.tsx +25 -29
  144. package/src/modules/Users/UserForm/style.tsx +15 -2
  145. package/src/modules/Users/UserList/UserItem/index.tsx +4 -4
  146. package/src/routes/index.tsx +1 -0
  147. package/src/types/index.tsx +2 -0
  148. /package/src/modules/Sites/SitesList/{RecentSiteItem → RecentSites/RecentSiteItem}/style.tsx +0 -0
@@ -0,0 +1,55 @@
1
+ import { getCompanyFromEmail, getMostFrequentRole } from "@ax/modules/Sites/SitesList/WelcomeModal/utils";
2
+
3
+ describe("getCompanyFromEmail", () => {
4
+ it("should extract company name from professional email", () => {
5
+ expect(getCompanyFromEmail("user@secuoyas.com")).toBe("Secuoyas");
6
+ });
7
+
8
+ it("should capitalize the first letter of the company name", () => {
9
+ expect(getCompanyFromEmail("user@acme.io")).toBe("Acme");
10
+ });
11
+
12
+ it("should return empty string for generic email providers", () => {
13
+ expect(getCompanyFromEmail("user@gmail.com")).toBe("");
14
+ expect(getCompanyFromEmail("user@hotmail.com")).toBe("");
15
+ expect(getCompanyFromEmail("user@outlook.com")).toBe("");
16
+ expect(getCompanyFromEmail("user@yahoo.com")).toBe("");
17
+ expect(getCompanyFromEmail("user@icloud.com")).toBe("");
18
+ });
19
+
20
+ it("should return empty string when email has no domain", () => {
21
+ expect(getCompanyFromEmail("usernodomain")).toBe("");
22
+ });
23
+
24
+ it("should return the full subdomain string before the TLD", () => {
25
+ expect(getCompanyFromEmail("user@mail.company.com")).toBe("Mail.company");
26
+ });
27
+ });
28
+
29
+ describe("getMostFrequentRole", () => {
30
+ it("should return null for empty userRoles", () => {
31
+ expect(getMostFrequentRole([])).toBeNull();
32
+ });
33
+
34
+ it("should return the ID of the most frequent role", () => {
35
+ const userRoles = [
36
+ { siteId: 1, roles: [1, 2] },
37
+ { siteId: 2, roles: [1] },
38
+ ];
39
+ expect(getMostFrequentRole(userRoles)).toBe(1);
40
+ });
41
+
42
+ it("should handle single role occurrence", () => {
43
+ const userRoles = [{ siteId: 1, roles: [5] }];
44
+ expect(getMostFrequentRole(userRoles)).toBe(5);
45
+ });
46
+
47
+ it("should return correct role when multiple roles have different frequencies", () => {
48
+ const userRoles = [
49
+ { siteId: 1, roles: [1, 2, 3] },
50
+ { siteId: 2, roles: [1, 2] },
51
+ { siteId: 3, roles: [1] },
52
+ ];
53
+ expect(getMostFrequentRole(userRoles)).toBe(1);
54
+ });
55
+ });
package/src/api/sites.tsx CHANGED
@@ -1,7 +1,7 @@
1
- import { AxiosResponse } from "axios";
2
- import { IGetGlobalPagesParams, IGetSitePagesParams, IGetSitesParams } from "@ax/types";
1
+ import type { AxiosResponse } from "axios";
2
+ import type { IGetGlobalPagesParams, IGetSitePagesParams, IGetSitesParams } from "@ax/types";
3
3
  import { template } from "./config";
4
- import { sendRequest, IServiceConfig, sendInitialRequest } from "./utils";
4
+ import { sendRequest, type IServiceConfig, sendInitialRequest } from "./utils";
5
5
 
6
6
  const SERVICES: { [key: string]: IServiceConfig } = {
7
7
  GET_ALL_SITES: {
@@ -123,7 +123,7 @@ const SERVICES: { [key: string]: IServiceConfig } = {
123
123
 
124
124
  const getAllSites = async (params: IGetSitesParams = { recentSitesNumber: 7 }) => {
125
125
  const { host, endpoint } = SERVICES.GET_ALL_SITES;
126
- const { token, language, recentSitesNumber, searchQuery, filterQuery, page, itemsPerPage, pagination } = params;
126
+ const { token, language, recentSitesNumber = 7, searchQuery, filterQuery, page, itemsPerPage, pagination } = params;
127
127
 
128
128
  const queryParams =
129
129
  language && recentSitesNumber
@@ -1,14 +1,32 @@
1
- import React from "react";
2
1
  import { getInitials } from "@ax/helpers";
3
2
 
4
3
  import * as S from "./style";
5
4
 
6
- const Avatar = (props: IAvatarProps): JSX.Element => {
7
- const { image, name, size } = props;
5
+ const AVATAR_COLORS = ["#7C9CDE", "#E07C7C", "#7CC8A0", "#C97CCC", "#D4A76A", "#6AB8C8", "#C8A86A", "#8E7CC8"];
6
+
7
+ const getAvatarColor = (name?: string): string => {
8
+ if (!name) return AVATAR_COLORS[0];
9
+ let hash = 0;
10
+ const normalized = name.toLowerCase();
11
+ for (let i = 0; i < normalized.length; i++) {
12
+ hash = ((hash << 5) - hash) + normalized.charCodeAt(i);
13
+ hash = hash & hash; // Convert to 32bit integer
14
+ }
15
+ return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
16
+ };
8
17
 
18
+ const Avatar = (props: IAvatarProps): JSX.Element => {
19
+ const { image, name, size, fontSize, bold, background } = props;
9
20
  return (
10
21
  <S.AvatarWrapper data-testid="avatar-wrapper" size={size}>
11
- <S.Avatar image={image} initials={getInitials(name)} data-testid="avatar" size={size} />
22
+ <S.Avatar
23
+ image={image}
24
+ initials={getInitials(name)}
25
+ color={background ? getAvatarColor(name) : undefined}
26
+ fontSize={fontSize}
27
+ bold={bold}
28
+ data-testid="avatar"
29
+ />
12
30
  </S.AvatarWrapper>
13
31
  );
14
32
  };
@@ -16,7 +34,10 @@ const Avatar = (props: IAvatarProps): JSX.Element => {
16
34
  export interface IAvatarProps {
17
35
  image?: string | null;
18
36
  name?: string;
19
- size?: string;
37
+ size?: number;
38
+ fontSize?: number;
39
+ background?: boolean;
40
+ bold?: boolean;
20
41
  }
21
42
 
22
43
  export default Avatar;
@@ -1,15 +1,14 @@
1
1
  import styled from "styled-components";
2
2
 
3
- const AvatarWrapper = styled.div<{ size?: string }>`
4
- width: ${(p) => (p.size === "s" ? " 24px" : "32px")};
5
- height: ${(p) => (p.size === "s" ? " 24px" : "32px")};
6
- `;
7
-
8
- const Avatar = styled.span<{ image?: string | null; initials: string; size?: string }>`
3
+ const Avatar = styled.span<{
4
+ image?: string | null;
5
+ initials: string;
6
+ color?: string;
7
+ fontSize?: number;
8
+ bold?: boolean;
9
+ }>`
9
10
  border-radius: 50%;
10
- width: ${(p) => (p.size === "s" ? " 24px" : "32px")};
11
- height: ${(p) => (p.size === "s" ? " 24px" : "32px")};
12
- background: ${(p) => `url(${p.image})`}, ${(p) => p.theme.colors.uiBackground03};
11
+ background: ${(p) => `url(${p.image})`}, ${(p) => (p.color ? p.color : p.theme.colors.uiBackground03)};
13
12
  background-size: cover;
14
13
  background-repeat: no-repeat;
15
14
  background-position: center center;
@@ -19,8 +18,19 @@ const Avatar = styled.span<{ image?: string | null; initials: string; size?: str
19
18
 
20
19
  &:after {
21
20
  content: ${(p) => !p.image && JSON.stringify(p.initials)};
22
- color: ${(p) => p.theme.colors.textMediumEmphasis};
21
+ color: ${(p) => (p.bold ? p.theme.colors.textHighEmphasis : p.theme.colors.textMediumEmphasis)};
23
22
  ${(p) => p.theme.textStyle.uiXS};
23
+ font-size: ${(p) => (p.fontSize ? `${p.fontSize}px` : "12px")};
24
+ }
25
+ `;
26
+
27
+ const AvatarWrapper = styled.div<{ size?: number }>`
28
+ width: ${(p) => (p.size ? `${p.size}px` : "32px")};
29
+ height: ${(p) => (p.size ? `${p.size}px` : "32px")};
30
+
31
+ ${Avatar} {
32
+ width: ${(p) => (p.size ? `${p.size}px` : "32px")};
33
+ height: ${(p) => (p.size ? `${p.size}px` : "32px")};
24
34
  }
25
35
  `;
26
36
 
@@ -88,10 +88,16 @@ const Browser = (props: IBrowserProps): JSX.Element => {
88
88
  const el = frameWrapperRef.current;
89
89
  if (!el) return;
90
90
 
91
+ const getScrollbarWidth = () => {
92
+ return window.innerWidth - document.documentElement.clientWidth || 15;
93
+ };
94
+
91
95
  let lastWidth = 0;
96
+ const scrollbarThreshold = getScrollbarWidth();
97
+
92
98
  const observer = new ResizeObserver(([entry]) => {
93
99
  const containerWidth = entry.contentRect.width;
94
- if (containerWidth > 0 && Math.abs(containerWidth - lastWidth) > 20) {
100
+ if (containerWidth > 0 && Math.abs(containerWidth - lastWidth) > scrollbarThreshold) {
95
101
  lastWidth = containerWidth;
96
102
  const resolution = calcDefaultResolution(containerWidth, resolutionOptions);
97
103
  const newZoom = calcAutoZoom(containerWidth, resolution);
@@ -1,19 +1,21 @@
1
- import React, { useEffect, useRef, useState } from "react";
1
+ import { useEffect, useRef, useState } from "react";
2
2
 
3
- import { isEmptyObj } from "@ax/helpers";
4
3
  import { Loading } from "@ax/components";
5
- import { IBreadcrumbItem, IUserEditing } from "@ax/types";
4
+ import { isEmptyObj } from "@ax/helpers";
5
+ import { useFirefoxScrollLock } from "@ax/hooks";
6
+ import type { IBreadcrumbItem, IUserEditing } from "@ax/types";
6
7
 
7
8
  import Form from "./Form";
8
- import NavigationForm from "./NavigationForm";
9
9
  import GlobalPageForm from "./GlobalPageForm";
10
- import PreviewForm from "./PreviewForm";
11
10
  import Header from "./Header";
11
+ import NavigationForm from "./NavigationForm";
12
+ import PreviewForm from "./PreviewForm";
12
13
 
13
14
  import * as S from "./style";
15
+
14
16
  const navigationModulesTypes = ["header", "footer"];
15
17
 
16
- const ConfigPanel = (props: IStateProps): JSX.Element => {
18
+ const ConfigPanel = (props: IStateProps) => {
17
19
  const {
18
20
  isLoading,
19
21
  schema,
@@ -48,10 +50,12 @@ const ConfigPanel = (props: IStateProps): JSX.Element => {
48
50
  useEffect(() => {
49
51
  if (lastElementAddedId && wrapperRef.current) {
50
52
  const element = document.querySelector(`.editorId-${lastElementAddedId}`);
51
- element && element.scrollIntoView();
53
+ element?.scrollIntoView();
52
54
  }
53
55
  }, [lastElementAddedId]);
54
56
 
57
+ useFirefoxScrollLock(wrapperRef);
58
+
55
59
  if (isLoading || isEmptyObj(schema)) {
56
60
  return <Loading />;
57
61
  }
@@ -1,8 +1,51 @@
1
- import React from "react";
1
+ import type React from "react";
2
+ import { useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+
2
5
  import { trimText } from "@ax/helpers";
3
6
 
4
7
  import * as S from "./style";
5
8
 
9
+ const PortalTooltip = (props: {
10
+ elementsRows: string[][];
11
+ defaultColor?: string;
12
+ colors?: any;
13
+ prefix: string;
14
+ rounded: boolean;
15
+ size: "S" | "M";
16
+ tooltipRef: React.RefObject<HTMLDivElement>;
17
+ isVisible: boolean;
18
+ elementRect: DOMRect | null;
19
+ }) => {
20
+ const { elementsRows, defaultColor, colors, prefix, rounded, size, tooltipRef, isVisible, elementRect } = props;
21
+
22
+ if (!isVisible || !elementRect) return null;
23
+
24
+ const top = elementRect.top - 8;
25
+ const left = elementRect.left + elementRect.width / 2;
26
+
27
+ return createPortal(
28
+ <S.PortalTooltip data-testid="portal-tooltip" style={{ top, left }} ref={tooltipRef}>
29
+ {elementsRows.map((row, idx) => {
30
+ return (
31
+ <S.Row data-testid="row-element" key={idx}>
32
+ {row.map((element, idx) => {
33
+ const color = defaultColor || colors?.[element];
34
+ return (
35
+ <S.Element data-testid="div-element" key={idx} color={color} rounded={rounded} size={size}>
36
+ {prefix}
37
+ {element}
38
+ </S.Element>
39
+ );
40
+ })}
41
+ </S.Row>
42
+ );
43
+ })}
44
+ </S.PortalTooltip>,
45
+ document.body,
46
+ );
47
+ };
48
+
6
49
  const ElementsTooltip = (props: IElementsTooltipProps): JSX.Element => {
7
50
  const {
8
51
  elements,
@@ -16,6 +59,11 @@ const ElementsTooltip = (props: IElementsTooltipProps): JSX.Element => {
16
59
  size = "S",
17
60
  } = props;
18
61
 
62
+ const tooltipRef = useRef<HTMLDivElement>(null);
63
+ const elementRef = useRef<HTMLDivElement>(null);
64
+ const [isTooltipVisible, setIsTooltipVisible] = useState(false);
65
+ const [elementRect, setElementRect] = useState<DOMRect | null>(null);
66
+
19
67
  if (!elements) return <></>;
20
68
 
21
69
  const visibleElements = elements.slice(0, defaultElements);
@@ -28,42 +76,56 @@ const ElementsTooltip = (props: IElementsTooltipProps): JSX.Element => {
28
76
  elementsRows[row] = isNewRow ? [element] : [...elementsRows[row], element];
29
77
  });
30
78
 
79
+ const handleMouseEnter = () => {
80
+ if (elementRef.current) {
81
+ setElementRect(elementRef.current.getBoundingClientRect());
82
+ }
83
+ setIsTooltipVisible(true);
84
+ };
85
+
86
+ const handleMouseLeave = () => {
87
+ setIsTooltipVisible(false);
88
+ };
89
+
31
90
  return (
32
- <S.Wrapper data-testid="elements-wrapper">
33
- {visibleElements.map((fullElement, idx) => {
34
- const element = defaultElements === 1 && maxChar ? trimText(fullElement, maxChar) : fullElement;
35
- const color = defaultColor ? defaultColor : colors && colors[element] ? colors[element] : undefined;
91
+ <>
92
+ <S.Wrapper data-testid="elements-wrapper">
93
+ {visibleElements.map((fullElement, idx) => {
94
+ const element = defaultElements === 1 && maxChar ? trimText(fullElement, maxChar) : fullElement;
95
+ const color = defaultColor || colors?.[element];
36
96
 
37
- return (
38
- <S.Element data-testid="element" key={idx} color={color} rounded={rounded} size={size}>
39
- {prefix}
40
- {element}
97
+ return (
98
+ <S.Element data-testid="element" key={idx} color={color} rounded={rounded} size={size}>
99
+ {prefix}
100
+ {element}
101
+ </S.Element>
102
+ );
103
+ })}
104
+ {remainingElements > 0 && (
105
+ <S.Element
106
+ data-testid="remaining-element"
107
+ rounded={rounded}
108
+ size={size}
109
+ ref={elementRef}
110
+ onMouseEnter={handleMouseEnter}
111
+ onMouseLeave={handleMouseLeave}
112
+ >
113
+ +{remainingElements}
41
114
  </S.Element>
42
- );
43
- })}
44
- {remainingElements > 0 && (
45
- <S.Element data-testid="remaining-element" rounded={rounded} size={size}>
46
- +{remainingElements}
47
- <S.Tooltip>
48
- {elementsRows.map((row, idx) => {
49
- return (
50
- <S.Row data-testid="row-element" key={idx}>
51
- {row.map((element, idx) => {
52
- const color = defaultColor ? defaultColor : colors && colors[element] ? colors[element] : undefined;
53
- return (
54
- <S.Element data-testid="div-element" key={idx} color={color} rounded={rounded} size={size}>
55
- {prefix}
56
- {element}
57
- </S.Element>
58
- );
59
- })}
60
- </S.Row>
61
- );
62
- })}
63
- </S.Tooltip>
64
- </S.Element>
65
- )}
66
- </S.Wrapper>
115
+ )}
116
+ </S.Wrapper>
117
+ <PortalTooltip
118
+ elementsRows={elementsRows}
119
+ defaultColor={defaultColor}
120
+ colors={colors}
121
+ prefix={prefix}
122
+ rounded={rounded}
123
+ size={size}
124
+ tooltipRef={tooltipRef}
125
+ isVisible={isTooltipVisible}
126
+ elementRect={elementRect}
127
+ />
128
+ </>
67
129
  );
68
130
  };
69
131
 
@@ -26,6 +26,7 @@ const Element = styled.div<{ color?: string; rounded: boolean; size: "S" | "M" }
26
26
  background-color: ${(p) => (p.color ? p.color : p.theme.colors.uiBackground01)};
27
27
  border-radius: ${(p) => (p.rounded ? "34px" : p.theme.radii.xs)};
28
28
  white-space: nowrap;
29
+ cursor: pointer;
29
30
 
30
31
  &:hover > ${Tooltip} {
31
32
  display: block;
@@ -36,4 +37,14 @@ const Row = styled.div`
36
37
  display: flex;
37
38
  `;
38
39
 
39
- export { Wrapper, Element, Tooltip, Row };
40
+ const PortalTooltip = styled.div`
41
+ position: fixed;
42
+ z-index: 9999;
43
+ padding: ${(p) => p.theme.spacing.s};
44
+ box-shadow: ${(p) => p.theme.shadow.shadowL};
45
+ background-color: ${(p) => p.theme.colors.uiBarBackground};
46
+ border-radius: ${(p) => p.theme.radii.s};
47
+ transform: translate(-50%, -100%);
48
+ `;
49
+
50
+ export { Wrapper, Element, Tooltip, Row, PortalTooltip };
@@ -1,9 +1,8 @@
1
- import React from "react";
2
-
3
- import { useModal } from "@ax/hooks";
4
- import { ISite } from "@ax/types";
1
+ import { FileGallery, Icon, Modal, TextField } from "@ax/components";
2
+ import { VALID_DOCUMENT_FORMATS } from "@ax/constants";
5
3
  import { formatBytes, getFormattedDateWithTimezone } from "@ax/helpers";
6
- import { Icon, Modal, TextField, FileGallery } from "@ax/components";
4
+ import { useModal } from "@ax/hooks";
5
+ import type { ISite } from "@ax/types";
7
6
 
8
7
  import * as S from "./style";
9
8
 
@@ -12,11 +11,8 @@ const FileField = (props: IFileFieldProps): JSX.Element => {
12
11
 
13
12
  const { isOpen, toggleModal } = useModal(false);
14
13
 
15
- const validFormats =
16
- allowedFormats && allowedFormats.length
17
- ? allowedFormats
18
- : ["pdf", "doc", "docx", "xls", "xlsx", "zip", "csv", "txt", "mov", "mp4", "wmv", "avi", "webm", "mkv"];
19
- const videoFormats = ["mov", "mp4", "wmv", "avi", "webm", "mkv"];
14
+ const hasCustomFormats = !!allowedFormats?.length;
15
+
20
16
  const handleClick = () => {
21
17
  if (!disabled) {
22
18
  toggleModal();
@@ -25,11 +21,10 @@ const FileField = (props: IFileFieldProps): JSX.Element => {
25
21
 
26
22
  const addFile = (newFile: any) => {
27
23
  onChange(newFile);
28
- toggleModal();
29
24
  };
30
25
 
31
26
  const handleOnClickUrl = () => {
32
- if (value && value.url) {
27
+ if (value?.url) {
33
28
  const win = window.open(value.url, "_blank");
34
29
  if (win) {
35
30
  win.focus();
@@ -60,9 +55,9 @@ const FileField = (props: IFileFieldProps): JSX.Element => {
60
55
  },
61
56
  ];
62
57
 
63
- const fileName = value && value.url ? value.url.split("/").pop() : "File title";
64
- const fileDate = value && value.uploadDate ? getFormattedDateWithTimezone(value.uploadDate, "d MMM Y") : "--";
65
- const fileSize = value && value.sizeBytes ? formatBytes(value.sizeBytes) : "-- KB";
58
+ const fileName = value?.url ? value.url.split("/").pop() : "File title";
59
+ const fileDate = value?.uploadDate ? getFormattedDateWithTimezone(value.uploadDate, "d MMM Y") : "--";
60
+ const fileSize = value?.sizeBytes ? formatBytes(value.sizeBytes) : "-- KB";
66
61
 
67
62
  return (
68
63
  <>
@@ -73,7 +68,7 @@ const FileField = (props: IFileFieldProps): JSX.Element => {
73
68
  <div>{fileSize}</div>
74
69
  </S.FileData>
75
70
  </S.FileDataWrapper>
76
- {value && value.url && (
71
+ {value?.url && (
77
72
  <S.TextFieldWrapper data-testid="text-field-wrapper">
78
73
  <TextField
79
74
  name="url"
@@ -93,7 +88,8 @@ const FileField = (props: IFileFieldProps): JSX.Element => {
93
88
  </S.IconWrapper>
94
89
  </S.Field>
95
90
  <S.HelpText>
96
- Valid formats: {validFormats.filter((format) => !videoFormats.includes(format)).join(", ")} and videos. Max.
91
+ Valid formats:{" "}
92
+ {hasCustomFormats ? allowedFormats.join(", ") : `${VALID_DOCUMENT_FORMATS.join(", ")} and videos`}. Max.
97
93
  size: 50MB
98
94
  </S.HelpText>
99
95
  </>
@@ -108,7 +104,9 @@ const FileField = (props: IFileFieldProps): JSX.Element => {
108
104
  </S.Component>
109
105
  )}
110
106
  <Modal isOpen={isOpen} hide={toggleModal} size="XL" title="Select file">
111
- {isOpen && <FileGallery validFormats={validFormats} addFile={addFile} toggleModal={toggleModal} site={site} />}
107
+ {isOpen && (
108
+ <FileGallery customFormats={allowedFormats} addFile={addFile} toggleModal={toggleModal} site={site} />
109
+ )}
112
110
  </Modal>
113
111
  </>
114
112
  );
@@ -28,7 +28,7 @@ const HeadingField = (props: IHeadingFieldProps): JSX.Element => {
28
28
  return (
29
29
  <>
30
30
  {toolbar ? (
31
- <Wysiwyg title={title} site={site} inline={true} value={contentValue} onChange={handleChange} />
31
+ <Wysiwyg site={site} inline={true} value={contentValue} onChange={handleChange} />
32
32
  ) : (
33
33
  <TextField value={contentValue} onChange={handleChange} />
34
34
  )}
@@ -1,10 +1,9 @@
1
- import React, { memo, useEffect, useRef, useState } from "react";
2
- import { Icon, IconAction, Gallery, Modal, Image, FieldsBehavior } from "@ax/components";
1
+ import { memo, useEffect, useMemo, useRef, useState } from "react";
3
2
 
3
+ import { FieldsBehavior, Gallery, Icon, IconAction, Image, ImageDragAndDrop, Modal } from "@ax/components";
4
4
  import { formatBytes, getFormattedDateWithTimezone } from "@ax/helpers";
5
- import { IImage, ISite } from "@ax/types";
6
5
  import { useModal } from "@ax/hooks";
7
- import ImageDragAndDrop from "@ax/modules/MediaGallery/ImageDragAndDrop";
6
+ import type { IImage, ISite } from "@ax/types";
8
7
 
9
8
  import * as S from "./style";
10
9
 
@@ -30,42 +29,17 @@ const ImageField = (props: IImageFieldProps) => {
30
29
  const { isOpen, toggleModal } = useModal(isModalOpen);
31
30
  const { isOpen: isOpenDD, toggleModal: toggleModalDD } = useModal(isModalOpen);
32
31
  const [previewSrc, setPreviewSrc] = useState<string>();
33
- const [previewHeight, setPreviewHeight] = useState<{ height: string | number }>({ height: "auto" });
34
32
  const previewRef = useRef<HTMLDivElement>(null);
35
- const [imageLoaded, setImageLoaded] = useState(false);
36
-
37
- const validFormats = ["jpeg", "jpg", "png", "svg", "gif"];
38
33
 
39
34
  const imageUrl = value ? (typeof value === "string" ? value : value.url) : "";
40
35
  const imagePosition = value && typeof value === "object" ? value.position : "center";
41
36
 
42
37
  useEffect(() => {
43
38
  setPreviewSrc(imageUrl);
44
- setImageLoaded(false);
45
39
  }, [imageUrl]);
46
40
 
47
- const calculateImageHeight = () => {
48
- if (previewRef.current) {
49
- const height = previewRef.current.getBoundingClientRect().height;
50
-
51
- if (!cropPreview && height < 180) {
52
- setPreviewHeight({ height: "180px" });
53
- } else {
54
- setPreviewHeight({ height: "auto" });
55
- }
56
- }
57
- };
58
-
59
- const handleImageLoad = () => {
60
- if (!imageLoaded) {
61
- calculateImageHeight();
62
- setImageLoaded(true);
63
- }
64
- };
65
-
66
41
  const handleDelete = () => {
67
42
  if (!disabled) {
68
- setPreviewHeight({ height: "auto" });
69
43
  setPreviewSrc("");
70
44
  onChange({});
71
45
  }
@@ -76,7 +50,7 @@ const ImageField = (props: IImageFieldProps) => {
76
50
  const url = typeof img === "string" ? img : img.url;
77
51
  setPreviewSrc(url);
78
52
  onChange(img);
79
- setIsGalleryOpened && setIsGalleryOpened();
53
+ setIsGalleryOpened?.();
80
54
  error && handleValidation && handleValidation(url, validators);
81
55
  isOpenDD && toggleModalDD();
82
56
  }
@@ -84,14 +58,14 @@ const ImageField = (props: IImageFieldProps) => {
84
58
 
85
59
  const handleClick = () => {
86
60
  if (!disabled) {
87
- setIsGalleryOpened && setIsGalleryOpened();
61
+ setIsGalleryOpened?.();
88
62
  noGallery ? toggleModalDD() : toggleModal();
89
63
  }
90
64
  };
91
65
 
92
66
  const handleChange = () => {
93
67
  if (!disabled) {
94
- setIsGalleryOpened && setIsGalleryOpened();
68
+ setIsGalleryOpened?.();
95
69
  noGallery ? toggleModalDD() : toggleModal();
96
70
  }
97
71
  };
@@ -113,6 +87,8 @@ const ImageField = (props: IImageFieldProps) => {
113
87
 
114
88
  const handleUpload = (file: IImage[]) => getImageSelected(file[0]);
115
89
 
90
+ const imageElement = useMemo(() => <Image url={previewSrc} width="320" />, [previewSrc]);
91
+
116
92
  return (
117
93
  <>
118
94
  <S.FieldWrapper
@@ -139,11 +115,7 @@ const ImageField = (props: IImageFieldProps) => {
139
115
  </S.ImageDataWrapper>
140
116
  )}
141
117
  <S.Preview preview={!!previewSrc} data-testid="previewDiv" ref={previewRef}>
142
- {previewSrc && (
143
- <S.ImageContainer style={{ height: previewHeight.height }}>
144
- <Image url={previewSrc} width={320} onLoad={handleImageLoad} />
145
- </S.ImageContainer>
146
- )}
118
+ {previewSrc && <S.ImageContainer cropPreview={cropPreview}>{imageElement}</S.ImageContainer>}
147
119
  <S.PositionWrapper>
148
120
  <S.PositionTitle>
149
121
  <Icon name="grid" size="16" />
@@ -184,7 +156,6 @@ const ImageField = (props: IImageFieldProps) => {
184
156
  <ImageDragAndDrop
185
157
  siteID={site ? site.id : "global"}
186
158
  isAllowedToUpload={true}
187
- validFormats={validFormats}
188
159
  handleUpload={handleUpload}
189
160
  visible={false}
190
161
  />
@@ -143,10 +143,21 @@ const ImageDataWrapper = styled.div`
143
143
  max-width: ${(p) => `calc(${p.theme.spacing.xl} * 5)`};
144
144
  `;
145
145
 
146
- const ImageContainer = styled.div`
146
+ const ImageContainer = styled.div<{ cropPreview?: boolean }>`
147
147
  display: flex;
148
148
  justify-content: center;
149
149
  align-items: center;
150
+ ${(p) => (p.cropPreview ? "height: 180px;" : "min-height: 180px;")}
151
+ width: 100%;
152
+ overflow: hidden;
153
+
154
+ img {
155
+ object-fit: ${(p) => (p.cropPreview ? "cover" : "contain")};
156
+ object-position: center;
157
+ max-width: 100%;
158
+ height: 100%;
159
+ width: 100%;
160
+ }
150
161
  `;
151
162
 
152
163
  const FileName = styled.div`
@@ -1,4 +1,4 @@
1
- import React, { useMemo } from "react";
1
+ import { useMemo } from "react";
2
2
 
3
3
  import * as S from "./style";
4
4