diff --git a/.eslintrc.json b/.eslintrc.json index 042555ce7..2e46d0d94 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -162,6 +162,7 @@ "url", "urls", "href", + "hashchange", "xhr", "kwargs", "svg", diff --git a/public/index.html b/public/index.html index 1297aabd0..82c56947f 100644 --- a/public/index.html +++ b/public/index.html @@ -8,7 +8,7 @@ manifest.json provides metadata used when your web app is added to the homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ --> - + - + - + diff --git a/src/common/history.js b/src/common/history.js new file mode 100644 index 000000000..38b08dc3f --- /dev/null +++ b/src/common/history.js @@ -0,0 +1,87 @@ +let appHistory = null + +export const setAppHistory = history => { + appHistory = history +} + +export const toRoutePath = path => { + const legacy = path?.match(/^\/?#(\/.*)$/) + return legacy ? legacy[1] : path +} + +export const isSameSitePath = path => /^\/(?![/\\])/.test(path || '') + +export const navigate = (path, replace=false) => { + const routePath = toRoutePath(path) + if(!isSameSitePath(routePath)) + return + if(!appHistory) { + if(replace) + window.location.replace(routePath) + else + window.location.assign(routePath) + return + } + if(replace) + appHistory.replace(routePath) + else + appHistory.push(routePath) +} + +export const legacyHashRoute = (location=window.location) => { + const { hash, search } = location + if(!hash.startsWith('#/') || /^#\/[/\\]/.test(hash) || /[?&]referrer=/.test(search)) + return null + let route = hash.slice(1) + const outer = search.replace(/^\?/, '') + if(outer) { + const hashAt = route.indexOf('#') + const tail = hashAt > -1 ? route.slice(hashAt) : '' + const head = hashAt > -1 ? route.slice(0, hashAt) : route + route = head + (head.includes('?') ? '&' : '?') + outer + tail + } + return route +} + +export const redirectLegacyHashRoute = () => { + const route = legacyHashRoute() + if(route) + navigate(route, true) +} + +const currentRoute = () => window.location.pathname + window.location.search + window.location.hash + +const isPlainLeftClick = event => event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey + +const appLinkRoute = event => { + const anchor = event.target?.closest?.('a[href]') + if(!anchor || (anchor.target && anchor.target !== '_self') || anchor.hasAttribute('download')) + return null + const href = anchor.getAttribute('href') + if(!href || (href.startsWith('#') && !href.startsWith('#/')) || /^(mailto|tel|javascript):/i.test(href)) + return null + if(href.startsWith('#/')) + return href.slice(1) + const url = new URL(anchor.href, window.location.href) + if(url.origin !== window.location.origin || !/^https?:$/.test(url.protocol)) + return null + if(url.hash && !url.hash.startsWith('#/') && url.pathname === window.location.pathname && url.search === window.location.search) + return null + return legacyHashRoute(url) || url.pathname + url.search + url.hash +} + +export const keepLinkClickBubbling = event => { + if(isPlainLeftClick(event) && appLinkRoute(event)) + event.stopPropagation = () => {} +} + +export const handleLinkClick = event => { + if(event.defaultPrevented || !isPlainLeftClick(event)) + return + const route = appLinkRoute(event) + if(!route) + return + event.preventDefault() + if(route !== currentRoute()) + navigate(route) +} diff --git a/src/common/utils.js b/src/common/utils.js index 3d971328a..2e6c7bc2c 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -16,12 +16,13 @@ import { import APIService from '../services/APIService'; import GAService from '../services/GAService'; import { SERVER_CONFIGS } from './serverConfigs'; +import { legacyHashRoute, isSameSitePath } from './history'; -export const currentPath = () => window.location.hash.split('?')[0]; +export const currentPath = () => window.location.pathname; -export const isAtGlobalSearch = () => window.location.hash.includes('#/search') || isAtRoot(); +export const isAtGlobalSearch = () => currentPath().startsWith('/search') || isAtRoot(); -export const isAtRoot = () => currentPath() === '#/'; +export const isAtRoot = () => currentPath() === '/'; export const formatDate = date => moment(date).format(DATE_FORMAT); export const formatTime = date => moment(date).format(TIME_FORMAT); @@ -120,7 +121,7 @@ export const getAPIURL = () => { return get(savedConfigs, 'url') || window.API_URL || process.env.API_URL; } -export const toFullURL = uri => window.location.origin + '/#' + uri; +export const toFullURL = uri => window.location.origin + uri; export const toFullAPIURL = uri => getAPIURL() + uri; @@ -246,7 +247,7 @@ export const arrayToObject = arr => { }, {}); } -export const currentUserHasAccess = () => hasAccessToURL(window.location.hash.replace('#/', '')) +export const currentUserHasAccess = () => hasAccessToURL(window.location.pathname) export const hasAccessToURL = url => { if(!isLoggedIn()) @@ -673,7 +674,7 @@ export const logoutUser = (redirectToLogin, forced) => { localStorage.removeItem('visits'); } - const returnTo = window.location.origin + '/' + window.location.hash + const returnTo = window.location.origin + window.location.pathname + window.location.search if(forced) sessionStorage.setItem('session_expired', 'true') @@ -687,10 +688,8 @@ export const logoutUser = (redirectToLogin, forced) => { window.location = logoutURL else if(redirectToLogin) getLoginURL(forced ? returnTo : undefined).then(url => { window.location.href = url }) - else { - window.location.hash = '#/'; - window.location.reload(); - } + else + window.location.assign('/'); } @@ -905,12 +904,25 @@ const routePath = route => { // Keycloak only redeems a code when the token request repeats the sign-in's redirect_uri exactly, and the // callback can't rebuild a page's query string (e.g. ?referrer= on links from openconceptlab.org). So sign-in -// always goes through LOGIN_REDIRECT_URL, and the page to come back to (its hash route) waits here, in this tab. +// always goes through LOGIN_REDIRECT_URL, and the page to come back to (its route) waits here, in this tab. +const returnToRoute = returnTo => { + if(!returnTo) + return null + try { + const url = new URL(returnTo, window.location.origin) + if(url.origin !== window.location.origin) + return null + return legacyHashRoute(url) || url.pathname + url.search + url.hash + } catch { + return null + } +} + const prepareOAuthReturnTo = returnTo => { - const route = returnTo?.includes('#') ? returnTo.slice(returnTo.indexOf('#') + 1) : null + const route = returnToRoute(returnTo) const path = route && routePath(route) // The router matches paths case-insensitively, so /SIGNUP would start a sign-up too. - if(path?.startsWith('/') && !/^\/(oidc\/login|signin|signup)(\/|$)/i.test(path)) + if(isSameSitePath(path) && !/^\/(oidc\/login|signin|signup)(\/|$)/i.test(path)) sessionStorage.setItem(OAUTH_RETURN_TO_KEY, route) else sessionStorage.removeItem(OAUTH_RETURN_TO_KEY) diff --git a/src/components/app/App.jsx b/src/components/app/App.jsx index 3d3d98159..77f774677 100644 --- a/src/components/app/App.jsx +++ b/src/components/app/App.jsx @@ -128,14 +128,13 @@ const App = props => { const queryParams = new URLSearchParams(search) const referrer = queryParams.get('referrer') if(isLoggedIn()) { - // OIDLoginCallback navigates away itself; resetting the hash here would undo that. - if(!pathname.startsWith('/oidc/login')) - window.location.hash = '#' + pathname + search + if(hash?.startsWith('#/') && !pathname.startsWith('/oidc/login')) + props.history.replace(pathname + search) } else if(isOtherOCLClientURL(referrer) && !isLoggedIn()) { const parts = hash ? hash.split('?') : referrer.split('?') let params = new URLSearchParams(parts[1]) if(params.get('auth') === 'true') { - getLoginURL(window.location.origin + '/#' + pathname).then(url => { window.location.href = url }) + getLoginURL(window.location.origin + pathname).then(url => { window.location.href = url }) } } } diff --git a/src/components/app/HeaderControls.jsx b/src/components/app/HeaderControls.jsx index 4ae1ee574..14f1c8c46 100644 --- a/src/components/app/HeaderControls.jsx +++ b/src/components/app/HeaderControls.jsx @@ -31,7 +31,7 @@ const HeaderControls = () => { { authenticated && <> - + diff --git a/src/components/app/LeftMenu.jsx b/src/components/app/LeftMenu.jsx index 4f5af14a7..fe6c8e410 100644 --- a/src/components/app/LeftMenu.jsx +++ b/src/components/app/LeftMenu.jsx @@ -63,7 +63,7 @@ const LeftMenu = ({ isOpen, onClose }) => { { px: 2, borderRadius: '100px' }} - href={`#${user?.url}repos`} + href={`${user?.url}repos`} className='no-anchor-styles' selected={location.pathname === (user?.url + 'repos')} > @@ -172,7 +172,7 @@ const LeftMenu = ({ isOpen, onClose }) => { padding: '0px 16px', borderRadius: '100px' }} - href={`#${org?.url}`} + href={org?.url} className='no-anchor-styles' selected={location.pathname === org?.url} > @@ -220,7 +220,7 @@ const LeftMenu = ({ isOpen, onClose }) => { padding: '0px 16px', borderRadius: '100px' }} - href={`#${followed.url}`} + href={followed.url} className='no-anchor-styles' selected={location.pathname === followed?.url} > diff --git a/src/components/collections/AddReferencesDialog.jsx b/src/components/collections/AddReferencesDialog.jsx index 0af42835e..eff885306 100644 --- a/src/components/collections/AddReferencesDialog.jsx +++ b/src/components/collections/AddReferencesDialog.jsx @@ -310,7 +310,7 @@ const AddReferencesDialog = ({ open, onClose, collectionUrl, onSuccess }) => { label={t('reference.open_repo', { name: seed.name || seed.id })} variant="outlined" clickable - onClick={() => window.open(window.location.origin + '/#' + seed.url, '_blank')} + onClick={() => window.open(window.location.origin + seed.url, '_blank')} sx={{ fontSize: '0.7rem', whiteSpace: 'nowrap' }} /> )} diff --git a/src/components/common/About.jsx b/src/components/common/About.jsx index 7307d86d8..33da4614e 100644 --- a/src/components/common/About.jsx +++ b/src/components/common/About.jsx @@ -32,9 +32,9 @@ const About = ({ title, text, style, expanded }) => { {title} } -
+
-
+
{ showReadMoreButton && diff --git a/src/components/common/BaseEntityChip.jsx b/src/components/common/BaseEntityChip.jsx index 45ed8dc01..fc372040f 100644 --- a/src/components/common/BaseEntityChip.jsx +++ b/src/components/common/BaseEntityChip.jsx @@ -178,7 +178,7 @@ const BaseEntityChip = ({ entity, icon, hideType, hideRepoVersion, primary, size onClick={noLink ? undefined : event => { event.stopPropagation() }} - href={noLink ? undefined : '#' + (entity?.version_url || entity?.url)} + href={noLink ? undefined : entity?.version_url || entity?.url} component='a' {...rest} /> diff --git a/src/components/common/EntityAttributesDialog.jsx b/src/components/common/EntityAttributesDialog.jsx index 2ec45c887..85f233a91 100644 --- a/src/components/common/EntityAttributesDialog.jsx +++ b/src/components/common/EntityAttributesDialog.jsx @@ -29,7 +29,7 @@ const EntityAttributesDialog = ({ entity, fields, open, onClose }) => { if(info.type === 'external_link') return formatWebsiteLink(value) if(info.type === 'user') - return + return if(info.type === 'json') return
{JSON.stringify(value, undefined, 2)}
if(info.type === 'locales-list') { diff --git a/src/components/common/Events.jsx b/src/components/common/Events.jsx index 547d3dc83..f557541f6 100644 --- a/src/components/common/Events.jsx +++ b/src/components/common/Events.jsx @@ -48,7 +48,7 @@ const EventDescription = ({ event, isFirst, isLast, isJoined }) => { {eventDescription} { rel ? - : + : null } diff --git a/src/components/common/OCLLogo.jsx b/src/components/common/OCLLogo.jsx index 807956f9f..37d4368c7 100644 --- a/src/components/common/OCLLogo.jsx +++ b/src/components/common/OCLLogo.jsx @@ -4,7 +4,7 @@ import { PRIMARY_COLORS } from '../../common/colors' const OCLLogo = ({ color, width, height, }) => { return ( - + diff --git a/src/components/common/OwnerButton.jsx b/src/components/common/OwnerButton.jsx index 7a96d8a13..9d020ceb1 100644 --- a/src/components/common/OwnerButton.jsx +++ b/src/components/common/OwnerButton.jsx @@ -29,7 +29,7 @@ const Owner = ({owner, ownerType, ownerURL, noIcons, sx, ...rest}) => { padding: '0 8px' }]} startIcon={!noIcons && } - href={ownerURL ? '#' + ownerURL : undefined} + href={ownerURL || undefined} component="button" {...rest} > diff --git a/src/components/common/ResourceReferences.jsx b/src/components/common/ResourceReferences.jsx index 043c778db..4bfba2372 100644 --- a/src/components/common/ResourceReferences.jsx +++ b/src/components/common/ResourceReferences.jsx @@ -31,7 +31,7 @@ const ResourceReferences = ({ references, resourceType }) => { diff --git a/src/components/concepts/Associations.jsx b/src/components/concepts/Associations.jsx index 0e96fd945..49ad61336 100644 --- a/src/components/concepts/Associations.jsx +++ b/src/components/concepts/Associations.jsx @@ -22,6 +22,7 @@ import AddIcon from '@mui/icons-material/Add'; import WarnIcon from '@mui/icons-material/WarningAmber'; import { get, isEmpty, forEach, map, find, compact, flatten, values, filter, without, uniqBy, orderBy } from 'lodash'; import { generateRandomString, dropVersion, URIToParentParams, toParentURI, getResourceIdFromUrl } from '../../common/utils' +import { navigate } from '../../common/history' import TagCountLabel from '../common/TagCountLabel' import RepoChip from '../repos/RepoChip' import AssociationMappingCells from '../mappings/AssociationMappingCells' @@ -62,7 +63,7 @@ const AssociationRow = ({mappings, id, mapType, isSelf, isIndirect, isHierarchy, return { hover: true, sx: {...(hide ? {display: 'none'} : {}), cursor: targetURL ? 'pointer' : 'default'}, - onClick: () => { if(targetURL) window.location.hash = targetURL } + onClick: () => { if(targetURL) navigate(targetURL) } } } return ( diff --git a/src/components/concepts/ConceptDetails.jsx b/src/components/concepts/ConceptDetails.jsx index 66d0735bd..97ab3279d 100644 --- a/src/components/concepts/ConceptDetails.jsx +++ b/src/components/concepts/ConceptDetails.jsx @@ -75,7 +75,7 @@ const ConceptDetails = ({ concept, repo, repoSummary, mappings, reverseMappings, {t('common.last_updated')} { loading ? : - <>{formatDateTime(concept.versioned_updated_on || concept.updated_on)} {t('common.by')} + <>{formatDateTime(concept.versioned_updated_on || concept.updated_on)} {t('common.by')} } diff --git a/src/components/concepts/ConceptForm.jsx b/src/components/concepts/ConceptForm.jsx index 004b75925..39221a71f 100644 --- a/src/components/concepts/ConceptForm.jsx +++ b/src/components/concepts/ConceptForm.jsx @@ -16,6 +16,7 @@ import { fetchDatatypes, fetchNameTypes, fetchDescriptionTypes, fetchConceptClasses, fetchLocales } from './utils'; import { toParentURI, dropVersion } from '../../common/utils' +import { navigate } from '../../common/history' import { OperationsContext } from '../app/LayoutContext'; import Button from '../common/Button' import AutocompleteGroupByRepoSummary from '../common/AutocompleteGroupByRepoSummary' @@ -441,7 +442,7 @@ class ConceptForm extends FormComponent { } {t('concept.form.id_live_at')}
- { `${window.location.origin}/#${source.url}concepts/` } + { `${window.location.origin}${source.url}concepts/` }
{fields.id.value || '[concept-id]'}/ @@ -499,7 +500,7 @@ class ConceptForm extends FormComponent { if([200, 201].includes(response?.status)) { setAlert({duration: 2000, message: this.props.edit ? this.props.t('concept.success_update') : this.props.t('concept.success_create'), severity: 'success'}) this.props.onClose(response.data) - window.location.hash = response.data.url + navigate(response.data.url) } else if (response?.status === 208) { let error = get(response?.data, '__all__.0') || this.props.t('common.already_exists') setAlert({duration: 10000, message: `${response.status}: ${error}`, severity: 'error'}) diff --git a/src/components/concepts/ConceptHeader.jsx b/src/components/concepts/ConceptHeader.jsx index 3b17a126e..e37c4f0d2 100644 --- a/src/components/concepts/ConceptHeader.jsx +++ b/src/components/concepts/ConceptHeader.jsx @@ -90,7 +90,7 @@ const ConceptHeader = ({concept, repo, onClose, repoURL, onEdit, onRetire, onCre size='small' color='primary' disabled={!detailsLoaded} - href={detailsLoaded ? `#${conceptSourceURL}` : undefined} + href={detailsLoaded ? conceptSourceURL : undefined} target='_blank' rel='noopener noreferrer' onClick={event => { diff --git a/src/components/concepts/History.jsx b/src/components/concepts/History.jsx index 2b3a2e266..e5f126591 100644 --- a/src/components/concepts/History.jsx +++ b/src/components/concepts/History.jsx @@ -47,7 +47,7 @@ const VersionMenu = ({version, repoVersion, onClose, anchorEl, resource, icon}) onClose={onClose} sx={{'.MuiPaper-root': {backgroundColor: 'surface.n94'}}} > - + {icon} @@ -101,7 +101,7 @@ const History = ({ versions, repoVersions, loading, icon, resource }) => { if(compareSelection.length !== 2) return undefined const [older, newer] = orderBy(compareSelection, 'version_created_on') - return `#/concepts/compare?lhs=${older.version_url}&rhs=${newer.version_url}` + return `/concepts/compare?lhs=${older.version_url}&rhs=${newer.version_url}` }, [compareSelection]) const onCompareClick = () => { if(compareSelection.length !== 2) diff --git a/src/components/dashboard/DashboardEvents.jsx b/src/components/dashboard/DashboardEvents.jsx index f5317e295..95c743e84 100644 --- a/src/components/dashboard/DashboardEvents.jsx +++ b/src/components/dashboard/DashboardEvents.jsx @@ -35,7 +35,7 @@ const EventCard = ({ event, highlight }) => { return ( - + { includeSubtitle && diff --git a/src/components/dashboard/UserDashboard.jsx b/src/components/dashboard/UserDashboard.jsx index bf2488d2d..63930a7bc 100644 --- a/src/components/dashboard/UserDashboard.jsx +++ b/src/components/dashboard/UserDashboard.jsx @@ -85,7 +85,7 @@ const UsageWidget = ({ username }) => { )}
{ ['changed_retired', 'changed_major', 'changed_minor'].includes(section) && - } diff --git a/src/components/repos/utils.js b/src/components/repos/utils.js index e6ade1f82..7fe8a64a8 100644 --- a/src/components/repos/utils.js +++ b/src/components/repos/utils.js @@ -112,5 +112,5 @@ export const createSimilarRepoHref = repo => { const ownerURL = canUseRepoOwner ? (repo?.owner_url || `/${isOrg ? 'orgs' : 'users'}/${repo.owner}/`) : `/users/${username}/` - return `#${ownerURL}repos/new?copyFrom=${encodeURIComponent(repo.url)}` + return `${ownerURL}repos/new?copyFrom=${encodeURIComponent(repo.url)}` } diff --git a/src/components/search/Search.jsx b/src/components/search/Search.jsx index d60a36759..ea23cca2c 100644 --- a/src/components/search/Search.jsx +++ b/src/components/search/Search.jsx @@ -160,12 +160,12 @@ const Search = props => { let queryStr = url.replace('?&', '?').split('?')[1] queryStr = queryStr ? '?' + queryStr : '' - return window.location.hash.replace('#', '').split('?')[0] + queryStr; + return window.location.pathname + queryStr; } const getFiltersFromQueryParams = () => { - const queryParams = new URLSearchParams(window.location.hash.split('?')[1]) + const queryParams = new URLSearchParams(window.location.search) let _filters = false if(resource === 'references') { _filters = {}; @@ -189,7 +189,7 @@ const Search = props => { } const setQueryParamsInState = (mustFetch, includeRepoDefaultFilters) => { - const queryParams = new URLSearchParams(window.location.hash.split('?')[1]) + const queryParams = new URLSearchParams(window.location.search) const value = queryParams.get('q') || '' const isMatch = queryParams.get('$match') === 'true' const isDiffFromPrevInput = value !== input diff --git a/src/components/url-registry/CanonicalResolveResult.jsx b/src/components/url-registry/CanonicalResolveResult.jsx index 495e2207a..8572f6654 100644 --- a/src/components/url-registry/CanonicalResolveResult.jsx +++ b/src/components/url-registry/CanonicalResolveResult.jsx @@ -44,7 +44,7 @@ const CanonicalResolveResult = ({ result }) => { icon={} size='small' sx={{margin: '0 4px'}} - href={'#' + result?.result?.url} + href={result?.result?.url} />, { icon={} size='small' sx={{margin: '0 4px'}} - href={'#' + result.result.owner_url} + href={result.result.owner_url} />, isResolvedInGlobalRegistry ? t('url_registry.global') : @@ -66,7 +66,7 @@ const CanonicalResolveResult = ({ result }) => { icon={} size='small' sx={{margin: '0 4px'}} - href={'#' + registryNamespace} + href={registryNamespace} />, ]} shouldUnescape @@ -86,7 +86,7 @@ const CanonicalResolveResult = ({ result }) => { icon={} size='small' sx={{margin: '0 4px'}} - href={'#' + result?.result?.url} + href={result?.result?.url} />, { icon={} size='small' sx={{margin: '0 4px'}} - href={'#' + result.result.owner_url} + href={result.result.owner_url} />, { icon={} size='small' sx={{margin: '0 4px'}} - href={'#' + requestedNamespace} + href={requestedNamespace} />, ]} shouldUnescape @@ -128,7 +128,7 @@ const CanonicalResolveResult = ({ result }) => { icon={} size='small' sx={{margin: '0 4px'}} - href={'#' + registryNamespace} + href={registryNamespace} />, ]} shouldUnescape @@ -156,7 +156,7 @@ const CanonicalResolveResult = ({ result }) => { icon={} size='small' sx={{margin: '0 4px'}} - href={'#' + requestedNamespace} + href={requestedNamespace} /> ]} shouldUnescape diff --git a/src/components/users/AddMenuList.jsx b/src/components/users/AddMenuList.jsx index cafb789a5..f24d41e7b 100644 --- a/src/components/users/AddMenuList.jsx +++ b/src/components/users/AddMenuList.jsx @@ -16,13 +16,13 @@ const AddMenuList = ({ anchorEl, open, onClose }) => { onClose={onClose} sx={{'.MuiPaper-root': {backgroundColor: 'surface.n94', minWidth: '200px'}}} > - + - + diff --git a/src/components/users/Following.jsx b/src/components/users/Following.jsx index 4e8200f23..bef3e679a 100644 --- a/src/components/users/Following.jsx +++ b/src/components/users/Following.jsx @@ -10,7 +10,7 @@ const DEFAULT_MEMBERS_TO_SHOW = 12 const Followed = ({ object }) => { return ( - + ) diff --git a/src/components/users/OIDLoginCallback.jsx b/src/components/users/OIDLoginCallback.jsx index 36b98cf56..980a4a26e 100644 --- a/src/components/users/OIDLoginCallback.jsx +++ b/src/components/users/OIDLoginCallback.jsx @@ -6,6 +6,7 @@ import { refreshCurrentUserCache, consumeStoredPKCECodeVerifier, consumeAndValidateOAuthState, consumeOAuthReturnTo, isSignupOAuthState, isLoggedIn, getLoginURL } from '../../common/utils'; +import { navigate } from '../../common/history'; import APIService from '../../services/APIService' import GAService from '../../services/GAService' import { OperationsContext } from '../app/LayoutContext'; @@ -84,15 +85,15 @@ class OIDLoginCallback extends React.Component { ) }) - window.location.hash = '#' + (next || '/') + navigate(next || '/', true) } cacheUserData() { refreshCurrentUserCache(() => { if(this.state.returnTo) - window.location.hash = '#' + this.state.returnTo + navigate(this.state.returnTo, true) else if(this.state.next) - window.location.hash = '#' + this.state.next + navigate(this.state.next, true) else { let returnToURL = '/' if(this.props?.location?.search) { @@ -100,7 +101,7 @@ class OIDLoginCallback extends React.Component { if(queryParams && queryParams.get('returnTo')) returnToURL = queryParams.get('returnTo') } - window.location.hash = '#' + returnToURL + navigate(returnToURL, true) } }) } diff --git a/src/components/users/UserForm.jsx b/src/components/users/UserForm.jsx index bfdc72555..57014355f 100644 --- a/src/components/users/UserForm.jsx +++ b/src/components/users/UserForm.jsx @@ -120,7 +120,7 @@ const UserForm = ({ user }) => { /> Your URL will be:
- {window.location.origin}/#/users/{user.username}/ + {window.location.origin}/users/{user.username}/
diff --git a/src/components/users/UserMenu.jsx b/src/components/users/UserMenu.jsx index f3ab81e15..ffbdddf5c 100644 --- a/src/components/users/UserMenu.jsx +++ b/src/components/users/UserMenu.jsx @@ -63,7 +63,7 @@ const UserMenu = ({ isOpen, onClose }) => { { authenticated ? - + @@ -97,7 +97,7 @@ const UserMenu = ({ isOpen, onClose }) => { } - + diff --git a/src/components/users/UserProfile.jsx b/src/components/users/UserProfile.jsx index dd265283b..7c9c22738 100644 --- a/src/components/users/UserProfile.jsx +++ b/src/components/users/UserProfile.jsx @@ -108,7 +108,7 @@ const UserProfile = ({ user }) => {
} /> diff --git a/src/components/users/UserStatistics.jsx b/src/components/users/UserStatistics.jsx index e4cbc5f48..5ca23b57a 100644 --- a/src/components/users/UserStatistics.jsx +++ b/src/components/users/UserStatistics.jsx @@ -32,7 +32,7 @@ const UserStatistics = ({ user }) => {
- + diff --git a/src/components/users/UserSummary.jsx b/src/components/users/UserSummary.jsx index c1a244326..66067ea98 100644 --- a/src/components/users/UserSummary.jsx +++ b/src/components/users/UserSummary.jsx @@ -16,7 +16,7 @@ const UserSummary = ({ user }) => {
- } /> + } />
) diff --git a/src/index.jsx b/src/index.jsx index 527b01cab..638135a91 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -3,13 +3,14 @@ import { createRoot } from 'react-dom/client'; import Fade from '@mui/material/Fade'; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; -import { HashRouter } from 'react-router-dom'; +import { BrowserRouter, useHistory } from 'react-router-dom'; import { ThemeProvider, StyledEngineProvider, createTheme } from '@mui/material/styles'; import App from './components/app/App'; import LayoutContext from './components/app/LayoutContext'; import './index.scss'; import { COLORS } from './common/colors'; import './i18n/config'; +import { setAppHistory, handleLinkClick, keepLinkClickBubbling, legacyHashRoute, redirectLegacyHashRoute } from './common/history'; const theme = createTheme(); const v5Theme = createTheme(theme, { @@ -121,10 +122,24 @@ const v5Theme = createTheme(theme, { }) +const HistoryBridge = () => { + setAppHistory(useHistory()) + return null +} + +const legacyRoute = legacyHashRoute() +if(legacyRoute) + window.history.replaceState(null, '', legacyRoute) + +window.addEventListener('click', keepLinkClickBubbling, true) +document.addEventListener('click', handleLinkClick) +window.addEventListener('hashchange', redirectLegacyHashRoute) + const container = document.getElementById('root'); const root = createRoot(container); root.render( - + + @@ -132,5 +147,5 @@ root.render( - + ); diff --git a/src/services/GAService.js b/src/services/GAService.js index 044521bf6..0d6969635 100644 --- a/src/services/GAService.js +++ b/src/services/GAService.js @@ -26,7 +26,7 @@ const GAService = { if(!enabled()) return; - ReactGA.send({ hitType: 'pageview', page: window.location.pathname + window.location.hash.split('?')[0] }); + ReactGA.send({ hitType: 'pageview', page: window.location.pathname }); }, recordUpsertEvent(category, edit, resource) { diff --git a/webpack.config.js b/webpack.config.js index 81857a362..418f6b332 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -18,9 +18,12 @@ module.exports = (env) => { // year (nginx/default.conf.template). HtmlWebpackPlugin writes the hashed // names into index.html. Dev keeps webpack's default [name].js / [id].js. output: isProduction ? { + publicPath: '/', filename: '[name].[contenthash:8].js', chunkFilename: '[name].[contenthash:8].js', - } : {}, + } : { + publicPath: '/', + }, module: { rules: [ { @@ -105,7 +108,8 @@ module.exports = (env) => { }, allowedHosts: 'all', historyApiFallback: { - index: 'index.html', + index: '/index.html', + disableDotRule: true, }, proxy: [ {