@elementor/editor-audits 4.3.4 → 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 +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +789 -1098
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +731 -1040
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -17
- package/src/audits/accessibility-statement.ts +2 -2
- package/src/audits/cookie-policy.ts +1 -0
- package/src/audits/images-alt-text.ts +6 -4
- package/src/audits/images-inefficient-format.ts +8 -9
- package/src/audits/images-too-large.ts +8 -9
- 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 +10 -2
- package/src/components/violation-icons.tsx +2 -2
- package/src/components/violation-row.tsx +118 -69
- 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 +0 -2
- package/src/register-promotions.ts +0 -23
- package/src/runner.ts +6 -2
- 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 +2 -0
- package/src/utils/all-image-sources.ts +17 -0
- package/src/utils/alt-text-image-sources.ts +24 -0
- package/src/utils/atomic-image-sources.ts +8 -4
- package/src/utils/audit-status-summary.ts +0 -57
- package/src/utils/image-like-sources.ts +0 -12
- package/src/utils/page-attachments.ts +3 -11
- 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
|
@@ -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,13 +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;
|
|
7
8
|
variant?: 'outlined' | 'text';
|
|
9
|
+
withIcon?: boolean;
|
|
8
10
|
};
|
|
9
11
|
|
|
10
|
-
export default function ViolationCtaButton( { ctaLabel, externalUrl, variant = 'outlined' }: Props ) {
|
|
12
|
+
export default function ViolationCtaButton( { ctaLabel, externalUrl, variant = 'outlined', withIcon = false }: Props ) {
|
|
11
13
|
const handleClick = ( event: React.MouseEvent< HTMLButtonElement > ) => {
|
|
12
14
|
event.stopPropagation();
|
|
13
15
|
event.preventDefault();
|
|
@@ -16,7 +18,13 @@ export default function ViolationCtaButton( { ctaLabel, externalUrl, variant = '
|
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
return (
|
|
19
|
-
<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
|
+
>
|
|
20
28
|
{ ctaLabel }
|
|
21
29
|
</Button>
|
|
22
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,50 +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
|
-
|
|
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 }
|
|
171
233
|
/>
|
|
172
|
-
{ violation.secondaryCtaLabel && violation.secondaryCtaUrl && (
|
|
173
|
-
<ViolationCtaButton
|
|
174
|
-
ctaLabel={ violation.secondaryCtaLabel }
|
|
175
|
-
externalUrl={ violation.secondaryCtaUrl }
|
|
176
|
-
variant="text"
|
|
177
|
-
/>
|
|
178
|
-
) }
|
|
179
234
|
</Box>
|
|
180
|
-
) : (
|
|
181
|
-
<EyeIcon
|
|
182
|
-
className="violation-hover-icon"
|
|
183
|
-
fontSize="tiny"
|
|
184
|
-
aria-hidden={ true }
|
|
185
|
-
/>
|
|
186
235
|
) }
|
|
187
236
|
</Box>
|
|
188
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
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useSingleExpandedCard( initialId: string | null = null ) {
|
|
4
|
+
const [ expandedId, setExpandedId ] = useState< string | null >( initialId );
|
|
5
|
+
|
|
6
|
+
const toggle = ( id: string ) => {
|
|
7
|
+
setExpandedId( ( current ) => ( current === id ? null : id ) );
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
return { expandedId, toggle };
|
|
11
|
+
}
|
package/src/init.ts
CHANGED
|
@@ -5,10 +5,12 @@ import { registerAppBarAuditsToggle } from './editor-app-bar';
|
|
|
5
5
|
import { auditPanel } from './editor-panel';
|
|
6
6
|
import { registerAllAudits } from './register-audits';
|
|
7
7
|
import { slice } from './store/slice';
|
|
8
|
+
import { syncReportStaleness } from './sync-report-staleness';
|
|
8
9
|
|
|
9
10
|
export function init(): void {
|
|
10
11
|
registerSlice( slice );
|
|
11
12
|
registerAllAudits();
|
|
12
13
|
registerFloatingPanel( auditPanel.panel );
|
|
13
14
|
registerAppBarAuditsToggle();
|
|
15
|
+
syncReportStaleness();
|
|
14
16
|
}
|
package/src/register-audits.ts
CHANGED
|
@@ -22,7 +22,6 @@ import * as robotsNoindex from './audits/robots-noindex';
|
|
|
22
22
|
import * as scanForCookies from './audits/scan-for-cookies';
|
|
23
23
|
import * as sectionsAndColumns from './audits/sections-and-columns';
|
|
24
24
|
import * as siteIdentity from './audits/site-identity';
|
|
25
|
-
import * as tooManyWidgets from './audits/too-many-widgets';
|
|
26
25
|
import { registerAudit } from './registry';
|
|
27
26
|
import { type Audit } from './types';
|
|
28
27
|
|
|
@@ -33,7 +32,6 @@ const AUDITS: Audit[] = [
|
|
|
33
32
|
|
|
34
33
|
hiddenElements.audit,
|
|
35
34
|
deprecatedWidgets.audit,
|
|
36
|
-
tooManyWidgets.audit,
|
|
37
35
|
sectionsAndColumns.audit,
|
|
38
36
|
deepNesting.audit,
|
|
39
37
|
nestedBoxedContainers.audit,
|
|
@@ -80,27 +80,4 @@ export const PROMOTIONS: PromotionConfig[] = [
|
|
|
80
80
|
},
|
|
81
81
|
getCtaUrl: firstFailExternalUrl,
|
|
82
82
|
},
|
|
83
|
-
{
|
|
84
|
-
auditId: 'audits/images-inefficient-format',
|
|
85
|
-
icon: ShieldHalfFilledIcon,
|
|
86
|
-
ctaLabel: __( 'Optimize all', 'elementor' ),
|
|
87
|
-
formatSubtitle: ( run ) => {
|
|
88
|
-
if ( run.result.status !== 'fail' ) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const count = run.result.metadata?.inefficientImageFormatCount ?? 0;
|
|
93
|
-
|
|
94
|
-
if ( count === 0 ) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return sprintf(
|
|
99
|
-
/* translators: %d: number of images served in an inefficient format. */
|
|
100
|
-
_n( '%d image', '%d images', count, 'elementor' ),
|
|
101
|
-
count
|
|
102
|
-
);
|
|
103
|
-
},
|
|
104
|
-
getCtaUrl: firstFailExternalUrl,
|
|
105
|
-
},
|
|
106
83
|
];
|
package/src/runner.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getHostDocumentElements, getHostDocumentId } from '@elementor/editor-elements';
|
|
2
2
|
|
|
3
3
|
import { fetchPageContext } from './api/page-context-client';
|
|
4
4
|
import { getRegisteredAudits } from './registry';
|
|
@@ -18,7 +18,11 @@ import { readKitSnapshot } from './utils/read-kit-snapshot';
|
|
|
18
18
|
import { buildSnapshotTree } from './utils/v1-snapshot';
|
|
19
19
|
|
|
20
20
|
export async function runPageAudit( documentId: number ): Promise< PageAuditReport > {
|
|
21
|
-
|
|
21
|
+
if ( documentId !== getHostDocumentId() ) {
|
|
22
|
+
throw new Error( 'runPageAudit: documentId must match the host document.' );
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tree = buildSnapshotTree( getHostDocumentElements() );
|
|
22
26
|
const imageSizeRequests = extractImageSizeRequests( tree );
|
|
23
27
|
const pageContext = await fetchPageContext( documentId, imageSizeRequests );
|
|
24
28
|
|
package/src/store/selectors.ts
CHANGED
package/src/store/slice.ts
CHANGED
|
@@ -6,9 +6,10 @@ type SliceState = {
|
|
|
6
6
|
status: 'idle' | 'loading' | 'error' | 'ready';
|
|
7
7
|
report: PageAuditReport | null;
|
|
8
8
|
error: string | null;
|
|
9
|
+
isStale: boolean;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
const initialState: SliceState = { status: 'idle', report: null, error: null };
|
|
12
|
+
const initialState: SliceState = { status: 'idle', report: null, error: null, isStale: false };
|
|
12
13
|
|
|
13
14
|
export const slice = __createSlice( {
|
|
14
15
|
name: 'audits',
|
|
@@ -21,6 +22,7 @@ export const slice = __createSlice( {
|
|
|
21
22
|
runSucceeded( state, action: PayloadAction< PageAuditReport > ) {
|
|
22
23
|
state.status = 'ready';
|
|
23
24
|
state.report = action.payload;
|
|
25
|
+
state.isStale = false;
|
|
24
26
|
},
|
|
25
27
|
runFailed( state, action: PayloadAction< string > ) {
|
|
26
28
|
state.status = 'error';
|
|
@@ -33,11 +35,18 @@ export const slice = __createSlice( {
|
|
|
33
35
|
state.status = 'ready';
|
|
34
36
|
state.report = action.payload;
|
|
35
37
|
state.error = null;
|
|
38
|
+
state.isStale = false;
|
|
36
39
|
},
|
|
37
40
|
reportCleared( state ) {
|
|
38
41
|
state.status = 'idle';
|
|
39
42
|
state.report = null;
|
|
40
43
|
state.error = null;
|
|
44
|
+
state.isStale = false;
|
|
45
|
+
},
|
|
46
|
+
reportStale( state ) {
|
|
47
|
+
if ( state.report ) {
|
|
48
|
+
state.isStale = true;
|
|
49
|
+
}
|
|
41
50
|
},
|
|
42
51
|
},
|
|
43
52
|
} );
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { __privateListenTo as listenTo, commandEndEvent, type CommandEvent } from '@elementor/editor-v1-adapters';
|
|
2
|
+
import { __dispatch as dispatch } from '@elementor/store';
|
|
3
|
+
|
|
4
|
+
import { slice } from './store/slice';
|
|
5
|
+
|
|
6
|
+
export function syncReportStaleness() {
|
|
7
|
+
listenTo( commandEndEvent( 'document/save/set-is-modified' ), ( e ) => {
|
|
8
|
+
const event = e as CommandEvent< { status: boolean } >;
|
|
9
|
+
|
|
10
|
+
if ( event.args?.status ) {
|
|
11
|
+
dispatch( slice.actions.reportStale() );
|
|
12
|
+
}
|
|
13
|
+
} );
|
|
14
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -72,6 +72,7 @@ export type PageContextResponse = {
|
|
|
72
72
|
ally_accessibility_statement_created: boolean;
|
|
73
73
|
ally_accessibility_statement_url: string;
|
|
74
74
|
ally_widget_settings_url: string;
|
|
75
|
+
ally_scan_url: string;
|
|
75
76
|
cookiez_plugin_active: boolean;
|
|
76
77
|
cookiez_plugin_url: string;
|
|
77
78
|
cookiez_plugin_installed: boolean;
|
|
@@ -80,6 +81,7 @@ export type PageContextResponse = {
|
|
|
80
81
|
cookiez_consent_mode_settings_url: string;
|
|
81
82
|
image_optimization_plugin_active: boolean;
|
|
82
83
|
image_optimization_plugin_url: string;
|
|
84
|
+
image_optimization_settings_url: string;
|
|
83
85
|
frontend_url: string | null;
|
|
84
86
|
site_identity: {
|
|
85
87
|
site_name_set: boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ElementSnapshotNode } from '../types';
|
|
2
|
+
import { walkAtomicBackgroundImageSources } from './atomic-background-image-sources';
|
|
3
|
+
import { walkAtomicImageSources } from './atomic-image-sources';
|
|
4
|
+
import { walkBackgroundImageSources } from './background-image-sources';
|
|
5
|
+
import { type ImageLikeMedia, walkImageLikeSources } from './image-like-sources';
|
|
6
|
+
|
|
7
|
+
export type ImageSourceVisit = {
|
|
8
|
+
node: ElementSnapshotNode;
|
|
9
|
+
media: ImageLikeMedia;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function walkAllImageSources( tree: ElementSnapshotNode[], visit: ( source: ImageSourceVisit ) => void ): void {
|
|
13
|
+
walkImageLikeSources( tree, visit );
|
|
14
|
+
walkBackgroundImageSources( tree, visit );
|
|
15
|
+
walkAtomicImageSources( tree, visit );
|
|
16
|
+
walkAtomicBackgroundImageSources( tree, visit );
|
|
17
|
+
}
|