@griddo/ax 11.4.14 → 11.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@griddo/ax",
3
3
  "description": "Griddo Author Experience",
4
- "version": "11.4.14",
4
+ "version": "11.4.15",
5
5
  "authors": [
6
6
  "Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
7
7
  "Diego M. Béjar <diego.bejar@secuoyas.com>",
@@ -118,7 +118,7 @@
118
118
  "react-dom": "18.2.0",
119
119
  "react-draft-wysiwyg": "1.15.0",
120
120
  "react-error-boundary": "4.0.13",
121
- "react-froala-wysiwyg": "3.2.7",
121
+ "react-froala-wysiwyg": "4.0.4",
122
122
  "react-redux": "7.2.9",
123
123
  "react-refresh": "0.14.2",
124
124
  "react-router-dom": "5.1.2",
@@ -152,6 +152,10 @@
152
152
  },
153
153
  "devDependencies": {
154
154
  "@babel/core": "7.26.10",
155
+ "@babel/helper-environment-visitor": "7.24.7",
156
+ "@babel/helper-function-name": "7.24.7",
157
+ "@babel/helper-hoist-variables": "7.24.7",
158
+ "@babel/helper-split-export-declaration": "7.24.7",
155
159
  "@babel/plugin-transform-optional-chaining": "7.25.9",
156
160
  "@babel/preset-env": "7.26.9",
157
161
  "@babel/preset-react": "7.26.3",
@@ -220,5 +224,5 @@
220
224
  "publishConfig": {
221
225
  "access": "public"
222
226
  },
223
- "gitHead": "91a7a9998319a6dc21dab4a22d38e87f8b59b6c5"
227
+ "gitHead": "f08b17b4a6fc51dfa1a5dbec2107b0442341df45"
224
228
  }
package/src/api/utils.tsx CHANGED
@@ -108,6 +108,11 @@ export const sendInitialRequest = async (
108
108
  const response = await axios(config);
109
109
  return response;
110
110
  } catch (e) {
111
+ if (e.message !== "Network Error") {
112
+ return (
113
+ e.response || { status: e.status, data: { code: e.code || "unknown", message: e.message || "Platform error" } }
114
+ );
115
+ }
111
116
  return e.response;
112
117
  }
113
118
  };
@@ -146,6 +151,11 @@ export const sendRequest = async (
146
151
  // return response;
147
152
  return await axios(requestConfig);
148
153
  } catch (e) {
154
+ if (e.message !== "Network Error") {
155
+ return (
156
+ e.response || { status: e.status, data: { code: e.code || "unknown", message: e.message || "Platform error" } }
157
+ );
158
+ }
149
159
  return e.response;
150
160
  }
151
161
  };
@@ -174,6 +184,11 @@ export const sendUploadRequest = async (
174
184
 
175
185
  return await axios(requestConfig);
176
186
  } catch (e) {
187
+ if (e.message !== "Network Error") {
188
+ return (
189
+ e.response || { status: e.status, data: { code: e.code || "unknown", message: e.message || "Platform error" } }
190
+ );
191
+ }
177
192
  return e.response;
178
193
  }
179
194
  };
@@ -1,11 +1,11 @@
1
1
  import React, { memo } from "react";
2
- import { IHeadingField } from "@ax/types";
3
- import { FieldsBehavior, TextField } from "@ax/components";
2
+ import { IHeadingField, ISite } from "@ax/types";
3
+ import { FieldsBehavior, TextField, Wysiwyg } from "@ax/components";
4
4
 
5
5
  import * as S from "./style";
6
6
 
