@griddo/ax 1.75.253 → 1.75.255

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": "1.75.253",
4
+ "version": "1.75.255",
5
5
  "authors": [
6
6
  "Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
7
7
  "Carlos Torres <carlos.torres@secuoyas.com>",
@@ -230,5 +230,5 @@
230
230
  "publishConfig": {
231
231
  "access": "public"
232
232
  },
233
- "gitHead": "40e60ce742c9e4dacd64889abc42adb0df7a00a2"
233
+ "gitHead": "5eb1acb0661051a96cf40b52512b0400eaa60651"
234
234
  }
@@ -0,0 +1,54 @@
1
+ import * as React from "react";
2
+ import { ThemeProvider } from "styled-components";
3
+ import { parseTheme } from "@ax/helpers";
4
+ import globalTheme from "@ax/themes/theme.json";
5
+ import { render, cleanup, screen } from "../../../../../config/jest/test-utils";
6
+ import { mock } from "jest-mock-extended";
7
+ import configureStore from "redux-mock-store";
8
+ import thunk from "redux-thunk";
9
+ import SummaryButton, { ISummaryButtonProps } from "@ax/components/Fields/SummaryButton";
10
+
11
+ afterEach(cleanup);
12
+
13
+ const middlewares: any = [thunk];
14
+ const mockStore = configureStore(middlewares);
15
+
16
+ const defaultProps = mock<ISummaryButtonProps>();
17
+
18
+ describe("Summary Button component rendering", () => {
19
+ it("should render the component", () => {
20
+ const initialStore = {
21
+ app: { globalSettings: { autoSummary: true } },
22
+ };
23
+ const store = mockStore(initialStore);
24
+
25
+ render(
26
+ <ThemeProvider theme={parseTheme(globalTheme)}>
27
+ <SummaryButton {...defaultProps} />
28
+ </ThemeProvider>,
29
+ { store }
30
+ );
31
+
32
+ const summaryButtonWrapper = screen.getByTestId("summary-button-wrapper");
33
+
34
+ expect(summaryButtonWrapper).toBeTruthy();
35
+ });
36
+
37
+ it("should not render the component", () => {
38
+ const initialStore = {
39
+ app: { globalSettings: { autoSummary: false } },
40
+ };
41
+ const store = mockStore(initialStore);
42
+
43
+ render(
44
+ <ThemeProvider theme={parseTheme(globalTheme)}>
45
+ <SummaryButton {...defaultProps} />
46
+ </ThemeProvider>,
47
+ { store }
48
+ );
49
+
50
+ const summaryButtonWrapper = screen.queryByTestId("summary-button-wrapper");
51
+
52
+ expect(summaryButtonWrapper).toBeFalsy();
53
+ });
54
+ });
@@ -0,0 +1,118 @@
1
+ import * as React from "react";
2
+ import { ThemeProvider } from "styled-components";
3
+ import { parseTheme } from "@ax/helpers";
4
+ import globalTheme from "@ax/themes/theme.json";
5
+ import { render, cleanup, screen } from "../../../../../config/jest/test-utils";
6
+ import { mock } from "jest-mock-extended";
7
+ import configureStore from "redux-mock-store";
8
+ import thunk from "redux-thunk";
9
+ import TranslateButton, { ITranslateButtonProps } from "@ax/components/Fields/TranslateButton";
10
+
11
+ afterEach(cleanup);
12
+
13
+ const middlewares: any = [thunk];
14
+ const mockStore = configureStore(middlewares);
15
+
16
+ const defaultProps = mock<ITranslateButtonProps>();
17
+
18
+ describe("Translate Button component rendering", () => {
19
+ it("should render the component", () => {
20
+ const initialStore = {
21
+ app: { globalSettings: { autoTranslation: true } },
22
+ pageEditor: { editorContent: { canBeTranslated: true }, isIATranslated: false },
23
+ };
24
+ const store = mockStore(initialStore);
25
+
26
+ render(
27
+ <ThemeProvider theme={parseTheme(globalTheme)}>
28
+ <TranslateButton {...defaultProps} />
29
+ </ThemeProvider>,
30
+ { store }
31
+ );
32
+
33
+ const translateButtonWrapper = screen.getByTestId("translate-button-wrapper");
34
+ expect(translateButtonWrapper).toBeTruthy();
35
+ const translatedNotificationWrapper = screen.queryByTestId("translated-notification-wrapper");
36
+ expect(translatedNotificationWrapper).toBeFalsy();
37
+ });
38
+
39
+ it("should not render the component if setting is off", () => {
40
+ const initialStore = {
41
+ app: { globalSettings: { autoTranslation: false } },
42
+ pageEditor: { editorContent: { canBeTranslated: true }, isIATranslated: false },
43
+ };
44
+ const store = mockStore(initialStore);
45
+
46
+ render(
47
+ <ThemeProvider theme={parseTheme(globalTheme)}>
48
+ <TranslateButton {...defaultProps} />
49
+ </ThemeProvider>,
50
+ { store }
51
+ );
52
+
53
+ const translateButtonWrapper = screen.queryByTestId("translate-button-wrapper");
54
+ const translatedNotificationWrapper = screen.queryByTestId("translated-notification-wrapper");
55
+ expect(translatedNotificationWrapper).toBeFalsy();
56
+ expect(translateButtonWrapper).toBeFalsy();
57
+ });
58
+
59
+ it("should not render the component if can't be translated", () => {
60
+ const initialStore = {
61
+ app: { globalSettings: { autoTranslation: true } },
62
+ pageEditor: { editorContent: { canBeTranslated: false }, isIATranslated: false },
63
+ };
64
+ const store = mockStore(initialStore);
65
+
66
+ render(
67
+ <ThemeProvider theme={parseTheme(globalTheme)}>
68
+ <TranslateButton {...defaultProps} />
69
+ </ThemeProvider>,
70
+ { store }
71
+ );
72
+
73
+ const translateButtonWrapper = screen.queryByTestId("translate-button-wrapper");
74
+ const translatedNotificationWrapper = screen.queryByTestId("translated-notification-wrapper");
75
+ expect(translatedNotificationWrapper).toBeFalsy();
76
+ expect(translateButtonWrapper).toBeFalsy();
77
+ });
78
+
79
+ it("should render the notification if it's already translated", () => {
80
+ const initialStore = {
81
+ app: { globalSettings: { autoTranslation: true } },
82
+ pageEditor: { editorContent: { canBeTranslated: false }, isIATranslated: true },
83
+ };
84
+ const store = mockStore(initialStore);
85
+
86
+ render(
87
+ <ThemeProvider theme={parseTheme(globalTheme)}>
88
+ <TranslateButton {...defaultProps} />
89
+ </ThemeProvider>,
90
+ { store }
91
+ );
92
+
93
+ const translateButtonWrapper = screen.queryByTestId("translate-button-wrapper");
94
+ const translatedNotificationWrapper = screen.queryByTestId("translated-notification-wrapper");
95
+ expect(translatedNotificationWrapper).toBeTruthy();
96
+ expect(translateButtonWrapper).toBeFalsy();
97
+ });
98
+
99
+ it("should render the component if it's already translated but can be translated", () => {
100
+ const initialStore = {
101
+ app: { globalSettings: { autoTranslation: true } },
102
+ pageEditor: { editorContent: { canBeTranslated: true }, isIATranslated: true },
103
+ };
104
+ const store = mockStore(initialStore);
105
+
106
+ render(
107
+ <ThemeProvider theme={parseTheme(globalTheme)}>
108
+ <TranslateButton {...defaultProps} />
109
+ </ThemeProvider>,
110
+ { store }
111
+ );
112
+
113
+ const translateButtonWrapper = screen.getByTestId("translate-button-wrapper");
114
+ expect(translateButtonWrapper).toBeTruthy();
115
+ const translatedNotificationWrapper = screen.queryByTestId("translated-notification-wrapper");
116
+ expect(translatedNotificationWrapper).toBeFalsy();
117
+ });
118
+ });
package/src/api/pages.tsx CHANGED
@@ -80,6 +80,16 @@ const SERVICES: { [key: string]: IServiceConfig } = {
80
80
  endpoint: "/page/check",
81
81
  method: "POST",
82
82
  },
