Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
"url",
"urls",
"href",
"hashchange",
"xhr",
"kwargs",
"svg",
Expand Down
37 changes: 16 additions & 21 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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/
-->
<link rel="shortcut icon" href="./favicon.ico">
<link rel="shortcut icon" href="/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Expand All @@ -19,7 +19,7 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<!-- CSS -->
<link rel="stylesheet" href="bootstrap.min.css">
<link rel="stylesheet" href="/bootstrap.min.css">
<!-- Font Awesome 6/7 full bundle -->
<link
rel="stylesheet"
Expand All @@ -32,27 +32,22 @@
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">

<!-- ENV config -->
<script src="./env-config.js"></script>
<script src="/env-config.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mark.js"></script>
<script>
if(window.location.pathname.startsWith('/search')) {
window.location = '/#/search/' + window.location.search;
}
if(window.location.hash.startsWith('#/state=')) {
window.location = '/#/oidc/login/' + window.location.hash.replace('#/', '?') + '&next=' + window.location.pathname;
}
if(window.location.hash.startsWith('#state=')) {
window.location = '/#/oidc/login/' + window.location.hash.replace('#', '?') + '&next=' + window.location.pathname;
}
if(!window.location.hash && (window.location.search || '').includes('code=')) {
window.location = '/#/oidc/login/' + window.location.search + '&next=' + window.location.pathname;
}
if(window.location.href.includes('/?session_state=')) {
window.location = '/';
}
if((window.location.search || '').includes('origin=openmrs')) {
window.location = '/#/search/?origin=openmrs&type=organizations'
}
(function() {
var loc = window.location;
var go = function(path) { window.history.replaceState(null, '', path); };
if(loc.hash.startsWith('#/state=') || loc.hash.startsWith('#state=')) {
go('/oidc/login/?' + loc.hash.replace(/^#\/?/, '') + '&next=' + loc.pathname);
} else if(loc.pathname === '/' && !loc.hash && (loc.search || '').includes('code=')) {
go('/oidc/login/' + loc.search + '&next=' + loc.pathname);
} else if(loc.href.includes('/?session_state=')) {
go('/');
} else if((loc.pathname === '/' || loc.pathname.startsWith('/search')) && (loc.search || '').includes('origin=openmrs')) {
go('/search/?origin=openmrs&type=organizations');
}
})();
</script>
<title></title>
</head>
Expand Down
87 changes: 87 additions & 0 deletions src/common/history.js
Original file line number Diff line number Diff line change
@@ -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)
}
38 changes: 25 additions & 13 deletions src/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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')

Expand All @@ -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('/');
}


Expand Down Expand Up @@ -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)
Expand Down
7 changes: 3 additions & 4 deletions src/components/app/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/app/HeaderControls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const HeaderControls = () => {
{
authenticated &&
<>
<IconButton sx={{marginRight: '8px'}} href="#/imports">
<IconButton sx={{marginRight: '8px'}} href="/imports">
<ImportIcon />
</IconButton>
<IconButton sx={{marginRight: '8px'}} onClick={onAddMenuOpen}>
Expand Down
8 changes: 4 additions & 4 deletions src/components/app/LeftMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const LeftMenu = ({ isOpen, onClose }) => {
<ListItem disablePadding sx={{ display: 'block' }}>
<ListItemButton
onClick={onClose}
href="/#/"
href="/"
className='no-anchor-styles'
selected={location.pathname === '/'}
sx={{
Expand Down Expand Up @@ -94,7 +94,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')}
>
Expand Down Expand Up @@ -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}
>
Expand Down Expand Up @@ -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}
>
Expand Down
2 changes: 1 addition & 1 deletion src/components/collections/AddReferencesDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
/>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/About.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ const About = ({ title, text, style, expanded }) => {
{title}
</Typography>
}
<div id='hidden-about' className='col-xs-12 padding-0 hidden' style={{display: 'none !important'}} dangerouslySetInnerHTML={{__html: text.replaceAll('href="/', 'href="/#/')}} />
<div id='hidden-about' className='col-xs-12 padding-0 hidden' style={{display: 'none !important'}} dangerouslySetInnerHTML={{__html: text}} />
<Collapse in={showAll} collapsedSize={75}>
<div id='about-text' className='col-xs-12 padding-0 md-content' dangerouslySetInnerHTML={{__html: text.replaceAll('href="/', 'href="/#/')}} />
<div id='about-text' className='col-xs-12 padding-0 md-content' dangerouslySetInnerHTML={{__html: text}} />
</Collapse>
{
showReadMoreButton &&
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/BaseEntityChip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/EntityAttributesDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const EntityAttributesDialog = ({ entity, fields, open, onClose }) => {
if(info.type === 'external_link')
return formatWebsiteLink(value)
if(info.type === 'user')
return <Link sx={{fontSize: '14px', justifyContent: 'flex-start'}} label={value} href={`#/users/${value}/`} />
return <Link sx={{fontSize: '14px', justifyContent: 'flex-start'}} label={value} href={`/users/${value}/`} />
if(info.type === 'json')
return <pre style={{margin: 0}}>{JSON.stringify(value, undefined, 2)}</pre>
if(info.type === 'locales-list') {
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/Events.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const EventDescription = ({ event, isFirst, isLast, isJoined }) => {
{eventDescription}
{
rel ?
<Link href={'#' + (event.referenced_object?.version_url || event.referenced_object?.url)} label={rel} sx={{fontSize: '14px', paddingLeft: 0, minWidth: 'auto', paddingTop: '1px'}} /> :
<Link href={event.referenced_object?.version_url || event.referenced_object?.url} label={rel} sx={{fontSize: '14px', paddingLeft: 0, minWidth: 'auto', paddingTop: '1px'}} /> :
null
}
</Typography>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/OCLLogo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PRIMARY_COLORS } from '../../common/colors'

const OCLLogo = ({ color, width, height, }) => {
return (
<a className='hover-no-bg-color flex-vertical-center' href='/#/'>
<a className='hover-no-bg-color flex-vertical-center' href='/'>
<SvgIcon sx={{color: 'primary.main', fill: color || PRIMARY_COLORS.main, width: width || '84px', height: height || '28px'}} href='/'>
<svg version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 306.14 111.63">
<g>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/OwnerButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const Owner = ({owner, ownerType, ownerURL, noIcons, sx, ...rest}) => {
padding: '0 8px'
}]}
startIcon={!noIcons && <OwnerIcon noTooltip ownerType={ownerType} {...iconProps} />}
href={ownerURL ? '#' + ownerURL : undefined}
href={ownerURL || undefined}
component="button"
{...rest}
>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/ResourceReferences.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const ResourceReferences = ({ references, resourceType }) => {
<ListItem key={reference.id || reference.uri} divider disablePadding>
<ListItemButton
component={reference.uri ? 'a' : 'div'}
href={reference.uri ? '#' + reference.uri : undefined}
href={reference.uri || undefined}
disableRipple={!reference.uri}
sx={{padding: '4px 16px', color: 'inherit'}}
>
Expand Down
3 changes: 2 additions & 1 deletion src/components/concepts/Associations.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 (
Expand Down
Loading
Loading