From 8693a913dec637a16bd2d9451d5b924f3b9d1bbe Mon Sep 17 00:00:00 2001 From: Hayden Spitzley Date: Thu, 25 May 2023 13:10:53 -0400 Subject: [PATCH 1/4] feat: allow making isLocationMatch use exact match in query string matcher Signed-off-by: Hayden Spitzley --- .../src/layout/Sidebar/SidebarSubmenuItem.tsx | 7 ++--- .../src/layout/Sidebar/utils.test.ts | 26 +++++++++++++++++++ .../src/layout/Sidebar/utils.ts | 10 +++++-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx index b08647d335..7ec0a40624 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx @@ -130,6 +130,7 @@ export type SidebarSubmenuItemProps = { to?: string; icon?: IconComponent; dropdownItems?: SidebarSubmenuItemDropdownItem[]; + exact?: boolean; }; /** @@ -138,7 +139,7 @@ export type SidebarSubmenuItemProps = { * @public */ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { - const { title, subtitle, to, icon: Icon, dropdownItems } = props; + const { title, subtitle, to, icon: Icon, dropdownItems, exact } = props; const classes = useStyles(); const { setIsHoveredOn } = useContext(SidebarItemWithSubmenuContext); const closeSubmenu = () => { @@ -146,7 +147,7 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { }; const toLocation = useResolvedPath(to ?? ''); const currentLocation = useLocation(); - let isActive = isLocationMatch(currentLocation, toLocation); + let isActive = isLocationMatch(currentLocation, toLocation, exact || false); const [showDropDown, setShowDropDown] = useState(false); const handleClickDropdown = () => { @@ -155,7 +156,7 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { if (dropdownItems !== undefined) { dropdownItems.some(item => { const resolvedPath = resolvePath(item.to); - isActive = isLocationMatch(currentLocation, resolvedPath); + isActive = isLocationMatch(currentLocation, resolvedPath, exact || false); return isActive; }); return ( diff --git a/packages/core-components/src/layout/Sidebar/utils.test.ts b/packages/core-components/src/layout/Sidebar/utils.test.ts index b08dcda0fc..976108a63d 100644 --- a/packages/core-components/src/layout/Sidebar/utils.test.ts +++ b/packages/core-components/src/layout/Sidebar/utils.test.ts @@ -95,4 +95,30 @@ describe('isLocationMatching', () => { toLocation = { pathname: '/catalog', search: '', hash: '' }; expect(isLocationMatch(currentLocation, toLocation)).toBe(true); }); + + describe('exact matching', () => { + it('return false when target query parameters are subset of current location query parameters', async () => { + currentLocation = { + pathname: '/catalog', + search: '?x=foo&y=bar', + state: null, + hash: '', + key: '', + }; + toLocation = { pathname: '/catalog', search: '?x=foo', hash: '' }; + expect(isLocationMatch(currentLocation, toLocation, true)).toBe(false); + }); + + it('return true when target query parameters are exact match with current location query parameters', async () => { + currentLocation = { + pathname: '/catalog', + search: '?x=foo&y=bar', + state: null, + hash: '', + key: '', + }; + toLocation = { pathname: '/catalog', search: '?y=bar&x=foo', hash: '' }; + expect(isLocationMatch(currentLocation, toLocation, true)).toBe(true); + }); + }); }); diff --git a/packages/core-components/src/layout/Sidebar/utils.ts b/packages/core-components/src/layout/Sidebar/utils.ts index 1afebe796d..5dba73f4cd 100644 --- a/packages/core-components/src/layout/Sidebar/utils.ts +++ b/packages/core-components/src/layout/Sidebar/utils.ts @@ -18,7 +18,11 @@ import { Location, Path } from 'history'; import { isEqual, isMatch } from 'lodash'; import qs from 'qs'; -export function isLocationMatch(currentLocation: Location, toLocation: Path) { +export function isLocationMatch( + currentLocation: Location, + toLocation: Path, + exact: boolean = false, +) { const toDecodedSearch = new URLSearchParams(toLocation.search).toString(); const toQueryParameters = qs.parse(toDecodedSearch); @@ -27,9 +31,11 @@ export function isLocationMatch(currentLocation: Location, toLocation: Path) { ).toString(); const currentQueryParameters = qs.parse(currentDecodedSearch); + const queryStringMatcher = exact ? isEqual : isMatch; + const matching = isEqual(toLocation.pathname, currentLocation.pathname) && - isMatch(currentQueryParameters, toQueryParameters); + queryStringMatcher(currentQueryParameters, toQueryParameters); return matching; } From 485c49192b2544d05a51ad29e7fe6ddee0403007 Mon Sep 17 00:00:00 2001 From: Hayden Spitzley Date: Tue, 30 May 2023 09:01:58 -0400 Subject: [PATCH 2/4] remove fallback value and update api report Signed-off-by: Hayden Spitzley --- packages/core-components/api-report.md | 1 + .../core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 9f4aca1dcf..df34abdd03 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -1087,6 +1087,7 @@ export type SidebarSubmenuItemProps = { to?: string; icon?: IconComponent; dropdownItems?: SidebarSubmenuItemDropdownItem[]; + exact?: boolean; }; // @public diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx index 7ec0a40624..d05ee23636 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx @@ -147,7 +147,7 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { }; const toLocation = useResolvedPath(to ?? ''); const currentLocation = useLocation(); - let isActive = isLocationMatch(currentLocation, toLocation, exact || false); + let isActive = isLocationMatch(currentLocation, toLocation, exact); const [showDropDown, setShowDropDown] = useState(false); const handleClickDropdown = () => { @@ -156,7 +156,7 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { if (dropdownItems !== undefined) { dropdownItems.some(item => { const resolvedPath = resolvePath(item.to); - isActive = isLocationMatch(currentLocation, resolvedPath, exact || false); + isActive = isLocationMatch(currentLocation, resolvedPath, exact); return isActive; }); return ( From 66ae4d8ca380104da66c08e1c86aeb8a444c3efe Mon Sep 17 00:00:00 2001 From: Hayden Spitzley Date: Tue, 30 May 2023 09:48:50 -0400 Subject: [PATCH 3/4] add changeset Signed-off-by: Hayden Spitzley --- .changeset/perfect-donkeys-cry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/perfect-donkeys-cry.md diff --git a/.changeset/perfect-donkeys-cry.md b/.changeset/perfect-donkeys-cry.md new file mode 100644 index 0000000000..83eafe69ca --- /dev/null +++ b/.changeset/perfect-donkeys-cry.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Add option to require exact matching for isLocationMatch From 0eede913b7d9d1705944af875b32fc00ba9731e0 Mon Sep 17 00:00:00 2001 From: Hayden Spitzley <105455169+hspitzley-czi@users.noreply.github.com> Date: Thu, 1 Jun 2023 09:22:13 -0400 Subject: [PATCH 4/4] Update .changeset/perfect-donkeys-cry.md Co-authored-by: Patrik Oldsberg Signed-off-by: Hayden Spitzley <105455169+hspitzley-czi@users.noreply.github.com> --- .changeset/perfect-donkeys-cry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/perfect-donkeys-cry.md b/.changeset/perfect-donkeys-cry.md index 83eafe69ca..744dc19d64 100644 --- a/.changeset/perfect-donkeys-cry.md +++ b/.changeset/perfect-donkeys-cry.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -Add option to require exact matching for isLocationMatch +Added `exact` prop to `SidebarSubmenuItem` which causes it to only highlight if the current location is an exact match.