83
+ PAGE_AI_SUMMARY: {
84
+ ...template,
85
+ endpoint: "/ai/summary/page",
86
+ method: "POST",
87
+ },
88
+ PAGE_AI_TRANSLATION: {
89
+ ...template,
90
+ endpoint: "/translations/page/",
91
+ method: "POST",
92
+ },
83
93
  };
84
94
 
85
95
  const getPageInfo = async (pageID: number) => {
@@ -210,6 +220,17 @@ const pageCheck = async (data: any) => {
210
220
  return sendRequest(SERVICES.PAGE_CHECK, { ...data });
211
221
  };
212
222
 
223
+ const getPageSummary = async (data: any) => {
224
+ return sendRequest(SERVICES.PAGE_AI_SUMMARY, { ...data });
225
+ };
226
+
227
+ const getPageTranslation = async (data: any, langID: number) => {
228
+ const { host, endpoint } = SERVICES.PAGE_AI_TRANSLATION;
229
+ SERVICES.PAGE_AI_TRANSLATION.dynamicUrl = `${host}${endpoint}${langID}`;
230
+
231
+ return sendRequest(SERVICES.PAGE_AI_TRANSLATION, { ...data });
232
+ };
233
+
213
234
  export default {
214
235
  getPageInfo,
215
236
  updatePage,
@@ -226,4 +247,6 @@ export default {
226
247
  sendPagePing,
227
248
  getPublicPage,
228
249
  pageCheck,
250
+ getPageSummary,
251
+ getPageTranslation
229
252
  };
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { Icon } from "@ax/components";
2
+ import { Icon, Loader } from "@ax/components";
3
3
 
4
4
  import * as S from "./style";
5
5
 
@@ -10,7 +10,7 @@ enum buttonStyles {
10
10
  INVERSE = "lineInverse",
11
11
  }
12
12
 
13
- const Button = ({ children, type, disabled, icon, buttonStyle, onClick, className }: IButtonProps): JSX.Element => {
13
+ const Button = ({ children, type, disabled, icon, buttonStyle, onClick, className, loader }: IButtonProps): JSX.Element => {
14
14
  const handleOnClick = (e: React.MouseEvent<HTMLButtonElement>) => {
15
15
  if (onClick !== undefined) {
16
16
  e.preventDefault();
@@ -18,13 +18,10 @@ const Button = ({ children, type, disabled, icon, buttonStyle, onClick, classNam
18
18
  }
19
19
  };
20
20
 
21
- let iconWrapper;
22
- if (icon) {
23
- iconWrapper = <Icon name={icon} />;
24
- }
21
+ let iconWrapper = icon ? <Icon name={icon} /> : loader ? <Loader name={loader} /> : undefined;
25
22
 
26
23
  const buttonContent = (
27
- <S.Label icon={icon}>
24
+ <S.Label icon={icon || loader}>
28
25
  {iconWrapper}
29
26
  <span>{children}</span>
30
27
  </S.Label>
@@ -79,6 +76,7 @@ export interface IButtonProps {
79
76
  type: "button" | "submit" | "reset" | undefined;
80
77
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
81
78
  icon?: string;
79
+ loader?: string;
82
80
  disabled?: boolean;
83
81
  buttonStyle?: "solid" | "text" | "line" | "lineInverse" | undefined; // TODO: investigar si podemos hacer esto más elegante y aprovechar el enum buttonStyles
84
82
  className?: string;
@@ -57,6 +57,9 @@ export const Button = styled.button`
57
57
  path {
58
58
  fill: #ffffff;
59
59
  }
60
+ circle {
61
+ stroke: #ffffff;
62
+ }
60
63
  }
61
64
  `;
62
65
 
@@ -167,7 +167,7 @@ const ManualPanel = (props: IProps) => {
167
167
  const source = state.sourceTitles.find((el: IDataSource) => el.id === item.structuredData);
168
168
  const disabled = (hasMaxItems && !isChecked) || !!hasVersionInPageLanguage;
169
169
  return (
170
- <Item
170
+ <>{source && <Item
171
171
  key={`${item.content.title}-${item.id}`}
172
172
  isChecked={isChecked || !!isSelectedOtherLanguage}
173
173
  handleOnClick={handleOnClick}
@@ -175,7 +175,7 @@ const ManualPanel = (props: IProps) => {
175
175
  source={source}
176
176
  disabled={disabled}
177
177
  tooltip={hasVersionInPageLanguage ? "Content has version in page language" : ""}
178
- />
178
+ />}</>
179
179
  );
180
180
  })}
181
181
  </S.ItemList>
@@ -0,0 +1,76 @@
1
+ import React, { useState } from "react";
2
+ import { connect } from "react-redux";
3
+ import { IRootState } from "@ax/types";
4
+ import { pageEditorActions } from "@ax/containers/PageEditor";
5
+
6
+ import * as S from "./style";
7
+
8
+ const SummaryButton = (props: ISummaryButtonProps): JSX.Element => {
9
+ const { autoSummary, getPageSummary } = props;
10
+
11
+ const initialState = {
12
+ isLoading: false,
13
+ isClicked: false,
14
+ error: false,
15
+ };
16
+
17
+ const [state, setState] = useState(initialState);
18
+
19
+ const handleClick = async () => {
20
+ setState((state) => ({ ...state, isLoading: true, error: false }));
21
+ const generated = await getPageSummary();
22
+ setState((state) => ({ ...state, isLoading: false }));
23
+ if (generated) {
24
+ setState((state) => ({ ...state, isClicked: true }));
25
+ } else {
26
+ setState((state) => ({ ...state, error: true }));
27
+ }
28
+ };
29
+
30
+ const buttonText = state.isLoading
31
+ ? "Processing..."
32
+ : state.isClicked
33
+ ? "Regenerate SEO data with AI"
34
+ : "Generate SEO data with AI";
35
+
36
+ return (
37
+ <>
38
+ {autoSummary ? (
39
+ <S.Wrapper data-testid="summary-button-wrapper">
40
+ <S.Text>
41
+ You can automatically generate <strong>SEO meta descriptions</strong> and <strong>keywords</strong> quickly
42
+ with <strong>AI</strong>.
43
+ </S.Text>
44
+ <S.ButtonWrapper>
45
+ <S.StyledButton
46
+ type="button"
47
+ onClick={handleClick}
48
+ icon={!state.isLoading ? "Ia" : undefined}
49
+ loader={state.isLoading ? "circle" : undefined}
50
+ >
51
+ {buttonText}
52
+ </S.StyledButton>
53
+ </S.ButtonWrapper>
54
+ {state.error && <S.ErrorText>We're having problems. Please try again in a few minutes.</S.ErrorText>}
55
+ </S.Wrapper>
56
+ ) : (
57
+ <></>
58
+ )}
59
+ </>
60
+ );
61
+ };
62
+
63
+ export interface ISummaryButtonProps {
64
+ autoSummary: boolean;
65
+ getPageSummary: () => Promise<boolean>;
66
+ }
67
+
68
+ const mapDispatchToProps = {
69
+ getPageSummary: pageEditorActions.getPageSummary,
70
+ };
71
+
72
+ const mapStateToProps = (state: IRootState) => ({
73
+ autoSummary: state.app.globalSettings.autoSummary,
74
+ });
75
+
76
+ export default connect(mapStateToProps, mapDispatchToProps)(SummaryButton);
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import styled from "styled-components";
3
+ import { Button } from "@ax/components";
4
+
5
+ const Wrapper = styled.div`
6
+ background-color: ${(p) => p.theme.color.uiBackground03};
7
+ padding: ${(p) => p.theme.spacing.s};
8
+ border-radius: ${(p) => p.theme.radii.s};
9
+ `;
10
+
11
+ const Text = styled.div`
12
+ ${(p) => p.theme.textStyle.uiXS};
13
+ color: ${(p) => p.theme.color.textMediumEmphasis};
14
+ margin-bottom: ${(p) => p.theme.spacing.xs};
15
+ `;
16
+
17
+ const ButtonWrapper = styled.div`
18
+ display: flex;
19
+ justify-content: center;
20
+ `;
21
+
22
+ const StyledButton = styled((props) => <Button {...props} />)`
23
+ width: 100%;
24
+ max-width: 264px;
25
+ `;
26
+
27
+ const ErrorText = styled.div`
28
+ ${(p) => p.theme.textStyle.uiXS};
29
+ color: ${(p) => p.theme.color.error};
30
+ margin-top: ${(p) => p.theme.spacing.xs};
31
+ `;
32
+
33
+ export { Wrapper, Text, ButtonWrapper, StyledButton, ErrorText };
@@ -0,0 +1,108 @@
1
+ import React, { useState } from "react";
2
+ import { connect } from "react-redux";
3
+ import { Icon, Modal } from "@ax/components";
4
+ import { useModal } from "@ax/hooks";
5
+ import { IRootState } from "@ax/types";
6
+ import { pageEditorActions } from "@ax/containers/PageEditor";
7
+
8
+ import * as S from "./style";
9
+
10
+ const TranslateButton = (props: ITranslateButtonProps): JSX.Element => {
11
+ const { lang, autoTranslation, getPageTranslation, content, isIATranslated, setIsTranslated } = props;
12
+
13
+ const { canBeTranslated } = content;
14
+
15
+ const initialState = {
16
+ isLoading: false,
17
+ error: false,
18
+ };
19
+
20
+ const [state, setState] = useState(initialState);
21
+ const { isOpen, toggleModal } = useModal();
22
+
23
+ const handleClick = async () => {
24
+ toggleModal();
25
+ setState((state) => ({ ...state, isLoading: true, error: false }));
26
+ const generated = await getPageTranslation(lang);
27
+ setState((state) => ({ ...state, isLoading: false }));
28
+ if (!generated) {
29
+ setState((state) => ({ ...state, error: true }));
30
+ }
31
+ };
32
+
33
+ const handleClose = () => setIsTranslated(false);
34
+
35
+ const buttonText = state.isLoading ? "Processing..." : "Translate page with AI";
36
+
37
+ return (
38
+ <>
39
+ {isIATranslated && !canBeTranslated && (
40
+ <S.Wrapper data-testid="translated-notification-wrapper">
41
+ <S.NotificationContent>
42
+ <S.Text>This page is translated with Artificial Intelligence.</S.Text>
43
+ <S.IconWrapper onClick={handleClose}>
44
+ <Icon name="close" size="16" />
45
+ </S.IconWrapper>
46
+ </S.NotificationContent>
47
+ </S.Wrapper>
48
+ )}
49
+ {autoTranslation && canBeTranslated ? (
50
+ <>
51
+ <S.Wrapper data-testid="translate-button-wrapper">
52
+ <S.Text>
53
+ Using <strong>Artificial Intelligence</strong>, you can translate the page automatically.
54
+ </S.Text>
55
+ <S.ButtonWrapper>
56
+ <S.StyledButton
57
+ type="button"
58
+ onClick={toggleModal}
59
+ icon={!state.isLoading ? "Ia" : undefined}
60
+ loader={state.isLoading ? "circle" : undefined}
61
+ >
62
+ {buttonText}
63
+ </S.StyledButton>
64
+ </S.ButtonWrapper>
65
+ {state.error && <S.ErrorText>We're having problems. Please try again in a few minutes.</S.ErrorText>}
66
+ </S.Wrapper>
67
+ <Modal
68
+ isOpen={isOpen}
69
+ hide={toggleModal}
70
+ size="S"
71
+ title="Translate page with AI"
72
+ mainAction={{ title: "Translate with AI", icon: "Ia", onClick: handleClick }}
73
+ secondaryAction={{ title: "Cancel", onClick: toggleModal }}
74
+ >
75
+ <S.ModalContent>
76
+ The automatic translation <strong>will replace all the text on the page</strong>. Before proceeding, make
77
+ sure that the existing text is not critical.
78
+ </S.ModalContent>
79
+ </Modal>
80
+ </>
81
+ ) : (
82
+ <></>
83
+ )}
84
+ </>
85
+ );
86
+ };
87
+
88
+ export interface ITranslateButtonProps {
89
+ lang: number;
90
+ autoTranslation: boolean;
91
+ content: any;
92
+ isIATranslated: boolean;
93
+ getPageTranslation: (langID: number) => Promise<boolean>;
94
+ setIsTranslated: (isTranslated: boolean) => Promise<void>;
95
+ }
96
+
97
+ const mapDispatchToProps = {
98
+ getPageTranslation: pageEditorActions.getPageTranslation,
99
+ setIsTranslated: pageEditorActions.setIsTranslated,
100
+ };
101
+
102
+ const mapStateToProps = (state: IRootState) => ({
103
+ autoTranslation: state.app.globalSettings.autoTranslation,
104
+ content: state.pageEditor.editorContent,
105
+ isIATranslated: state.pageEditor.isIATranslated
106
+ });
107
+
108
+ export default connect(mapStateToProps, mapDispatchToProps)(TranslateButton);
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import styled from "styled-components";
3
+ import { Button } from "@ax/components";
4
+
5
+ const Wrapper = styled.div`
6
+ background-color: ${(p) => p.theme.color.uiBackground03};
7
+ padding: ${(p) => p.theme.spacing.s};
8
+ border-radius: ${(p) => p.theme.radii.s};
9
+ `;
10
+
11
+ const Text = styled.div`
12
+ ${(p) => p.theme.textStyle.uiXS};
13
+ color: ${(p) => p.theme.color.textMediumEmphasis};
14
+ `;
15
+
16
+ const ButtonWrapper = styled.div`
17
+ display: flex;
18
+ justify-content: center;
19
+ margin-top: ${(p) => p.theme.spacing.xs};
20
+ `;
21
+
22
+ const StyledButton = styled((props) => <Button {...props} />)`
23
+ width: 100%;
24
+ max-width: 264px;
25
+ `;
26
+
27
+ const ErrorText = styled.div`
28
+ ${(p) => p.theme.textStyle.uiXS};
29
+ color: ${(p) => p.theme.color.error};
30
+ margin-top: ${(p) => p.theme.spacing.xs};
31
+ `;
32
+
33
+ const ModalContent = styled.div`
34
+ padding: ${(p) => p.theme.spacing.m};
35
+ `;
36
+
37
+ const IconWrapper = styled.div`
38
+ width: ${(p) => p.theme.spacing.s};
39
+ height: ${(p) => p.theme.spacing.s};
40
+ cursor: pointer;
41
+ margin-left: auto;
42
+ `;
43
+
44
+ const NotificationContent = styled.div`
45
+ display: flex;
46
+ `;
47
+
48
+ export { Wrapper, Text, ButtonWrapper, StyledButton, ErrorText, ModalContent, NotificationContent, IconWrapper };
@@ -26,11 +26,13 @@ import ReferenceField from "./ReferenceField";
26
26
  import RichText from "./RichText";
27
27
  import Select from "./Select";
28
28
  import SliderField from "./SliderField";
29
+ import SummaryButton from "./SummaryButton";
29
30
  import TagField from "./TagField";
30
31
  import TextArea from "./TextArea";
31
32
  import TextField from "./TextField";
32
33
  import TimeField from "./TimeField";
33
34
  import ToggleField from "./ToggleField";
35
+ import TranslateButton from "./TranslateButton";
34
36
  import UniqueCheck from "./UniqueCheck";
35
37
  import UrlField from "./UrlField";
36
38
  import VisualOption from "./VisualOption";
@@ -66,11 +68,13 @@ export {
66
68
  RichText,
67
69
  Select,
68
70
  SliderField,
71
+ SummaryButton,
69
72
  TagField,
70
73
  TextArea,
71
74
  TextField,
72
75
  TimeField,
73
76
  ToggleField,
77
+ TranslateButton,
74
78
  UniqueCheck,
75
79
  UrlField,
76
80
  VisualOption,
@@ -0,0 +1,12 @@
1
+ import * as React from "react";
2
+ const SvgIa = (props) => (
3
+ <svg xmlns="http://www.w3.org/2000/svg" width={24} height={24} fill="none" {...props}>
4
+ <path
5
+ fill="#5057FF"
6
+ fillRule="evenodd"
7
+ d="M11.063 3.063 12 1l.938 2.063L15 4l-2.063.938L12 7l-.938-2.063L9 4l2.063-.938Zm-.22 10.093L8.5 8l-2.344 5.156L1 15.5l5.156 2.344L8.5 23l2.344-5.156L16 15.5l-5.156-2.344ZM18.5 5l-1.406 3.094L14 9.5l3.094 1.406L18.5 14l1.406-3.094L23 9.5l-3.094-1.406L18.5 5Z"
8
+ clipRule="evenodd"
9
+ />
10
+ </svg>
11
+ );
12
+ export default SvgIa;
@@ -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 fill-rule="evenodd" clip-rule="evenodd" d="M11.0625 3.0625L12 1L12.9375 3.0625L15 4L12.9375 4.9375L12 7L11.0625 4.9375L9 4L11.0625 3.0625ZM10.8438 13.1562L8.5 8L6.15625 13.1562L1 15.5L6.15625 17.8438L8.5 23L10.8438 17.8438L16 15.5L10.8438 13.1562ZM18.5 5L17.0938 8.09375L14 9.5L17.0938 10.9062L18.5 14L19.9062 10.9062L23 9.5L19.9062 8.09375L18.5 5Z" fill="#5057FF"/>
3
+ </svg>
@@ -19,9 +19,10 @@ const ActionMenuBtn = (props: any) => {
19
19
  menu &&
20
20
  menu.button && (
21
21
  <S.ButtonWrapper>
22
- <Button type="button" buttonStyle="line" onClick={menu.button.action}>
22
+ <Button type="button" buttonStyle="line" onClick={menu.button.action} disabled={menu.button.disabled}>
23
23
  {menu.button.label}
24
24
  </Button>
25
+ {menu.button.helpText && <S.HelpText>{menu.button.helpText}</S.HelpText>}
25
26
  </S.ButtonWrapper>
26
27
  )
27
28
  );
@@ -89,6 +89,14 @@ const ButtonWrapper = styled.div`
89
89
  }
90
90
  `;
91
91
 
92
+ const HelpText = styled.div`
93
+ ${(p) => p.theme.textStyle.uiXS};
94
+ color: ${(p) => p.theme.color.textMediumEmphasis};
95
+ white-space: normal;
96
+ width: 160px;
97
+ margin-top: ${(p) => p.theme.spacing.xs};
98
+ `;
99
+
92
100
  const LanguageWrapper = styled.div`
93
101
  position: relative;
94
102
  display: flex;
@@ -206,4 +214,5 @@ export {
206
214
  WrapperTitle,
207
215
  TabsContent,
208
216
  StatusBtn,
217
+ HelpText,
209
218
  };
@@ -11,7 +11,7 @@ const Modal = (props: IModalProps): JSX.Element | null => {
11
11
  const titleContent = title ? <S.Title>{title}</S.Title> : "";
12
12
 
13
13
  const mainActionButton = mainAction ? (
14
- <Button type="button" onClick={mainAction.onClick} disabled={mainAction.disabled}>
14
+ <Button type="button" onClick={mainAction.onClick} disabled={mainAction.disabled} icon={mainAction.icon}>
15
15
  {mainAction.title}
16
16
  </Button>
17
17
  ) : (
@@ -73,6 +73,7 @@ interface IAction {
73
73
  title: string;
74
74
  onClick: () => void;
75
75
  disabled?: boolean;
76
+ icon?: string;
76
77
  }
77
78
 
78
79
  export default memo(Modal);
@@ -21,10 +21,12 @@ import {
21
21
  ReferenceField,
22
22
  RichText,
23
23
  Select,
24
+ SummaryButton,
24
25
  TextArea,
25
26
  TextField,
26
27
  TimeField,
27
28
  ToggleField,
29
+ TranslateButton,
28
30
  UniqueCheck,
29
31
  UrlField,
30
32
  VisualOption,
@@ -117,10 +119,12 @@ export {
117
119
  ReferenceField,
118
120
  RichText,
119
121
  Select,
122
+ SummaryButton,
120
123
  TextArea,
121
124
  TextField,
122
125
  TimeField,
123
126
  ToggleField,
127
+ TranslateButton,
124
128
  UniqueCheck,
125
129
  UrlField,
126
130
  VisualOption,
@@ -48,6 +48,8 @@ export interface IGlobalSettings {
48
48
  welcomeText1: string;
49
49
  welcomeText2: string;
50
50
  skipReviewOnPublish?: boolean;
51
+ autoSummary: boolean;
52
+ autoTranslation: boolean;
51
53
  }
52
54
 
53
55
  export const initialState = {
@@ -76,6 +78,8 @@ export const initialState = {
76
78
  siteLogoMini: "",
77
79
  welcomeText1: "",
78
80
  welcomeText2: "",
81
+ autoSummary: false,
82
+ autoTranslation: false,
79
83
  },
80
84
  sessionStartedAt: null,
81
85
  };
@@ -58,6 +58,7 @@ import {
58
58
  SET_USER_EDITING,
59
59
  SET_LAST_ELEMENT_ADDED_ID,
60
60
  SET_COPY_MODULE,
61
+ SET_IS_IA_TRANSLATED,
61
62
  } from "./constants";
62
63
 
63
64
  import {
@@ -93,6 +94,7 @@ import {
93
94
  pageStatus,
94
95
  ISetLastElementAddedId,
95
96
  ISetCopyModule,
97
+ ISetIsIATranslated,
96
98
  } from "./interfaces";
97
99
 
98
100
  const { setIsLoading, setIsSaving, handleError } = appActions;
@@ -190,6 +192,10 @@ function setCopyModule(moduleCopy: Record<string, unknown> | null): ISetCopyModu
190
192
  return { type: SET_COPY_MODULE, payload: { moduleCopy } };
191
193
  }
192
194
 
195
+ function setIsIATranslated(isIATranslated: boolean): ISetIsIATranslated {
196
+ return { type: SET_IS_IA_TRANSLATED, payload: { isIATranslated } };
197
+ }
198
+
193
199
  function setTranslatedParent(): (dispatch: Dispatch, getState: any) => void {
194
200
  return async (dispatch, getState) => {
195
201
  try {
@@ -325,7 +331,11 @@ function getPage(pageID?: number, global?: boolean): (dispatch: Dispatch, getSta
325
331
 
326
332
  const { liveStatus, component } = getDefaultSchema(baseSchema);
327
333
 
328
- if (isNewTranslation) page["liveStatus"] = liveStatus;
334
+ if (isNewTranslation) {
335
+ page["liveStatus"] = liveStatus;
336
+ page["canBeTranslated"] = true;
337
+ page["originalLanguage"] = page.language;
338
+ }
329
339
 
330
340
  if (global) page["component"] = component;
331
341
 
@@ -1108,6 +1118,7 @@ function resetPageEditor(): (dispatch: Dispatch) => Promise<void> {
1108
1118
  dispatch(setSitePageID(null));
1109
1119
  dispatch(setUserEditing(null));
1110
1120
  dispatch(setLastElementAddedId(null));
1121
+ dispatch(setIsIATranslated(false));
1111
1122
  localStorage.removeItem("selectedID");
1112
1123
  } catch (e) {
1113
1124
  console.log("Error", e); //TODO
@@ -1343,6 +1354,70 @@ function restorePageNavigation(key: string): (dispatch: Dispatch, getState: any)
1343
1354
  };
1344
1355
  }
1345
1356
 
1357
+ function getPageSummary(): (dispatch: Dispatch, getState: any) => Promise<boolean> {
1358
+ return async (dispatch, getState) => {
1359
+ try {
1360
+ const {
1361
+ pageEditor: { editorContent },
1362
+ } = getState();
1363
+
1364
+ const responseActions = {
1365
+ handleSuccess: (data: { summary: string; keywords: string[] }) => {
1366
+ const content = deepClone(editorContent);
1367
+ content["metaDescription"] = data.summary;
1368
+ content["metaKeywords"] = data.keywords;
1369
+ generatePageContent(content, dispatch, getState);
1370
+ },
1371
+ handleError: () => console.log("Error en GetPageSummary"),
1372
+ };
1373
+
1374
+ const callback = async () => pages.getPageSummary(editorContent);
1375
+
1376
+ return await handleRequest(callback, responseActions, [])(dispatch);
1377
+ } catch (e) {
1378
+ console.log(e);
1379
+ return false;
1380
+ }
1381
+ };
1382
+ }
1383
+
1384
+ function getPageTranslation(langID: number): (dispatch: Dispatch, getState: any) => Promise<boolean> {
1385
+ return async (dispatch, getState) => {
1386
+ try {
1387
+ const {
1388
+ pageEditor: { editorContent },
1389
+ } = getState();
1390
+
1391
+ const responseActions = {
1392
+ handleSuccess: (data: IPage) => {
1393
+ data["canBeTranslated"] = false;
1394
+ generatePageContent(data, dispatch, getState);
1395
+ dispatch(setCurrentPageName(data.title));
1396
+ dispatch(setIsIATranslated(true));
1397
+ },
1398
+ handleError: () => console.log("Error en GetPageTranslation"),
1399
+ };
1400
+
1401
+ const callback = async () => pages.getPageTranslation(editorContent, langID);
1402
+
1403
+ return await handleRequest(callback, responseActions, [])(dispatch);
1404
+ } catch (e) {
1405
+ console.log(e);
1406
+ return false;
1407
+ }
1408
+ };
1409
+ }
1410
+
1411
+ function setIsTranslated(isTranslated: boolean): (dispatch: Dispatch) => Promise<void> {
1412
+ return async (dispatch) => {
1413
+ try {
1414
+ dispatch(setIsIATranslated(isTranslated));
1415
+ } catch (e) {
1416
+ console.log("Error", e);
1417
+ }
1418
+ };
1419
+ }
1420
+
1346
1421
  export {
1347
1422
  setEditorContent,
1348
1423
  setTemplate,
@@ -1391,4 +1466,7 @@ export {
1391
1466
  pasteModule,
1392
1467
  removeNavigationFromPage,
1393
1468
  restorePageNavigation,
1469
+ getPageSummary,
1470
+ getPageTranslation,
1471
+ setIsTranslated
1394
1472
  };
@@ -20,6 +20,7 @@ export const SET_SITE_PAGE_ID = `${NAME}/SET_SITE_PAGE_ID`;
20
20
  export const SET_USER_EDITING = `${NAME}/SET_USER_EDITING`;
21
21
  export const SET_LAST_ELEMENT_ADDED_ID = `${NAME}/SET_LAST_ELEMENT_ADDED_ID`;
22
22
  export const SET_COPY_MODULE = `${NAME}/SET_COPY_MODULE`;
23
+ export const SET_IS_IA_TRANSLATED = `${NAME}/SET_IS_IA_TRANSLATED`;
23
24
 
24
25
  export const INITIAL_TEMPLATE = "BasicTemplate";
25
26
  export const MULTIMEDIA_COMPONENTS = ["LinkableImage", "Video"];
@@ -19,6 +19,7 @@ import {
19
19
  SET_USER_EDITING,
20
20
  SET_LAST_ELEMENT_ADDED_ID,
21
21
  SET_COPY_MODULE,
22
+ SET_IS_IA_TRANSLATED,
22
23
  } from "./constants";
23
24
  import { IBreadcrumbItem, ISchema, IPage, IErrorItem, IUserEditing } from "@ax/types";
24
25
 
@@ -122,6 +123,11 @@ export interface ISetCopyModule {
122
123
  payload: { moduleCopy: Record<string, unknown> | null };
123
124
  }
124
125
 
126
+ export interface ISetIsIATranslated {
127
+ type: typeof SET_IS_IA_TRANSLATED;
128
+ payload: { isIATranslated: boolean };
129
+ }
130
+
125
131
  export interface IFieldProps {
126
132
  id: number;
127
133
  key: string;
@@ -22,6 +22,7 @@ import {
22
22
  SET_USER_EDITING,
23
23
  SET_LAST_ELEMENT_ADDED_ID,
24
24
  SET_COPY_MODULE,
25
+ SET_IS_IA_TRANSLATED,
25
26
  } from "./constants";
26
27
 
27
28
  import { PageEditorActionsCreators } from "./interfaces";
@@ -47,6 +48,7 @@ export interface IPageEditorState {
47
48
  userEditing: IUserEditing | null;
48
49
  lastElementAddedId: null | number;
49
50
  moduleCopy: { date: string; element: Record<string, unknown> } | null;
51
+ isIATranslated: boolean;
50
52
  }
51
53
 
52
54
  export const initialState = {
@@ -70,6 +72,7 @@ export const initialState = {
70
72
  userEditing: null,
71
73
  lastElementAddedId: null,
72
74
  moduleCopy: null,
75
+ isIATranslated: false,
73
76
  };
74
77
 
75
78
  export function reducer(state = initialState, action: PageEditorActionsCreators): IPageEditorState {
@@ -95,6 +98,7 @@ export function reducer(state = initialState, action: PageEditorActionsCreators)
95
98
  case SET_USER_EDITING:
96
99
  case SET_LAST_ELEMENT_ADDED_ID:
97
100
  case SET_COPY_MODULE:
101
+ case SET_IS_IA_TRANSLATED:
98
102
  return { ...state, ...action.payload };
99
103
  default:
100
104
  return state;
@@ -67,6 +67,8 @@ const PageEditor = (props: IProps) => {
67
67
  const isTranslated = pageLanguages.length > 1;
68
68
  const structuredData = editorContent ? editorContent.structuredData : "";
69
69
  const isEditLive = isPublished && hasDraft;
70
+ const canBeUnpublished = editorContent && editorContent.canBeUnpublished;
71
+ const deleteHelpText = !canBeUnpublished ? "This is the canonical site of the page. You cannot unpublish it." : null;
70
72
 
71
73
  const errorNotificationText =
72
74
  "There are some errors on the page so you can not publish yet. Please review them in the error panel.";
@@ -76,6 +78,7 @@ const PageEditor = (props: IProps) => {
76
78
  const { pageID, getPage, setTab, sendPagePing } = props;
77
79
  const defaultTab = "content";
78
80
  const handleGetPage = async () => await getPage(pageID);
81
+
79
82
  setTab(defaultTab);
80
83
  handleGetPage();
81
84
 
@@ -245,6 +248,8 @@ const PageEditor = (props: IProps) => {
245
248
  : {
246
249
  label: "Unpublish",
247
250
  action: hasDraft ? toggleUnpublishModal : unpublishPage,
251
+ disabled: !canBeUnpublished,
252
+ helpText: deleteHelpText,
248
253
  };
249
254
  case pageStatus.MODIFIED:
250
255
  return {
@@ -7,6 +7,11 @@ export default {
7
7
  {
8
8
  title: "content",
9
9
  fields: [
10
+ {
11
+ title: "",
12
+ type: "TranslateButton",
13
+ key: "translate",
14
+ },
10
15
  {
11
16
  title: "Title",
12
17
  type: "TextField",
@@ -52,6 +57,11 @@ export default {
52
57
  key: "seoData",
53
58
  collapsed: true,
54
59
  fields: [
60
+ {
61
+ title: "",
62
+ type: "SummaryButton",
63
+ key: "summary",
64
+ },
55
65
  {
56
66
  title: "Meta title",
57
67
  type: "TextField",
@@ -62,6 +72,11 @@ export default {
62
72
  type: "TextArea",
63
73
  key: "metaDescription",
64
74
  },
75
+ {
76
+ title: "Keywords",
77
+ type: "TagField",
78
+ key: "metaKeywords",
79
+ },
65
80
  {
66
81
  title: "Meta robots index",
67
82
  type: "RadioGroup",
@@ -9,6 +9,11 @@ export default {
9
9
  {
10
10
  title: "content",
11
11
  fields: [
12
+ {
13
+ title: "",
14
+ type: "TranslateButton",
15
+ key: "translate",
16
+ },
12
17
  {
13
18
  title: "Title",
14
19
  type: "TextField",
@@ -111,6 +116,11 @@ export default {
111
116
  key: "seoData",
112
117
  collapsed: true,
113
118
  fields: [
119
+ {
120
+ title: "",
121
+ type: "SummaryButton",
122
+ key: "summary",
123
+ },
114
124
  {
115
125
  title: "Meta title",
116
126
  type: "TextField",
@@ -121,6 +131,11 @@ export default {
121
131
  type: "TextArea",
122
132
  key: "metaDescription",
123
133
  },
134
+ {
135
+ title: "Keywords",
136
+ type: "TagField",
137
+ key: "metaKeywords",
138
+ },
124
139
  {
125
140
  title: "Canonical URL",
126
141
  type: "TextField",
@@ -90,6 +90,9 @@ export interface IAPIPage {
90
90
  liveChanged: boolean;
91
91
  templateId: string;
92
92
  structuredDataContent: Record<string, []>;
93
+ canBeTranslated: boolean;
94
+ originalLanguage: number;
95
+ language: number;
93
96
  }
94
97
 
95
98
  export interface IPage extends IAPIPage {