7
7
  const HeadingField = (props: IHeadingFieldProps): JSX.Element => {
8
- const { value, onChange, options, showAdvanced } = props;
8
+ const { value, onChange, options, showAdvanced, toolbar = false } = props;
9
9
 
10
10
  const getContentValue = () => value?.content || props.default?.content || "";
11
11
  const getTagValue = () => value?.tag || props.default?.tag || "";
@@ -22,7 +22,11 @@ const HeadingField = (props: IHeadingFieldProps): JSX.Element => {
22
22
 
23
23
  return (
24
24
  <>
25
- <TextField {...props} value={getContentValue()} onChange={handleChange} />
25
+ {toolbar ? (
26
+ <Wysiwyg {...props} inline={true} value={getContentValue()} onChange={handleChange} />
27
+ ) : (
28
+ <TextField {...props} value={getContentValue()} onChange={handleChange} />
29
+ )}
26
30
  {showAdvanced && (
27
31
  <S.AdvancedWrapper data-testid="text-field-advanced-wrapper">
28
32
  <FieldsBehavior fieldType="Select" options={options} value={getTagValue()} onChange={handleSelectChange} />
@@ -39,6 +43,8 @@ interface IHeadingFieldProps {
39
43
  options: IOption[];
40
44
  showAdvanced: boolean;
41
45
  default: IHeadingField;
46
+ site: ISite;
47
+ toolbar?: boolean;
42
48
  }
43
49
 
44
50
  interface IOption {
@@ -1,4 +1,6 @@
1
- import { getRichTextConfig, parseClassNames } from "./helpers";
1
+ import FroalaEditor from "froala-editor";
2
+ import { getLanguageMenuHtml, getRichTextConfig, parseClassNames } from "./helpers";
3
+ import { languages } from "./languages";
2
4
 
3
5
  const API_URL = process.env.GRIDDO_API_URL || process.env.REACT_APP_API_ENDPOINT;
4
6
  const richTextConfig = getRichTextConfig();
@@ -23,6 +25,7 @@ const miscButtons = [
23
25
  "html",
24
26
  "insertTable",
25
27
  paragraphStyles ? "paragraphStyle" : undefined,
28
+ "langDropdown",
26
29
  ];
27
30
 
28
31
  const buttonsFull = {
@@ -42,9 +45,19 @@ const buttonsFull = {
42
45
  };
43
46
 
44
47
  const buttons = [
45
- ["bold", "italic", "formatUL", "insertLink", "paragraphFormat", paragraphStyles ? "paragraphStyle" : undefined],
48
+ [
49
+ "bold",
50
+ "italic",
51
+ "formatUL",
52
+ "insertLink",
53
+ "paragraphFormat",
54
+ paragraphStyles ? "paragraphStyle" : undefined,
55
+ "langDropdown",
56
+ ],
46
57
  ];
47
58
 
59
+ const inlineToolbar = ["langDropdown", "undo", "redo"];
60
+
48
61
  const wysiwygConfig = {
49
62
  key: process.env.REACT_APP_FROALA_KEY,
50
63
  // pastePlain: false,
@@ -85,4 +98,29 @@ const wysiwygConfig = {
85
98
  imageUploadRemoteUrls: false,
86
99
  };
87
100
 
88
- export { buttonsFull, buttons, wysiwygConfig };
101
+ FroalaEditor.DefineIcon("langIcon", {
102
+ PATH: "m12.87 15.07-2.54-2.51.03-.03A17.52 17.52 0 0 0 14.07 6H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04ZM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12Zm-2.62 7 1.62-4.33L19.12 17h-3.24Z",
103
+ template: "svg",
104
+ });
105
+
106
+ FroalaEditor.RegisterCommand("langDropdown", {
107
+ title: "Language",
108
+ type: "dropdown",
109
+ icon: "langIcon",
110
+ undo: true,
111
+ focus: true,
112
+ refreshAfterCallback: true,
113
+ html: function () {
114
+ return getLanguageMenuHtml(richTextConfig?.editorLangs ? richTextConfig.editorLangs : languages);
115
+ },
116
+ callback: function (cmd: any, val: string) {
117
+ const html = this.selection.text();
118
+ if (val !== "") {
119
+ this.html.insert(`<span lang="${val}" translate="no">${html}</span>`, true);
120
+ } else {
121
+ this.html.insert(html, true);
122
+ }
123
+ },
124
+ });
125
+
126
+ export { buttonsFull, buttons, inlineToolbar, wysiwygConfig };
@@ -1,3 +1,6 @@
1
+ /* eslint-disable jsx-a11y/anchor-is-valid */
2
+ import React from "react";
3
+ import { renderToString } from "react-dom/server";
1
4
  import { config } from "components";
2
5
 
3
6
  const getRichTextConfig = (): IRichTextConfig | null => {
@@ -14,6 +17,72 @@ interface IRichTextConfig {
14
17
  paragraphStyles?: { label: string; className: string }[];
15
18
  tableStyles?: { label: string; className: string }[];
16
19
  tableCellStyles?: { label: string; className: string }[];
20
+ editorLangs?: { name: string; iso: string }[];
17
21
  }
18
22
 
19
- export { getRichTextConfig, parseClassNames };
23
+ const getLanguageMenuHtml = (languages: { name: string; iso: string; featured?: boolean }[]) => {
24
+ const borderStyle = {
25
+ borderBottom: "1px solid #DBDDE9",
26
+ paddingBottom: "4px",
27
+ marginBottom: "4px",
28
+ };
29
+
30
+ const featuredLanguages = languages
31
+ .filter((lang) => lang.featured === true)
32
+ .map((lang, index, { length }) => (
33
+ <li role="presentation" key={lang.iso} style={index + 1 === length ? borderStyle : {}}>
34
+ <a
35
+ className="fr-command"
36
+ tabIndex={-1}
37
+ role="option"
38
+ data-cmd="langDropdown"
39
+ data-param1={lang.iso}
40
+ title={lang.name}
41
+ aria-selected="false"
42
+ >
43
+ {lang.name}
44
+ </a>
45
+ </li>
46
+ ));
47
+
48
+ const otherLanguages = languages
49
+ .filter((lang) => lang.featured !== true)
50
+ .map((lang) => (
51
+ <li role="presentation" key={lang.iso}>
52
+ <a
53
+ className="fr-command"
54
+ tabIndex={-1}
55
+ role="option"
56
+ data-cmd="langDropdown"
57
+ data-param1={lang.iso}
58
+ title={lang.name}
59
+ aria-selected="false"
60
+ >
61
+ {lang.name}
62
+ </a>
63
+ </li>
64
+ ));
65
+
66
+ return renderToString(
67
+ <ul className="fr-dropdown-list" role="presentation">
68
+ <li role="presentation" key="remove" style={borderStyle}>
69
+ <a
70
+ className="fr-command"
71
+ tabIndex={-1}
72
+ role="option"
73
+ data-cmd="langDropdown"
74
+ data-param1={""}
75
+ title="Remove"
76
+ aria-selected="false"
77
+ style={{ fontStyle: "italic" }}
78
+ >
79
+ Remove
80
+ </a>
81
+ </li>
82
+ {featuredLanguages}
83
+ {otherLanguages}
84
+ </ul>
85
+ );
86
+ };
87
+
88
+ export { getRichTextConfig, parseClassNames, getLanguageMenuHtml };
@@ -1,11 +1,12 @@
1
1
  import React from "react";
2
2
  import { connect } from "react-redux";
3
- import FroalaEditor from "react-froala-wysiwyg";
3
+ import FroalaEditorComponent from "react-froala-wysiwyg";
4
+ import FroalaEditor from "froala-editor";
4
5
  import { decodeEntities } from "@ax/helpers";
5
6
  import { IImage, ISite, IRootState } from "@ax/types";
6
7
  import { galleryActions } from "@ax/containers/Gallery";
7
8
 
8
- import { wysiwygConfig, buttons, buttonsFull } from "./config";
9
+ import { wysiwygConfig, buttons, buttonsFull, inlineToolbar } from "./config";
9
10
  import "./vendors";
10
11
  import * as S from "./style";
11
12
 
@@ -21,6 +22,7 @@ const Wysiwyg = (props: IWysiwygProps): JSX.Element => {
21
22
  site,
22
23
  token,
23
24
  uploadImage,
25
+ inline = false,
24
26
  } = props;
25
27
 
26
28
  const imageSite = site ? site.id : "global";
@@ -33,13 +35,18 @@ const Wysiwyg = (props: IWysiwygProps): JSX.Element => {
33
35
 
34
36
  const dynamicConfig = {
35
37
  placeholderText: placeholder,
36
- toolbarButtons: full ? buttonsFull : buttons,
37
- imageUpload: full ? true : false,
38
+ toolbarButtons: inline ? inlineToolbar : full ? buttonsFull : buttons,
39
+ toolbarInline: inline,
40
+ charCounterCount: !inline,
41
+ imageUpload: full && !inline ? true : false,
42
+ multiLine: !inline,
43
+ enter: inline ? FroalaEditor.ENTER_BR : FroalaEditor.ENTER_P,
38
44
  requestHeaders: {
39
45
  Authorization: `bearer ${token}`,
40
46
  },
41
47
  events: {
42
48
  initialized() {
49
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
43
50
  const editor: any = this;
44
51
  if (disabled) {
45
52
  setTimeout(() => {
@@ -48,12 +55,14 @@ const Wysiwyg = (props: IWysiwygProps): JSX.Element => {
48
55
  }
49
56
  },
50
57
  "image.beforeUpload": async function (images: FileList) {
58
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
51
59
  const editor: any = this;
52
60
  const { url } = await uploadImage(images[0], imageSite);
53
61
  editor.image.insert(url, true, null, editor.image.get(), null);
54
62
  return false;
55
63
  },
56
64
  blur: function () {
65
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
57
66
  const editor: any = this;
58
67
  const html = editor.html.get();
59
68
  const stripedHtml = decodeEntities(html);
@@ -66,13 +75,12 @@ const Wysiwyg = (props: IWysiwygProps): JSX.Element => {
66
75
 
67
76
  return (
68
77
  <S.EditorWrapper error={error} disabled={disabled} data-testid="wysiwyg-wrapper">
69
- <FroalaEditor tag="textarea" model={value} config={config} onModelChange={handleChange} />
78
+ <FroalaEditorComponent tag="textarea" model={value} config={config} onModelChange={handleChange} />
70
79
  </S.EditorWrapper>
71
80
  );
72
81
  };
73
82
 
74
83
  interface IProps {
75
- name: string;
76
84
  value: string;
77
85
  title: string;
78
86
  full?: boolean;
@@ -84,6 +92,7 @@ interface IProps {
84
92
  disabled?: boolean;
85
93
  handleValidation?: (value: string) => void;
86
94
  site: ISite;
95
+ inline?: boolean;
87
96
  }
88
97
 
89
98
  const mapStateToProps = (state: IRootState) => ({
@@ -0,0 +1,136 @@
1
+ const languages = [
2
+ {
3
+ name: "Arabic (Saudi Arabia)",
4
+ iso: "ar-SA",
5
+ },
6
+ {
7
+ name: "Chinese (China)",
8
+ iso: "zh-CN",
9
+ },
10
+ {
11
+ name: "Chinese (Taiwan)",
12
+ iso: "zh-TW",
13
+ },
14
+ {
15
+ name: "Czech",
16
+ iso: "cs-CZ",
17
+ },
18
+ {
19
+ name: "Danish",
20
+ iso: "da-DK",
21
+ },
22
+ {
23
+ name: "Dutch",
24
+ iso: "nl-NL",
25
+ },
26
+ {
27
+ name: "English (United Kingdom)",
28
+ iso: "en-GB",
29
+ },
30
+ {
31
+ name: "English (United States)",
32
+ iso: "en-US",
33
+ },
34
+ {
35
+ name: "Finnish",
36
+ iso: "fi-FI",
37
+ },
38
+ {
39
+ name: "French (Canada)",
40
+ iso: "fr-CA",
41
+ },
42
+ {
43
+ name: "French (France)",
44
+ iso: "fr-FR",
45
+ },
46
+ {
47
+ name: "German",
48
+ iso: "de-DE",
49
+ },
50
+ {
51
+ name: "Greek",
52
+ iso: "el-GR",
53
+ },
54
+ {
55
+ name: "Hebrew",
56
+ iso: "he-IL",
57
+ },
58
+ {
59
+ name: "Hindi",
60
+ iso: "hi-IN",
61
+ },
62
+ {
63
+ name: "Hungarian",
64
+ iso: "hu-HU",
65
+ },
66
+ {
67
+ name: "Italian",
68
+ iso: "it-IT",
69
+ },
70
+ {
71
+ name: "Japanese",
72
+ iso: "ja-JP",
73
+ },
74
+ {
75
+ name: "Korean",
76
+ iso: "ko-KR",
77
+ },
78
+ {
79
+ name: "Norwegian",
80
+ iso: "no-NO",
81
+ },
82
+ {
83
+ name: "Polish",
84
+ iso: "pl-PL",
85
+ },
86
+ {
87
+ name: "Portuguese (Brazil)",
88
+ iso: "pt-BR",
89
+ },
90
+ {
91
+ name: "Portuguese (Portugal)",
92
+ iso: "pt-PT",
93
+ },
94
+ {
95
+ name: "Romanian",
96
+ iso: "ro-RO",
97
+ },
98
+ {
99
+ name: "Russian",
100
+ iso: "ru-RU",
101
+ },
102
+ {
103
+ name: "Slovak",
104
+ iso: "sk-SK",
105
+ },
106
+ {
107
+ name: "Spanish (Mexico)",
108
+ iso: "es-MX",
109
+ },
110
+ {
111
+ name: "Spanish (Spain)",
112
+ iso: "es-ES",
113
+ },
114
+ {
115
+ name: "Swedish",
116
+ iso: "sv-SE",
117
+ },
118
+ {
119
+ name: "Thai",
120
+ iso: "th-TH",
121
+ },
122
+ {
123
+ name: "Turkish",
124
+ iso: "tr-TR",
125
+ },
126
+ {
127
+ name: "Ukrainian",
128
+ iso: "uk-UA",
129
+ },
130
+ {
131
+ name: "Vietnamese",
132
+ iso: "vi-VN",
133
+ },
134
+ ];
135
+
136
+ export { languages };
@@ -22,6 +22,9 @@ export const EditorWrapper = styled.div<{ error: boolean | undefined; disabled?:
22
22
  ${(p) => p.theme.textStyle.fieldContent};
23
23
  margin-block-start: 1em;
24
24
  margin-block-end: 1em;
25
+ span[lang] {
26
+ color: #F9861B;
27
+ }
25
28
  }
26
29
  p:first-child {
27
30
  margin-block-start: 0;
@@ -59,6 +62,13 @@ export const EditorWrapper = styled.div<{ error: boolean | undefined; disabled?:
59
62
  .fr-popup.fr-desktop.fr-active {
60
63
  margin-left: -70px;
61
64
  }
65
+
66
+ .fr-command.fr-btn svg {
67
+ margin: 5px 5px;
68
+ }
69
+ }
70
+ .fr-toolbar.fr-toolbar-open {
71
+ padding-bottom: 16px;
62
72
  }
63
73
  .fr-second-toolbar {
64
74
  border-color: ${(p) =>
@@ -71,15 +81,15 @@ export const EditorWrapper = styled.div<{ error: boolean | undefined; disabled?:
71
81
  p.error === true
72
82
  ? p.theme.color.error
73
83
  : p.disabled
74
- ? p.theme.color.interactiveDisabled
75
- : p.theme.color.uiLine};
84
+ ? p.theme.color.interactiveDisabled
85
+ : p.theme.color.uiLine};
76
86
  border-right: 1px solid
77
87
  ${(p) =>
78
88
  p.error === true
79
89
  ? p.theme.color.error
80
90
  : p.disabled
81
- ? p.theme.color.interactiveDisabled
82
- : p.theme.color.uiLine};
91
+ ? p.theme.color.interactiveDisabled
92
+ : p.theme.color.uiLine};
83
93
  }
84
94
  .fr-disabled {
85
95
  color: ${(p) => p.theme.color.interactiveDisabled};
@@ -91,8 +101,8 @@ export const EditorWrapper = styled.div<{ error: boolean | undefined; disabled?:
91
101
  p.error === true
92
102
  ? p.theme.color.error
93
103
  : p.disabled
94
- ? p.theme.color.interactiveDisabled
95
- : p.theme.color.interactive01};
104
+ ? p.theme.color.interactiveDisabled
105
+ : p.theme.color.interactive01};
96
106
  }
97
107
  .fr-wrapper {
98
108
  border-left: 1px solid
@@ -100,16 +110,49 @@ export const EditorWrapper = styled.div<{ error: boolean | undefined; disabled?:
100
110
  p.error === true
101
111
  ? p.theme.color.error
102
112
  : p.disabled
103
- ? p.theme.color.interactiveDisabled
104
- : p.theme.color.interactive01};
113
+ ? p.theme.color.interactiveDisabled
114
+ : p.theme.color.interactive01};
105
115
  border-right: 1px solid
106
116
  ${(p) =>
107
117
  p.error === true
108
118
  ? p.theme.color.error
109
119
  : p.disabled
120
+ ? p.theme.color.interactiveDisabled
121
+ : p.theme.color.interactive01};
122
+ }
123
+ }
124
+ }
125
+ .fr-inline {
126
+ .fr-wrapper {
127
+ background: ${(p) => p.theme.color.uiBackground02};
128
+ border-radius: ${(p) => p.theme.radii.s};
129
+ min-height: ${(p) => p.theme.spacing.l};
130
+ border: 1px solid
131
+ ${(p) =>
132
+ p.error === true
133
+ ? p.theme.color.error
134
+ : p.disabled
110
135
  ? p.theme.color.interactiveDisabled
111
- : p.theme.color.interactive01};
136
+ : p.theme.color.uiLine};
137
+ }
138
+ .fr-element {
139
+ ${(p) => p.theme.textStyle.fieldContent};
140
+ color: ${(p) => p.theme.color.textHighEmphasis};
141
+ padding: ${(p) => `12px ${p.theme.spacing.s}`};
142
+
143
+ span[lang] {
144
+ color: #F9861B;
112
145
  }
113
146
  }
147
+ ${Wrapper}:focus-within & {
148
+ .fr-wrapper {
149
+ border: 1px solid
150
+ ${(p) =>
151
+ p.error === true
152
+ ? p.theme.color.error
153
+ : p.disabled
154
+ ? p.theme.color.interactiveDisabled
155
+ : p.theme.color.interactive01};
156
+ }
114
157
  }
115
158
  `;
@@ -0,0 +1,10 @@
1
+ import * as React from "react";
2
+ const SvgTranslate = (props) => (
3
+ <svg xmlns="http://www.w3.org/2000/svg" width={24} height={24} fill="none" {...props}>
4
+ <path
5
+ fill="#5057FF"
6
+ d="m12.87 15.07-2.54-2.51.03-.03A17.52 17.52 0 0 0 14.07 6H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04ZM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12Zm-2.62 7 1.62-4.33L19.12 17h-3.24Z"
7
+ />
8
+ </svg>
9
+ );
10
+ export default SvgTranslate;
@@ -0,0 +1,3 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M12.87 15.07L10.33 12.56L10.36 12.53C12.1 10.59 13.34 8.36 14.07 6H17V4H10V2H8V4H1V5.99H12.17C11.5 7.92 10.44 9.75 9 11.35C8.07 10.32 7.3 9.19 6.69 8H4.69C5.42 9.63 6.42 11.17 7.67 12.56L2.58 17.58L4 19L9 14L12.11 17.11L12.87 15.07ZM18.5 10H16.5L12 22H14L15.12 19H19.87L21 22H23L18.5 10ZM15.88 17L17.5 12.67L19.12 17H15.88Z" fill="#5057FF"/>
3
+ </svg>
@@ -1,5 +1,7 @@
1
1
  import React from "react";
2
2
  import { IErrorItem, ILanguage } from "@ax/types";
3
+ import { useNetworkStatus } from "@ax/hooks";
4
+ import { Notification } from "@ax/components";
3
5
 
4
6
  import AppBar from "./AppBar";
5
7
 
@@ -8,11 +10,19 @@ import * as S from "./style";
8
10
  const MainWrapper = (props: IWrapperProps): JSX.Element => {
9
11
  const { children, fixedAppBar, fullWidth, hasAnimation } = props;
10
12
 
13
+ const { isOnline } = useNetworkStatus();
14
+
11
15
  return (
12
16
  <S.Wrapper fixedAppBar={fixedAppBar} data-testid="main-wrapper">
13
17
  {hasAnimation && <S.BackgroundAnimation />}
14
18
  <AppBar {...props} />
15
19
  <S.Main fullWidth={fullWidth} data-testid="main-component" id="main-component">
20
+ {!isOnline && (
21
+ <Notification
22
+ type="error"
23
+ text="You are offline. Please, check your internet connection. Changes you make may not be saved."
24
+ />
25
+ )}
16
26
  {children}
17
27
  </S.Main>
18
28
  </S.Wrapper>
package/src/global.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  declare module "node";
2
2
  declare module "components";
3
+ declare module "froala-editor";
3
4
  declare module "react-froala-wysiwyg";
4
5
  declare module "lodash.isequal";
5
6
  declare module "is-wsl";
@@ -1,4 +1,3 @@
1
- import { setError } from "@ax/containers/App/actions";
2
1
  import { Dispatch } from "redux";
3
2
 
4
3
  const isReqOk = (reqStatus: number) => reqStatus >= 200 && reqStatus < 400;
@@ -15,23 +14,14 @@ function handleRequest(
15
14
 
16
15
  const response = await callback();
17
16
 
18
- if (!response) {
19
- dispatch(
20
- setError({
21
- text: "You are offline. Please, check your internet connection. Changes you make may not be saved.",
22
- })
23
- );
24
- return false;
25
- }
26
-
27
17
  const responseArr = Array.isArray(response) ? response : [response];
28
18
 
29
19
  let result = true;
30
20
 
31
21
  responseArr.forEach((response) => {
32
- if (!isReqOk(response.status)) {
22
+ if (!response || !isReqOk(response.status)) {
33
23
  result = false;
34
- handleError(response);
24
+ !!response && handleError(response);
35
25
  }
36
26
  });
37
27
 
@@ -7,6 +7,7 @@ import { useWindowSize } from "./window";
7
7
  import { useOnMessageReceivedFromIframe, useOnMessageReceivedFromOutside } from "./iframe";
8
8
  import { usePermission, useGlobalPermission } from "./users";
9
9
  import { useResizable } from "./resize";
10
+ import { useNetworkStatus } from "./network";
10
11
 
11
12
  export {
12
13
  useModal,
@@ -28,4 +29,5 @@ export {
28
29
  useGlobalPermission,
29
30
  useOnMessageReceivedFromIframe,
30
31
  useOnMessageReceivedFromOutside,
32
+ useNetworkStatus,
31
33
  };
@@ -0,0 +1,31 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ const useNetworkStatus = () => {
4
+ const [isOnline, setOnline] = useState<boolean>(true);
5
+
6
+ const updateNetworkStatus = () => {
7
+ setOnline(navigator.onLine);
8
+ };
9
+
10
+ // sometimes, the load event does not trigger on some browsers, that is why manually calling updateNetworkStatus on initial mount
11
+ useEffect(() => {
12
+ updateNetworkStatus();
13
+ }, []);
14
+
15
+ useEffect(() => {
16
+ window.addEventListener("load", updateNetworkStatus);
17
+ window.addEventListener("online", updateNetworkStatus);
18
+ window.addEventListener("offline", updateNetworkStatus);
19
+
20
+ return () => {
21
+ window.removeEventListener("load", updateNetworkStatus);
22
+ window.removeEventListener("online", updateNetworkStatus);
23
+ window.removeEventListener("offline", updateNetworkStatus);
24
+ };
25
+ // eslint-disable-next-line react-hooks/exhaustive-deps
26
+ }, [navigator.onLine]);
27
+
28
+ return { isOnline };
29
+ };
30
+
31
+ export { useNetworkStatus };
@@ -2,7 +2,7 @@ import React, { useReducer, useEffect } from "react";
2
2
  import { connect } from "react-redux";
3
3
 
4
4
  import { IRootState, IStructuredData, ITemplateOption, IThemeElements } from "@ax/types";
5
- import { filterThemeTemplates, getThumbnailProps, removeDuplicatesByProperty } from "@ax/helpers";
5
+ import { filterThemeTemplates, getThumbnailProps } from "@ax/helpers";
6
6
  import { MenuItem, RadioGroup } from "@ax/components";
7
7
  import { structuredDataActions } from "@ax/containers/StructuredData";
8
8
  import { SecondaryActionButton, MainActionButton } from "./../atoms";
@@ -93,6 +93,18 @@ const OptionTable = (props: IOptionTableProps): JSX.Element => {
93
93
  isOptionInType(filteredOptionsByDataPack) &&
94
94
  getThumbnailProps(state.selectedOption, true, theme);
95
95
 
96
+ const removeDuplicatesByTypeAndMode = (arr: ITemplateOption[]) => {
97
+ const seen = new Set();
98
+ return arr.filter((item) => {
99
+ const key = `${item.type}::${item.mode}`;
100
+ if (item.mode && seen.has(key)) {
101
+ return false;
102
+ }
103
+ seen.add(key);
104
+ return true;
105
+ });
106
+ };
107
+
96
108
  const displayOptions = (item: IOptionFilter) => {
97
109
  const { value } = item;
98
110
 
@@ -123,7 +135,7 @@ const OptionTable = (props: IOptionTableProps): JSX.Element => {
123
135
  }
124
136
  });
125
137
 
126
- filteredOptionsByDataPack = removeDuplicatesByProperty(filteredOptionsByDataPack, "type");
138
+ filteredOptionsByDataPack = removeDuplicatesByTypeAndMode(filteredOptionsByDataPack);
127
139
 
128
140
  filteredOptionsByDataPack.sort((ele, comp) => {
129
141
  // Use localeCompare to compare strings alphabetically