@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,24 @@
|
|
|
1
|
+
import { type ElementSnapshotNode } from '../types';
|
|
2
|
+
import { type ImageSourceVisit } from './all-image-sources';
|
|
3
|
+
import { ATOMIC_CONTENT_IMAGE_PROP_KEYS, walkAtomicImageSources } from './atomic-image-sources';
|
|
4
|
+
import { walkImageLikeSources } from './image-like-sources';
|
|
5
|
+
|
|
6
|
+
export function walkAltTextImageSources(
|
|
7
|
+
tree: ElementSnapshotNode[],
|
|
8
|
+
visit: ( source: ImageSourceVisit ) => void
|
|
9
|
+
): void {
|
|
10
|
+
walkImageLikeSources( tree, visit );
|
|
11
|
+
walkAtomicImageSources( tree, visit, ATOMIC_CONTENT_IMAGE_PROP_KEYS );
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasAltTextImages( tree: ElementSnapshotNode[] ): boolean {
|
|
15
|
+
let found = false;
|
|
16
|
+
|
|
17
|
+
walkAltTextImageSources( tree, ( { media } ) => {
|
|
18
|
+
if ( media.id || media.url ) {
|
|
19
|
+
found = true;
|
|
20
|
+
}
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
@@ -10,7 +10,9 @@ import { type ElementSnapshotNode } from '../types';
|
|
|
10
10
|
import { type ImageLikeMedia } from './image-like-sources';
|
|
11
11
|
import { walkElements } from './walk';
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
export const ATOMIC_CONTENT_IMAGE_PROP_KEYS = [ 'image' ] as const;
|
|
14
|
+
|
|
15
|
+
const ATOMIC_IMAGE_PROP_KEYS = [ ...ATOMIC_CONTENT_IMAGE_PROP_KEYS, 'poster' ] as const;
|
|
14
16
|
|
|
15
17
|
export type AtomicImageSourceVisit = {
|
|
16
18
|
node: ElementSnapshotNode;
|
|
@@ -19,14 +21,15 @@ export type AtomicImageSourceVisit = {
|
|
|
19
21
|
|
|
20
22
|
export function walkAtomicImageSources(
|
|
21
23
|
tree: ElementSnapshotNode[],
|
|
22
|
-
visit: ( source: AtomicImageSourceVisit ) => void
|
|
24
|
+
visit: ( source: AtomicImageSourceVisit ) => void,
|
|
25
|
+
propKeys: readonly string[] = ATOMIC_IMAGE_PROP_KEYS
|
|
23
26
|
): void {
|
|
24
27
|
walkElements( tree, ( node ) => {
|
|
25
28
|
if ( node.elType !== 'widget' ) {
|
|
26
29
|
return;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
for ( const key of
|
|
32
|
+
for ( const key of propKeys ) {
|
|
30
33
|
const media = extractAtomicImageMedia( node.settings[ key ] );
|
|
31
34
|
|
|
32
35
|
if ( media ) {
|
|
@@ -56,7 +59,8 @@ export function extractAtomicImageMedia( imageProp: unknown ): ImageLikeMedia |
|
|
|
56
59
|
return null;
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
const alt = stringPropTypeUtil.extract( src.alt ) ?? undefined;
|
|
59
63
|
const size = stringPropTypeUtil.extract( image.size ) ?? undefined;
|
|
60
64
|
|
|
61
|
-
return { id, url, size };
|
|
65
|
+
return { id, url, alt, size };
|
|
62
66
|
}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { type ChipProps } from '@elementor/ui';
|
|
2
|
-
import { __ } from '@wordpress/i18n';
|
|
3
|
-
|
|
4
1
|
import { type AuditCategory, type AuditResult, type AuditRun, type PageAuditReport } from '../types';
|
|
5
2
|
import { isScoredAudit } from './is-scored-audit';
|
|
6
3
|
import { sortFailedAuditResults } from './sort-failed-audits';
|
|
7
4
|
|
|
8
|
-
export type AuditStatusGroup = 'fail' | 'pass' | 'skipped';
|
|
9
|
-
|
|
10
5
|
export type PartitionedAuditResults = {
|
|
11
6
|
failed: Array< AuditRun & { result: Extract< AuditResult, { status: 'fail' } > } >;
|
|
12
7
|
passed: Array< AuditRun & { result: Extract< AuditResult, { status: 'pass' } > } >;
|
|
@@ -61,58 +56,6 @@ export function partitionAuditResults(
|
|
|
61
56
|
};
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
export function auditStatusDisplayCounts( report: PageAuditReport ): Record< AuditStatusGroup, number > {
|
|
65
|
-
let pass = 0;
|
|
66
|
-
let skipped = 0;
|
|
67
|
-
let totalViolations = 0;
|
|
68
|
-
|
|
69
|
-
for ( const { audit, result } of report.auditResults ) {
|
|
70
|
-
if ( ! isScoredAudit( audit ) ) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
switch ( result.status ) {
|
|
75
|
-
case 'fail':
|
|
76
|
-
totalViolations += result.violations.length;
|
|
77
|
-
break;
|
|
78
|
-
case 'pass':
|
|
79
|
-
pass++;
|
|
80
|
-
break;
|
|
81
|
-
case 'skipped':
|
|
82
|
-
skipped++;
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
fail: totalViolations,
|
|
89
|
-
pass,
|
|
90
|
-
skipped,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function auditStatusColor( status: AuditStatusGroup ): ChipProps[ 'color' ] {
|
|
95
|
-
switch ( status ) {
|
|
96
|
-
case 'fail':
|
|
97
|
-
return 'error';
|
|
98
|
-
case 'pass':
|
|
99
|
-
return 'success';
|
|
100
|
-
case 'skipped':
|
|
101
|
-
return 'default';
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function auditStatusLabel( status: AuditStatusGroup ): string {
|
|
106
|
-
switch ( status ) {
|
|
107
|
-
case 'fail':
|
|
108
|
-
return __( 'Failed audits', 'elementor' );
|
|
109
|
-
case 'pass':
|
|
110
|
-
return __( 'Passed audits', 'elementor' );
|
|
111
|
-
case 'skipped':
|
|
112
|
-
return __( 'Skipped audits', 'elementor' );
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
59
|
export function getPopulatedCategories(
|
|
117
60
|
categoryTotals: PageAuditReport[ 'categories' ],
|
|
118
61
|
categories: readonly AuditCategory[]
|
|
@@ -15,18 +15,6 @@ export type ImageLikeSourceVisit = {
|
|
|
15
15
|
media: ImageLikeMedia;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
export function hasPageImages( tree: ElementSnapshotNode[] ): boolean {
|
|
19
|
-
let found = false;
|
|
20
|
-
|
|
21
|
-
walkImageLikeSources( tree, ( { media } ) => {
|
|
22
|
-
if ( media.id || media.url ) {
|
|
23
|
-
found = true;
|
|
24
|
-
}
|
|
25
|
-
} );
|
|
26
|
-
|
|
27
|
-
return found;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
18
|
export function walkImageLikeSources(
|
|
31
19
|
tree: ElementSnapshotNode[],
|
|
32
20
|
visit: ( source: ImageLikeSourceVisit ) => void
|
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import { type ElementSnapshotNode } from '../types';
|
|
2
|
-
import {
|
|
3
|
-
import { walkAtomicImageSources } from './atomic-image-sources';
|
|
4
|
-
import { walkBackgroundImageSources } from './background-image-sources';
|
|
5
|
-
import { type ImageLikeMedia, walkImageLikeSources } from './image-like-sources';
|
|
2
|
+
import { walkAllImageSources } from './all-image-sources';
|
|
6
3
|
import { buildImageSizeKey } from './image-size-key';
|
|
7
4
|
|
|
8
5
|
export function extractImageSizeRequests( tree: ElementSnapshotNode[] ): string[] {
|
|
9
6
|
const keys = new Set< string >();
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
walkAllImageSources( tree, ( { media } ) => {
|
|
12
9
|
if ( media.id ) {
|
|
13
10
|
keys.add( buildImageSizeKey( { id: media.id, size: media.size } ) );
|
|
14
11
|
}
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
walkImageLikeSources( tree, ( { media } ) => collect( media ) );
|
|
18
|
-
walkBackgroundImageSources( tree, ( { media } ) => collect( media ) );
|
|
19
|
-
walkAtomicImageSources( tree, ( { media } ) => collect( media ) );
|
|
20
|
-
walkAtomicBackgroundImageSources( tree, ( { media } ) => collect( media ) );
|
|
12
|
+
} );
|
|
21
13
|
|
|
22
14
|
return Array.from( keys ).sort();
|
|
23
15
|
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { getWidgetsCache } from '@elementor/editor-elements';
|
|
2
|
-
import { __ } from '@wordpress/i18n';
|
|
3
|
-
|
|
4
|
-
import { type Audit, type ElementSnapshotNode } from '../types';
|
|
5
|
-
import { walkElements } from '../utils/walk';
|
|
6
|
-
|
|
7
|
-
const WIDGET_COUNT_THRESHOLD = 100;
|
|
8
|
-
|
|
9
|
-
function isCountableWidget( node: ElementSnapshotNode, widgetsCache: ReturnType< typeof getWidgetsCache > ): boolean {
|
|
10
|
-
if ( node.elType === 'widget' ) {
|
|
11
|
-
return true;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return !! widgetsCache?.[ node.elType ]?.meta?.is_compound;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const audit: Audit = {
|
|
18
|
-
id: 'audits/too-many-widgets',
|
|
19
|
-
title: __( 'Too many widgets', 'elementor' ),
|
|
20
|
-
description: __( 'Excessive DOM size caused by too many widgets degrades rendering performance.', 'elementor' ),
|
|
21
|
-
fixHint: __( 'Reduce the number of widgets on the page by removing or combining elements.', 'elementor' ),
|
|
22
|
-
categories: [ 'best-practices', 'performance' ],
|
|
23
|
-
severity: 'warning',
|
|
24
|
-
weight: 5,
|
|
25
|
-
evaluate: ( ctx ) => {
|
|
26
|
-
if ( ctx.elements.tree.length === 0 ) {
|
|
27
|
-
return { status: 'skipped', reason: __( 'No elements', 'elementor' ) };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const widgetsCache = getWidgetsCache();
|
|
31
|
-
let widgetCount = 0;
|
|
32
|
-
|
|
33
|
-
walkElements( ctx.elements.tree, ( node ) => {
|
|
34
|
-
if ( isCountableWidget( node, widgetsCache ) ) {
|
|
35
|
-
widgetCount++;
|
|
36
|
-
}
|
|
37
|
-
} );
|
|
38
|
-
|
|
39
|
-
if ( widgetCount <= WIDGET_COUNT_THRESHOLD ) {
|
|
40
|
-
return { status: 'pass' };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
status: 'fail',
|
|
45
|
-
violations: [
|
|
46
|
-
{
|
|
47
|
-
auditId: audit.id,
|
|
48
|
-
label: __( 'Page has too many widgets.', 'elementor' ),
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
};
|
|
52
|
-
},
|
|
53
|
-
};
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { Box, Divider, Typography } from '@elementor/ui';
|
|
3
|
-
import { __, sprintf } from '@wordpress/i18n';
|
|
4
|
-
|
|
5
|
-
import { type PageAuditReport } from '../types';
|
|
6
|
-
import {
|
|
7
|
-
auditStatusColor,
|
|
8
|
-
auditStatusDisplayCounts,
|
|
9
|
-
type AuditStatusGroup,
|
|
10
|
-
auditStatusLabel,
|
|
11
|
-
} from '../utils/audit-status-summary';
|
|
12
|
-
import CountSummaryCircle from './count-summary-circle';
|
|
13
|
-
|
|
14
|
-
type Props = {
|
|
15
|
-
report: PageAuditReport;
|
|
16
|
-
onStatusClick: ( status: AuditStatusGroup ) => void;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const STATUS_ARIA_LABELS: Record< AuditStatusGroup, ( count: number ) => string > = {
|
|
20
|
-
pass: ( count ) =>
|
|
21
|
-
sprintf(
|
|
22
|
-
/* translators: %d: number of passed audits. */
|
|
23
|
-
__( '%d passed audits, view all', 'elementor' ),
|
|
24
|
-
count
|
|
25
|
-
),
|
|
26
|
-
fail: ( count ) =>
|
|
27
|
-
sprintf(
|
|
28
|
-
/* translators: %d: number of failed audit violations. */
|
|
29
|
-
__( '%d failed audits, view all', 'elementor' ),
|
|
30
|
-
count
|
|
31
|
-
),
|
|
32
|
-
skipped: ( count ) =>
|
|
33
|
-
sprintf(
|
|
34
|
-
/* translators: %d: number of skipped audits. */
|
|
35
|
-
__( '%d skipped audits, view all', 'elementor' ),
|
|
36
|
-
count
|
|
37
|
-
),
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const STATUS_GROUPS = [ 'fail', 'pass', 'skipped' ] as const;
|
|
41
|
-
|
|
42
|
-
export default function AuditStatusesSection( { report, onStatusClick }: Props ) {
|
|
43
|
-
const statusCounts = auditStatusDisplayCounts( report );
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<>
|
|
47
|
-
<Typography variant="subtitle1" fontWeight="bold">
|
|
48
|
-
{ __( 'Audit statuses', 'elementor' ) }
|
|
49
|
-
</Typography>
|
|
50
|
-
<Box sx={ { display: 'flex', justifyContent: 'space-around', gap: 2 } }>
|
|
51
|
-
{ STATUS_GROUPS.map( ( status ) => (
|
|
52
|
-
<CountSummaryCircle
|
|
53
|
-
key={ status }
|
|
54
|
-
ariaLabel={ STATUS_ARIA_LABELS[ status ]( statusCounts[ status ] ) }
|
|
55
|
-
color={ auditStatusColor( status ) }
|
|
56
|
-
count={ statusCounts[ status ] }
|
|
57
|
-
label={ auditStatusLabel( status ) }
|
|
58
|
-
onClick={ () => onStatusClick( status ) }
|
|
59
|
-
/>
|
|
60
|
-
) ) }
|
|
61
|
-
</Box>
|
|
62
|
-
<Divider />
|
|
63
|
-
</>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { Box, Chip, type ChipProps, type Theme, Typography } from '@elementor/ui';
|
|
3
|
-
|
|
4
|
-
type Props = {
|
|
5
|
-
ariaLabel: string;
|
|
6
|
-
color: ChipProps[ 'color' ];
|
|
7
|
-
count: number;
|
|
8
|
-
label: string;
|
|
9
|
-
onClick: () => void;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const COUNT_SUMMARY_CIRCLE_SIZE = 64;
|
|
13
|
-
const COUNT_SUMMARY_BORDER_WIDTH = 4;
|
|
14
|
-
|
|
15
|
-
function chipBorderColor( theme: Theme, color: ChipProps[ 'color' ] ): string {
|
|
16
|
-
if ( color === 'default' || ! color ) {
|
|
17
|
-
return theme.palette.text.secondary;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return theme.palette[ color ].main;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export default function CountSummaryCircle( { ariaLabel, color, count, label, onClick }: Props ) {
|
|
24
|
-
return (
|
|
25
|
-
<Box
|
|
26
|
-
aria-label={ ariaLabel }
|
|
27
|
-
component="button"
|
|
28
|
-
onClick={ onClick }
|
|
29
|
-
type="button"
|
|
30
|
-
sx={ {
|
|
31
|
-
alignItems: 'center',
|
|
32
|
-
background: 'none',
|
|
33
|
-
border: 'none',
|
|
34
|
-
cursor: 'pointer',
|
|
35
|
-
display: 'flex',
|
|
36
|
-
flex: 1,
|
|
37
|
-
flexDirection: 'column',
|
|
38
|
-
gap: 0.5,
|
|
39
|
-
padding: 0,
|
|
40
|
-
} }
|
|
41
|
-
>
|
|
42
|
-
<Chip
|
|
43
|
-
color={ color }
|
|
44
|
-
label={ count }
|
|
45
|
-
size="small"
|
|
46
|
-
variant="standard"
|
|
47
|
-
sx={ ( theme: Theme ) => ( {
|
|
48
|
-
border: `${ COUNT_SUMMARY_BORDER_WIDTH }px solid ${ chipBorderColor( theme, color ) }`,
|
|
49
|
-
borderRadius: '50%',
|
|
50
|
-
height: COUNT_SUMMARY_CIRCLE_SIZE,
|
|
51
|
-
maxWidth: COUNT_SUMMARY_CIRCLE_SIZE,
|
|
52
|
-
width: COUNT_SUMMARY_CIRCLE_SIZE,
|
|
53
|
-
'& .MuiChip-label': {
|
|
54
|
-
fontSize: theme.typography.body2.fontSize,
|
|
55
|
-
fontWeight: 700,
|
|
56
|
-
lineHeight: 1,
|
|
57
|
-
padding: 0,
|
|
58
|
-
},
|
|
59
|
-
} ) }
|
|
60
|
-
/>
|
|
61
|
-
<Typography color="text.secondary" sx={ { textAlign: 'center' } } variant="caption">
|
|
62
|
-
{ label }
|
|
63
|
-
</Typography>
|
|
64
|
-
</Box>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { Box } from '@elementor/ui';
|
|
3
|
-
import { __ } from '@wordpress/i18n';
|
|
4
|
-
|
|
5
|
-
import { type PageAuditReport } from '../../types';
|
|
6
|
-
import {
|
|
7
|
-
auditStatusColor,
|
|
8
|
-
type AuditStatusGroup,
|
|
9
|
-
auditStatusLabel,
|
|
10
|
-
partitionAuditResults,
|
|
11
|
-
} from '../../utils/audit-status-summary';
|
|
12
|
-
import StatusSection from '../status-section';
|
|
13
|
-
import SubpageHeader from '../subpage-header';
|
|
14
|
-
import ViolationRow from '../violation-row';
|
|
15
|
-
|
|
16
|
-
type Props = {
|
|
17
|
-
initialExpandedStatus?: AuditStatusGroup;
|
|
18
|
-
onBack: () => void;
|
|
19
|
-
report: PageAuditReport;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export default function AllAuditsPage( { initialExpandedStatus, onBack, report }: Props ) {
|
|
23
|
-
const { failed, passed, skipped, totalViolations } = partitionAuditResults( report );
|
|
24
|
-
const expandFail = ! initialExpandedStatus || initialExpandedStatus === 'fail';
|
|
25
|
-
const expandPass = initialExpandedStatus === 'pass';
|
|
26
|
-
const expandSkipped = initialExpandedStatus === 'skipped';
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<Box key={ initialExpandedStatus ?? 'default' }>
|
|
30
|
-
<SubpageHeader title={ __( 'All audits', 'elementor' ) } onBack={ onBack } />
|
|
31
|
-
<Box sx={ { p: 1 } }>
|
|
32
|
-
<StatusSection
|
|
33
|
-
label={ auditStatusLabel( 'fail' ) }
|
|
34
|
-
count={ totalViolations }
|
|
35
|
-
color={ auditStatusColor( 'fail' ) }
|
|
36
|
-
defaultExpanded={ expandFail }
|
|
37
|
-
>
|
|
38
|
-
{ failed.map( ( r ) => (
|
|
39
|
-
<ViolationRow key={ r.audit.id } audit={ r.audit } violations={ r.result.violations } />
|
|
40
|
-
) ) }
|
|
41
|
-
</StatusSection>
|
|
42
|
-
<StatusSection
|
|
43
|
-
label={ auditStatusLabel( 'pass' ) }
|
|
44
|
-
count={ passed.length }
|
|
45
|
-
color={ auditStatusColor( 'pass' ) }
|
|
46
|
-
defaultExpanded={ expandPass }
|
|
47
|
-
>
|
|
48
|
-
{ passed.map( ( r ) => (
|
|
49
|
-
<ViolationRow key={ r.audit.id } audit={ r.audit } />
|
|
50
|
-
) ) }
|
|
51
|
-
</StatusSection>
|
|
52
|
-
<StatusSection
|
|
53
|
-
label={ auditStatusLabel( 'skipped' ) }
|
|
54
|
-
count={ skipped.length }
|
|
55
|
-
color={ auditStatusColor( 'skipped' ) }
|
|
56
|
-
defaultExpanded={ expandSkipped }
|
|
57
|
-
>
|
|
58
|
-
{ skipped.map( ( r ) => (
|
|
59
|
-
<ViolationRow key={ r.audit.id } audit={ r.audit } skipReason={ r.result.reason } />
|
|
60
|
-
) ) }
|
|
61
|
-
</StatusSection>
|
|
62
|
-
</Box>
|
|
63
|
-
</Box>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Box, Link, Typography } from '@elementor/ui';
|
|
4
|
-
import { __ } from '@wordpress/i18n';
|
|
5
|
-
|
|
6
|
-
import { ALL_CATEGORIES, CATEGORY_LABELS } from '../../constants';
|
|
7
|
-
import { type AuditCategory, type PageAuditReport } from '../../types';
|
|
8
|
-
import { type AuditStatusGroup, getPopulatedCategories } from '../../utils/audit-status-summary';
|
|
9
|
-
import { countSeverities, type SeverityCounts } from '../../utils/severity-counts';
|
|
10
|
-
import AuditStatusesSection from '../audit-statuses-section';
|
|
11
|
-
import IssuesCategoryRow from '../issues-category-row';
|
|
12
|
-
import SeverityFilterChips, { type SeverityFilter } from '../severity-filter-chips';
|
|
13
|
-
|
|
14
|
-
type Props = {
|
|
15
|
-
report: PageAuditReport;
|
|
16
|
-
onCategoryClick: ( category: AuditCategory ) => void;
|
|
17
|
-
onAllAuditsClick: () => void;
|
|
18
|
-
onStatusClick: ( status: AuditStatusGroup ) => void;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
function filterCounts( counts: SeverityCounts, filter: SeverityFilter ): SeverityCounts {
|
|
22
|
-
if ( filter === 'all' ) {
|
|
23
|
-
return counts;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return { error: 0, warning: 0, info: 0, [ filter ]: counts[ filter ] };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export default function IssuesPage( { report, onCategoryClick, onAllAuditsClick, onStatusClick }: Props ) {
|
|
30
|
-
const [ filter, setFilter ] = useState< SeverityFilter >( 'all' );
|
|
31
|
-
const populatedCategories = getPopulatedCategories( report.categories, ALL_CATEGORIES );
|
|
32
|
-
|
|
33
|
-
const categoryRows = populatedCategories
|
|
34
|
-
.map( ( category ) => ( { category, counts: filterCounts( countSeverities( report, category ), filter ) } ) )
|
|
35
|
-
.filter( ( { counts } ) => Object.values( counts ).some( ( count ) => count > 0 ) );
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<Box sx={ { display: 'flex', flexDirection: 'column', gap: 4, p: 2 } }>
|
|
39
|
-
<AuditStatusesSection report={ report } onStatusClick={ onStatusClick } />
|
|
40
|
-
<Link
|
|
41
|
-
component="button"
|
|
42
|
-
underline="none"
|
|
43
|
-
color="inherit"
|
|
44
|
-
onClick={ onAllAuditsClick }
|
|
45
|
-
sx={ { textAlign: 'start' } }
|
|
46
|
-
>
|
|
47
|
-
<Typography variant="subtitle1" component="h2">
|
|
48
|
-
{ __( 'All issues', 'elementor' ) }
|
|
49
|
-
</Typography>
|
|
50
|
-
</Link>
|
|
51
|
-
<SeverityFilterChips selected={ filter } onChange={ setFilter } />
|
|
52
|
-
<Box sx={ { display: 'flex', flexDirection: 'column', gap: 1 } }>
|
|
53
|
-
{ categoryRows.map( ( { category, counts } ) => (
|
|
54
|
-
<IssuesCategoryRow
|
|
55
|
-
key={ category }
|
|
56
|
-
category={ category }
|
|
57
|
-
label={ CATEGORY_LABELS[ category ] }
|
|
58
|
-
counts={ counts }
|
|
59
|
-
onClick={ () => onCategoryClick( category ) }
|
|
60
|
-
/>
|
|
61
|
-
) ) }
|
|
62
|
-
</Box>
|
|
63
|
-
</Box>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { ChevronRightIcon } from '@elementor/icons';
|
|
3
|
-
import { Box, LinearProgress, Rotate, Typography, useTheme } from '@elementor/ui';
|
|
4
|
-
|
|
5
|
-
import { onKeyboardClick } from '../utils/keyboard-click';
|
|
6
|
-
import { getScoreTier } from '../utils/score-thresholds';
|
|
7
|
-
|
|
8
|
-
type Props = {
|
|
9
|
-
label: string;
|
|
10
|
-
score: number;
|
|
11
|
-
onClick?: () => void;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export default function ScoreBar( { label, score, onClick }: Props ) {
|
|
15
|
-
const isRtl = 'rtl' === useTheme().direction;
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<Box
|
|
19
|
-
role={ onClick ? 'button' : undefined }
|
|
20
|
-
tabIndex={ onClick ? 0 : undefined }
|
|
21
|
-
onClick={ onClick }
|
|
22
|
-
onKeyDown={ onClick ? onKeyboardClick( onClick ) : undefined }
|
|
23
|
-
sx={ {
|
|
24
|
-
display: 'flex',
|
|
25
|
-
alignItems: 'center',
|
|
26
|
-
gap: 1,
|
|
27
|
-
cursor: onClick ? 'pointer' : 'default',
|
|
28
|
-
outline: 'none',
|
|
29
|
-
'&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main', borderRadius: 1 },
|
|
30
|
-
py: 0.5,
|
|
31
|
-
} }
|
|
32
|
-
>
|
|
33
|
-
<Typography variant="body2" sx={ { minWidth: 96 } }>
|
|
34
|
-
{ label }
|
|
35
|
-
</Typography>
|
|
36
|
-
<LinearProgress
|
|
37
|
-
variant="determinate"
|
|
38
|
-
value={ score }
|
|
39
|
-
color={ getScoreTier( score ).color }
|
|
40
|
-
sx={ { flex: 1, height: 6, borderRadius: 4, bgcolor: 'action.disabledBackground' } }
|
|
41
|
-
/>
|
|
42
|
-
<Typography
|
|
43
|
-
variant="body2"
|
|
44
|
-
color="text.primary"
|
|
45
|
-
sx={ { minWidth: 24, textAlign: 'right', fontWeight: 900 } }
|
|
46
|
-
>
|
|
47
|
-
{ score }
|
|
48
|
-
</Typography>
|
|
49
|
-
{ onClick && (
|
|
50
|
-
<Rotate in={ isRtl }>
|
|
51
|
-
<ChevronRightIcon fontSize="small" color="action" />
|
|
52
|
-
</Rotate>
|
|
53
|
-
) }
|
|
54
|
-
</Box>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { Box, CircularProgress, Typography } from '@elementor/ui';
|
|
3
|
-
|
|
4
|
-
import { getScoreTier, type ScoreColor } from '../utils/score-thresholds';
|
|
5
|
-
|
|
6
|
-
type Props = {
|
|
7
|
-
color?: ScoreColor;
|
|
8
|
-
score: number;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const SCORE_CIRCLE_SIZE = 88;
|
|
12
|
-
const SCORE_CIRCLE_THICKNESS = 3;
|
|
13
|
-
|
|
14
|
-
export default function ScoreCircle( { color, score }: Props ) {
|
|
15
|
-
const progressColor = color ?? getScoreTier( score ).color;
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<Box
|
|
19
|
-
role="progressbar"
|
|
20
|
-
aria-valuenow={ score }
|
|
21
|
-
aria-valuemin={ 0 }
|
|
22
|
-
aria-valuemax={ 100 }
|
|
23
|
-
sx={ { position: 'relative', display: 'inline-flex' } }
|
|
24
|
-
>
|
|
25
|
-
<CircularProgress
|
|
26
|
-
variant="determinate"
|
|
27
|
-
value={ 100 }
|
|
28
|
-
size={ SCORE_CIRCLE_SIZE }
|
|
29
|
-
thickness={ SCORE_CIRCLE_THICKNESS }
|
|
30
|
-
sx={ { color: 'action.disabledBackground' } }
|
|
31
|
-
/>
|
|
32
|
-
<CircularProgress
|
|
33
|
-
variant="determinate"
|
|
34
|
-
value={ score }
|
|
35
|
-
size={ SCORE_CIRCLE_SIZE }
|
|
36
|
-
thickness={ SCORE_CIRCLE_THICKNESS }
|
|
37
|
-
color={ progressColor }
|
|
38
|
-
sx={ { position: 'absolute', left: 0 } }
|
|
39
|
-
/>
|
|
40
|
-
<Box
|
|
41
|
-
sx={ {
|
|
42
|
-
position: 'absolute',
|
|
43
|
-
inset: 0,
|
|
44
|
-
display: 'flex',
|
|
45
|
-
alignItems: 'center',
|
|
46
|
-
justifyContent: 'center',
|
|
47
|
-
} }
|
|
48
|
-
>
|
|
49
|
-
<Typography
|
|
50
|
-
variant="h4"
|
|
51
|
-
component="span"
|
|
52
|
-
color="text.primary"
|
|
53
|
-
sx={ { fontWeight: 700, lineHeight: 1 } }
|
|
54
|
-
>
|
|
55
|
-
{ score }
|
|
56
|
-
</Typography>
|
|
57
|
-
</Box>
|
|
58
|
-
</Box>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { Box, Chip } from '@elementor/ui';
|
|
3
|
-
import { __ } from '@wordpress/i18n';
|
|
4
|
-
|
|
5
|
-
import { type AuditSeverity } from '../types';
|
|
6
|
-
import { ALL_SEVERITIES, severityPluralLabel } from '../utils/severity-counts';
|
|
7
|
-
import SeverityIcon from './severity-icons';
|
|
8
|
-
|
|
9
|
-
export type SeverityFilter = 'all' | AuditSeverity;
|
|
10
|
-
|
|
11
|
-
type Props = {
|
|
12
|
-
selected: SeverityFilter;
|
|
13
|
-
onChange: ( filter: SeverityFilter ) => void;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export default function SeverityFilterChips( { selected, onChange }: Props ) {
|
|
17
|
-
return (
|
|
18
|
-
<Box sx={ { display: 'flex', alignItems: 'center', gap: 1 } }>
|
|
19
|
-
<FilterChip
|
|
20
|
-
label={ __( 'All', 'elementor' ) }
|
|
21
|
-
isSelected={ selected === 'all' }
|
|
22
|
-
onClick={ () => onChange( 'all' ) }
|
|
23
|
-
/>
|
|
24
|
-
{ ALL_SEVERITIES.map( ( severity ) => (
|
|
25
|
-
<FilterChip
|
|
26
|
-
key={ severity }
|
|
27
|
-
label={ severityPluralLabel( severity ) }
|
|
28
|
-
icon={ <SeverityIcon severity={ severity } /> }
|
|
29
|
-
isSelected={ selected === severity }
|
|
30
|
-
onClick={ () => onChange( severity ) }
|
|
31
|
-
/>
|
|
32
|
-
) ) }
|
|
33
|
-
</Box>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
type FilterChipProps = {
|
|
38
|
-
label: string;
|
|
39
|
-
icon?: React.ReactElement;
|
|
40
|
-
isSelected: boolean;
|
|
41
|
-
onClick: () => void;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
function FilterChip( { label, icon, isSelected, onClick }: FilterChipProps ) {
|
|
45
|
-
return (
|
|
46
|
-
<Chip
|
|
47
|
-
label={ label }
|
|
48
|
-
icon={ icon }
|
|
49
|
-
size="small"
|
|
50
|
-
variant="filled"
|
|
51
|
-
color="default"
|
|
52
|
-
onClick={ onClick }
|
|
53
|
-
aria-pressed={ isSelected }
|
|
54
|
-
sx={ { bgcolor: isSelected ? 'action.selected' : 'transparent' } }
|
|
55
|
-
/>
|
|
56
|
-
);
|
|
57
|
-
}
|