@griddo/ax 11.11.7 → 11.11.8-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/jest/componentsMock.js +7 -5
- package/package.json +2 -2
- package/src/__tests__/components/Browser/Browser.test.tsx +438 -87
- package/src/__tests__/components/Browser/Browser.utils.test.ts +55 -0
- package/src/__tests__/components/ConfigPanel/ConfigPanel.test.tsx +1 -3
- package/src/__tests__/components/Fields/Button/Button.test.tsx +29 -27
- package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +158 -0
- package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorsBanner.test.tsx +90 -0
- package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.test.tsx +178 -0
- package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +150 -0
- package/src/__tests__/components/KeywordsPreviewModal/KeywordItem/KeywordItem.test.tsx +91 -0
- package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.test.tsx +122 -0
- package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.utils.test.ts +15 -0
- package/src/__tests__/components/KeywordsPreviewModal/atoms.test.tsx +101 -0
- package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +1 -1
- package/src/__tests__/modules/FramePreview/FramePreview.test.tsx +318 -0
- package/src/__tests__/modules/FramePreview/FramePreview.utils.test.ts +242 -0
- package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +185 -0
- package/src/components/Browser/index.tsx +294 -144
- package/src/components/Browser/style.tsx +75 -6
- package/src/components/Browser/utils.tsx +13 -0
- package/src/components/BrowserContent/index.tsx +2 -2
- package/src/components/Button/index.tsx +2 -1
- package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/Field/index.tsx +2 -4
- package/src/components/Fields/AsyncSelect/style.tsx +13 -0
- package/src/components/Fields/FieldGroup/index.tsx +5 -2
- package/src/components/Fields/FieldGroup/style.tsx +32 -7
- package/src/components/Fields/HeadingField/index.tsx +2 -2
- package/src/components/Fields/HiddenField/style.tsx +1 -1
- package/src/components/Fields/NumberField/index.tsx +15 -16
- package/src/components/Fields/NumberField/style.tsx +2 -0
- package/src/components/Fields/ReferenceField/index.tsx +1 -1
- package/src/components/Fields/SEOPreview/index.tsx +36 -0
- package/src/components/Fields/SEOPreview/style.tsx +24 -0
- package/src/components/Fields/Select/index.tsx +5 -1
- package/src/components/Fields/Select/style.tsx +56 -0
- package/src/components/Fields/SummaryButton/index.tsx +18 -9
- package/src/components/Fields/SummaryButton/style.tsx +1 -2
- package/src/components/Fields/TagsField/index.tsx +8 -9
- package/src/components/Fields/UrlField/index.tsx +26 -27
- package/src/components/Fields/index.tsx +2 -0
- package/src/components/FloatingNote/index.tsx +35 -0
- package/src/components/FloatingNote/style.tsx +26 -0
- package/src/components/FloatingPanel/index.tsx +5 -2
- package/src/components/FloatingPanel/style.tsx +2 -1
- package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +85 -0
- package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/style.tsx +80 -0
- package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +57 -0
- package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +82 -0
- package/src/components/HeadingsPreviewModal/HeadingItem/index.tsx +71 -0
- package/src/components/HeadingsPreviewModal/HeadingItem/style.tsx +77 -0
- package/src/components/HeadingsPreviewModal/index.tsx +146 -0
- package/src/components/HeadingsPreviewModal/style.tsx +82 -0
- package/src/components/HeadingsPreviewModal/utils.tsx +257 -0
- package/src/components/IconAction/index.tsx +1 -1
- package/src/components/KeywordsPreviewModal/KeywordItem/index.tsx +46 -0
- package/src/components/KeywordsPreviewModal/KeywordItem/style.tsx +64 -0
- package/src/components/KeywordsPreviewModal/atoms.tsx +96 -0
- package/src/components/KeywordsPreviewModal/index.tsx +99 -0
- package/src/components/KeywordsPreviewModal/style.tsx +87 -0
- package/src/components/KeywordsPreviewModal/utils.tsx +22 -0
- package/src/components/MainWrapper/AppBar/index.tsx +8 -1
- package/src/components/MainWrapper/index.tsx +7 -1
- package/src/components/Notification/index.tsx +2 -2
- package/src/components/OcassionalToast/index.tsx +8 -1
- package/src/components/OcassionalToast/style.tsx +15 -1
- package/src/components/PageFinder/index.tsx +1 -1
- package/src/components/ResizePanel/index.tsx +4 -3
- package/src/components/ResizePanel/style.tsx +1 -1
- package/src/components/SearchField/style.tsx +2 -2
- package/src/components/SideModal/index.tsx +2 -1
- package/src/components/Tabs/index.tsx +13 -4
- package/src/components/Tabs/style.tsx +7 -8
- package/src/components/Toast/index.tsx +4 -2
- package/src/components/Tooltip/index.tsx +4 -3
- package/src/components/index.tsx +8 -2
- package/src/forms/fields.tsx +70 -68
- package/src/hooks/forms.tsx +22 -1
- package/src/hooks/index.tsx +13 -3
- package/src/hooks/modals.tsx +103 -15
- package/src/hooks/users.tsx +25 -8
- package/src/modules/Forms/atoms.tsx +2 -2
- package/src/modules/FramePreview/HeadingsOverlay/index.tsx +113 -0
- package/src/modules/FramePreview/HeadingsOverlay/style.tsx +24 -0
- package/src/modules/FramePreview/index.tsx +55 -16
- package/src/modules/FramePreview/style.tsx +34 -2
- package/src/modules/FramePreview/utils.tsx +140 -0
- package/src/modules/GlobalEditor/Editor/index.tsx +37 -3
- package/src/modules/GlobalEditor/PageBrowser/index.tsx +19 -2
- package/src/modules/GlobalEditor/Preview/index.tsx +0 -2
- package/src/modules/GlobalEditor/Preview/style.tsx +1 -1
- package/src/modules/GlobalEditor/index.tsx +119 -57
- package/src/modules/PageEditor/Editor/index.tsx +33 -2
- package/src/modules/PageEditor/PageBrowser/index.tsx +20 -2
- package/src/modules/PageEditor/Preview/index.tsx +0 -2
- package/src/modules/PageEditor/Preview/style.tsx +1 -1
- package/src/modules/PageEditor/atoms.tsx +1 -1
- package/src/modules/PageEditor/index.tsx +130 -66
- package/src/modules/PublicPreview/index.tsx +8 -5
- package/src/schemas/pages/GlobalPage.ts +87 -70
- package/src/schemas/pages/Page.ts +87 -70
- package/src/types/index.tsx +12 -0
- package/src/components/PageInfoBanner/index.tsx +0 -38
- package/src/components/PageInfoBanner/styles.tsx +0 -40
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useRef } from "react";
|
|
2
2
|
import { Tag } from "@ax/components";
|
|
3
3
|
|
|
4
4
|
import * as S from "./style";
|
|
5
5
|
|
|
6
6
|
const TagsField = (props: IProps): JSX.Element => {
|
|
7
|
-
const { value, onChange, disabled } = props;
|
|
7
|
+
const { value, onChange, disabled, placeholder } = props;
|
|
8
8
|
const valueArray = value && Array.isArray(value) ? value : [];
|
|
9
9
|
const [inputValue, setInputValue] = useState("");
|
|
10
10
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
@@ -33,22 +33,21 @@ const TagsField = (props: IProps): JSX.Element => {
|
|
|
33
33
|
onChange(newValue);
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
const
|
|
36
|
+
const finalPlaceholder = valueArray.length > 0 ? "" : placeholder ? placeholder : "Type a tag...";
|
|
37
37
|
|
|
38
38
|
return (
|
|
39
39
|
<S.Wrapper data-testid="tag-field-wrapper" onClick={_handleClick}>
|
|
40
|
-
{valueArray
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
})}
|
|
40
|
+
{valueArray?.map((tag: string, index: number) => {
|
|
41
|
+
const handleDelete = () => deleteTag(index);
|
|
42
|
+
return <Tag key={tag} text={tag} onDeleteAction={disabled ? undefined : handleDelete} />;
|
|
43
|
+
})}
|
|
45
44
|
<S.Input
|
|
46
45
|
data-testid="tag-field-input"
|
|
47
46
|
ref={inputRef}
|
|
48
47
|
type="text"
|
|
49
48
|
value={inputValue}
|
|
50
49
|
onChange={_handleChange}
|
|
51
|
-
placeholder={
|
|
50
|
+
placeholder={finalPlaceholder}
|
|
52
51
|
onKeyDown={_handleKeyDown}
|
|
53
52
|
disabled={disabled}
|
|
54
53
|
spellCheck={true}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { memo, useEffect, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import { IconAction, TextField, FloatingPanel, FieldsBehavior, PageFinder } from "@ax/components";
|
|
4
4
|
import { useModal } from "@ax/hooks";
|
|
5
5
|
import { isReqOk } from "@ax/helpers";
|
|
6
|
-
import { IPage, IUrlField, Field, ISelectOption } from "@ax/types";
|
|
6
|
+
import type { IPage, IUrlField, Field, ISelectOption } from "@ax/types";
|
|
7
7
|
import { pages as pagesApi } from "@ax/api";
|
|
8
8
|
import { findAnchorsFromPage, findAnchorsFromTab, findTabsFromPage } from "./utils";
|
|
9
9
|
|
|
@@ -29,7 +29,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
29
29
|
const [internalPageName, setInternalPageName] = useState(null);
|
|
30
30
|
const [pageData, setPageData] = useState<IPage | null>(null);
|
|
31
31
|
|
|
32
|
-
const pageID = value
|
|
32
|
+
const pageID = value?.linkTo ? value.linkTo : null;
|
|
33
33
|
|
|
34
34
|
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
|
|
35
35
|
useEffect(() => {
|
|
@@ -67,13 +67,13 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
67
67
|
|
|
68
68
|
const handleOnClick = () => {
|
|
69
69
|
toggleModal();
|
|
70
|
-
handlePanel
|
|
70
|
+
handlePanel?.(isOpen);
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
const handleReset = () => {
|
|
74
74
|
onChange(null);
|
|
75
75
|
setInternalPageName(null);
|
|
76
|
-
resetValidation
|
|
76
|
+
resetValidation?.();
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
const handleSetPage = (page: IPage | IPage[]) => {
|
|
@@ -85,10 +85,10 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
85
85
|
linkTo: selectedPage.id,
|
|
86
86
|
linkToURL: selectedPage.fullUrl,
|
|
87
87
|
title: selectedPage.title,
|
|
88
|
-
noFollow: !selectedPage.follow
|
|
88
|
+
noFollow: !selectedPage.follow,
|
|
89
89
|
});
|
|
90
|
-
handleValidation
|
|
91
|
-
handlePanel
|
|
90
|
+
handleValidation?.(selectedPage.id.toString(), validators);
|
|
91
|
+
handlePanel?.(isOpen);
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
const handleChange = (newValue: string) => onChange({ ...value, href: newValue });
|
|
@@ -113,7 +113,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
const validator = { format: "fullURL" };
|
|
116
|
-
const defensiveHref = value
|
|
116
|
+
const defensiveHref = value?.href ? value.href : "";
|
|
117
117
|
|
|
118
118
|
let field = (
|
|
119
119
|
<TextField
|
|
@@ -140,7 +140,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
const titleValue = value
|
|
143
|
+
const titleValue = value?.title ? value.title : internalPageName ? internalPageName : "";
|
|
144
144
|
const noFollowValue =
|
|
145
145
|
value && value.noFollow !== undefined
|
|
146
146
|
? value.noFollow
|
|
@@ -160,7 +160,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
160
160
|
type: "UniqueCheck" as Field,
|
|
161
161
|
name: "newTab",
|
|
162
162
|
options: [{ title: "Open in new tab" }],
|
|
163
|
-
value: value
|
|
163
|
+
value: value?.newTab ? value.newTab : null,
|
|
164
164
|
onChange: handleNewTabChange,
|
|
165
165
|
},
|
|
166
166
|
{
|
|
@@ -175,7 +175,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
175
175
|
return (
|
|
176
176
|
<S.UrlFieldWrapper data-testid="url-field-wrapper">
|
|
177
177
|
{field}
|
|
178
|
-
{value
|
|
178
|
+
{value?.linkTo && isTabsVisible && (
|
|
179
179
|
<S.AnchorWrapper>
|
|
180
180
|
<FieldsBehavior
|
|
181
181
|
title="Tab"
|
|
@@ -188,7 +188,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
188
188
|
/>
|
|
189
189
|
</S.AnchorWrapper>
|
|
190
190
|
)}
|
|
191
|
-
{value
|
|
191
|
+
{value?.linkTo && isVisible && (
|
|
192
192
|
<S.AnchorWrapper>
|
|
193
193
|
<FieldsBehavior
|
|
194
194
|
title="Anchor"
|
|
@@ -203,20 +203,19 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
|
|
|
203
203
|
)}
|
|
204
204
|
{showAdvanced && (
|
|
205
205
|
<S.AdvancedWrapper>
|
|
206
|
-
{advancedFields
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
})}
|
|
206
|
+
{advancedFields?.map((adField) => {
|
|
207
|
+
return (
|
|
208
|
+
<FieldsBehavior
|
|
209
|
+
key={adField.name}
|
|
210
|
+
options={adField.options}
|
|
211
|
+
name={adField.name}
|
|
212
|
+
title={adField.title}
|
|
213
|
+
fieldType={adField.type}
|
|
214
|
+
value={adField.value}
|
|
215
|
+
onChange={adField.onChange}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
220
219
|
</S.AdvancedWrapper>
|
|
221
220
|
)}
|
|
222
221
|
<FloatingPanel
|
|
@@ -17,6 +17,7 @@ import FormCategorySelect from "./FormCategorySelect";
|
|
|
17
17
|
import FormContainer from "./FormContainer";
|
|
18
18
|
import FormFieldArray from "./FormFieldArray";
|
|
19
19
|
import HeadingField from "./HeadingField";
|
|
20
|
+
import SEOPreview from "./SEOPreview";
|
|
20
21
|
import HiddenField from "./HiddenField";
|
|
21
22
|
import ImageField from "./ImageField";
|
|
22
23
|
import LinkField from "./LinkField";
|
|
@@ -64,6 +65,7 @@ export {
|
|
|
64
65
|
FormContainer,
|
|
65
66
|
FormFieldArray,
|
|
66
67
|
HeadingField,
|
|
68
|
+
SEOPreview,
|
|
67
69
|
HiddenField,
|
|
68
70
|
ImageField,
|
|
69
71
|
LinkField,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Button, Icon } from "@ax/components";
|
|
2
|
+
|
|
3
|
+
import * as S from "./style";
|
|
4
|
+
|
|
5
|
+
const FloatingNote = (props: IFloatingNoteProps): JSX.Element => {
|
|
6
|
+
const { message, icon, className, btnText, onClick } = props;
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<S.Wrapper data-testid="floating-note-wrapper" className={className}>
|
|
10
|
+
{icon && (
|
|
11
|
+
<S.IconWrapper>
|
|
12
|
+
<Icon name={icon} size="16" />
|
|
13
|
+
</S.IconWrapper>
|
|
14
|
+
)}
|
|
15
|
+
<S.Text data-testid="floating-note-message">{message}</S.Text>
|
|
16
|
+
{btnText && (
|
|
17
|
+
<S.ActionWrapper>
|
|
18
|
+
<Button type="button" buttonStyle="minimal" onClick={onClick}>
|
|
19
|
+
{btnText}
|
|
20
|
+
</Button>
|
|
21
|
+
</S.ActionWrapper>
|
|
22
|
+
)}
|
|
23
|
+
</S.Wrapper>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface IFloatingNoteProps {
|
|
28
|
+
message: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
className?: string;
|
|
31
|
+
btnText?: string;
|
|
32
|
+
onClick?: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default FloatingNote;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
|
|
3
|
+
const Wrapper = styled.div`
|
|
4
|
+
display: flex;
|
|
5
|
+
background-color: ${(p) => p.theme.color.uiBackground03};
|
|
6
|
+
padding: ${(p) => p.theme.spacing.s};
|
|
7
|
+
border-radius: ${(p) => p.theme.radii.s};
|
|
8
|
+
z-index: 98;
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
const Text = styled.div`
|
|
12
|
+
${(p) => p.theme.textStyle.uiS};
|
|
13
|
+
color: ${(p) => p.theme.color.textMediumEmphasis};
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const IconWrapper = styled.div`
|
|
17
|
+
width: ${(p) => p.theme.spacing.s};
|
|
18
|
+
height: ${(p) => p.theme.spacing.s};
|
|
19
|
+
margin-right: ${(p) => p.theme.spacing.xs};
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const ActionWrapper = styled.div`
|
|
23
|
+
margin-left: auto;
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export { Wrapper, Text, IconWrapper, ActionWrapper };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { memo, useRef } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
|
|
4
4
|
import { useHandleClickOutside } from "@ax/hooks";
|
|
@@ -16,12 +16,13 @@ const FloatingPanel = (props: IFloatingPanelProps): JSX.Element | null => {
|
|
|
16
16
|
handlePanel,
|
|
17
17
|
secondary,
|
|
18
18
|
closeOnOutsideClick = true,
|
|
19
|
+
width,
|
|
19
20
|
} = props;
|
|
20
21
|
|
|
21
22
|
const node = useRef<HTMLElement>(null);
|
|
22
23
|
|
|
23
24
|
const handleClickOutside = (e: any) => {
|
|
24
|
-
if (
|
|
25
|
+
if (node.current?.contains(e.target) || isOpenedSecond || !closeOnOutsideClick) {
|
|
25
26
|
return;
|
|
26
27
|
}
|
|
27
28
|
toggleModal();
|
|
@@ -46,6 +47,7 @@ const FloatingPanel = (props: IFloatingPanelProps): JSX.Element | null => {
|
|
|
46
47
|
isOpen={isOpen}
|
|
47
48
|
isOpenedSecond={isOpenedSecond}
|
|
48
49
|
secondary={secondary}
|
|
50
|
+
width={width}
|
|
49
51
|
>
|
|
50
52
|
<S.Header>
|
|
51
53
|
<S.Title>{title}</S.Title>
|
|
@@ -70,6 +72,7 @@ export interface IFloatingPanelProps {
|
|
|
70
72
|
handlePanel?: (value: boolean) => void;
|
|
71
73
|
secondary?: boolean;
|
|
72
74
|
closeOnOutsideClick?: boolean;
|
|
75
|
+
width?: number;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
export default memo(FloatingPanel);
|
|
@@ -17,12 +17,13 @@ export const Wrapper = styled.section<{
|
|
|
17
17
|
isOpen: boolean;
|
|
18
18
|
isOpenedSecond: boolean | undefined;
|
|
19
19
|
secondary?: boolean;
|
|
20
|
+
width?: number;
|
|
20
21
|
}>`
|
|
21
22
|
position: fixed;
|
|
22
23
|
right: 0;
|
|
23
24
|
top: 0;
|
|
24
25
|
z-index: 1200;
|
|
25
|
-
width:
|
|
26
|
+
width: ${(p) => (p.width ? `${p.width}px` : `calc(${p.theme.spacing.xl} * 6)`)};
|
|
26
27
|
height: 100vh;
|
|
27
28
|
background: ${(p) => p.theme.colors.uiBackground01};
|
|
28
29
|
box-shadow: ${(p) => (p.secondary || !p.isOpen ? `none` : p.theme.shadow.rightPanel)};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { Icon, Tooltip } from "@ax/components";
|
|
4
|
+
|
|
5
|
+
import type { IHeadingError } from "../../utils";
|
|
6
|
+
|
|
7
|
+
import * as S from "./style";
|
|
8
|
+
|
|
9
|
+
const ErrorsItem = (props: IErrorsItemProps) => {
|
|
10
|
+
const { error, onSelectHeading } = props;
|
|
11
|
+
const { message, description, headingIds } = error;
|
|
12
|
+
|
|
13
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
14
|
+
const [isDeleted, setIsDeleted] = useState(false);
|
|
15
|
+
const [currentIndex, setCurrentIndex] = useState<number | null>(null);
|
|
16
|
+
|
|
17
|
+
const handlePrevious = () => {
|
|
18
|
+
if (!currentIndex) return;
|
|
19
|
+
const newIndex = currentIndex - 1;
|
|
20
|
+
setCurrentIndex(newIndex);
|
|
21
|
+
onSelectHeading(headingIds[newIndex])();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const handleNext = () => {
|
|
25
|
+
const newIndex = currentIndex === null ? 0 : currentIndex + 1;
|
|
26
|
+
if (newIndex > headingIds.length - 1) return;
|
|
27
|
+
setCurrentIndex(newIndex);
|
|
28
|
+
onSelectHeading(headingIds[newIndex])();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleOpen = () => {
|
|
32
|
+
setIsOpen(!isOpen);
|
|
33
|
+
setCurrentIndex(0);
|
|
34
|
+
onSelectHeading(headingIds[0])();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const errorText =
|
|
38
|
+
headingIds.length > 1 && currentIndex !== null
|
|
39
|
+
? `${currentIndex + 1} of ${headingIds.length} headings`
|
|
40
|
+
: `${headingIds.length} headings`;
|
|
41
|
+
|
|
42
|
+
if (isDeleted) {
|
|
43
|
+
return <></>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<S.ErrorItem>
|
|
48
|
+
<Tooltip content={description}>
|
|
49
|
+
<S.ErrorHeader>
|
|
50
|
+
<S.ErrorMessage>{message}</S.ErrorMessage>
|
|
51
|
+
<S.ErrorActions>
|
|
52
|
+
{headingIds.length > 0 && (
|
|
53
|
+
<S.IconWrapper onClick={handleOpen}>
|
|
54
|
+
<Icon name={isOpen ? "UpArrow" : "DownArrow"} size="16" />
|
|
55
|
+
</S.IconWrapper>
|
|
56
|
+
)}
|
|
57
|
+
<S.IconWrapper onClick={() => setIsDeleted(true)}>
|
|
58
|
+
<Icon name="close" size="16" />
|
|
59
|
+
</S.IconWrapper>
|
|
60
|
+
</S.ErrorActions>
|
|
61
|
+
</S.ErrorHeader>
|
|
62
|
+
</Tooltip>
|
|
63
|
+
<S.ErrorContentWrapper isOpen={isOpen}>
|
|
64
|
+
<S.ErrorContent>
|
|
65
|
+
<S.ErrorContentActions>
|
|
66
|
+
<S.IconWrapper onClick={handlePrevious} isDisabled={!currentIndex}>
|
|
67
|
+
<Icon name="UpArrow" size="16" />
|
|
68
|
+
</S.IconWrapper>
|
|
69
|
+
<S.IconWrapper onClick={handleNext} isDisabled={currentIndex === headingIds.length - 1}>
|
|
70
|
+
<Icon name="DownArrow" size="16" />
|
|
71
|
+
</S.IconWrapper>
|
|
72
|
+
</S.ErrorContentActions>
|
|
73
|
+
<S.ErrorContentText>{errorText}</S.ErrorContentText>
|
|
74
|
+
</S.ErrorContent>
|
|
75
|
+
</S.ErrorContentWrapper>
|
|
76
|
+
</S.ErrorItem>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
interface IErrorsItemProps {
|
|
81
|
+
error: IHeadingError;
|
|
82
|
+
onSelectHeading: (id: number) => () => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default ErrorsItem;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
|
|
3
|
+
const ErrorItem = styled.li`
|
|
4
|
+
display: flex;
|
|
5
|
+
background-color: ${(p) => p.theme.colors.uiBackground02};
|
|
6
|
+
border: ${(p) => `1px solid ${p.theme.colors.uiLine}`};
|
|
7
|
+
padding: ${(p) => p.theme.spacing.xs};
|
|
8
|
+
margin-bottom: ${(p) => p.theme.spacing.xxs};
|
|
9
|
+
border-radius: ${(p) => p.theme.radii.s};
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
|
|
12
|
+
&:last-child {
|
|
13
|
+
margin-bottom: 0;
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const ErrorHeader = styled.div`
|
|
18
|
+
display: flex;
|
|
19
|
+
width: 100%;
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const ErrorMessage = styled.div`
|
|
23
|
+
${(p) => p.theme.textStyle.uiS};
|
|
24
|
+
color: ${(p) => p.theme.colors.textHighEmphasis};
|
|
25
|
+
font-weight: 400;
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const ErrorActions = styled.div`
|
|
29
|
+
display: flex;
|
|
30
|
+
margin-left: auto;
|
|
31
|
+
align-items: start;
|
|
32
|
+
gap: ${(p) => p.theme.spacing.xxs};
|
|
33
|
+
padding-left: ${(p) => p.theme.spacing.xs};
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const IconWrapper = styled.button<{ isDisabled?: boolean }>`
|
|
37
|
+
width: ${(p) => p.theme.spacing.s};
|
|
38
|
+
height: ${(p) => p.theme.spacing.s};
|
|
39
|
+
svg {
|
|
40
|
+
path {
|
|
41
|
+
fill: ${(p) => (p.isDisabled ? p.theme.color.interactiveDisabled : p.theme.color.interactive01)};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const ErrorContentWrapper = styled.div<{ isOpen: boolean }>`
|
|
47
|
+
width: 100%;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
height: ${(p) => (p.isOpen ? "auto" : "0")};
|
|
50
|
+
transition: height 1s ease-in-out;
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const ErrorContent = styled.div`
|
|
54
|
+
display: flex;
|
|
55
|
+
width: 100%;
|
|
56
|
+
margin-top: ${(p) => p.theme.spacing.xxs};
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const ErrorContentActions = styled.div`
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: ${(p) => p.theme.spacing.xxs};
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const ErrorContentText = styled.div`
|
|
65
|
+
${(p) => p.theme.textStyle.uiXS};
|
|
66
|
+
color: ${(p) => p.theme.colors.textMediumEmphasis};
|
|
67
|
+
margin-left: ${(p) => p.theme.spacing.s};
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
ErrorItem,
|
|
72
|
+
ErrorHeader,
|
|
73
|
+
ErrorMessage,
|
|
74
|
+
ErrorActions,
|
|
75
|
+
IconWrapper,
|
|
76
|
+
ErrorContentWrapper,
|
|
77
|
+
ErrorContent,
|
|
78
|
+
ErrorContentActions,
|
|
79
|
+
ErrorContentText,
|
|
80
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { Icon } from "@ax/components";
|
|
4
|
+
|
|
5
|
+
import type { IHeadingError } from "../utils";
|
|
6
|
+
import ErrorItem from "./ErrorItem";
|
|
7
|
+
|
|
8
|
+
import * as S from "./style";
|
|
9
|
+
|
|
10
|
+
const ErrorsBanner = (props: IErrorsBannerProps) => {
|
|
11
|
+
const { errors, onSelectHeading, isOpen, setIsOpen, resetKey } = props;
|
|
12
|
+
|
|
13
|
+
const [isDeleted, setIsDeleted] = useState(false);
|
|
14
|
+
|
|
15
|
+
if (isDeleted) {
|
|
16
|
+
return <></>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<S.ErrorsWrapper>
|
|
21
|
+
<S.ErrorsHeader>
|
|
22
|
+
<S.WarningWrapper>
|
|
23
|
+
<Icon name="warning" size="16" />
|
|
24
|
+
</S.WarningWrapper>
|
|
25
|
+
<S.HeaderText>SEO Alerts</S.HeaderText>
|
|
26
|
+
<S.HeaderActions>
|
|
27
|
+
<S.ToggleWrapper onClick={() => setIsOpen(!isOpen)}>
|
|
28
|
+
<Icon name={isOpen ? "UpArrow" : "DownArrow"} size="24" />
|
|
29
|
+
</S.ToggleWrapper>
|
|
30
|
+
<S.IconWrapper onClick={() => setIsDeleted(true)}>
|
|
31
|
+
<Icon name="close" size="16" />
|
|
32
|
+
</S.IconWrapper>
|
|
33
|
+
</S.HeaderActions>
|
|
34
|
+
</S.ErrorsHeader>
|
|
35
|
+
<S.ErrorsContent isOpen={isOpen}>
|
|
36
|
+
<S.Description>
|
|
37
|
+
Review <strong>suggestions and warnings</strong> to enhance your page's search engine optimization.
|
|
38
|
+
</S.Description>
|
|
39
|
+
<S.ErrorListWrapper>
|
|
40
|
+
{errors.map((error) => (
|
|
41
|
+
<ErrorItem key={`${error.message}-${resetKey}`} error={error} onSelectHeading={onSelectHeading} />
|
|
42
|
+
))}
|
|
43
|
+
</S.ErrorListWrapper>
|
|
44
|
+
</S.ErrorsContent>
|
|
45
|
+
</S.ErrorsWrapper>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
interface IErrorsBannerProps {
|
|
50
|
+
errors: IHeadingError[];
|
|
51
|
+
onSelectHeading: (id: number) => () => void;
|
|
52
|
+
isOpen: boolean;
|
|
53
|
+
setIsOpen: (value: boolean) => void;
|
|
54
|
+
resetKey: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default ErrorsBanner;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
|
|
3
|
+
const ErrorsWrapper = styled.div`
|
|
4
|
+
position: sticky;
|
|
5
|
+
top: 0;
|
|
6
|
+
background-color: ${(p) => p.theme.colors.uiBackground03};
|
|
7
|
+
width: 100%;
|
|
8
|
+
padding: ${(p) => p.theme.spacing.s};
|
|
9
|
+
margin-bottom: ${(p) => p.theme.spacing.xs};
|
|
10
|
+
border-radius: ${(p) => p.theme.radii.s};
|
|
11
|
+
z-index: 2;
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const ErrorsHeader = styled.div`
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const WarningWrapper = styled.div`
|
|
20
|
+
width: ${(p) => p.theme.spacing.s};
|
|
21
|
+
height: ${(p) => p.theme.spacing.s};
|
|
22
|
+
svg {
|
|
23
|
+
path {
|
|
24
|
+
fill: ${(p) => p.theme.color.interactive02};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const HeaderText = styled.div`
|
|
30
|
+
${(p) => p.theme.textStyle.uiM};
|
|
31
|
+
color: ${(p) => p.theme.colors.textHighEmphasis};
|
|
32
|
+
margin-left: ${(p) => p.theme.spacing.xxs};
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const HeaderActions = styled.div`
|
|
37
|
+
display: flex;
|
|
38
|
+
margin-left: auto;
|
|
39
|
+
gap: ${(p) => p.theme.spacing.xxs};
|
|
40
|
+
align-items: center;
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const ToggleWrapper = styled.button`
|
|
44
|
+
width: ${(p) => p.theme.spacing.m};
|
|
45
|
+
height: ${(p) => p.theme.spacing.m};
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
const IconWrapper = styled.button`
|
|
49
|
+
width: ${(p) => p.theme.spacing.s};
|
|
50
|
+
height: ${(p) => p.theme.spacing.s};
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const ErrorsContent = styled.div<{ isOpen: boolean }>`
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
height: ${(p) => (p.isOpen ? "auto" : "0")};
|
|
58
|
+
transition: height 1s ease-in-out;
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const Description = styled.div`
|
|
62
|
+
${(p) => p.theme.textStyle.uiXS};
|
|
63
|
+
color: ${(p) => p.theme.colors.textMediumEmphasis};
|
|
64
|
+
margin-top: ${(p) => p.theme.spacing.xs};
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const ErrorListWrapper = styled.ul`
|
|
68
|
+
margin-top: ${(p) => p.theme.spacing.xs};
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
ErrorsWrapper,
|
|
73
|
+
ErrorsHeader,
|
|
74
|
+
IconWrapper,
|
|
75
|
+
WarningWrapper,
|
|
76
|
+
HeaderText,
|
|
77
|
+
HeaderActions,
|
|
78
|
+
ErrorsContent,
|
|
79
|
+
ToggleWrapper,
|
|
80
|
+
Description,
|
|
81
|
+
ErrorListWrapper,
|
|
82
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Icon } from "@ax/components";
|
|
2
|
+
import type { HeadingNode } from "@ax/types";
|
|
3
|
+
|
|
4
|
+
import * as S from "./style";
|
|
5
|
+
|
|
6
|
+
const HeadingItem = ({
|
|
7
|
+
head,
|
|
8
|
+
index,
|
|
9
|
+
isFiltering,
|
|
10
|
+
selected,
|
|
11
|
+
onHeadingClick,
|
|
12
|
+
counter,
|
|
13
|
+
parentPath,
|
|
14
|
+
}: IHeadingItemProps) => {
|
|
15
|
+
counter.value += 1;
|
|
16
|
+
const headingId = counter.value;
|
|
17
|
+
const uniqueKey = `${parentPath}${head.tag}-${head.level}-${index}-${head.text.slice(0, 20)}`;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<S.HeadItem
|
|
22
|
+
level={isFiltering ? 1 : head.level}
|
|
23
|
+
tag={head.tag}
|
|
24
|
+
tabIndex={0}
|
|
25
|
+
role="button"
|
|
26
|
+
onClick={onHeadingClick(headingId)}
|
|
27
|
+
isSelected={selected === headingId}
|
|
28
|
+
isHidden={head.isHidden}
|
|
29
|
+
data-heading-id={headingId}
|
|
30
|
+
>
|
|
31
|
+
<S.HeadTag>
|
|
32
|
+
<div>{head.tag}</div>
|
|
33
|
+
</S.HeadTag>
|
|
34
|
+
<S.StyledTooltip content={head.isHidden ? "Hidden with CSS" : null}>
|
|
35
|
+
<S.HeadText>
|
|
36
|
+
<div>{head.text}</div>
|
|
37
|
+
{head.isHidden && (
|
|
38
|
+
<S.HiddenIcon>
|
|
39
|
+
<Icon name="hide" size="16" />
|
|
40
|
+
</S.HiddenIcon>
|
|
41
|
+
)}
|
|
42
|
+
</S.HeadText>
|
|
43
|
+
</S.StyledTooltip>
|
|
44
|
+
</S.HeadItem>
|
|
45
|
+
{head.children.map((child, childIndex) => (
|
|
46
|
+
<HeadingItem
|
|
47
|
+
key={`${uniqueKey}-child-${childIndex}`}
|
|
48
|
+
head={child}
|
|
49
|
+
index={childIndex}
|
|
50
|
+
isFiltering={isFiltering}
|
|
51
|
+
selected={selected}
|
|
52
|
+
onHeadingClick={onHeadingClick}
|
|
53
|
+
counter={counter}
|
|
54
|
+
parentPath={`${uniqueKey}-`}
|
|
55
|
+
/>
|
|
56
|
+
))}
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
interface IHeadingItemProps {
|
|
62
|
+
head: HeadingNode;
|
|
63
|
+
index: number;
|
|
64
|
+
isFiltering: boolean;
|
|
65
|
+
selected: number | null;
|
|
66
|
+
onHeadingClick: (id: number) => () => void;
|
|
67
|
+
counter: { value: number };
|
|
68
|
+
parentPath: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default HeadingItem;
|