diff --git a/src/optimizer-page/scan-results/ScanResults.scss b/src/optimizer-page/scan-results/ScanResults.scss index 8a58cd9a3..5b42b135f 100644 --- a/src/optimizer-page/scan-results/ScanResults.scss +++ b/src/optimizer-page/scan-results/ScanResults.scss @@ -143,19 +143,27 @@ align-items: center; font-size: small; gap: 40px; + } - span { + .section-collapsible-header-action-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + color: #000000; + font-variant-numeric: lining-nums tabular-nums; + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 28px; + + p{ + width: 20px; + margin: 0; display: flex; align-items: center; - justify-content: space-between; - gap: 8px; - color: #000000; - font-variant-numeric: lining-nums tabular-nums; - font-family: Inter, sans-serif; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 28px; + align-self: center; } } @@ -168,7 +176,7 @@ } .section-collapsible-item { - margin-right: -24px; + margin-right: -36px; } .section-collapsible-item-body { @@ -273,3 +281,25 @@ margin-top: 1.5rem !important; } } + +.open-section-rounded { + border: .5px solid rgb(0 0 0 / .15); + + &.is-open { + &:not(:first-child) { + border-radius: 8px; + } + + border-radius: 8px; + } +} + +.closed-section-rounded-top { + border-top-left-radius: 8px !important; + border-top-right-radius: 8px !important; +} + +.closed-section-rounded-bottom { + border-bottom-left-radius: 8px !important; + border-bottom-right-radius: 8px !important; +} diff --git a/src/optimizer-page/scan-results/ScanResults.tsx b/src/optimizer-page/scan-results/ScanResults.tsx index 041e7b5eb..f471a948c 100644 --- a/src/optimizer-page/scan-results/ScanResults.tsx +++ b/src/optimizer-page/scan-results/ScanResults.tsx @@ -1,4 +1,9 @@ -import { useState, useMemo, FC } from 'react'; +import { + useEffect, + useState, + useMemo, + FC, +} from 'react'; import { Card, Chip, @@ -43,6 +48,7 @@ const ScanResults: FC = ({ data }) => { externalForbiddenLinks: false, }; const [filters, setFilters] = useState(initialFilters); + const [openStates, setOpenStates] = useState([]); const [buttonRef, setButtonRef] = useState(null); const { @@ -56,17 +62,50 @@ const ScanResults: FC = ({ data }) => { add, remove, set, clear, }] = useCheckboxSetValues(activeFilters); + useEffect(() => { + setOpenStates(data?.sections ? data.sections.map(() => false) : []); + }, [data?.sections]); if (!data?.sections) { return ; } const { sections } = data; - + const handleToggle = (index: number) => { + setOpenStates(prev => prev.map((isOpened, i) => (i === index ? !isOpened : isOpened))); + }; const filterOptions = [ { name: intl.formatMessage(messages.brokenLabel), value: 'brokenLinks' }, { name: intl.formatMessage(messages.manualLabel), value: 'externalForbiddenLinks' }, { name: intl.formatMessage(messages.lockedLabel), value: 'lockedLinks' }, ]; + const shouldSectionRender = (sectionIndex: number): boolean => ( + (!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks) + || (filters.brokenLinks && brokenLinksCounts[sectionIndex] > 0) + || (filters.externalForbiddenLinks && externalForbiddenLinksCounts[sectionIndex] > 0) + || (filters.lockedLinks && lockedLinksCounts[sectionIndex] > 0) + ); + + const findPreviousVisibleSection = (currentIndex: number): number => { + let prevIndex = currentIndex - 1; + while (prevIndex >= 0) { + if (shouldSectionRender(prevIndex)) { + return prevIndex; + } + prevIndex--; + } + return -1; + }; + + const findNextVisibleSection = (currentIndex: number): number => { + let nextIndex = currentIndex + 1; + while (nextIndex < sections.length) { + if (shouldSectionRender(nextIndex)) { + return nextIndex; + } + nextIndex++; + } + return -1; + }; return (
@@ -138,53 +177,58 @@ const ScanResults: FC = ({ data }) => { )} {sections?.map((section, index) => { - const shouldRenderSection = ( - (!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks) - || (filters.brokenLinks && brokenLinksCounts[index] > 0) - || (filters.externalForbiddenLinks && externalForbiddenLinksCounts[index] > 0) - || (filters.lockedLinks && lockedLinksCounts[index] > 0) - ); - - if (shouldRenderSection) { - hasSectionsRendered = true; - return ( - - {section.subsections.map((subsection) => ( - <> - {subsection.units.map((unit) => { - if ( - (!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks) - || (filters.brokenLinks && unit.blocks.some(block => block.brokenLinks.length > 0)) - || (filters.externalForbiddenLinks - && unit.blocks.some(block => block.externalForbiddenLinks.length > 0)) - || (filters.lockedLinks && unit.blocks.some(block => block.lockedLinks.length > 0)) - ) { - return ( -
- -
- ); - } - return null; - })} - - ))} -
- ); + if (!shouldSectionRender(index)) { + return null; } - return hasSectionsRendered === false ? ( -
-

{intl.formatMessage(messages.noResultsFound)}

-
- ) : null; + hasSectionsRendered = true; + return ( + 0 ? (() => { + const prevVisibleIndex = findPreviousVisibleSection(index); + return prevVisibleIndex >= 0 && openStates[prevVisibleIndex]; + })() : true} + hasNextAndIsOpen={index < sections.length - 1 ? (() => { + const nextVisibleIndex = findNextVisibleSection(index); + return nextVisibleIndex >= 1 && openStates[nextVisibleIndex]; + })() : true} + key={section.id} + title={section.displayName} + brokenNumber={brokenLinksCounts[index]} + manualNumber={externalForbiddenLinksCounts[index]} + lockedNumber={lockedLinksCounts[index]} + className="section-collapsible-header" + > + {section.subsections.map((subsection) => ( + <> + {subsection.units.map((unit) => { + if ( + (!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks) + || (filters.brokenLinks && unit.blocks.some(block => block.brokenLinks.length > 0)) + || (filters.externalForbiddenLinks + && unit.blocks.some(block => block.externalForbiddenLinks.length > 0)) + || (filters.lockedLinks && unit.blocks.some(block => block.lockedLinks.length > 0)) + ) { + return ( +
+ +
+ ); + } + return null; + })} + + ))} +
+ ); })} + {hasSectionsRendered === false && ( +
+

{intl.formatMessage(messages.noResultsFound)}

+
+ )}
); }; diff --git a/src/optimizer-page/scan-results/SectionCollapsible.tsx b/src/optimizer-page/scan-results/SectionCollapsible.tsx index 8eea7973d..cf06926ec 100644 --- a/src/optimizer-page/scan-results/SectionCollapsible.tsx +++ b/src/optimizer-page/scan-results/SectionCollapsible.tsx @@ -1,4 +1,4 @@ -import { useState, FC } from 'react'; +import { FC } from 'react'; import { Collapsible, Icon, @@ -14,6 +14,11 @@ import lockedIcon from './lockedIcon'; import ManualIcon from './manualIcon'; interface Props { + index: number; + handleToggle: Function; + isOpen: boolean; + hasPrevAndIsOpen: boolean; + hasNextAndIsOpen: boolean; title: string; children: React.ReactNode; brokenNumber: number; @@ -23,10 +28,19 @@ interface Props { } const SectionCollapsible: FC = ({ - title, children, brokenNumber = 0, manualNumber = 0, lockedNumber = 0, className = '', + index, + handleToggle, + isOpen, + hasPrevAndIsOpen, + hasNextAndIsOpen, + title, + children, + brokenNumber, + manualNumber, + lockedNumber, + className, }) => { - const [isOpen, setIsOpen] = useState(false); - const styling = 'card-lg rounded-sm'; + const styling = `card-lg open-section-rounded ${hasPrevAndIsOpen ? 'closed-section-rounded-top' : ''} ${hasNextAndIsOpen ? 'closed-section-rounded-bottom' : ''}`; const collapsibleTitle = (
@@ -34,18 +48,18 @@ const SectionCollapsible: FC = ({ {title}
- +
- {brokenNumber} - - +

{brokenNumber}

+
+
- {manualNumber} - - +

{manualNumber}

+
+
- {lockedNumber} - +

{lockedNumber}

+
); @@ -63,7 +77,7 @@ const SectionCollapsible: FC = ({ iconWhenClosed="" iconWhenOpen="" open={isOpen} - onToggle={() => setIsOpen(!isOpen)} + onToggle={() => handleToggle(index)} > {children}