@elementor/editor-audits 4.3.3 → 4.3.5
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/dist/index.d.mts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +1103 -1146
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1075 -1108
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -15
- package/src/api/page-context-client.ts +8 -5
- package/src/audits/accessibility-statement.ts +39 -0
- package/src/audits/accessibility-widget.ts +41 -0
- package/src/audits/cookie-policy.ts +1 -0
- package/src/audits/google-consent-mode.ts +4 -7
- package/src/audits/images-alt-text.ts +6 -4
- package/src/audits/images-inefficient-format.ts +66 -0
- package/src/audits/images-too-large.ts +23 -10
- package/src/audits/nested-boxed-containers.ts +5 -0
- package/src/components/audit-feedback.tsx +4 -1
- package/src/components/audit-panel.tsx +13 -13
- package/src/components/audit-toggle-icon.tsx +22 -0
- package/src/components/fix-violation-with-angie.tsx +18 -7
- package/src/components/pages/category-page.tsx +27 -3
- package/src/components/pages/overview-page.tsx +24 -29
- package/src/components/report-shell.tsx +10 -91
- package/src/components/rescan-button.tsx +38 -0
- package/src/components/severity-icons.tsx +5 -1
- package/src/components/severity-summary-cards.tsx +52 -0
- package/src/components/violation-cta-button.tsx +11 -2
- package/src/components/violation-icons.tsx +2 -2
- package/src/components/violation-row.tsx +120 -62
- package/src/hooks/use-audit-report.ts +5 -4
- package/src/hooks/use-audit-toggle-props.ts +2 -2
- package/src/hooks/use-single-expanded-card.ts +11 -0
- package/src/init.ts +2 -0
- package/src/register-audits.ts +6 -2
- package/src/runner.ts +15 -7
- package/src/store/selectors.ts +4 -0
- package/src/store/slice.ts +10 -1
- package/src/sync-report-staleness.ts +14 -0
- package/src/types.ts +11 -1
- package/src/utils/all-image-sources.ts +17 -0
- package/src/utils/alt-text-image-sources.ts +24 -0
- package/src/utils/atomic-background-image-sources.ts +56 -0
- package/src/utils/atomic-image-sources.ts +66 -0
- package/src/utils/audit-status-summary.ts +0 -57
- package/src/utils/background-image-sources.ts +32 -0
- package/src/utils/image-alt.ts +3 -1
- package/src/utils/image-like-sources.ts +5 -14
- package/src/utils/image-size-key.ts +8 -0
- package/src/utils/page-attachments.ts +7 -6
- package/src/utils/scan-accessibility-widgets.ts +7 -0
- package/src/audits/too-many-widgets.ts +0 -53
- package/src/components/audit-statuses-section.tsx +0 -65
- package/src/components/count-summary-circle.tsx +0 -66
- package/src/components/pages/all-audits-page.tsx +0 -65
- package/src/components/pages/issues-page.tsx +0 -65
- package/src/components/score-bar.tsx +0 -56
- package/src/components/score-circle.tsx +0 -60
- package/src/components/severity-filter-chips.tsx +0 -57
- package/src/utils/score-thresholds.ts +0 -23
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { Box,
|
|
2
|
+
import { Box, Typography } from '@elementor/ui';
|
|
3
3
|
import { __ } from '@wordpress/i18n';
|
|
4
4
|
|
|
5
5
|
import { ALL_CATEGORIES, CATEGORY_LABELS } from '../../constants';
|
|
6
6
|
import { type AuditCategory, type PageAuditReport } from '../../types';
|
|
7
7
|
import { getPopulatedCategories } from '../../utils/audit-status-summary';
|
|
8
|
-
import {
|
|
8
|
+
import { countSeverities } from '../../utils/severity-counts';
|
|
9
|
+
import IssuesCategoryRow from '../issues-category-row';
|
|
9
10
|
import Promotions from '../promotions';
|
|
10
|
-
import
|
|
11
|
-
import ScoreCircle from '../score-circle';
|
|
11
|
+
import SeveritySummaryCards from '../severity-summary-cards';
|
|
12
12
|
|
|
13
13
|
type Props = {
|
|
14
14
|
onCategoryClick: ( category: AuditCategory ) => void;
|
|
@@ -17,35 +17,30 @@ type Props = {
|
|
|
17
17
|
|
|
18
18
|
export default function OverviewPage( { onCategoryClick, report }: Props ) {
|
|
19
19
|
const populatedCategories = getPopulatedCategories( report.categories, ALL_CATEGORIES );
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
const categoryRows = populatedCategories
|
|
22
|
+
.map( ( category ) => ( { category, counts: countSeverities( report, category ) } ) )
|
|
23
|
+
.filter( ( { counts } ) => Object.values( counts ).some( ( count ) => count > 0 ) );
|
|
21
24
|
|
|
22
25
|
return (
|
|
23
|
-
<Box
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
<Box display="flex" flexDirection="column" gap={ 3 } p={ 2 }>
|
|
27
|
+
<SeveritySummaryCards report={ report } />
|
|
28
|
+
<Box display="flex" flexDirection="column" gap={ 1 }>
|
|
29
|
+
<Typography variant="subtitle1" component="h2">
|
|
30
|
+
{ __( 'All issues', 'elementor' ) }
|
|
31
|
+
</Typography>
|
|
32
|
+
<Box display="flex" flexDirection="column" gap={ 1 }>
|
|
33
|
+
{ categoryRows.map( ( { category, counts } ) => (
|
|
34
|
+
<IssuesCategoryRow
|
|
35
|
+
key={ category }
|
|
36
|
+
category={ category }
|
|
37
|
+
label={ CATEGORY_LABELS[ category ] }
|
|
38
|
+
counts={ counts }
|
|
39
|
+
onClick={ () => onCategoryClick( category ) }
|
|
40
|
+
/>
|
|
41
|
+
) ) }
|
|
37
42
|
</Box>
|
|
38
43
|
</Box>
|
|
39
|
-
<Box sx={ { display: 'flex', flexDirection: 'column', gap: 2 } }>
|
|
40
|
-
{ populatedCategories.map( ( category ) => (
|
|
41
|
-
<ScoreBar
|
|
42
|
-
key={ category }
|
|
43
|
-
label={ CATEGORY_LABELS[ category ] }
|
|
44
|
-
score={ report.categories[ category ].score }
|
|
45
|
-
onClick={ () => onCategoryClick( category ) }
|
|
46
|
-
/>
|
|
47
|
-
) ) }
|
|
48
|
-
</Box>
|
|
49
44
|
<Promotions report={ report } />
|
|
50
45
|
</Box>
|
|
51
46
|
);
|
|
@@ -1,114 +1,33 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
-
import { Box
|
|
4
|
-
import { __ } from '@wordpress/i18n';
|
|
3
|
+
import { Box } from '@elementor/ui';
|
|
5
4
|
|
|
6
5
|
import { type AuditCategory, type PageAuditReport } from '../types';
|
|
7
|
-
import { type AuditStatusGroup } from '../utils/audit-status-summary';
|
|
8
|
-
import AllAuditsPage from './pages/all-audits-page';
|
|
9
6
|
import CategoryPage from './pages/category-page';
|
|
10
|
-
import IssuesPage from './pages/issues-page';
|
|
11
7
|
import OverviewPage from './pages/overview-page';
|
|
12
8
|
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
type ActivePage =
|
|
16
|
-
| 'overview'
|
|
17
|
-
| 'issues'
|
|
18
|
-
| { backTo: TabId; category: AuditCategory }
|
|
19
|
-
| { backTo: TabId; expand?: AuditStatusGroup; page: 'all-audits' };
|
|
9
|
+
type ActivePage = 'overview' | { category: AuditCategory };
|
|
20
10
|
|
|
21
11
|
type Props = {
|
|
22
12
|
report: PageAuditReport;
|
|
23
13
|
};
|
|
24
14
|
|
|
25
|
-
function
|
|
26
|
-
if ( page === 'overview' ) {
|
|
27
|
-
return 'overview';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if ( page === 'issues' ) {
|
|
31
|
-
return 'issues';
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return page.backTo;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function isAllAuditsPage( page: ActivePage ): page is { backTo: TabId; expand?: AuditStatusGroup; page: 'all-audits' } {
|
|
38
|
-
return typeof page === 'object' && 'page' in page && page.page === 'all-audits';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isCategoryPage( page: ActivePage ): page is { backTo: TabId; category: AuditCategory } {
|
|
15
|
+
function isCategoryPage( page: ActivePage ): page is { category: AuditCategory } {
|
|
42
16
|
return typeof page === 'object' && 'category' in page;
|
|
43
17
|
}
|
|
44
18
|
|
|
45
19
|
export default function ReportShell( { report }: Props ) {
|
|
46
20
|
const [ activePage, setActivePage ] = useState< ActivePage >( 'overview' );
|
|
47
21
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const handleTabChange = ( _: React.SyntheticEvent, value: TabId ) => {
|
|
51
|
-
setActivePage( value );
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const openCategory = ( category: AuditCategory, backTo: TabId ) => {
|
|
55
|
-
setActivePage( { category, backTo } );
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const openAllAudits = ( expand?: AuditStatusGroup, backTo: TabId = 'issues' ) => {
|
|
59
|
-
setActivePage( { page: 'all-audits', expand, backTo } );
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const backFromSubPage = () => {
|
|
63
|
-
if ( isAllAuditsPage( activePage ) || isCategoryPage( activePage ) ) {
|
|
64
|
-
setActivePage( activePage.backTo );
|
|
65
|
-
}
|
|
66
|
-
};
|
|
22
|
+
const openCategory = ( category: AuditCategory ) => setActivePage( { category } );
|
|
23
|
+
const backToOverview = () => setActivePage( 'overview' );
|
|
67
24
|
|
|
68
25
|
return (
|
|
69
|
-
<Box sx={ {
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
onChange={ handleTabChange }
|
|
75
|
-
textColor="secondary"
|
|
76
|
-
indicatorColor="secondary"
|
|
77
|
-
size="small"
|
|
78
|
-
centered={ true }
|
|
79
|
-
variant="fullWidth"
|
|
80
|
-
>
|
|
81
|
-
<Tab value="overview" label={ __( 'Overview', 'elementor' ) } />
|
|
82
|
-
<Tab value="issues" label={ __( 'Issues', 'elementor' ) } />
|
|
83
|
-
</Tabs>
|
|
84
|
-
<Divider />
|
|
85
|
-
</Box>
|
|
86
|
-
<Box sx={ { flex: 1, minHeight: 0, overflowY: 'auto' } }>
|
|
87
|
-
{ activePage === 'overview' && (
|
|
88
|
-
<OverviewPage
|
|
89
|
-
report={ report }
|
|
90
|
-
onCategoryClick={ ( category ) => openCategory( category, 'overview' ) }
|
|
91
|
-
/>
|
|
92
|
-
) }
|
|
93
|
-
{ activePage === 'issues' && (
|
|
94
|
-
<IssuesPage
|
|
95
|
-
report={ report }
|
|
96
|
-
onCategoryClick={ ( category ) => openCategory( category, 'issues' ) }
|
|
97
|
-
onAllAuditsClick={ () => openAllAudits() }
|
|
98
|
-
onStatusClick={ ( status ) => openAllAudits( status ) }
|
|
99
|
-
/>
|
|
100
|
-
) }
|
|
101
|
-
{ isAllAuditsPage( activePage ) && (
|
|
102
|
-
<AllAuditsPage
|
|
103
|
-
report={ report }
|
|
104
|
-
initialExpandedStatus={ activePage.expand }
|
|
105
|
-
onBack={ backFromSubPage }
|
|
106
|
-
/>
|
|
107
|
-
) }
|
|
108
|
-
{ isCategoryPage( activePage ) && (
|
|
109
|
-
<CategoryPage category={ activePage.category } report={ report } onBack={ backFromSubPage } />
|
|
110
|
-
) }
|
|
111
|
-
</Box>
|
|
26
|
+
<Box sx={ { flex: 1, minHeight: 0, overflowY: 'auto' } }>
|
|
27
|
+
{ activePage === 'overview' && <OverviewPage report={ report } onCategoryClick={ openCategory } /> }
|
|
28
|
+
{ isCategoryPage( activePage ) && (
|
|
29
|
+
<CategoryPage category={ activePage.category } report={ report } onBack={ backToOverview } />
|
|
30
|
+
) }
|
|
112
31
|
</Box>
|
|
113
32
|
);
|
|
114
33
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useFloatingPanelZIndex } from '@elementor/editor-floating-panels';
|
|
3
|
+
import { ReloadIcon } from '@elementor/icons';
|
|
4
|
+
import { Badge, Button, Tooltip } from '@elementor/ui';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
6
|
+
|
|
7
|
+
import { AUDIT_PANEL_ID } from '../constants';
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
hasScanned: boolean;
|
|
11
|
+
isStale: boolean;
|
|
12
|
+
disabled: boolean;
|
|
13
|
+
onClick: () => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function RescanButton( { hasScanned, isStale, disabled, onClick }: Props ) {
|
|
17
|
+
const panelZIndex = useFloatingPanelZIndex( AUDIT_PANEL_ID );
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Tooltip
|
|
21
|
+
title={ __( 'Page content changed - rescan advised', 'elementor' ) }
|
|
22
|
+
disableHoverListener={ ! isStale }
|
|
23
|
+
PopperProps={ { sx: { zIndex: panelZIndex } } }
|
|
24
|
+
>
|
|
25
|
+
<Badge variant="dot" color="warning" invisible={ ! isStale } overlap="rectangular">
|
|
26
|
+
<Button
|
|
27
|
+
variant="contained"
|
|
28
|
+
size="small"
|
|
29
|
+
startIcon={ hasScanned ? <ReloadIcon fontSize="small" /> : undefined }
|
|
30
|
+
onClick={ onClick }
|
|
31
|
+
disabled={ disabled }
|
|
32
|
+
>
|
|
33
|
+
{ hasScanned ? __( 'Rescan', 'elementor' ) : __( 'Run page audit', 'elementor' ) }
|
|
34
|
+
</Button>
|
|
35
|
+
</Badge>
|
|
36
|
+
</Tooltip>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -20,7 +20,11 @@ type Props = {
|
|
|
20
20
|
severity: AuditSeverity;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
const INFO_ICON_VERTICAL_NUDGE = 'translateY(-2px)';
|
|
24
|
+
|
|
23
25
|
export default function SeverityIcon( { severity }: Props ) {
|
|
24
26
|
const { Icon, color } = SEVERITY_CONFIG[ severity ];
|
|
25
|
-
|
|
27
|
+
const sx = 'info' === severity ? { transform: INFO_ICON_VERTICAL_NUDGE } : undefined;
|
|
28
|
+
|
|
29
|
+
return <Icon fontSize="small" color={ color } sx={ sx } />;
|
|
26
30
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Box, Typography } from '@elementor/ui';
|
|
3
|
+
|
|
4
|
+
import { type AuditSeverity, type PageAuditReport } from '../types';
|
|
5
|
+
import { ALL_SEVERITIES, countSeverities, severityPluralLabel } from '../utils/severity-counts';
|
|
6
|
+
import SeverityIcon from './severity-icons';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
report: PageAuditReport;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function SeveritySummaryCards( { report }: Props ) {
|
|
13
|
+
const counts = countSeverities( report );
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Box display="flex" gap={ 1.5 }>
|
|
17
|
+
{ ALL_SEVERITIES.map( ( severity ) => (
|
|
18
|
+
<SeverityCard key={ severity } severity={ severity } count={ counts[ severity ] } />
|
|
19
|
+
) ) }
|
|
20
|
+
</Box>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type SeverityCardProps = {
|
|
25
|
+
severity: AuditSeverity;
|
|
26
|
+
count: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function SeverityCard( { severity, count }: SeverityCardProps ) {
|
|
30
|
+
return (
|
|
31
|
+
<Box
|
|
32
|
+
display="flex"
|
|
33
|
+
flex={ 1 }
|
|
34
|
+
flexDirection="column"
|
|
35
|
+
gap={ 1 }
|
|
36
|
+
bgcolor="action.hover"
|
|
37
|
+
borderRadius={ 1.5 }
|
|
38
|
+
px={ 1.5 }
|
|
39
|
+
py={ 1 }
|
|
40
|
+
>
|
|
41
|
+
<SeverityIcon severity={ severity } />
|
|
42
|
+
<Box display="flex" flexDirection="column">
|
|
43
|
+
<Typography variant="h5" fontWeight={ 700 } component="span">
|
|
44
|
+
{ count }
|
|
45
|
+
</Typography>
|
|
46
|
+
<Typography variant="caption" color="text.secondary">
|
|
47
|
+
{ severityPluralLabel( severity ) }
|
|
48
|
+
</Typography>
|
|
49
|
+
</Box>
|
|
50
|
+
</Box>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { WandIcon } from '@elementor/icons';
|
|
2
3
|
import { Button } from '@elementor/ui';
|
|
3
4
|
|
|
4
5
|
type Props = {
|
|
5
6
|
ctaLabel: string;
|
|
6
7
|
externalUrl: string;
|
|
8
|
+
variant?: 'outlined' | 'text';
|
|
9
|
+
withIcon?: boolean;
|
|
7
10
|
};
|
|
8
11
|
|
|
9
|
-
export default function ViolationCtaButton( { ctaLabel, externalUrl }: Props ) {
|
|
12
|
+
export default function ViolationCtaButton( { ctaLabel, externalUrl, variant = 'outlined', withIcon = false }: Props ) {
|
|
10
13
|
const handleClick = ( event: React.MouseEvent< HTMLButtonElement > ) => {
|
|
11
14
|
event.stopPropagation();
|
|
12
15
|
event.preventDefault();
|
|
@@ -15,7 +18,13 @@ export default function ViolationCtaButton( { ctaLabel, externalUrl }: Props ) {
|
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
return (
|
|
18
|
-
<Button
|
|
21
|
+
<Button
|
|
22
|
+
variant={ variant }
|
|
23
|
+
color="secondary"
|
|
24
|
+
size="small"
|
|
25
|
+
startIcon={ withIcon ? <WandIcon fontSize="tiny" aria-hidden={ true } /> : undefined }
|
|
26
|
+
onClick={ handleClick }
|
|
27
|
+
>
|
|
19
28
|
{ ctaLabel }
|
|
20
29
|
</Button>
|
|
21
30
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { FileSettingsIcon, SettingsIcon
|
|
2
|
+
import { FileSettingsIcon, SettingsIcon } from '@elementor/icons';
|
|
3
3
|
import { Box } from '@elementor/ui';
|
|
4
4
|
|
|
5
5
|
import { type AuditViolation } from '../types';
|
|
@@ -29,5 +29,5 @@ export default function ViolationIcon( { violation, widgetIcon }: Props ) {
|
|
|
29
29
|
return <SettingsIcon fontSize="inherit" aria-hidden={ true } />;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
return <
|
|
32
|
+
return <SettingsIcon fontSize="inherit" aria-hidden={ true } />;
|
|
33
33
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { useState } from 'react';
|
|
3
2
|
import { getElementIcon, getElementTitle } from '@elementor/editor-elements';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { useFloatingPanelZIndex } from '@elementor/editor-floating-panels';
|
|
4
|
+
import { CheckIcon, ChevronDownIcon, HelpIcon, InfoCircleIcon } from '@elementor/icons';
|
|
5
|
+
import { Alert, Box, Collapse, IconButton, Tooltip, Typography } from '@elementor/ui';
|
|
6
6
|
import { __ } from '@wordpress/i18n';
|
|
7
7
|
|
|
8
|
+
import { AUDIT_PANEL_ID } from '../constants';
|
|
8
9
|
import { focusViolation } from '../hooks/focus-violation';
|
|
9
10
|
import { type AuditMeta, type AuditViolation } from '../types';
|
|
10
11
|
import { buildAngiePrompt } from '../utils/build-angie-prompt';
|
|
@@ -17,6 +18,8 @@ import ViolationIcon from './violation-icons';
|
|
|
17
18
|
|
|
18
19
|
type Props = {
|
|
19
20
|
audit: AuditMeta;
|
|
21
|
+
expanded: boolean;
|
|
22
|
+
onToggleExpand: () => void;
|
|
20
23
|
skipReason?: string;
|
|
21
24
|
violations?: AuditViolation[];
|
|
22
25
|
};
|
|
@@ -35,12 +38,12 @@ function StatusIndicator( { audit, violations }: Pick< Props, 'audit' | 'violati
|
|
|
35
38
|
if ( violations ) {
|
|
36
39
|
return (
|
|
37
40
|
<>
|
|
41
|
+
<SeverityIcon severity={ audit.severity } />
|
|
38
42
|
{ isScoredAudit( audit ) && (
|
|
39
43
|
<Typography variant="caption" color="text.secondary" fontWeight="bold">
|
|
40
44
|
{ violations.length }
|
|
41
45
|
</Typography>
|
|
42
46
|
) }
|
|
43
|
-
<SeverityIcon severity={ audit.severity } />
|
|
44
47
|
</>
|
|
45
48
|
);
|
|
46
49
|
}
|
|
@@ -48,10 +51,73 @@ function StatusIndicator( { audit, violations }: Pick< Props, 'audit' | 'violati
|
|
|
48
51
|
return <CheckIcon fontSize="small" color="success" />;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
function GuidanceAction( {
|
|
55
|
+
violation,
|
|
56
|
+
sharedAngiePrompt,
|
|
57
|
+
panelZIndex,
|
|
58
|
+
}: {
|
|
59
|
+
violation: AuditViolation;
|
|
60
|
+
sharedAngiePrompt?: string;
|
|
61
|
+
panelZIndex: number;
|
|
62
|
+
} ) {
|
|
63
|
+
const hasPrimaryCta = !! ( violation.ctaLabel && violation.externalUrl );
|
|
53
64
|
|
|
54
|
-
|
|
65
|
+
if ( ! hasPrimaryCta && ! sharedAngiePrompt ) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Box display="flex" alignItems="center" gap={ 1 } mt={ 1 }>
|
|
71
|
+
{ hasPrimaryCta && (
|
|
72
|
+
<ViolationCtaButton
|
|
73
|
+
ctaLabel={ violation.ctaLabel as string }
|
|
74
|
+
externalUrl={ violation.externalUrl as string }
|
|
75
|
+
withIcon
|
|
76
|
+
/>
|
|
77
|
+
) }
|
|
78
|
+
{ hasPrimaryCta && violation.secondaryCtaLabel && violation.secondaryCtaUrl && (
|
|
79
|
+
<ViolationCtaButton
|
|
80
|
+
ctaLabel={ violation.secondaryCtaLabel }
|
|
81
|
+
externalUrl={ violation.secondaryCtaUrl }
|
|
82
|
+
variant="text"
|
|
83
|
+
/>
|
|
84
|
+
) }
|
|
85
|
+
{ sharedAngiePrompt && (
|
|
86
|
+
<FixViolationWithAngie prompt={ sharedAngiePrompt } panelZIndex={ panelZIndex } variant="button" />
|
|
87
|
+
) }
|
|
88
|
+
</Box>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getSharedAngiePrompt( violations?: AuditViolation[] ): string | undefined {
|
|
93
|
+
if ( ! violations || violations.length < 2 ) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [ first, ...rest ] = violations;
|
|
98
|
+
|
|
99
|
+
if ( ! first.angieFix || ! first.angiePrompt ) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const allShareTheSameFix = rest.every(
|
|
104
|
+
( violation ) => violation.angieFix && violation.angiePrompt === first.angiePrompt
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return allShareTheSameFix ? first.angiePrompt : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default function ViolationRow( { audit, expanded, onToggleExpand, skipReason, violations }: Props ) {
|
|
111
|
+
const primaryViolation = violations?.[ 0 ];
|
|
112
|
+
const isGuidanceFocusable = !! primaryViolation;
|
|
113
|
+
const panelZIndex = useFloatingPanelZIndex( AUDIT_PANEL_ID );
|
|
114
|
+
const sharedAngiePrompt = getSharedAngiePrompt( violations );
|
|
115
|
+
|
|
116
|
+
const handleGuidanceClick = () => {
|
|
117
|
+
if ( isGuidanceFocusable && primaryViolation ) {
|
|
118
|
+
focusViolation( primaryViolation );
|
|
119
|
+
}
|
|
120
|
+
};
|
|
55
121
|
|
|
56
122
|
return (
|
|
57
123
|
<Box sx={ { borderBottom: 1, borderColor: 'divider', paddingBlock: 0.5 } }>
|
|
@@ -65,9 +131,9 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
|
|
|
65
131
|
gap: 0.5,
|
|
66
132
|
minWidth: 0,
|
|
67
133
|
} }
|
|
68
|
-
onClick={
|
|
134
|
+
onClick={ onToggleExpand }
|
|
69
135
|
>
|
|
70
|
-
<Typography variant="
|
|
136
|
+
<Typography variant="subtitle2" color="text.secondary" sx={ { flex: 1 } }>
|
|
71
137
|
{ audit.title }
|
|
72
138
|
</Typography>
|
|
73
139
|
{ ! skipReason && <StatusIndicator audit={ audit } violations={ violations } /> }
|
|
@@ -76,7 +142,7 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
|
|
|
76
142
|
<IconButton
|
|
77
143
|
size="small"
|
|
78
144
|
aria-label={ expanded ? __( 'Collapse', 'elementor' ) : __( 'Expand', 'elementor' ) }
|
|
79
|
-
onClick={
|
|
145
|
+
onClick={ onToggleExpand }
|
|
80
146
|
>
|
|
81
147
|
<ChevronDownIcon
|
|
82
148
|
fontSize="small"
|
|
@@ -88,44 +154,40 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
|
|
|
88
154
|
</IconButton>
|
|
89
155
|
</Box>
|
|
90
156
|
<Collapse in={ expanded }>
|
|
91
|
-
<Box
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
157
|
+
<Box paddingBlock={ 1 }>
|
|
158
|
+
<Tooltip title={ audit.fixHint } placement="top">
|
|
159
|
+
<Alert
|
|
160
|
+
severity="secondary"
|
|
161
|
+
sx={ { p: 1, cursor: isGuidanceFocusable ? 'pointer' : undefined } }
|
|
162
|
+
icon={ <InfoCircleIcon fontSize="small" aria-hidden={ true } /> }
|
|
163
|
+
role={ isGuidanceFocusable ? 'button' : undefined }
|
|
164
|
+
tabIndex={ isGuidanceFocusable ? 0 : undefined }
|
|
165
|
+
onClick={ isGuidanceFocusable ? handleGuidanceClick : undefined }
|
|
166
|
+
onKeyDown={ isGuidanceFocusable ? onKeyboardClick( handleGuidanceClick ) : undefined }
|
|
167
|
+
>
|
|
168
|
+
<Typography variant="caption" component="p" color="text.secondary">
|
|
169
|
+
{ audit.description }
|
|
100
170
|
</Typography>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
>
|
|
111
|
-
<AlertTitle>
|
|
112
|
-
<Typography variant="caption" component="p" color="text.primary" fontWeight="bold">
|
|
113
|
-
{ __( 'How to resolve', 'elementor' ) }
|
|
114
|
-
</Typography>
|
|
115
|
-
</AlertTitle>
|
|
116
|
-
<Typography variant="caption" component="p" color="text.secondary">
|
|
117
|
-
{ audit.fixHint }
|
|
118
|
-
</Typography>
|
|
119
|
-
</Alert>
|
|
171
|
+
{ primaryViolation && (
|
|
172
|
+
<GuidanceAction
|
|
173
|
+
violation={ primaryViolation }
|
|
174
|
+
sharedAngiePrompt={ sharedAngiePrompt }
|
|
175
|
+
panelZIndex={ panelZIndex }
|
|
176
|
+
/>
|
|
177
|
+
) }
|
|
178
|
+
</Alert>
|
|
179
|
+
</Tooltip>
|
|
120
180
|
</Box>
|
|
121
181
|
{ violations && violations.length > 0 && (
|
|
122
|
-
<Box role="list" sx={ { paddingBlockEnd: 1
|
|
182
|
+
<Box role="list" sx={ { paddingBlockEnd: 1 } }>
|
|
123
183
|
{ violations.map( ( violation, idx ) => {
|
|
124
184
|
const widgetIcon = violation.elementId ? getElementIcon( violation.elementId ) : null;
|
|
125
185
|
const elementTitle = violation.elementId ? getElementTitle( violation.elementId ) : null;
|
|
126
|
-
const
|
|
186
|
+
const displayLabel = elementTitle ?? violation.label;
|
|
187
|
+
const angiePromptContext = elementTitle
|
|
127
188
|
? `${ elementTitle } - ${ violation.label }`
|
|
128
189
|
: violation.label;
|
|
190
|
+
const showsOwnFixAction = !! violation.angieFix && ! sharedAngiePrompt;
|
|
129
191
|
|
|
130
192
|
return (
|
|
131
193
|
<Box
|
|
@@ -139,41 +201,37 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
|
|
|
139
201
|
alignItems: 'center',
|
|
140
202
|
gap: 1,
|
|
141
203
|
paddingBlock: 0.5,
|
|
142
|
-
paddingInline:
|
|
204
|
+
paddingInline: 1,
|
|
143
205
|
borderRadius: 1,
|
|
144
206
|
cursor: 'pointer',
|
|
145
|
-
'& .violation-hover-icon': { opacity: 0, transition: 'opacity .15s' },
|
|
146
207
|
'&:hover': { bgcolor: 'action.hover' },
|
|
147
|
-
'&:hover .violation-
|
|
148
|
-
|
|
149
|
-
|
|
208
|
+
'&:hover .violation-row-fix-action, &:focus-visible .violation-row-fix-action':
|
|
209
|
+
{
|
|
210
|
+
opacity: 1,
|
|
211
|
+
},
|
|
150
212
|
} }
|
|
151
213
|
>
|
|
152
214
|
<ViolationIcon violation={ violation } widgetIcon={ widgetIcon } />
|
|
153
215
|
<Box sx={ { flex: 1 } }>
|
|
154
|
-
<Typography variant="caption">{
|
|
216
|
+
<Typography variant="caption">{ displayLabel }</Typography>
|
|
155
217
|
{ violation.detail && (
|
|
156
218
|
<Typography variant="caption" color="text.secondary">
|
|
157
219
|
{ violation.detail }
|
|
158
220
|
</Typography>
|
|
159
221
|
) }
|
|
160
222
|
</Box>
|
|
161
|
-
{
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
className="violation-hover-icon"
|
|
174
|
-
fontSize="tiny"
|
|
175
|
-
aria-hidden={ true }
|
|
176
|
-
/>
|
|
223
|
+
{ showsOwnFixAction && (
|
|
224
|
+
<Box
|
|
225
|
+
className="violation-row-fix-action"
|
|
226
|
+
sx={ { opacity: 0, transition: 'opacity .15s' } }
|
|
227
|
+
>
|
|
228
|
+
<FixViolationWithAngie
|
|
229
|
+
prompt={
|
|
230
|
+
violation.angiePrompt ?? buildAngiePrompt( angiePromptContext )
|
|
231
|
+
}
|
|
232
|
+
panelZIndex={ panelZIndex }
|
|
233
|
+
/>
|
|
234
|
+
</Box>
|
|
177
235
|
) }
|
|
178
236
|
</Box>
|
|
179
237
|
);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { getHostDocumentId } from '@elementor/editor-elements';
|
|
3
3
|
import { __useDispatch as useDispatch, __useSelector as useSelector } from '@elementor/store';
|
|
4
4
|
|
|
5
5
|
import { runPageAudit } from '../runner';
|
|
6
|
-
import { type GlobalState, selectError, selectReport, selectStatus, slice } from '../store';
|
|
6
|
+
import { type GlobalState, selectError, selectIsStale, selectReport, selectStatus, slice } from '../store';
|
|
7
7
|
import { getPersistedReport, persistReport } from '../utils/report-storage';
|
|
8
8
|
import { SessionExpiredError } from '../utils/session-expiration';
|
|
9
9
|
|
|
@@ -11,8 +11,9 @@ export function useAuditReport() {
|
|
|
11
11
|
const status = useSelector( ( state: GlobalState ) => selectStatus( state ) );
|
|
12
12
|
const report = useSelector( ( state: GlobalState ) => selectReport( state ) );
|
|
13
13
|
const error = useSelector( ( state: GlobalState ) => selectError( state ) );
|
|
14
|
+
const isStale = useSelector( ( state: GlobalState ) => selectIsStale( state ) );
|
|
14
15
|
const dispatch = useDispatch();
|
|
15
|
-
const documentId =
|
|
16
|
+
const documentId = getHostDocumentId() ?? 0;
|
|
16
17
|
|
|
17
18
|
useEffect( () => {
|
|
18
19
|
if ( ! documentId || report?.documentId === documentId ) {
|
|
@@ -45,5 +46,5 @@ export function useAuditReport() {
|
|
|
45
46
|
}
|
|
46
47
|
};
|
|
47
48
|
|
|
48
|
-
return { status, report, error, run };
|
|
49
|
+
return { status, report, error, isStale, run };
|
|
49
50
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ToggleActionProps } from '@elementor/editor-app-bar';
|
|
2
|
-
import { ShieldCheckIcon } from '@elementor/icons';
|
|
3
2
|
import { __ } from '@wordpress/i18n';
|
|
4
3
|
|
|
4
|
+
import AuditToggleIcon from '../components/audit-toggle-icon';
|
|
5
5
|
import { auditPanel } from '../editor-panel';
|
|
6
6
|
|
|
7
7
|
export function useAuditToggleProps(): ToggleActionProps {
|
|
@@ -10,7 +10,7 @@ export function useAuditToggleProps(): ToggleActionProps {
|
|
|
10
10
|
|
|
11
11
|
return {
|
|
12
12
|
title: __( 'Audit Page', 'elementor' ),
|
|
13
|
-
icon:
|
|
13
|
+
icon: AuditToggleIcon,
|
|
14
14
|
selected: isOpen,
|
|
15
15
|
onClick: () => toggle(),
|
|
16
16
|
};
|