From 3ce7f124506ba89efe3b660e17c1964256c4d361 Mon Sep 17 00:00:00 2001 From: myarmolinsky Date: Fri, 27 Dec 2024 07:21:37 -0500 Subject: [PATCH] Support both `react-router-dom` v5 `history` and v6 `navigate` Change-type: minor --- src/AutoUI/Filters/PersistentFilters.tsx | 61 +++++++++++++++++------- src/AutoUI/index.tsx | 38 +++++++++------ src/AutoUIApp/Content.tsx | 13 ++++- src/AutoUIApp/index.tsx | 17 +++++-- src/components/Filters/FocusSearch.tsx | 12 ++++- src/contexts/ContextProvider.tsx | 5 +- 6 files changed, 106 insertions(+), 40 deletions(-) diff --git a/src/AutoUI/Filters/PersistentFilters.tsx b/src/AutoUI/Filters/PersistentFilters.tsx index 87b4c92f..6c116484 100644 --- a/src/AutoUI/Filters/PersistentFilters.tsx +++ b/src/AutoUI/Filters/PersistentFilters.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import qs from 'qs'; import type { JSONSchema7 as JSONSchema } from 'json-schema'; -import type { History } from 'history'; import type { FiltersProps } from '../../components/Filters'; import { Filters } from '../../components/Filters'; import type { FilterDescription } from '../../components/Filters/SchemaSieve'; @@ -13,7 +12,6 @@ import { } from '../../components/Filters/SchemaSieve'; import { isJSONSchema } from '../../AutoUI/schemaOps'; import { useAnalyticsContext } from '@balena/ui-shared-components'; -import { useLocation } from 'react-router'; export interface ListQueryStringFilterObject { n: string; @@ -52,7 +50,7 @@ export function listFilterQuery(filters: JSONSchema[], stringify = true) { filter.title === FULL_TEXT_SLUG ? [parseFilterDescription(filter)].filter( (f): f is FilterDescription => !!f, - ) + ) : filter.anyOf ?.filter((f): f is JSONSchema => isJSONSchema(f)) .map( @@ -60,7 +58,7 @@ export function listFilterQuery(filters: JSONSchema[], stringify = true) { ({ ...parseFilterDescription(f), operatorSlug: f.title, - }) as FilterDescription & { operatorSlug?: string }, + } as FilterDescription & { operatorSlug?: string }), ) .filter((f) => !!f); @@ -80,14 +78,14 @@ export function listFilterQuery(filters: JSONSchema[], stringify = true) { return stringify ? qs.stringify(queryStringFilters, { strictNullHandling: true, - }) + }) : queryStringFilters; } export const loadRulesFromUrl = ( searchLocation: string, schema: JSONSchema, - history: History, + history: unknown, ): JSONSchema[] => { const { properties } = schema; if (!searchLocation || !properties) { @@ -158,7 +156,18 @@ export const loadRulesFromUrl = ( // In case of invalid signatures, remove search params to avoid Errors. if (isSignaturesInvalid) { - history.replace({ search: '' }); + if ( + history != null && + typeof history === 'object' && + 'replace' in history && + typeof history.replace === 'function' + ) { + // react-router-dom v5 history object + history?.replace?.({ search: '' }); + } else if (typeof history === 'function') { + // react-router-dom v6 navigate function + history({ search: '' }, { replace: true }); + } return; } @@ -181,7 +190,7 @@ export const loadRulesFromUrl = ( }; interface PersistentFiltersProps extends FiltersProps { - history: History; + history: unknown; } export const PersistentFilters = ({ @@ -196,11 +205,10 @@ export const PersistentFilters = ({ }: PersistentFiltersProps & Required>) => { const { state: analytics } = useAnalyticsContext(); - const { pathname } = useLocation(); - const locationSearch = history?.location?.search ?? ''; + const { pathname, search } = document.location; const storedFilters = React.useMemo(() => { - return loadRulesFromUrl(locationSearch, schema, history); - }, [locationSearch, schema, history]); + return loadRulesFromUrl(search, schema, history); + }, [schema, history]); const onFiltersUpdate = React.useCallback( (updatedFilters: JSONSchema[]) => { @@ -210,14 +218,31 @@ export const PersistentFilters = ({ strictNullHandling: true, }); - history?.replace?.({ - pathname, - search: filterQuery, - }); + if ( + history != null && + typeof history === 'object' && + 'replace' in history && + typeof history.replace === 'function' + ) { + // react-router-dom v5 history object + history.replace({ + pathname, + search: filterQuery, + }); + } else if (typeof history === 'function') { + // react-router-dom v6 navigate function + history( + { + pathname, + search: filterQuery, + }, + { replace: true }, + ); + } onFiltersChange?.(updatedFilters); - if (filterQuery !== locationSearch.substring(1)) { + if (filterQuery !== search.substring(1)) { analytics.webTracker?.track('Update table filters', { current_url: location.origin + location.pathname, // Need to reduce to a nested object instead of nested array for Amplitude to pick up on the property @@ -225,7 +250,7 @@ export const PersistentFilters = ({ }); } }, - [onFiltersChange, analytics.webTracker, history, locationSearch, pathname], + [onFiltersChange, analytics.webTracker, history], ); // When the component mounts, filters from the page URL, diff --git a/src/AutoUI/index.tsx b/src/AutoUI/index.tsx index d8b018ec..c85c9005 100644 --- a/src/AutoUI/index.tsx +++ b/src/AutoUI/index.tsx @@ -240,7 +240,7 @@ export const AutoUI = >({ $skip: page * itemsPerPage, }, (v) => v != null, - ) + ) : null; setInternalPineFilter(pineFilter); onChange?.({ @@ -284,7 +284,7 @@ export const AutoUI = >({ ...oldState, affectedEntries: items, checkedState: newCheckedState, - } + } : undefined, ); }, @@ -295,8 +295,8 @@ export const AutoUI = >({ const totalItems = serverSide ? pagination.totalItems : Array.isArray(data) - ? data.length - : undefined; + ? data.length + : undefined; const hideUtils = React.useMemo( () => @@ -367,7 +367,17 @@ export const AutoUI = >({ const url = new URL(getBaseUrl(row)); window.open(url.toString(), '_blank'); } catch { - history.push?.(getBaseUrl(row)); + if ( + typeof history === 'object' && + 'push' in history && + typeof history.push === 'function' + ) { + // react-router-dom v5 history object + history.push(getBaseUrl(row)); + } else if (typeof history === 'function') { + // react-router-dom v6 navigate function + history(getBaseUrl(row)); + } } } }, @@ -407,7 +417,7 @@ export const AutoUI = >({ sdkTags, t, ), - } + } : null; return { @@ -754,8 +764,8 @@ const hasPropertyEnabled = ( return Array.isArray(value) && value.some((v) => v === propertyKey) ? true : typeof value === 'boolean' - ? true - : false; + ? true + : false; }; const getColumnsFromSchema = >({ @@ -827,10 +837,10 @@ const getColumnsFromSchema = >({ ) ? 'primary' : definedPriorities.secondary.find( - (prioritizedKey) => prioritizedKey === key, - ) - ? 'secondary' - : 'tertiary'; + (prioritizedKey) => prioritizedKey === key, + ) + ? 'secondary' + : 'tertiary'; const widgetSchema = { ...val, title: undefined }; // TODO: Refactor this logic to create an object structure and retrieve the correct property using the refScheme. @@ -869,8 +879,8 @@ const getColumnsFromSchema = >({ xNoSort || val.format === 'tag' ? false : typeof fieldCustomSort === 'function' - ? fieldCustomSort - : getSortingFunction(key, val), + ? fieldCustomSort + : getSortingFunction(key, val), render: (fieldVal: string, entry: T) => { const calculatedField = autoUIAdaptRefScheme(fieldVal, val); return ( diff --git a/src/AutoUIApp/Content.tsx b/src/AutoUIApp/Content.tsx index b84b087b..66b771f9 100644 --- a/src/AutoUIApp/Content.tsx +++ b/src/AutoUIApp/Content.tsx @@ -178,7 +178,18 @@ export const Content = ({ openApiJson, openActionSidebar }: ContentProps) => { data={memoizedData} {...(!endsWithValidId && { onEntityClick: (entity) => { - history.push(`${pathname}/${entity.id}`); + if ( + history != null && + typeof history === 'object' && + 'push' in history && + typeof history.push === 'function' + ) { + // react-router-dom v5 history object + history.push(`${pathname}/${entity.id}`); + } else if (typeof history === 'function') { + // react-router-dom v6 navigate function + history(`${pathname}/${entity.id}`); + } }, })} /> diff --git a/src/AutoUIApp/index.tsx b/src/AutoUIApp/index.tsx index 4d2176f3..402fd733 100644 --- a/src/AutoUIApp/index.tsx +++ b/src/AutoUIApp/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Navbar } from './Navbar'; import { Sidebar } from './Sidebar'; -import { Router, Switch, Route } from 'react-router-dom'; +import { Router, Switch, Route, BrowserRouter } from 'react-router-dom'; import { Content } from './Content'; import { createGlobalStyle } from 'styled-components'; import type { OpenApiJson } from './openApiJson'; @@ -11,6 +11,7 @@ import { Provider } from 'rendition'; import { useHistory } from '../hooks/useHistory'; import { Material } from '@balena/ui-shared-components'; import { useClickOutsideOrEsc } from '../hooks/useClickOutsideOrEsc'; +import { History } from 'history'; const { Box } = Material; const SIDEBAR_WIDTH = 166; @@ -56,9 +57,19 @@ export const AutoUIApp = ({ openApiJson, title, logo }: AutoUIAppProps) => { 'openApiJson' > | null>(); + const RouterComponent = React.useCallback( + ({ children }: { children: Array }) => + typeof history === 'object' ? ( + {children} + ) : ( + {children} + ), + [history], + ); + return ( - + @@ -106,7 +117,7 @@ export const AutoUIApp = ({ openApiJson, title, logo }: AutoUIAppProps) => { )} - + ); }; diff --git a/src/components/Filters/FocusSearch.tsx b/src/components/Filters/FocusSearch.tsx index ca28050d..99ea92a9 100644 --- a/src/components/Filters/FocusSearch.tsx +++ b/src/components/Filters/FocusSearch.tsx @@ -136,7 +136,17 @@ export const FocusSearch = ({ const url = new URL(autouiContext.getBaseUrl(entity)); window.open(url.toString(), '_blank'); } catch { - history.push?.(autouiContext.getBaseUrl(entity)); + if ( + typeof history === 'object' && + 'push' in history && + typeof history.push === 'function' + ) { + // react-router-dom v5 history object + history.push(autouiContext.getBaseUrl(entity)); + } else if (typeof history === 'function') { + // react-router-dom v6 navigate function + history(autouiContext.getBaseUrl(entity)); + } } } }} diff --git a/src/contexts/ContextProvider.tsx b/src/contexts/ContextProvider.tsx index b70d7169..61a99ede 100644 --- a/src/contexts/ContextProvider.tsx +++ b/src/contexts/ContextProvider.tsx @@ -1,13 +1,12 @@ -import type { History } from 'history'; import { createContext } from 'react'; import type { Dictionary } from '../common'; import type { TFunction } from '../hooks/useTranslation'; export interface ContextProviderInterface { - history: History; + history: unknown; t?: TFunction; externalTranslationMap?: Dictionary; } export const ContextProvider = createContext({ - history: {} as History, + history: {}, });