From 8dc82f61269fde13bc4e8dd0d96b35dee2705ae8 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Thu, 30 May 2024 12:57:10 +0800 Subject: [PATCH 1/9] Add layout and functionality for 2d broken link checker --- src/assets/images/NoBrokenLinksImage.tsx | 1215 +++++++---------- src/components/paginateBtn.jsx | 34 + src/layouts/LinkReport/LinksReport.tsx | 678 +++++---- .../LinkReportModal/LinkReportModal.tsx | 528 +++++++ 4 files changed, 1502 insertions(+), 953 deletions(-) create mode 100644 src/components/paginateBtn.jsx create mode 100644 src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx diff --git a/src/assets/images/NoBrokenLinksImage.tsx b/src/assets/images/NoBrokenLinksImage.tsx index 51bcb4077..902dbea0c 100644 --- a/src/assets/images/NoBrokenLinksImage.tsx +++ b/src/assets/images/NoBrokenLinksImage.tsx @@ -3,696 +3,539 @@ export const NoBrokenLinksImage = ( ): JSX.Element => { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/src/components/paginateBtn.jsx b/src/components/paginateBtn.jsx new file mode 100644 index 000000000..60906635e --- /dev/null +++ b/src/components/paginateBtn.jsx @@ -0,0 +1,34 @@ +import { Text, HStack } from "@chakra-ui/react" +import { + IconButton, + BxChevronLeft, + BxChevronRight, +} from "@opengovsg/design-system-react" + +import { typography } from "theme/foundations/typography" + +export default function PaginateBtn({ currentPage, totalPage, onPageChange }) { + return ( + + + Page {currentPage} out of {totalPage}{" "} + + } + onClick={() => onPageChange(currentPage - 1)} + isDisabled={currentPage === 1} + marginLeft="20px" + marginRight="12px" + /> + } + onClick={() => onPageChange(currentPage + 1)} + isDisabled={currentPage === totalPage} + /> + + ) +} diff --git a/src/layouts/LinkReport/LinksReport.tsx b/src/layouts/LinkReport/LinksReport.tsx index 06849e56d..b19748f05 100644 --- a/src/layouts/LinkReport/LinksReport.tsx +++ b/src/layouts/LinkReport/LinksReport.tsx @@ -2,6 +2,8 @@ import { Box, BreadcrumbItem, Center, + Grid, + GridItem, HStack, Table, TableContainer, @@ -12,13 +14,21 @@ import { Thead, Tr, VStack, + useDisclosure, + keyframes, } from "@chakra-ui/react" import { useFeatureIsOn } from "@growthbook/growthbook-react" -import { Badge, Breadcrumb, Button, Link } from "@opengovsg/design-system-react" -import { set } from "lodash" -import { useEffect, useState } from "react" -import { useQueryClient } from "react-query" -import { Redirect, useParams } from "react-router-dom" +import { + Badge, + Breadcrumb, + Button, + BxRightArrowAlt, +} from "@opengovsg/design-system-react" +import React, { useEffect, useState } from "react" +import { BiLoaderAlt } from "react-icons/bi" +import { Redirect, useParams, Link as RouterLink } from "react-router-dom" + +import PaginateBtn from "components/paginateBtn" import { SITE_LINK_CHECKER_STATUS_KEY } from "constants/queryKeys" @@ -27,23 +37,21 @@ import { useGetBrokenLinks } from "hooks/siteDashboardHooks/useGetLinkChecker" import { useRefreshLinkChecker } from "hooks/siteDashboardHooks/useRefreshLinkChecker" import { NoBrokenLinksImage } from "assets" +import { colors } from "theme/foundations/colors" +import { typography } from "theme/foundations/typography" import { isBrokenRefError, NonPermalinkError, NonPermalinkErrorDto, RepoError, + RepoErrorDto, } from "types/linkReport" import { SiteViewHeader } from "../layouts/SiteViewLayout/SiteViewHeader" +import { LinkReportModal } from "./components/LinkReportModal/LinkReportModal" + const getBreadcrumb = (viewablePageInCms: string): string => { - /** - * There are four main types of pages - * 1. /folders/parentFolder/subfolders/childFolder/editPage/page.md -> parentFolder/childFolder/page - * 2. /folders/parentFolder/editPage/page.md -> parentFolder/page - * 3. /editPage/page.md -> page - * 4. /resourceRoom/resourceRmName/resourceCategory/resourceCatName/editPage/page.md -> resourceRmName/resourceCatName/page - */ const paths = viewablePageInCms.split("/") let breadcrumb = paths .filter((_, index) => index % 2 === 0) @@ -53,7 +61,6 @@ const getBreadcrumb = (viewablePageInCms: string): string => { if (breadcrumb.endsWith(".md")) { breadcrumb = breadcrumb.slice(0, -3) } - return breadcrumb } @@ -63,50 +70,56 @@ export const LinksReportBanner = () => { const onClick = () => { refreshLinkChecker(siteName) } + const isBrokenLinksReporterEnabled = useFeatureIsOn( "is_broken_links_report_enabled" ) - const { data: brokenLinks, error: brokenLinksError, isLoading: isBrokenLinksFetching, } = useGetBrokenLinks(siteName, isBrokenLinksReporterEnabled) - const isBrokenLinksLoading = brokenLinks?.status === "loading" || isBrokenLinksFetching + return (
- - - - Experimental feature - - - Broken references report - - - - This report contains a list of broken references found in your site. - - - {!brokenLinksError && ( - - )} - + + + + + + Experimental feature + + Broken references report for {siteName} + + + {!brokenLinksError && ( + + )} + + +
) } @@ -122,213 +135,22 @@ const normaliseUrl = (url: string): string => { return normalisedUrl } -const SiteReportCard = ({ - breadcrumb, - links, -}: { - breadcrumb: string - links: NonPermalinkErrorDto[] -}) => { - // can use any link since we know all the links are from the same page - const { viewablePageInStaging, viewablePageInCms } = links[0] - const { siteName } = useParams<{ siteName: string }>() - const { data: stagingUrl, isLoading: isStagingUrlLoading } = useGetStagingUrl( - siteName - ) - - const normalisedStagingUrl = normaliseUrl(stagingUrl || "") - const normalisedViewablePageInStaging = normaliseUrl(viewablePageInStaging) - const viewableLinkInStaging = `${normalisedStagingUrl}/${normalisedViewablePageInStaging}` - - return ( - - - - {breadcrumb.split("/").map((item) => { - return ( - - {item} - - ) - })} - - - - View on staging - - - Edit page - - - - - - - - - - - - - - {links.map((link) => { - const errorType = link.type - .split("-") - .map( - (word: string) => word.charAt(0).toUpperCase() + word.slice(1) - ) - .join(" ") - - const isBrokenLink = link.type === "broken-link" - - if (isBrokenLink) { - return ( - - - - {link.linkToAsset ? ( - - ) : ( - - )} - - {link.linkedText ? ( - - ) : ( - - )} - - ) - } - - return ( - - - - - - ) - })} - -
Error typeBroken URLLink Text
{errorType}{link.linkToAsset}No URL linked{link.linkedText}Empty link text
{errorType}{link.linkToAsset}Not applicable
-
-
- ) -} - -const LinkContent = ({ brokenLinks }: { brokenLinks: RepoError[] }) => { - const links: NonPermalinkError[] = (brokenLinks.filter((error) => - isBrokenRefError(error) - ) as NonPermalinkErrorDto[]).map((error) => { - return { - ...error, - breadcrumb: getBreadcrumb(error.viewablePageInCms), - } - }) - - const pagesWithBrokenLinks: Map = new Map() - const brokenLink: number = links.filter( - (error) => error.type === "broken-link" - ).length - const brokenImage: number = links.filter( - (error) => error.type === "broken-image" - ).length - // create a set of pairs - const siteToErrorMap = new Map() - links.forEach((error) => { - const { breadcrumb } = error - - if (siteToErrorMap.has(breadcrumb)) { - siteToErrorMap.get(breadcrumb)?.push(error) - } else { - siteToErrorMap.set(breadcrumb, [error]) - pagesWithBrokenLinks.set(breadcrumb, error.viewablePageInStaging) - } - }) - - return ( - - - - Pages with broken links - - - {Array.from(pagesWithBrokenLinks.keys()).map((page) => ( - // safe to assert as we know the key exists - - {page} - - ))} - - - - - Broken links - {brokenLink} - - - Broken images - {brokenImage} - - - {Array.from(siteToErrorMap.keys()).map((breadcrumb) => { - return ( - - ) - })} - - - ) -} - const NoBrokenLinks = () => { + const { siteName } = useParams<{ siteName: string }>() return (
- No broken links found + Hurrah! All your references are nice and sturdy. - Your site is in good shape. No broken references were found. + We couldn't find any broken references on your site. You can come + back anytime to run the checker again. +
) @@ -344,15 +166,10 @@ const ErrorLoading = () => { {`We couldn't generate a broken report for this site.`} - {" "} You might want to try running the check again. If the issue persists, reach out to Isomer Support. - @@ -361,14 +178,26 @@ const ErrorLoading = () => { } const LoadingLinkChecker = () => { + const spin = keyframes` + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + ` + return ( -
- - - Scanning your site for broken references{" "} +
+ + + + Sniffing out broken links on your site... - - This may take a while... + + Pages with broken references will appear here. This might take a + while.
@@ -376,14 +205,22 @@ const LoadingLinkChecker = () => { } const LinkBody = () => { + const { + isOpen: isLinkReportModalOpen, + onOpen: onLinkReportModalOpen, + onClose: onLinkReportModalClose, + } = useDisclosure() + const [selectedLinkCms, setSelectedLinkCms] = useState("" as string) + const [selectedLinkStaging, setSelectedLinkStaging] = useState("" as string) const isBrokenLinksReporterEnabled = useFeatureIsOn( "is_broken_links_report_enabled" ) const { siteName } = useParams<{ siteName: string }>() - const { data: brokenLinks, isError: isBrokenLinksError } = useGetBrokenLinks( - siteName, - isBrokenLinksReporterEnabled - ) + const { + data: brokenLinks, + isError: isBrokenLinksError, + isLoading: isBrokenLinksFetching, + } = useGetBrokenLinks(siteName, isBrokenLinksReporterEnabled) if ( !isBrokenLinksReporterEnabled || @@ -395,31 +232,338 @@ const LinkBody = () => { if (brokenLinks?.status === "success") { if (brokenLinks?.errors?.length === 0) { - return + return ( + <> + + + + ) } - // todo: remove this once design is ready with showing duplicate permalinks const onlyDuplicatePermalinks = brokenLinks.errors.every( (error) => error.type === "duplicate-permalink" ) if (onlyDuplicatePermalinks) { - return + return ( + <> + + + + ) } - return + const links: NonPermalinkError[] = brokenLinks.errors + .filter(isBrokenRefError) + .map((error) => ({ + ...error, + breadcrumb: getBreadcrumb(error.viewablePageInCms), + })) + + const uniqueLinks = [ + ...new Set(links.map((error) => error.viewablePageInCms)), + ] + + const sortedUniqueLinks = uniqueLinks.sort((a, b) => { + const countA = links.filter((link) => link.viewablePageInCms === a).length + const countB = links.filter((link) => link.viewablePageInCms === b).length + return countB - countA + }) + + return ( + <> + link.viewablePageInCms === selectedLinkCms + )} + pageCmsUrl={selectedLinkCms} + pageStagingUrl={selectedLinkStaging} + /> + + + + + + {sortedUniqueLinks.length} Pages with {links.length} broken + references + + + + Click 'Review page' to view a detailed list of + references broken on that page. + + + + + + + + + + + + + {sortedUniqueLinks.map((itemUrl) => { + const uniqueBreadcrumb = getBreadcrumb(itemUrl) + return ( + + + + + + ) + })} + +
+ + Page + + + + Broken References + + + + View Details + +
+ + + {uniqueBreadcrumb.split("/").at(-1)} + + + {uniqueBreadcrumb.split("/").map((item) => ( + + {item} + + ))} + + + + + { + links.filter( + (link) => link.viewablePageInCms === itemUrl + ).length + } + + + +
+
+
+
+
+ + ) } - return + return ( + <> + + + + + + + Pages with broken references + + + + Click 'Review page' to view a detailed list of + references broken on that page. + + + + + + + + + + + + + + + + +
+ + Page + + + + Broken References + + + + View Details + +
+ +
+
+
+
+
+ + ) } export const LinksReport = () => { return ( - <> + - + - + ) } diff --git a/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx b/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx new file mode 100644 index 000000000..291867cde --- /dev/null +++ b/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx @@ -0,0 +1,528 @@ +import { + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalProps, + Text, + Box, + VStack, + HStack, + Center, + TableContainer, + Table, + Tbody, + Td, + Th, + Thead, + Tr, + Flex, + Grid, + GridItem, + Icon, + keyframes, +} from "@chakra-ui/react" +import { useFeatureIsOn } from "@growthbook/growthbook-react" +import { + Button, + ModalCloseButton, + Link, + Badge, + Breadcrumb, + Pagination, + BxRightArrowAlt, +} from "@opengovsg/design-system-react" +import { set } from "lodash" +import React, { useEffect, useState } from "react" +import { BiLoaderAlt } from "react-icons/bi" +import { useQueryClient } from "react-query" +import { useParams } from "react-router-dom" + +import { Modal as CustomModal } from "components/Modal" +import PaginateBtn from "components/paginateBtn" + +import { SITE_LINK_CHECKER_STATUS_KEY } from "constants/queryKeys" + +import { invalidateMergeRelatedQueries } from "hooks/reviewHooks" +import { useGetStagingUrl } from "hooks/siteDashboardHooks" +import { useGetBrokenLinks } from "hooks/siteDashboardHooks/useGetLinkChecker" +import { useRefreshLinkChecker } from "hooks/siteDashboardHooks/useRefreshLinkChecker" + +import { NoBrokenLinksImage } from "assets" +import { colors } from "theme/foundations/colors" +import { typography } from "theme/foundations/typography" +import { + isBrokenRefError, + NonPermalinkError, + NonPermalinkErrorDto, + RepoError, +} from "types/linkReport" + +export const LinkReportModal = ({ + props, + linksArr, + pageCmsUrl, + pageStagingUrl, +}: { + props: Omit + linksArr: NonPermalinkError[] + pageCmsUrl: string + pageStagingUrl: string +}): JSX.Element => { + return ( + + + + + + + +
+ +
+
+ +
+
+ ) +} + +const LinkReportModalBanner = (props: Omit) => { + const { siteName } = useParams<{ siteName: string }>() + const { mutate: refreshLinkChecker } = useRefreshLinkChecker(siteName) + const onClick = () => { + refreshLinkChecker(siteName) + } + + const isBrokenLinksReporterEnabled = useFeatureIsOn( + "is_broken_links_report_enabled" + ) + + const { + data: brokenLinks, + error: brokenLinksError, + isLoading: isBrokenLinksFetching, + } = useGetBrokenLinks(siteName, isBrokenLinksReporterEnabled) + + const isBrokenLinksLoading = + brokenLinks?.status === "loading" || isBrokenLinksFetching + + const { onClose } = props + return ( + + + + + + {!brokenLinksError && ( + + )} + + + + ) +} + +const LinksReportDetails = ({ + linksArr, + pageCmsUrl, + pageStagingUrl, + props, +}: { + linksArr: NonPermalinkError[] + pageCmsUrl: string + pageStagingUrl: string + props: Omit +}) => { + const [pageNum, setPageNum] = useState(1) + + // Sort based on error type + const detailedErrorArr = generateDetailedError(linksArr).sort((a, b) => { + return a.detailedType.localeCompare(b.detailedType) + }) + const { siteName } = useParams<{ siteName: string }>() + const isBrokenLinksReporterEnabled = useFeatureIsOn( + "is_broken_links_report_enabled" + ) + const { + data: brokenLinks, + error: brokenLinksError, + isLoading: isBrokenLinksFetching, + } = useGetBrokenLinks(siteName, isBrokenLinksReporterEnabled) + + const isBrokenLinksLoading = + brokenLinks?.status === "loading" || isBrokenLinksFetching + + return ( + + + + + + {pageCmsUrl.split("/").at(-1)} + + + + + {isBrokenLinksLoading + ? "Re-scanning page..." + : `${detailedErrorArr.length} broken references found`} + + + + View page on staging + + + + + Edit page on CMS + + + + {!isBrokenLinksLoading && detailedErrorArr.length === 0 && ( + + )} + + {(isBrokenLinksLoading || detailedErrorArr.length !== 0) && ( + <> + + + After fixing the references, you can click the "Run check + for this page" in the top right hand corner. + + + setPageNum(newPage)} + /> + + + + + + + + + + + {!isBrokenLinksLoading && ( + + {detailedErrorArr + .slice((pageNum - 1) * 5, pageNum * 5) + .map((link) => { + const isBrokenLink = link.type === "broken-link" + return ( + + + + + ) + })} + + )} + + {isBrokenLinksLoading && ( + + + + + + )} +
+ + + Type + + + + + Reference and Error + +
+ + + {getErrorText(link)} + + + + + {isBrokenLink && ( + + {link.linkedText + ? `"${link.linkedText}"` + : "Empty Link Text"} + + )} + + {link.linkToAsset + ? link.linkToAsset + : "No URL linked"} + + + + + + + {getSuggestion(link)} + + + +
+ +
+
+ + )} +
+
+
+ ) +} + +type DetailedNonPermaLinkError = NonPermalinkError & { + detailedType: string +} + +const isEmailError = (error: NonPermalinkError): boolean => { + return ( + error.type === "broken-link" && + /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test(error.linkToAsset) + ) +} + +const isMissingHttpsError = (error: NonPermalinkError): boolean => { + return !/^(https:\/\/|http:\/\/|\/)/.test(error.linkToAsset) +} + +const generateDetailedError = ( + errors: NonPermalinkError[] +): DetailedNonPermaLinkError[] => { + return errors.map((error) => { + if (error.type === "broken-image") + return { ...error, detailedType: "broken-image" } + if (error.type === "broken-file") + return { ...error, detailedType: "broken-file" } + if (isEmailError(error)) return { ...error, detailedType: "email" } + if (isMissingHttpsError(error)) + return { ...error, detailedType: "missing-https" } + return { ...error, detailedType: "broken-link" } + }) +} + +const getErrorText = (error: DetailedNonPermaLinkError): string => { + switch (error.detailedType) { + case "broken-image": + return "Image" + case "broken-file": + return "File" + case "missing-https": + case "broken-link": + return "Hyperlink" + default: + return "Email" + } +} + +const getSuggestion = (error: DetailedNonPermaLinkError): string => { + switch (error.detailedType) { + case "broken-image": + return "Image doesn't exist." + case "broken-file": + return "File doesn't exist." + case "broken-link": + return "Page doesn't exist." + case "missing-https": + return 'Add a "https://".' + default: + return 'Add a "mailto:".' + } +} + +const LinkReportModalNoBrokenLink = (props: Omit) => { + const { onClose } = props + return ( +
+ + + + Hurrah! You’ve fixed all broken references on this page. + + + You can come back to scan this page again if you make edits to it. + + + +
+ ) +} + +const LinkReportModalLoading = () => { + const spin = keyframes` + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + ` + + return ( +
+ + + + Taking another closer look at this page... + + + Broken references will appear here. This might take a while. + + +
+ ) +} From 24703029e6302c9807b2d7deed026217f28d44d4 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Thu, 30 May 2024 16:01:28 +0800 Subject: [PATCH 2/9] Interface changes for no errors, px to rem. --- src/layouts/LinkReport/LinksReport.tsx | 91 +++++++++++-------- .../LinkReportModal/LinkReportModal.tsx | 89 +++++++++++------- src/layouts/SiteDashboard/SiteDashboard.tsx | 5 +- 3 files changed, 113 insertions(+), 72 deletions(-) diff --git a/src/layouts/LinkReport/LinksReport.tsx b/src/layouts/LinkReport/LinksReport.tsx index b19748f05..1b0c37b50 100644 --- a/src/layouts/LinkReport/LinksReport.tsx +++ b/src/layouts/LinkReport/LinksReport.tsx @@ -88,14 +88,14 @@ export const LinksReportBanner = () => { templateColumns="repeat(12, 1fr)" w="100%" maxW="1440px" - gap="16px" + gap="1rem" mx="auto" - px="32px" + px="2rem" > - + Experimental feature { const NoBrokenLinks = () => { const { siteName } = useParams<{ siteName: string }>() return ( -
- +
+ - + Hurrah! All your references are nice and sturdy. - + We couldn't find any broken references on your site. You can come back anytime to run the checker again. - @@ -189,8 +198,8 @@ const LoadingLinkChecker = () => { Sniffing out broken links on your site... @@ -210,8 +219,8 @@ const LinkBody = () => { onOpen: onLinkReportModalOpen, onClose: onLinkReportModalClose, } = useDisclosure() - const [selectedLinkCms, setSelectedLinkCms] = useState("" as string) - const [selectedLinkStaging, setSelectedLinkStaging] = useState("" as string) + const [selectedLinkCms, setSelectedLinkCms] = useState("") + const [selectedLinkStaging, setSelectedLinkStaging] = useState("") const isBrokenLinksReporterEnabled = useFeatureIsOn( "is_broken_links_report_enabled" ) @@ -243,7 +252,9 @@ const LinkBody = () => { pageCmsUrl={selectedLinkCms} pageStagingUrl={selectedLinkStaging} /> - +
+ +
) } @@ -263,7 +274,9 @@ const LinkBody = () => { pageCmsUrl={selectedLinkCms} pageStagingUrl={selectedLinkStaging} /> - +
+ +
) } @@ -302,14 +315,14 @@ const LinkBody = () => { templateColumns="repeat(12, 1fr)" w="100%" maxW="1440px" - gap="16px" + gap="1rem" mx="auto" - px="32px" + px="2rem" h="100%" > - + { {sortedUniqueLinks.length} Pages with {links.length} broken references - + { - - - - @@ -468,14 +485,14 @@ const LinkBody = () => { templateColumns="repeat(12, 1fr)" w="100%" maxW="1440px" - gap="16px" + gap="1rem" mx="auto" - px="32px" + px="2rem" h="100%" > - + { > Pages with broken references - + {
+ { { Broken References + { borderTop="1px" borderColor="base.divider.medium" > - - + + { + { } +
- - - - @@ -369,20 +341,12 @@ const LinkBody = () => { textAlign="center" padding=".375rem 1rem" > - + Broken References @@ -399,10 +363,7 @@ const LinkBody = () => { > diff --git a/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx b/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx index b8b07daa9..abee76a87 100644 --- a/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx +++ b/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx @@ -17,47 +17,27 @@ import { Th, Thead, Tr, - Flex, Grid, GridItem, - Icon, keyframes, } from "@chakra-ui/react" import { useFeatureIsOn } from "@growthbook/growthbook-react" -import { - Button, - ModalCloseButton, - Link, - Badge, - Breadcrumb, - Pagination, - BxRightArrowAlt, -} from "@opengovsg/design-system-react" -import { set } from "lodash" -import React, { useEffect, useState } from "react" -import { BiLoaderAlt } from "react-icons/bi" -import { useQueryClient } from "react-query" +import { Button, Link } from "@opengovsg/design-system-react" +import _ from "lodash" +import { useState } from "react" +import { BiLoaderAlt, BiLeftArrowAlt, BiSolidInfoCircle } from "react-icons/bi" +// import { MdInfo } from "react-icons/md"; import { useParams, Link as RouterLink } from "react-router-dom" import { Modal as CustomModal } from "components/Modal" -import PaginateBtn from "components/paginateBtn" - -import { SITE_LINK_CHECKER_STATUS_KEY } from "constants/queryKeys" +import PaginateButton from "components/paginateButton" -import { invalidateMergeRelatedQueries } from "hooks/reviewHooks" -import { useGetStagingUrl } from "hooks/siteDashboardHooks" import { useGetBrokenLinks } from "hooks/siteDashboardHooks/useGetLinkChecker" import { useRefreshLinkChecker } from "hooks/siteDashboardHooks/useRefreshLinkChecker" import { NoBrokenLinksImage } from "assets" import { colors } from "theme/foundations/colors" -import { typography } from "theme/foundations/typography" -import { - isBrokenRefError, - NonPermalinkError, - NonPermalinkErrorDto, - RepoError, -} from "types/linkReport" +import { NonPermalinkError } from "types/linkReport" export const LinkReportModal = ({ props, @@ -74,7 +54,7 @@ export const LinkReportModal = ({ - + @@ -118,31 +98,26 @@ const LinkReportModalBanner = (props: Omit) => { @@ -153,7 +128,7 @@ const LinkReportModalBanner = (props: Omit) => { isDisabled={isBrokenLinksLoading} height="1.5rem" padding=".5rem 1rem" - fontSize=".875rem" + textStyle="subhead-2" size="xs" > {isBrokenLinksLoading @@ -181,16 +156,16 @@ const LinksReportDetails = ({ const [pageNum, setPageNum] = useState(1) // Sort based on error type - const detailedErrorArr = generateDetailedError(linksArr).sort((a, b) => { - return a.detailedType.localeCompare(b.detailedType) - }) + const detailedErrorArr = _.sortBy( + generateDetailedError(linksArr), + (err) => err.detailedType + ) const { siteName } = useParams<{ siteName: string }>() const isBrokenLinksReporterEnabled = useFeatureIsOn( "is_broken_links_report_enabled" ) const { data: brokenLinks, - error: brokenLinksError, isLoading: isBrokenLinksFetching, } = useGetBrokenLinks(siteName, isBrokenLinksReporterEnabled) @@ -201,7 +176,6 @@ const LinksReportDetails = ({ @@ -229,29 +200,16 @@ const LinksReportDetails = ({ width="100%" paddingBottom="1.25rem" > - + {isBrokenLinksLoading ? "Re-scanning page..." : `${detailedErrorArr.length} broken references found`} - - - View page on staging - + + View page on staging - - - Edit page on CMS - + + Edit page on CMS {!isBrokenLinksLoading && detailedErrorArr.length === 0 && ( @@ -266,16 +224,11 @@ const LinksReportDetails = ({ width="100%" paddingBottom="1.25rem" > - - After fixing the references, you can click the "Run check - for this page" in the top right hand corner. + + {`After fixing the references , you can click the "Run check for this page" in the top right hand corner.`} - - @@ -327,23 +275,13 @@ const LinksReportDetails = ({ const isBrokenLink = link.type === "broken-link" return ( -
+ { { Broken References + ) => { templateColumns="repeat(12, 1fr)" w="100%" maxW="1440px" - gap="16px" + gap="1rem" mx="auto" - px="32px" + px="2rem" bg="white" > @@ -137,11 +137,11 @@ const LinkReportModalBanner = (props: Omit) => { color={colors.interaction.links.default} onClick={onClose} > - + Back to main report @@ -151,9 +151,9 @@ const LinkReportModalBanner = (props: Omit) => { onClick={onClick} variant="solid" isDisabled={isBrokenLinksLoading} - height="24px" - padding="8px 16px" - fontSize="14px" + height="1.5rem" + padding=".5rem 1rem" + fontSize=".875rem" size="xs" > {isBrokenLinksLoading @@ -202,13 +202,13 @@ const LinksReportDetails = ({ templateColumns="repeat(12, 1fr)" w="100%" maxW="1440px" - gap="16px" + gap="1rem" mx="auto" - px="32px" + px="2rem" > - +
+ @@ -308,7 +308,7 @@ const LinksReportDetails = ({ + - + {link.linkedText @@ -369,7 +369,7 @@ const LinksReportDetails = ({ {link.linkToAsset @@ -485,17 +485,38 @@ const getSuggestion = (error: DetailedNonPermaLinkError): string => { const LinkReportModalNoBrokenLink = (props: Omit) => { const { onClose } = props + const { siteName } = useParams<{ siteName: string }>() return ( -
- +
+ - + Hurrah! You’ve fixed all broken references on this page. - + You can come back to scan this page again if you make edits to it. - + +
) @@ -513,8 +534,8 @@ const LinkReportModalLoading = () => { Taking another closer look at this page... diff --git a/src/layouts/SiteDashboard/SiteDashboard.tsx b/src/layouts/SiteDashboard/SiteDashboard.tsx index 01ddc2823..149c279ee 100644 --- a/src/layouts/SiteDashboard/SiteDashboard.tsx +++ b/src/layouts/SiteDashboard/SiteDashboard.tsx @@ -236,7 +236,10 @@ export const SiteDashboard = (): JSX.Element => { {brokenLinks?.status === "success" && - brokenLinks?.errors.length} + brokenLinks?.errors.filter( + (error) => + error.type !== "duplicate-permalink" + ).length} broken references found From b3ee0180c1506ae9689385849a555349ec8a726f Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:58:48 +0800 Subject: [PATCH 3/9] Fixes according to comments --- .../{paginateBtn.jsx => paginateButton.jsx} | 12 +- src/layouts/LinkReport/LinksReport.tsx | 90 ++------ .../LinkReportModal/LinkReportModal.tsx | 192 ++++++------------ 3 files changed, 87 insertions(+), 207 deletions(-) rename src/components/{paginateBtn.jsx => paginateButton.jsx} (80%) diff --git a/src/components/paginateBtn.jsx b/src/components/paginateButton.jsx similarity index 80% rename from src/components/paginateBtn.jsx rename to src/components/paginateButton.jsx index 60906635e..c2e3ed754 100644 --- a/src/components/paginateBtn.jsx +++ b/src/components/paginateButton.jsx @@ -7,7 +7,11 @@ import { import { typography } from "theme/foundations/typography" -export default function PaginateBtn({ currentPage, totalPage, onPageChange }) { +export default function PaginateButton({ + currentPage, + totalPage, + onPageChange, +}) { return ( @@ -18,9 +22,9 @@ export default function PaginateBtn({ currentPage, totalPage, onPageChange }) { aria-label="Previous Page" icon={} onClick={() => onPageChange(currentPage - 1)} - isDisabled={currentPage === 1} - marginLeft="20px" - marginRight="12px" + isDisabled={currentPage <= 1} + marginLeft="1.25rem" + marginRight=".75rem" /> { {
- + Hurrah! All your references are nice and sturdy. - + We couldn't find any broken references on your site. You can come back anytime to run the checker again. @@ -314,7 +291,6 @@ const LinkBody = () => { { textStyle="h4" alignSelf="flex-start" textAlign="left" - color="base.content.strong" + textColor="base.content.strong" > {sortedUniqueLinks.length} Pages with {links.length} broken references @@ -355,11 +331,7 @@ const LinkBody = () => {
- + Page - + View Details - + {uniqueBreadcrumb.split("/").at(-1)} { @@ -437,7 +397,6 @@ const LinkBody = () => {
- - Page - + Page { textAlign="center" padding=".375rem 1rem" > - + Broken References - + View Details
+ Type @@ -309,11 +261,7 @@ const LinksReportDetails = ({ - + Reference and Error
- - - {getErrorText(link)} - - + + + {getErrorText(link)} + {isBrokenLink && ( {link.linkedText ? `"${link.linkedText}"` @@ -367,32 +304,23 @@ const LinksReportDetails = ({ )} {link.linkToAsset ? link.linkToAsset : "No URL linked"} - - - + + {getSuggestion(link)} @@ -425,7 +353,12 @@ const LinksReportDetails = ({ } type DetailedNonPermaLinkError = NonPermalinkError & { - detailedType: string + detailedType: + | "broken-image" + | "broken-file" + | "email" + | "missing-https" + | "broken-link" } const isEmailError = (error: NonPermalinkError): boolean => { @@ -454,7 +387,7 @@ const generateDetailedError = ( }) } -const getErrorText = (error: DetailedNonPermaLinkError): string => { +const getErrorText = (error: DetailedNonPermaLinkError) => { switch (error.detailedType) { case "broken-image": return "Image" @@ -463,8 +396,12 @@ const getErrorText = (error: DetailedNonPermaLinkError): string => { case "missing-https": case "broken-link": return "Hyperlink" - default: + case "email": return "Email" + default: { + const exception: never = error.detailedType + throw new Error(exception) + } } } @@ -478,8 +415,12 @@ const getSuggestion = (error: DetailedNonPermaLinkError): string => { return "Page doesn't exist." case "missing-https": return 'Add a "https://".' - default: + case "email": return 'Add a "mailto:".' + default: { + const exception: never = error.detailedType + throw new Error(exception) + } } } @@ -490,19 +431,10 @@ const LinkReportModalNoBrokenLink = (props: Omit) => {
- + Hurrah! You’ve fixed all broken references on this page. - + You can come back to scan this page again if you make edits to it. From be9ebca04d4522681047827921ec30c98ab126e0 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:00:46 +0800 Subject: [PATCH 4/9] Add leading 0 to decimal values. --- .../components/LinkReportModal/LinkReportModal.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx b/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx index abee76a87..cce597a83 100644 --- a/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx +++ b/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx @@ -127,7 +127,7 @@ const LinkReportModalBanner = (props: Omit) => { variant="solid" isDisabled={isBrokenLinksLoading} height="1.5rem" - padding=".5rem 1rem" + padding="0.5rem 1rem" textStyle="subhead-2" size="xs" > @@ -182,7 +182,7 @@ const LinksReportDetails = ({ > - + Type -
+ Reference and Error @@ -296,7 +296,7 @@ const LinksReportDetails = ({ {link.linkedText ? `"${link.linkedText}"` @@ -429,7 +429,7 @@ const LinkReportModalNoBrokenLink = (props: Omit) => { const { siteName } = useParams<{ siteName: string }>() return (
- + Hurrah! You’ve fixed all broken references on this page. From 9ec638b4005c28e095b56d44a5c0ddc020dfb39c Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Tue, 4 Jun 2024 15:31:30 +0800 Subject: [PATCH 5/9] Change to typescript --- src/components/{paginateButton.jsx => paginateButton.tsx} | 4 ++++ 1 file changed, 4 insertions(+) rename src/components/{paginateButton.jsx => paginateButton.tsx} (91%) diff --git a/src/components/paginateButton.jsx b/src/components/paginateButton.tsx similarity index 91% rename from src/components/paginateButton.jsx rename to src/components/paginateButton.tsx index c2e3ed754..245193f5e 100644 --- a/src/components/paginateButton.jsx +++ b/src/components/paginateButton.tsx @@ -11,6 +11,10 @@ export default function PaginateButton({ currentPage, totalPage, onPageChange, +}: { + currentPage: number + totalPage: number + onPageChange: (arg: number) => void }) { return ( From 1b41903d6596b7f0f3a1c63e332f2227d4c6def0 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:19:55 +0800 Subject: [PATCH 6/9] Prevent first column of table in modal from being squeeze when reference is long --- src/layouts/LinkReport/LinksReport.tsx | 50 +++++++++++++------ .../LinkReportModal/LinkReportModal.tsx | 4 +- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/layouts/LinkReport/LinksReport.tsx b/src/layouts/LinkReport/LinksReport.tsx index bf236032b..bf3ee8e7f 100644 --- a/src/layouts/LinkReport/LinksReport.tsx +++ b/src/layouts/LinkReport/LinksReport.tsx @@ -81,7 +81,7 @@ export const LinksReportBanner = () => { - + Experimental feature { const { siteName } = useParams<{ siteName: string }>() return (
- + - + Hurrah! All your references are nice and sturdy. @@ -298,7 +298,7 @@ const LinkBody = () => { > - + { border="1px" borderColor="base.divider.medium" > - +
- - @@ -450,7 +468,7 @@ const LinkBody = () => { > - + {
+ Page @@ -339,13 +344,13 @@ const LinkBody = () => { w="12.5rem" h="3.5rem" textAlign="center" - padding=".375rem 1rem" + padding="0.375rem 1rem" > Broken References + View Details @@ -361,15 +366,28 @@ const LinkBody = () => { borderTop="1px" borderColor="base.divider.medium" > - - - + + + {uniqueBreadcrumb.split("/").at(-1)} {uniqueBreadcrumb.split("/").map((item) => ( @@ -412,7 +430,7 @@ const LinkBody = () => {
- -
+ Page Broken References + View Details diff --git a/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx b/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx index cce597a83..7849a7cfc 100644 --- a/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx +++ b/src/layouts/LinkReport/components/LinkReportModal/LinkReportModal.tsx @@ -246,7 +246,7 @@ const LinksReportDetails = ({ border="1px" borderColor="base.divider.medium" > - +
From 2cd1b08b9580fb6752ecd3b453a1fbf3d92ed33f Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Tue, 4 Jun 2024 18:26:44 +0800 Subject: [PATCH 7/9] Fix table width --- src/layouts/LinkReport/LinksReport.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/layouts/LinkReport/LinksReport.tsx b/src/layouts/LinkReport/LinksReport.tsx index bf3ee8e7f..216db252f 100644 --- a/src/layouts/LinkReport/LinksReport.tsx +++ b/src/layouts/LinkReport/LinksReport.tsx @@ -320,7 +320,6 @@ const LinkBody = () => { { Date: Fri, 7 Jun 2024 09:50:42 +0800 Subject: [PATCH 8/9] Fixing nits --- src/components/paginateButton.tsx | 4 +- src/layouts/LinkReport/LinksReport.tsx | 44 +++++++------------ .../LinkReportModal/LinkReportModal.tsx | 2 +- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/src/components/paginateButton.tsx b/src/components/paginateButton.tsx index 245193f5e..2a760460b 100644 --- a/src/components/paginateButton.tsx +++ b/src/components/paginateButton.tsx @@ -18,7 +18,7 @@ export default function PaginateButton({ }) { return ( - + Page {currentPage} out of {totalPage}{" "} onPageChange(currentPage - 1)} isDisabled={currentPage <= 1} marginLeft="1.25rem" - marginRight=".75rem" + marginRight="0.75rem" /> { const paths = viewablePageInCms.split("/") let breadcrumb = paths - .filter((_, index) => index % 2 === 0) + .filter((placeholder, index) => index % 2 === 0) .slice(2) .join(" / ") .replace(/-/g, " ") @@ -110,17 +111,6 @@ export const LinksReportBanner = () => { ) } -const normaliseUrl = (url: string): string => { - let normalisedUrl = url - if (url.endsWith("/")) { - normalisedUrl = url.slice(0, -1) - } - if (url.startsWith("/")) { - normalisedUrl = url.slice(1) - } - return normalisedUrl -} - const NoBrokenLinks = () => { const { siteName } = useParams<{ siteName: string }>() return ( @@ -131,8 +121,8 @@ const NoBrokenLinks = () => { Hurrah! All your references are nice and sturdy. - We couldn't find any broken references on your site. You can come - back anytime to run the checker again. + {`We couldn't find any broken references on your site. You can come + back anytime to run the checker again.`}
@@ -317,7 +317,6 @@ const LinksReportDetails = ({ size="1rem" fill={colors.utility.feedback.critical} /> - )} - {isBrokenLinksLoading && (
- Page + + Page + Date: Fri, 7 Jun 2024 10:08:50 +0800 Subject: [PATCH 9/9] Capture case rename --- src/components/{paginateButton.tsx => PaginateButton.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/{paginateButton.tsx => PaginateButton.tsx} (100%) diff --git a/src/components/paginateButton.tsx b/src/components/PaginateButton.tsx similarity index 100% rename from src/components/paginateButton.tsx rename to src/components/PaginateButton.tsx