diff --git a/changelog/7942-eng-3461-router-link-legacy-behavior.yaml b/changelog/7942-eng-3461-router-link-legacy-behavior.yaml new file mode 100644 index 00000000000..8011aab9e12 --- /dev/null +++ b/changelog/7942-eng-3461-router-link-legacy-behavior.yaml @@ -0,0 +1,4 @@ +type: Developer Experience +description: Replaced deprecated `next/link` `legacyBehavior`/`passHref` usage in the Admin UI with a shared `RouterLink` component for internal navigation. +pr: 7942 +labels: [] diff --git a/clients/admin-ui/cypress/e2e/action-center/aggregate-results.cy.ts b/clients/admin-ui/cypress/e2e/action-center/aggregate-results.cy.ts index 773dde91604..486ba3ff5c4 100644 --- a/clients/admin-ui/cypress/e2e/action-center/aggregate-results.cy.ts +++ b/clients/admin-ui/cypress/e2e/action-center/aggregate-results.cy.ts @@ -81,16 +81,20 @@ describe("Action center", () => { // TODO: [HJ-337] uncomment when Add button is implemented // cy.getByTestId(`add-button-${webMonitorKey}`).should("exist"); // Review button - cy.getByTestId(`review-button-${webMonitorKey}`).should( - "have.attr", - "href", - `${ACTION_CENTER_ROUTE}/${APIMonitorType.WEBSITE}/${webMonitorKey}`, - ); - cy.getByTestId(`review-button-${integrationMonitorKey}`).should( - "have.attr", - "href", - `${ACTION_CENTER_ROUTE}/${APIMonitorType.DATASTORE}/${integrationMonitorKey}`, - ); + cy.getByTestId(`review-button-${webMonitorKey}`) + .closest("a") + .should( + "have.attr", + "href", + `${ACTION_CENTER_ROUTE}/${APIMonitorType.WEBSITE}/${webMonitorKey}`, + ); + cy.getByTestId(`review-button-${integrationMonitorKey}`) + .closest("a") + .should( + "have.attr", + "href", + `${ACTION_CENTER_ROUTE}/${APIMonitorType.DATASTORE}/${integrationMonitorKey}`, + ); }); it.skip("Should paginate results", () => { // TODO: mock pagination and also test skeleton loading state diff --git a/clients/admin-ui/src/features/access-policies/ControlForm.tsx b/clients/admin-ui/src/features/access-policies/ControlForm.tsx index 8df25a8d152..bb087edd200 100644 --- a/clients/admin-ui/src/features/access-policies/ControlForm.tsx +++ b/clients/admin-ui/src/features/access-policies/ControlForm.tsx @@ -1,7 +1,7 @@ import { Button, Flex, Form, Input } from "fidesui"; -import NextLink from "next/link"; import { useMemo } from "react"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { CONTROLS_ROUTE } from "~/features/common/nav/routes"; import type { Control } from "./access-policies.slice"; @@ -59,9 +59,9 @@ const ControlForm = ({ - + - + ); diff --git a/clients/admin-ui/src/features/access-policies/PolicyCard.tsx b/clients/admin-ui/src/features/access-policies/PolicyCard.tsx index ed5cca40290..b73393f7da4 100644 --- a/clients/admin-ui/src/features/access-policies/PolicyCard.tsx +++ b/clients/admin-ui/src/features/access-policies/PolicyCard.tsx @@ -8,10 +8,9 @@ import { Tag, Text, Tooltip, - Typography, } from "fidesui"; -import NextLink from "next/link"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { ACCESS_POLICY_EDIT_ROUTE } from "~/features/common/nav/routes"; import DecisionTag from "./DecisionTag"; @@ -19,8 +18,6 @@ import styles from "./PolicyCard.module.scss"; import { AccessPolicyListItem } from "./types"; import { formatRelativeTime } from "./utils"; -const { Link: LinkText } = Typography; - interface PolicyCardProps { policy: AccessPolicyListItem; onToggle: (policy: AccessPolicyListItem) => void; @@ -34,25 +31,18 @@ const PolicyCard = ({ policy, onToggle }: PolicyCardProps) => { {/* Header */} - {/* legacyBehavior is required: Typography.Link renders , and - Next.js 13 Link also renders — without it we'd get nested anchors */} - - - {policy.name} - - + {policy.name} + {policy.is_recommendation && ( diff --git a/clients/admin-ui/src/features/common/nav/RouterLink.test.tsx b/clients/admin-ui/src/features/common/nav/RouterLink.test.tsx new file mode 100644 index 00000000000..8ab1d0a50c4 --- /dev/null +++ b/clients/admin-ui/src/features/common/nav/RouterLink.test.tsx @@ -0,0 +1,298 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { Button } from "fidesui"; + +import { formatHref, RouterLink } from "./RouterLink"; + +const mockPush = jest.fn().mockResolvedValue(true); +const mockReplace = jest.fn().mockResolvedValue(true); +const mockPrefetch = jest.fn().mockResolvedValue(undefined); + +jest.mock("next/router", () => ({ + useRouter: () => ({ + push: mockPush, + replace: mockReplace, + prefetch: mockPrefetch, + pathname: "/", + query: {}, + asPath: "/", + }), +})); + +describe("formatHref", () => { + it("returns just the pathname when no query or hash is present", () => { + expect(formatHref({ pathname: "/policies" })).toBe("/policies"); + }); + + it("returns an empty string when given an empty UrlObject", () => { + expect(formatHref({})).toBe(""); + }); + + it("serializes a single query param", () => { + expect(formatHref({ pathname: "/policies", query: { id: "abc" } })).toBe( + "/policies?id=abc", + ); + }); + + it("serializes multiple query params", () => { + expect( + formatHref({ pathname: "/policies", query: { id: "abc", page: "2" } }), + ).toBe("/policies?id=abc&page=2"); + }); + + it("serializes array query values as repeated keys", () => { + expect( + formatHref({ pathname: "/policies", query: { tag: ["a", "b"] } }), + ).toBe("/policies?tag=a&tag=b"); + }); + + it("skips null and undefined query values", () => { + expect( + formatHref({ + pathname: "/policies", + query: { + id: "abc", + missing: undefined, + also: null as unknown as string, + }, + }), + ).toBe("/policies?id=abc"); + }); + + it("URL-encodes special characters in query values", () => { + expect( + formatHref({ pathname: "/policies", query: { name: "hello world&" } }), + ).toBe("/policies?name=hello+world%26"); + }); + + it("appends a hash, prefixing # when missing", () => { + expect(formatHref({ pathname: "/docs", hash: "section" })).toBe( + "/docs#section", + ); + expect(formatHref({ pathname: "/docs", hash: "#section" })).toBe( + "/docs#section", + ); + }); + + it("uses an explicit search string and ignores query when both are set", () => { + expect( + formatHref({ + pathname: "/policies", + search: "id=abc", + query: { ignored: "yes" }, + }), + ).toBe("/policies?id=abc"); + }); + + it("preserves a leading ? on search", () => { + expect(formatHref({ pathname: "/policies", search: "?id=abc" })).toBe( + "/policies?id=abc", + ); + }); + + it("treats a string query as a pre-encoded query string", () => { + expect(formatHref({ pathname: "/policies", query: "id=abc&page=2" })).toBe( + "/policies?id=abc&page=2", + ); + }); + + it("combines pathname, query, and hash", () => { + expect( + formatHref({ + pathname: "/policies", + query: { id: "abc" }, + hash: "details", + }), + ).toBe("/policies?id=abc#details"); + }); +}); + +describe("RouterLink", () => { + beforeEach(() => { + mockPush.mockClear(); + mockReplace.mockClear(); + mockPrefetch.mockClear(); + }); + + describe("with an antd Button child", () => { + it("wraps the button in a next/link anchor with the given href", () => { + render( + + + , + ); + + const anchor = screen.getByRole("link", { name: "Go" }); + expect(anchor).toHaveAttribute("href", "/dashboard"); + expect(anchor.querySelector("button")).not.toBeNull(); + }); + + it("forwards target and rel to the next/link anchor", () => { + render( + + + , + ); + + const anchor = screen.getByRole("link", { name: "External" }); + expect(anchor).toHaveAttribute("target", "_blank"); + expect(anchor).toHaveAttribute("rel", "noopener noreferrer"); + }); + + it("does not call router.push on click (next/link handles it)", () => { + render( + + + , + ); + + fireEvent.click(screen.getByRole("button", { name: "Go" })); + expect(mockPush).not.toHaveBeenCalled(); + }); + }); + + describe("with non-Button children", () => { + it("renders a Typography.Link-styled anchor with the resolved href", () => { + render(Details); + + const anchor = screen.getByRole("link", { name: "Details" }); + expect(anchor).toHaveAttribute("href", "/details"); + }); + + it("serializes a UrlObject href for the anchor attribute", () => { + render( + + My policy + , + ); + + expect(screen.getByRole("link", { name: "My policy" })).toHaveAttribute( + "href", + "/policies?id=abc", + ); + }); + + it("calls router.push with the original href on plain left click", () => { + const href = { pathname: "/policies", query: { id: "abc" } }; + render(My policy); + + fireEvent.click(screen.getByRole("link", { name: "My policy" }), { + button: 0, + }); + expect(mockPush).toHaveBeenCalledWith(href, undefined, { + scroll: undefined, + }); + }); + + it("does not intercept meta/ctrl/shift/alt/middle clicks", () => { + render(Details); + const anchor = screen.getByRole("link", { name: "Details" }); + + fireEvent.click(anchor, { button: 0, metaKey: true }); + fireEvent.click(anchor, { button: 0, ctrlKey: true }); + fireEvent.click(anchor, { button: 0, shiftKey: true }); + fireEvent.click(anchor, { button: 0, altKey: true }); + fireEvent.click(anchor, { button: 1 }); + + expect(mockPush).not.toHaveBeenCalled(); + }); + + it("runs a custom onClick before the default handler and respects preventDefault", () => { + const onClick = jest.fn((e) => e.preventDefault()); + render( + + Details + , + ); + + fireEvent.click(screen.getByRole("link", { name: "Details" })); + expect(onClick).toHaveBeenCalledTimes(1); + expect(mockPush).not.toHaveBeenCalled(); + }); + + it("does not intercept clicks when target=_blank", () => { + render( + + External details + , + ); + const anchor = screen.getByRole("link", { name: "External details" }); + expect(anchor).toHaveAttribute("target", "_blank"); + expect(anchor).toHaveAttribute("rel", "noopener noreferrer"); + + fireEvent.click(anchor, { button: 0 }); + expect(mockPush).not.toHaveBeenCalled(); + }); + + it("falls through to text mode when a Button is wrapped in a fragment", () => { + render( + + {/* eslint-disable-next-line react/jsx-no-useless-fragment */} + <> + + + , + ); + + // Text mode uses Typography.Link which renders an ; clicking should + // trigger router.push, proving the component did not take the wrap path. + fireEvent.click(screen.getByRole("link")); + expect(mockPush).toHaveBeenCalledWith("/details", undefined, { + scroll: undefined, + }); + }); + + it("calls router.replace instead of router.push when replace is true", () => { + render( + + Details + , + ); + + fireEvent.click(screen.getByRole("link", { name: "Details" })); + expect(mockReplace).toHaveBeenCalledWith("/details", undefined, { + scroll: undefined, + }); + expect(mockPush).not.toHaveBeenCalled(); + }); + + it("forwards the scroll option to router.push", () => { + render( + + Details + , + ); + + fireEvent.click(screen.getByRole("link", { name: "Details" })); + expect(mockPush).toHaveBeenCalledWith("/details", undefined, { + scroll: false, + }); + }); + + it("does not prefetch on mount when prefetch is false", () => { + render( + + Details + , + ); + expect(mockPrefetch).not.toHaveBeenCalled(); + }); + + it("does not prefetch on mount when prefetch is null but does on hover", () => { + render( + + Details + , + ); + expect(mockPrefetch).not.toHaveBeenCalled(); + + fireEvent.mouseEnter(screen.getByRole("link", { name: "Details" })); + expect(mockPrefetch).toHaveBeenCalledWith("/details"); + }); + + it("does not prefetch on mount in non-production builds", () => { + // jest runs with NODE_ENV=test, so default prefetch behaviour is a no-op + render(Details); + expect(mockPrefetch).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/clients/admin-ui/src/features/common/nav/RouterLink.tsx b/clients/admin-ui/src/features/common/nav/RouterLink.tsx new file mode 100644 index 00000000000..ecd7d19ab9f --- /dev/null +++ b/clients/admin-ui/src/features/common/nav/RouterLink.tsx @@ -0,0 +1,169 @@ +import { Button, Typography } from "fidesui"; +import NextLink, { LinkProps as NextLinkProps } from "next/link"; +import { useRouter } from "next/router"; +import { + Children, + ComponentProps, + isValidElement, + MouseEvent, + ReactNode, + useCallback, + useEffect, +} from "react"; +import type { UrlObject } from "url"; + +const { Link: TypographyLink } = Typography; + +type Href = string | UrlObject; + +export const formatHref = (url: UrlObject): string => { + const pathname = url.pathname ?? ""; + let search = ""; + if (url.search) { + search = url.search.startsWith("?") ? url.search : `?${url.search}`; + } else if (url.query) { + const params = + typeof url.query === "string" + ? url.query + : new URLSearchParams( + Object.entries(url.query).flatMap(([k, v]) => { + if (v === null || v === undefined) { + return []; + } + return Array.isArray(v) + ? v.map((item) => [k, String(item)] as [string, string]) + : [[k, String(v)] as [string, string]]; + }), + ).toString(); + if (params) { + search = `?${params}`; + } + } + let hash = ""; + if (url.hash) { + hash = url.hash.startsWith("#") ? url.hash : `#${url.hash}`; + } + return `${pathname}${search}${hash}`; +}; + +type TypographyLinkProps = Omit< + ComponentProps, + "href" | "onClick" +>; + +export interface RouterLinkProps extends TypographyLinkProps { + href: Href; + children: ReactNode; + onClick?: (e: MouseEvent) => void; + replace?: NextLinkProps["replace"]; + scroll?: NextLinkProps["scroll"]; + prefetch?: NextLinkProps["prefetch"]; +} + +const isAntButtonChild = (children: ReactNode): boolean => { + const arr = Children.toArray(children); + if (arr.length !== 1) { + return false; + } + const [only] = arr; + return isValidElement(only) && only.type === Button; +}; + +/** + * Shared internal-navigation link for admin-ui. + * + * - When the single child is an antd `Button`, wraps it in `next/link` so + * Next renders its own `` around the button. Preserves client-side + * routing, prefetch, and modifier-click handling. + * - Otherwise renders a `Typography.Link`-styled anchor that intercepts + * plain left-clicks and uses `router.push` for client-side navigation. + * Modifier, middle, and right clicks fall through to the browser so + * new-tab / copy-link behaviour continues to work. + * + * Detection is structural (only a single antd `Button` element is detected). + * If you need to wrap a custom button-like component, extract its render + * and wrap the real antd `Button`, or add a `button` escape-hatch prop. + */ +export const RouterLink = ({ + href, + children, + onClick, + replace, + scroll, + prefetch, + target, + rel, + ...typographyProps +}: RouterLinkProps) => { + const router = useRouter(); + const isButtonChild = isAntButtonChild(children); + const hrefString = typeof href === "string" ? href : formatHref(href); + + const prefetchHref = useCallback(() => { + router.prefetch(hrefString).catch(() => { + // prefetch is best-effort; ignore failures + }); + }, [router, hrefString]); + + useEffect(() => { + if ( + isButtonChild || + process.env.NODE_ENV !== "production" || + prefetch === false || + prefetch === null || + target === "_blank" + ) { + return; + } + prefetchHref(); + }, [isButtonChild, prefetch, prefetchHref, target]); + + if (isButtonChild) { + return ( + + {children} + + ); + } + + return ( + { + onClick?.(e); + if ( + e.defaultPrevented || + e.button !== 0 || + e.metaKey || + e.ctrlKey || + e.shiftKey || + e.altKey || + target === "_blank" + ) { + // Let the browser handle new-tab / modified clicks natively. + return; + } + e.preventDefault(); + const navigation = replace + ? router.replace(href, undefined, { scroll }) + : router.push(href, undefined, { scroll }); + navigation.catch(() => { + // navigation errors are surfaced by Next's own error handling + }); + }} + {...typographyProps} + > + {children} + + ); +}; diff --git a/clients/admin-ui/src/features/common/table/cells/LinkCell.tsx b/clients/admin-ui/src/features/common/table/cells/LinkCell.tsx index d3e1da67922..1eb9d4eda10 100644 --- a/clients/admin-ui/src/features/common/table/cells/LinkCell.tsx +++ b/clients/admin-ui/src/features/common/table/cells/LinkCell.tsx @@ -1,8 +1,9 @@ import { Flex, FlexProps, Typography } from "fidesui"; import { Url } from "next/dist/shared/lib/router/router"; -import NextLink from "next/link"; import { ComponentProps } from "react"; +import { RouterLink } from "~/features/common/nav/RouterLink"; + const { Link: LinkText, Text } = Typography; export const LinkCell = ({ @@ -19,19 +20,18 @@ export const LinkCell = ({ children && ( {href ? ( - - e.stopPropagation()} - variant="primary" - {...props} - > - - {children} - - - + e.stopPropagation()} + variant="primary" + {...props} + > + + {children} + + ) : ( {children} diff --git a/clients/admin-ui/src/features/consent-settings/tcf/PublisherRestrictionsTable.tsx b/clients/admin-ui/src/features/consent-settings/tcf/PublisherRestrictionsTable.tsx index c73e66cf8c5..af3cb364e63 100644 --- a/clients/admin-ui/src/features/consent-settings/tcf/PublisherRestrictionsTable.tsx +++ b/clients/admin-ui/src/features/consent-settings/tcf/PublisherRestrictionsTable.tsx @@ -1,8 +1,8 @@ import { Button, ColumnsType, Skeleton, Table, Tag, Typography } from "fidesui"; -import NextLink from "next/link"; import React, { useMemo } from "react"; import { useAppSelector } from "~/app/hooks"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { selectPurposes } from "~/features/common/purpose.slice"; import { InfoCell } from "~/features/common/table/cells"; import { MappedPurpose, TCFConfigurationDetail } from "~/types/api"; @@ -102,18 +102,14 @@ export const PublisherRestrictionsTable = ({ key: "actions", width: 100, render: (_, record) => ( - + - + ), }, ], diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/ConfidenceCard.tsx b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/ConfidenceCard.tsx index d264c06b66a..0dbc5ba8829 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/ConfidenceCard.tsx +++ b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/ConfidenceCard.tsx @@ -1,7 +1,7 @@ import { Avatar, Button, Card, Icons, Space, SparkleIcon, Text } from "fidesui"; -import NextLink from "next/link"; import { ReactNode } from "react"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { SeverityGauge } from "~/features/common/progress/SeverityGauge"; import { nFormatter, pluralize } from "~/features/common/utils"; import { ConfidenceBucket } from "~/types/api/models/ConfidenceBucket"; @@ -33,12 +33,11 @@ const getActions = ({ onConfirmAll, }: GetActionsParams): ReactNode[] => { const actions: ReactNode[] = [ - - , + , ]; if (item.severity === ConfidenceBucket.HIGH && onConfirmAll) { actions.push( diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/EmptyMonitorsResult.tsx b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/EmptyMonitorsResult.tsx index 548fb706e1b..cbb6e4b298a 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/EmptyMonitorsResult.tsx +++ b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/EmptyMonitorsResult.tsx @@ -10,10 +10,10 @@ import { Text, Title, } from "fidesui"; -import NextLink from "next/link"; import { useSelector } from "react-redux"; import { selectUser } from "~/features/auth"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { SYSTEM_ROUTE } from "~/features/common/nav/routes"; import { useGetSystemsQuery } from "~/features/system"; @@ -131,9 +131,9 @@ export const EmptyMonitorsResult = () => { } > - + - + ); }; diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/MonitorResult.tsx b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/MonitorResult.tsx index 1dbbd1a401f..b79c197625c 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/MonitorResult.tsx +++ b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/MonitorResult.tsx @@ -17,6 +17,7 @@ import palette from "fidesui/src/palette/palette.module.scss"; import NextLink from "next/link"; import { useState } from "react"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { formatDate, formatUser, @@ -151,7 +152,7 @@ export const MonitorResult = ({ , ] : []), - + - , + , ]} > - + - + - + } className="mb-4" /> diff --git a/clients/admin-ui/src/features/privacy-requests/dashboard/DuplicateRequestsButton.tsx b/clients/admin-ui/src/features/privacy-requests/dashboard/DuplicateRequestsButton.tsx index 5c526f4e7d3..0f98c165873 100644 --- a/clients/admin-ui/src/features/privacy-requests/dashboard/DuplicateRequestsButton.tsx +++ b/clients/admin-ui/src/features/privacy-requests/dashboard/DuplicateRequestsButton.tsx @@ -1,7 +1,7 @@ import { Button } from "fidesui"; -import Link from "next/link"; import { useRouter } from "next/router"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { pluralize } from "~/features/common/utils"; import { useSearchPrivacyRequestsQuery } from "~/features/privacy-requests/privacy-requests.slice"; import { PrivacyRequestStatus } from "~/types/api"; @@ -44,13 +44,11 @@ export const DuplicateRequestsButton = ({ } return ( - - + - + { loading={isLoading} className="h-full" extra={ - + Connect more systems - + } > diff --git a/clients/admin-ui/src/pages/access-policies/controls/index.tsx b/clients/admin-ui/src/pages/access-policies/controls/index.tsx index 49eb40bc3a2..fdc4e6a8e4c 100644 --- a/clients/admin-ui/src/pages/access-policies/controls/index.tsx +++ b/clients/admin-ui/src/pages/access-policies/controls/index.tsx @@ -1,6 +1,5 @@ import { Button, List, Text, Typography, useMessage, useModal } from "fidesui"; import type { NextPage } from "next"; -import NextLink from "next/link"; import { Control, @@ -9,6 +8,7 @@ import { } from "~/features/access-policies/access-policies.slice"; import { getErrorMessage } from "~/features/common/helpers"; import Layout from "~/features/common/Layout"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { ACCESS_POLICIES_ROUTE, CONTROLS_EDIT_ROUTE, @@ -55,9 +55,9 @@ const ControlsPage: NextPage = () => { { title: "Controls" }, ]} rightContent={ - + - + } >
@@ -94,13 +94,12 @@ const ControlsPage: NextPage = () => { > Delete , - - , + , ]} > { rightContent={ hasPolicies ? ( - + - + {flags.alphaPrivacyDocUpload && ( - + ) : undefined } diff --git a/clients/admin-ui/src/pages/add-systems/multiple.tsx b/clients/admin-ui/src/pages/add-systems/multiple.tsx index 61198562e9e..1acc7f89259 100644 --- a/clients/admin-ui/src/pages/add-systems/multiple.tsx +++ b/clients/admin-ui/src/pages/add-systems/multiple.tsx @@ -1,12 +1,8 @@ -import { - ChakraBox as Box, - ChakraText as Text, - Link as LinkText, -} from "fidesui"; +import { ChakraBox as Box, ChakraText as Text } from "fidesui"; import type { NextPage } from "next"; -import NextLink from "next/link"; import Layout from "~/features/common/Layout"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { ADD_SYSTEMS_MANUAL_ROUTE, ADD_SYSTEMS_ROUTE, @@ -33,9 +29,9 @@ const AddMultipleSystemsPage: NextPage = () => ( {DESCRIBE_SYSTEM_COPY} - - Add a system - {" "} + + Add a system + {" "} page. diff --git a/clients/admin-ui/src/pages/dataset/index.tsx b/clients/admin-ui/src/pages/dataset/index.tsx index 6736833cd45..4155e9ee71c 100644 --- a/clients/admin-ui/src/pages/dataset/index.tsx +++ b/clients/admin-ui/src/pages/dataset/index.tsx @@ -17,7 +17,6 @@ import { Icons, } from "fidesui"; import type { NextPage } from "next"; -import NextLink from "next/link"; import { useRouter } from "next/router"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useDispatch } from "react-redux"; @@ -26,6 +25,7 @@ import { usePollForClassifications } from "~/features/common/classifications"; import ErrorPage from "~/features/common/errors/ErrorPage"; import { useFeatures } from "~/features/common/features"; import Layout from "~/features/common/Layout"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { DATASET_DETAIL_ROUTE } from "~/features/common/nav/routes"; import PageHeader from "~/features/common/PageHeader"; import { @@ -219,9 +219,9 @@ const DataSets: NextPage = () => { }, ]} rightContent={ - + - + } /> diff --git a/clients/admin-ui/src/pages/privacy-assessments/[id].tsx b/clients/admin-ui/src/pages/privacy-assessments/[id].tsx index bd99269bb80..d344e7afb5d 100644 --- a/clients/admin-ui/src/pages/privacy-assessments/[id].tsx +++ b/clients/admin-ui/src/pages/privacy-assessments/[id].tsx @@ -10,12 +10,12 @@ import { useModal, } from "fidesui"; import type { NextPage } from "next"; -import NextLink from "next/link"; import { useRouter } from "next/router"; import { useFeatures } from "~/features/common/features"; import { getErrorMessage } from "~/features/common/helpers"; import Layout from "~/features/common/Layout"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { PRIVACY_ASSESSMENTS_ROUTE } from "~/features/common/nav/routes"; import PageHeader from "~/features/common/PageHeader"; import { @@ -129,9 +129,9 @@ const PrivacyAssessmentDetailPage: NextPage = () => { subTitle="There was an error loading this privacy assessment. Please try again." extra={ - + - + diff --git a/clients/admin-ui/src/pages/settings/rbac/index.tsx b/clients/admin-ui/src/pages/settings/rbac/index.tsx index efd65483a49..03d2e94bc3c 100644 --- a/clients/admin-ui/src/pages/settings/rbac/index.tsx +++ b/clients/admin-ui/src/pages/settings/rbac/index.tsx @@ -1,10 +1,10 @@ import Layout from "common/Layout"; import { Button, Flex, Space, Table, Tag, Typography } from "fidesui"; import type { NextPage } from "next"; -import NextLink from "next/link"; import React from "react"; import ErrorPage from "~/features/common/errors/ErrorPage"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { RBAC_ROLE_NEW_ROUTE } from "~/features/common/nav/routes"; import PageHeader from "~/features/common/PageHeader"; import { LinkCell } from "~/features/common/table/cells/LinkCell"; @@ -85,9 +85,9 @@ const RBACPage: NextPage = () => { isSticky={false} className="pb-0" rightContent={ - + - + } > diff --git a/clients/admin-ui/src/pages/settings/rbac/roles/new.tsx b/clients/admin-ui/src/pages/settings/rbac/roles/new.tsx index f08db56bba2..c46d5d5d49b 100644 --- a/clients/admin-ui/src/pages/settings/rbac/roles/new.tsx +++ b/clients/admin-ui/src/pages/settings/rbac/roles/new.tsx @@ -10,11 +10,11 @@ import { useMessage, } from "fidesui"; import type { NextPage } from "next"; -import NextLink from "next/link"; import { useRouter } from "next/router"; import React, { useMemo } from "react"; import { getErrorMessage } from "~/features/common/helpers"; +import { RouterLink } from "~/features/common/nav/RouterLink"; import { RBAC_ROUTE } from "~/features/common/nav/routes"; import PageHeader from "~/features/common/PageHeader"; import { useCreateRoleMutation, useGetRolesQuery } from "~/features/rbac"; @@ -144,9 +144,9 @@ const NewRolePage: NextPage = () => { - + - +