From b0faef59d2bd317ba142b52c73e0f14fd96ae4eb Mon Sep 17 00:00:00 2001 From: dkmyta <43220201+dkmyta@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:55:07 -0700 Subject: [PATCH] Protect: Add FreeList and PaidList pagination (#39058) * Add threats list pagination * changelog * Add items per page constant, and handling * Fix mobile view * Simplify and consolidate * Fix versions * Use custom media query to improve responsiveness * Revert versions updates * Update and improve implementation * Compact * Set iconSize on parent component level * Adjustments to pagination design and accessibility (#39249) --------- Co-authored-by: Nate Weller --- .../add-protect-threats-list-pagination | 4 + .../js/components/threats-list/free-list.jsx | 67 +++++---- .../js/components/threats-list/pagination.jsx | 142 ++++++++++++++++++ .../js/components/threats-list/paid-list.jsx | 103 +++++++------ .../threats-list/styles.module.scss | 30 +++- 5 files changed, 265 insertions(+), 81 deletions(-) create mode 100644 projects/plugins/protect/changelog/add-protect-threats-list-pagination create mode 100644 projects/plugins/protect/src/js/components/threats-list/pagination.jsx diff --git a/projects/plugins/protect/changelog/add-protect-threats-list-pagination b/projects/plugins/protect/changelog/add-protect-threats-list-pagination new file mode 100644 index 0000000000000..dc57fd7f5dbf9 --- /dev/null +++ b/projects/plugins/protect/changelog/add-protect-threats-list-pagination @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Adds threats list pagination diff --git a/projects/plugins/protect/src/js/components/threats-list/free-list.jsx b/projects/plugins/protect/src/js/components/threats-list/free-list.jsx index b463be6b71feb..af87cac2cb25e 100644 --- a/projects/plugins/protect/src/js/components/threats-list/free-list.jsx +++ b/projects/plugins/protect/src/js/components/threats-list/free-list.jsx @@ -5,6 +5,7 @@ import React, { useCallback } from 'react'; import { JETPACK_SCAN_SLUG } from '../../constants'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; import FreeAccordion, { FreeAccordionItem } from '../free-accordion'; +import Pagination from './pagination'; import styles from './styles.module.scss'; const ThreatAccordionItem = ( { @@ -85,38 +86,42 @@ const ThreatAccordionItem = ( { const FreeList = ( { list } ) => { return ( - - { list.map( - ( { - description, - fixedIn, - icon, - id, - label, - name, - source, - table, - title, - type, - version, - } ) => ( - - ) + + { ( { currentItems } ) => ( + + { currentItems.map( + ( { + description, + fixedIn, + icon, + id, + label, + name, + source, + table, + title, + type, + version, + } ) => ( + + ) + ) } + ) } - + ); }; diff --git a/projects/plugins/protect/src/js/components/threats-list/pagination.jsx b/projects/plugins/protect/src/js/components/threats-list/pagination.jsx new file mode 100644 index 0000000000000..3e17bed0eeac4 --- /dev/null +++ b/projects/plugins/protect/src/js/components/threats-list/pagination.jsx @@ -0,0 +1,142 @@ +import { Button, useBreakpointMatch } from '@automattic/jetpack-components'; +import { __, sprintf } from '@wordpress/i18n'; +import { chevronLeft, chevronRight } from '@wordpress/icons'; +import React, { useCallback, useState, useMemo } from 'react'; +import styles from './styles.module.scss'; + +const PaginationButton = ( { pageNumber, currentPage, onPageChange } ) => { + const isCurrentPage = useMemo( () => currentPage === pageNumber, [ currentPage, pageNumber ] ); + + const handleClick = useCallback( () => { + onPageChange( pageNumber ); + }, [ onPageChange, pageNumber ] ); + + return ( + + ); +}; + +const Pagination = ( { list, itemPerPage = 10, children } ) => { + const [ isSm ] = useBreakpointMatch( 'sm' ); + + const [ currentPage, setCurrentPage ] = useState( 1 ); + + const handlePreviousPageClick = useCallback( + () => setCurrentPage( currentPage - 1 ), + [ currentPage, setCurrentPage ] + ); + const handleNextPageClick = useCallback( + () => setCurrentPage( currentPage + 1 ), + [ currentPage, setCurrentPage ] + ); + + const totalPages = useMemo( () => Math.ceil( list.length / itemPerPage ), [ list, itemPerPage ] ); + + const currentItems = useMemo( () => { + const indexOfLastItem = currentPage * itemPerPage; + const indexOfFirstItem = indexOfLastItem - itemPerPage; + return list.slice( indexOfFirstItem, indexOfLastItem ); + }, [ currentPage, list, itemPerPage ] ); + + const pageNumbers = useMemo( () => { + if ( isSm ) { + return [ currentPage ]; + } + + const result = [ 1 ]; + if ( currentPage > 3 && totalPages > 4 ) { + result.push( '…' ); + } + + if ( currentPage === 1 ) { + // Current page is the first page. + // i.e. [ 1 ] 2 3 4 ... 10 + result.push( currentPage + 1, currentPage + 2, currentPage + 3 ); + } else if ( currentPage === 2 ) { + // Current page is the second to first page. + // i.e. 1 [ 2 ] 3 4 ... 10 + result.push( currentPage, currentPage + 1, currentPage + 2 ); + } else if ( currentPage < totalPages - 1 ) { + // Current page is positioned in the middle of the pagination. + // i.e. 1 ... 3 [ 4 ] 5 ... 10 + result.push( currentPage - 1, currentPage, currentPage + 1 ); + } else if ( currentPage === totalPages - 1 ) { + // Current page is the second to last page. + // i.e. 1 ... 7 8 [ 9 ] 10 + currentPage > 3 && result.push( currentPage - 2 ); + currentPage > 2 && result.push( currentPage - 1 ); + result.push( currentPage ); + } else if ( currentPage === totalPages ) { + // Current page is the last page. + // i.e. 1 ... 7 8 9 [ 10 ] + currentPage >= 5 && result.push( currentPage - 3 ); + currentPage >= 4 && result.push( currentPage - 2 ); + result.push( currentPage - 1 ); + } + + if ( result[ result.length - 1 ] < totalPages - 1 ) { + result.push( '…' ); + result.push( totalPages ); + } else if ( result[ result.length - 1 ] < totalPages ) { + result.push( totalPages ); + } + + return result.filter( pageNumber => pageNumber <= totalPages || isNaN( pageNumber ) ); + }, [ currentPage, isSm, totalPages ] ); + + return ( + <> + { children( { currentItems } ) } + { totalPages > 1 && ( + + ) } + + ); +}; + +export default Pagination; diff --git a/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx b/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx index 24cbd217c74d8..5d12a4329d15d 100644 --- a/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx +++ b/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx @@ -7,6 +7,7 @@ import { STORE_ID } from '../../state/store'; import DiffViewer from '../diff-viewer'; import MarkedLines from '../marked-lines'; import PaidAccordion, { PaidAccordionItem } from '../paid-accordion'; +import Pagination from './pagination'; import styles from './styles.module.scss'; const ThreatAccordionItem = ( { @@ -177,62 +178,66 @@ const PaidList = ( { list, hideAutoFixColumn = false } ) => { return ( <> { ! isSmall && ( -
+
{ __( 'Details', 'jetpack-protect' ) } { __( 'Severity', 'jetpack-protect' ) } { ! hideAutoFixColumn && { __( 'Auto-fix', 'jetpack-protect' ) } }
) } - - { list.map( - ( { - context, - description, - diff, - filename, - firstDetected, - fixedIn, - fixedOn, - icon, - fixable, - id, - label, - name, - severity, - source, - table, - title, - type, - version, - status, - } ) => ( - - ) + + { ( { currentItems } ) => ( + + { currentItems.map( + ( { + context, + description, + diff, + filename, + firstDetected, + fixedIn, + fixedOn, + icon, + fixable, + id, + label, + name, + severity, + source, + table, + title, + type, + version, + status, + } ) => ( + + ) + ) } + ) } - + ); }; diff --git a/projects/plugins/protect/src/js/components/threats-list/styles.module.scss b/projects/plugins/protect/src/js/components/threats-list/styles.module.scss index 6942d4ecf7dc7..4a50d87b2562b 100644 --- a/projects/plugins/protect/src/js/components/threats-list/styles.module.scss +++ b/projects/plugins/protect/src/js/components/threats-list/styles.module.scss @@ -57,7 +57,7 @@ } } -.accordion-heading { +.accordion-header { display: grid; grid-template-columns: repeat( 9, 1fr ); background-color: white; @@ -99,3 +99,31 @@ } } } + +.pagination-container { + display: flex; + justify-content: center; + align-items: center; + gap: 4px; + margin-top: calc( var( --spacing-base ) * 4 ); // 24px + margin-bottom: calc(var(--spacing-base) * 2); // 16px + + button { + font-size: var( --font-body ); + width: auto; + height: auto; + padding: 0 var( --spacing-base ); // 0 | 8px + line-height: 32px; + min-width: 32px; + + &.unfocused { + color: var( --jp-black ); + background: none; + + &:hover:not(:disabled) { + color: var( --jp-black ); + background: none; + } + } + } +}