diff --git a/Dockerfile b/Dockerfile index f0f21162d..777571351 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,7 @@ ENV OIDC_REALM=${OIDC_REALM} ENV ANALYTICS_API=${ANALYTICS_API} ENV AI_ASSISTANT_API_URL=${AI_ASSISTANT_API_URL} ENV OCL_ONLINE_API_URL=${OCL_ONLINE_API_URL} +ENV MAPPER_URL=${MAPPER_URL} RUN mkdir /app WORKDIR /app ENV PATH /app/node_modules/.bin:$PATH diff --git a/docker-compose.yml b/docker-compose.yml index c73aa5bc2..305f9c75e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,7 @@ services: - ANALYTICS_API=${ANALYTICS_API-} - AI_ASSISTANT_API_URL=${AI_ASSISTANT_API_URL-} - OCL_ONLINE_API_URL=${OCL_ONLINE_API_URL-} + - MAPPER_URL=${MAPPER_URL-} - ERRBIT_URL - ERRBIT_KEY - HOTJAR_ID diff --git a/package.json b/package.json index fcc0b98fb..a30c780c0 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,8 @@ "react-is": "^18.3.1" }, "scripts": { - "start": "./node_modules/webpack-dev-server/bin/webpack-dev-server.js --progress --host 0.0.0.0 --port ${WEB_PORT} --env API_URL=${API_URL} --env NODE_ENV=${NODE_ENV} --env RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY} --env GA_ACCOUNT_ID=${GA_ACCOUNT_ID} --env HOTJAR_ID=${HOTJAR_ID} --env ERRBIT_URL=${ERRBIT_URL} --env ERRBIT_KEY=${ERRBIT_KEY} --env LOGIN_REDIRECT_URL=${LOGIN_REDIRECT_URL} --env OIDC_RP_CLIENT_ID=${OIDC_RP_CLIENT_ID} --env OIDC_SERVER_URL=${OIDC_SERVER_URL} --env OIDC_REALM=${OIDC_REALM} --env ANALYTICS_API=${ANALYTICS_API} --env AI_ASSISTANT_API_URL=${AI_ASSISTANT_API_URL} --env OCL_ONLINE_API_URL=${OCL_ONLINE_API_URL} --mode ${NODE_ENV} --hot", - "build": "node --max-old-space-size=1536 ./node_modules/webpack/bin/webpack.js --progress --env API_URL=${API_URL} --env NODE_ENV=${NODE_ENV} --env RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY} --env GA_ACCOUNT_ID=${GA_ACCOUNT_ID} --env HOTJAR_ID=${HOTJAR_ID} --env ERRBIT_URL=${ERRBIT_URL} --env ERRBIT_KEY=${ERRBIT_KEY} --env LOGIN_REDIRECT_URL=${LOGIN_REDIRECT_URL} --env OIDC_RP_CLIENT_ID=${OIDC_RP_CLIENT_ID} --env OIDC_SERVER_URL=${OIDC_SERVER_URL} --env OIDC_REALM=${OIDC_REALM} --env ANALYTICS_API=${ANALYTICS_API} --env AI_ASSISTANT_API_URL=${AI_ASSISTANT_API_URL} --env OCL_ONLINE_API_URL=${OCL_ONLINE_API_URL} --mode ${NODE_ENV}", + "start": "./node_modules/webpack-dev-server/bin/webpack-dev-server.js --progress --host 0.0.0.0 --port ${WEB_PORT} --env API_URL=${API_URL} --env NODE_ENV=${NODE_ENV} --env RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY} --env GA_ACCOUNT_ID=${GA_ACCOUNT_ID} --env HOTJAR_ID=${HOTJAR_ID} --env ERRBIT_URL=${ERRBIT_URL} --env ERRBIT_KEY=${ERRBIT_KEY} --env LOGIN_REDIRECT_URL=${LOGIN_REDIRECT_URL} --env OIDC_RP_CLIENT_ID=${OIDC_RP_CLIENT_ID} --env OIDC_SERVER_URL=${OIDC_SERVER_URL} --env OIDC_REALM=${OIDC_REALM} --env ANALYTICS_API=${ANALYTICS_API} --env AI_ASSISTANT_API_URL=${AI_ASSISTANT_API_URL} --env OCL_ONLINE_API_URL=${OCL_ONLINE_API_URL} --env MAPPER_URL=${MAPPER_URL} --mode ${NODE_ENV} --hot", + "build": "node --max-old-space-size=1536 ./node_modules/webpack/bin/webpack.js --progress --env API_URL=${API_URL} --env NODE_ENV=${NODE_ENV} --env RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY} --env GA_ACCOUNT_ID=${GA_ACCOUNT_ID} --env HOTJAR_ID=${HOTJAR_ID} --env ERRBIT_URL=${ERRBIT_URL} --env ERRBIT_KEY=${ERRBIT_KEY} --env LOGIN_REDIRECT_URL=${LOGIN_REDIRECT_URL} --env OIDC_RP_CLIENT_ID=${OIDC_RP_CLIENT_ID} --env OIDC_SERVER_URL=${OIDC_SERVER_URL} --env OIDC_REALM=${OIDC_REALM} --env ANALYTICS_API=${ANALYTICS_API} --env AI_ASSISTANT_API_URL=${AI_ASSISTANT_API_URL} --env OCL_ONLINE_API_URL=${OCL_ONLINE_API_URL} --env MAPPER_URL=${MAPPER_URL} --mode ${NODE_ENV}", "eslint": "./node_modules/.bin/eslint ./src --ext .jsx,.js", "test": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test 'src/**/__tests__/**/*.test.js'" }, diff --git a/src/common/__tests__/appUrls.test.js b/src/common/__tests__/appUrls.test.js new file mode 100644 index 000000000..e4e83cb7c --- /dev/null +++ b/src/common/__tests__/appUrls.test.js @@ -0,0 +1,23 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +const { resolveMapperURL } = await import('../appUrls.js') + +test('configured URL wins and loses its trailing slash', () => { + assert.equal(resolveMapperURL('http://localhost:4004/', 'https://app.v3.qa.openconceptlab.org'), 'http://localhost:4004') + assert.equal(resolveMapperURL(' https://mapper.example.org ', 'https://tb.example.org'), 'https://mapper.example.org') +}) + +test('official hosts map to the matching Mapper environment', () => { + assert.equal(resolveMapperURL('', 'https://app.v3.openconceptlab.org'), 'https://map.openconceptlab.org') + assert.equal(resolveMapperURL('', 'https://app.openconceptlab.org'), 'https://map.openconceptlab.org') + assert.equal(resolveMapperURL('', 'https://app.v3.qa.openconceptlab.org'), 'https://map.qa.openconceptlab.org') + assert.equal(resolveMapperURL(undefined, 'https://app.staging.openconceptlab.org'), 'https://map.staging.openconceptlab.org') +}) + +test('localhost, custom hosts and a missing origin have no Mapper without configuration', () => { + assert.equal(resolveMapperURL('', 'http://localhost:4002'), '') + assert.equal(resolveMapperURL('', 'https://terms.example.org'), '') + assert.equal(resolveMapperURL('', 'https://app.example.org'), '') + assert.equal(resolveMapperURL('', undefined), '') +}) diff --git a/src/common/__tests__/mapProjects.test.js b/src/common/__tests__/mapProjects.test.js new file mode 100644 index 000000000..5e3bb515f --- /dev/null +++ b/src/common/__tests__/mapProjects.test.js @@ -0,0 +1,71 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +const { mergeProjects, projectCount, canSeeMapProjects, filterProjects, fetchAllPages } = await import('../mapProjects.js') + +test('mergeProjects combines user and organization projects without duplicates', () => { + const merged = mergeProjects( + [{url: '/users/a/map-projects/1/'}, {url: '/users/a/map-projects/2/'}], + [{url: '/orgs/o/map-projects/3/'}, {url: '/users/a/map-projects/2/'}] + ) + assert.deepEqual(merged.map(p => p.url), ['/users/a/map-projects/1/', '/users/a/map-projects/2/', '/orgs/o/map-projects/3/']) +}) + +test('projectCount sums num_found across user and organization lists', () => { + assert.equal(projectCount({headers: {num_found: '4'}}, {headers: {num_found: '3'}}), 7) + assert.equal(projectCount({headers: {}}, undefined), 0) +}) + +test('canSeeMapProjects shows a user their own projects, not anyone else\'s', () => { + const member = {username: 'member', subscribed_orgs: [{id: 'CIEL'}]} + assert.equal(canSeeMapProjects(member, 'users', 'member'), true) + assert.equal(canSeeMapProjects(member, 'users', 'stranger'), false) +}) + +test('canSeeMapProjects shows an organization\'s projects to its members only', () => { + const member = {username: 'member', subscribed_orgs: [{id: 'CIEL'}]} + assert.equal(canSeeMapProjects(member, 'orgs', 'CIEL'), true) + assert.equal(canSeeMapProjects(member, 'orgs', 'WHO'), false) + assert.equal(canSeeMapProjects({username: 'member'}, 'orgs', 'CIEL'), false) +}) + +test('canSeeMapProjects shows staff every profile\'s projects', () => { + const staff = {username: 'admin', is_staff: true} + assert.equal(canSeeMapProjects(staff, 'users', 'stranger'), true) + assert.equal(canSeeMapProjects(staff, 'orgs', 'WHO'), true) +}) + +test('canSeeMapProjects hides projects when signed out', () => { + assert.equal(canSeeMapProjects(null, 'users', 'member'), false) + assert.equal(canSeeMapProjects({}, 'orgs', 'CIEL'), false) +}) + +test('filterProjects matches name or ID, ignoring case and spaces around the term', () => { + const projects = [{id: 181, name: 'Malaria labs'}, {id: 182, name: 'Kenya HIV'}, {id: 1818, name: 'Other'}] + assert.deepEqual(filterProjects(projects, ' malaria ').map(p => p.id), [181]) + assert.deepEqual(filterProjects(projects, '181').map(p => p.id), [181, 1818]) + assert.equal(filterProjects(projects, ''), projects) + assert.equal(filterProjects(null, 'x'), null) +}) + +const page = (data, next) => ({status: 200, data, headers: next ? {next: 'yes'} : {}}) + +test('fetchAllPages loads every page until there is no next one', async () => { + const pages = {1: page([{url: 'a'}], true), 2: page([{url: 'b'}], true), 3: page([{url: 'c'}])} + const all = await fetchAllPages(n => Promise.resolve(pages[n]), 20) + assert.deepEqual(all.map(p => p.url), ['a', 'b', 'c']) +}) + +test('fetchAllPages rejects when any page fails, so a partial list never looks complete', async () => { + // APIService resolves a failed request with the error body, not a response + const pages = {1: page([{url: 'a'}], true), 2: {detail: 'Server error'}} + await assert.rejects(fetchAllPages(n => Promise.resolve(pages[n]), 20)) + await assert.rejects(fetchAllPages(() => Promise.resolve(undefined), 20)) +}) + +test('fetchAllPages stops at maxPages', async () => { + const requested = [] + const all = await fetchAllPages(n => { requested.push(n); return Promise.resolve(page([{url: String(n)}], true)) }, 2) + assert.deepEqual(requested, [1, 2]) + assert.equal(all.length, 2) +}) diff --git a/src/common/__tests__/navigation.test.js b/src/common/__tests__/navigation.test.js new file mode 100644 index 000000000..7b5fd72e4 --- /dev/null +++ b/src/common/__tests__/navigation.test.js @@ -0,0 +1,41 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +const { trayItems, createItems } = await import('../navigation.js') + +const user = {url: '/users/test/'} + +test('tray lists dashboard, repositories and mapping projects', () => { + assert.deepEqual(trayItems(user, true).map(i => [i.key, i.path]), [ + ['dashboard', '/'], + ['repos', '/users/test/repos'], + ['map-projects', '/users/test/map-projects'], + ]) +}) + +test('tray omits mapping projects without a mapper', () => { + assert.deepEqual(trayItems(user, false).map(i => i.key), ['dashboard', 'repos']) +}) + +test('create menu sends the mapping project to the mapper app', () => { + const items = createItems(user, true) + assert.equal(items.find(i => i.key === 'map-project').app, 'mapper') + assert.equal(createItems(user, false).some(i => i.key === 'map-project'), false) + assert.equal(items.find(i => i.key === 'import').divider, true) +}) + +const { isTrayItemSelected } = await import('../navigation.js') + +test('mapping projects stays selected on a project page', () => { + const item = trayItems(user, true).find(i => i.key === 'map-projects') + assert.equal(isTrayItemSelected(item, '/users/test/map-projects'), true) + assert.equal(isTrayItemSelected(item, '/users/test/map-projects/'), true) + assert.equal(isTrayItemSelected(item, '/users/test/map-projects/123/'), true) + assert.equal(isTrayItemSelected(item, '/users/test/repos'), false) +}) + +test('dashboard is selected only on the root path', () => { + const item = trayItems(user, true).find(i => i.key === 'dashboard') + assert.equal(isTrayItemSelected(item, '/'), true) + assert.equal(isTrayItemSelected(item, '/users/test/map-projects'), false) +}) diff --git a/src/common/appUrls.js b/src/common/appUrls.js new file mode 100644 index 000000000..409883f77 --- /dev/null +++ b/src/common/appUrls.js @@ -0,0 +1,10 @@ +const OFFICIAL_ORIGIN = /^https:\/\/app\.(v3\.)?([a-z]+\.)*openconceptlab\.org$/ + +export const resolveMapperURL = (configured, origin) => { + const url = (configured || '').trim().replace(/\/+$/, '') + if(url) + return url + if(OFFICIAL_ORIGIN.test(origin || '')) + return origin.replace(/\/\/app\.(v3\.)?/, '//map.') + return '' +} diff --git a/src/common/constants.js b/src/common/constants.js index 184147241..7dfc058ec 100644 --- a/src/common/constants.js +++ b/src/common/constants.js @@ -110,3 +110,4 @@ export const TBV3_ANNOUNCEMENT_LETTER_URL = 'https://openconceptlab.org/blog/let export const TBV3_ANNOUNCEMENT_PRICING_URL = 'https://openconceptlab.org/pricing?utm_source=tbv3&utm_medium=banner&utm_campaign=subscription_dates' export const TERMS_OF_USE_URL = 'https://openconceptlab.org/terms-of-use' export const PRIVACY_POLICY_URL = 'https://openconceptlab.org/privacy-policy' +export const COMMUNITY_SITE_URL = 'https://openconceptlab.org' diff --git a/src/common/mapProjects.js b/src/common/mapProjects.js new file mode 100644 index 000000000..acc900aec --- /dev/null +++ b/src/common/mapProjects.js @@ -0,0 +1,41 @@ +export const mergeProjects = (...lists) => { + const seen = new Set() + return lists.flat().filter(project => !seen.has(project.url) && seen.add(project.url)) +} + +export const projectCount = (...responses) => responses.reduce( + (total, response) => total + (parseInt(response?.headers?.num_found) || 0), + 0 +) + +// Mapping projects are private: only their owner, members of the owning +// organization and staff can see them, so only they get the profile tab. +export const canSeeMapProjects = (currentUser, ownerType, owner) => { + if(!currentUser?.username || !owner) + return false + if(currentUser.is_staff) + return true + if(ownerType === 'users') + return currentUser.username === owner + if(ownerType === 'orgs') + return Boolean(currentUser.subscribed_orgs?.some(org => org?.id === owner)) + return false +} + +// The API can't search mapping projects, so the profile tab filters the +// list it loaded, by name and ID. +export const filterProjects = (projects, query) => { + const term = (query || '').trim().toLowerCase() + if(!term || !Array.isArray(projects)) + return projects + return projects.filter(project => [project.name, project.id].some(value => String(value ?? '').toLowerCase().includes(term))) +} + +// Loads every page of a mapping-project list (up to maxPages). A page that fails +// rejects the whole load, so a partial list never shows as if it were complete. +export const fetchAllPages = (getPage, maxPages, page = 1, loaded = []) => getPage(page).then(response => { + if(!(response?.status >= 200 && response.status < 300)) + throw new Error(`Mapping projects page ${page} could not be loaded`) + const all = [...loaded, ...(Array.isArray(response.data) ? response.data : [])] + return response.headers?.next && page < maxPages ? fetchAllPages(getPage, maxPages, page + 1, all) : all +}) diff --git a/src/common/navigation.js b/src/common/navigation.js new file mode 100644 index 000000000..9eec4caca --- /dev/null +++ b/src/common/navigation.js @@ -0,0 +1,21 @@ +export const trayItems = (user, hasMapper) => [ + {key: 'dashboard', app: 'v3', path: '/', labelKey: 'dashboard.my'}, + {key: 'repos', app: 'v3', path: `${user?.url}repos`, labelKey: 'user.my_repositories'}, + ...(hasMapper ? [{key: 'map-projects', app: 'v3', path: `${user?.url}map-projects`, labelKey: 'user.my_mapping_projects'}] : []) +] + +export const createItems = (user, hasMapper) => [ + {key: 'org', app: 'v3', path: '/orgs/new'}, + {key: 'repo', app: 'v3', path: `${user?.url}repos/new`}, + ...(hasMapper ? [{key: 'map-project', app: 'mapper', path: '/map-projects/new'}] : []), + {key: 'import', app: 'v3', path: '/imports', divider: true} +] + +export const isTrayItemSelected = (item, pathname) => { + if(!pathname) + return false + if(item.key === 'dashboard') + return pathname === item.path + const base = item.path.replace(/\/+$/, '') + return pathname === base || pathname === `${base}/` || pathname.startsWith(`${base}/`) +} diff --git a/src/common/plugins/loader.js b/src/common/plugins/loader.js index 94822e85e..3b81214f0 100644 --- a/src/common/plugins/loader.js +++ b/src/common/plugins/loader.js @@ -17,3 +17,7 @@ export function loadUsageDashboard() { export function loadUserProfileExtension() { return loadPrivateExport("UserProfileExtension"); } + +export function loadProfileTabs() { + return loadPrivateExport("ProfileTabs"); +} diff --git a/src/common/plugins/usage-dashboard.stub.js b/src/common/plugins/usage-dashboard.stub.js index 905fb6a44..0a4905bb6 100644 --- a/src/common/plugins/usage-dashboard.stub.js +++ b/src/common/plugins/usage-dashboard.stub.js @@ -1,3 +1,4 @@ export const UsageDashboard = null; export const UserProfileExtension = null; +export const ProfileTabs = null; export default null; diff --git a/src/common/utils.js b/src/common/utils.js index e294d18be..508791b29 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -11,8 +11,7 @@ import { } from 'lodash'; import { DATE_FORMAT, TIME_FORMAT, DATETIME_FORMAT, OCL_SERVERS_GROUP, OCL_FHIR_SERVERS_GROUP, HAPI_FHIR_SERVERS_GROUP, - OPENMRS_URL, DEFAULT_FHIR_SERVER_FOR_LOCAL_ID, OPERATIONS_PANEL_GROUP, ID_REGEX, - RESERVED_ROUTE_KEYWORDS, OCL_CLIENT_HEADERS + OPENMRS_URL, DEFAULT_FHIR_SERVER_FOR_LOCAL_ID, OPERATIONS_PANEL_GROUP, OCL_CLIENT_HEADERS } from './constants'; import APIService from '../services/APIService'; import GAService from '../services/GAService'; @@ -20,6 +19,7 @@ import { ensureIdToken, prepareSignOut, settleForcedSignOut, renewSessionSilentl import { getEnv } from './env'; import { SERVER_CONFIGS } from './serverConfigs'; import { legacyHashRoute, isSameSitePath } from './history'; +import { resolveMapperURL } from './appUrls'; export { dropVersion } from './uri'; @@ -1088,100 +1088,18 @@ export const handleLookupValuesResponse = (data, callback, attr) => { callback(orderBy(uniqBy(map(data, cc => ({id: get(cc, _attr), name: get(cc, _attr)})), 'name')), 'name'); } -export const toMapperURL = path => { - let url = 'https://map.openconceptlab.org' - if(window.location.host?.includes('localhost')) - url = 'http://localhost:4004' - if(['app.v3.qa.openconceptlab.org', 'app.v3.demo.openconceptlab.org'].includes(window.location.host)) - url = 'https://map.qa.openconceptlab.org' - if(window.location.host.match('app.v3.*.openconceptlab.org')) - url = window.location.origin.replace('//app.v3.', '//map.') - - let referrerParams = `referrer=${window.location.href}` - if(isLoggedIn()) - referrerParams += '?auth=true' - - - return `${url}/#${path || '/'}?${referrerParams}` -} - -const SHARED_REPO_TABS = ['concepts', 'mappings', 'references', 'versions', 'about'] -const EXPANSION_SCOPED_TABS = ['concepts', 'mappings'] -const RESERVED_REPO_SEGMENTS = [...SHARED_REPO_TABS, ...RESERVED_ROUTE_KEYWORDS, 'edit', 'compare-versions'] -const SHARED_ROOT_PATHS = ['/search', '/imports', '/concepts/compare', '/mappings/compare'] -const SHARED_OWNER_PATHS = {users: ['settings'], orgs: ['edit']} - -const isRouteId = segment => Boolean(segment) && ID_REGEX.test(segment) - -export const toV2Path = path => { - const segments = (path || '').split('?')[0].split('/').filter(Boolean) - - if(segments.length === 0) - return '/' - - const fullPath = '/' + segments.join('/') - if(SHARED_ROOT_PATHS.includes(fullPath)) - return fullPath - - const [ownerType, owner, repoType, repo] = segments - - if(!['users', 'orgs'].includes(ownerType) || !isRouteId(owner)) - return '/' +export const getMapperURL = () => resolveMapperURL(window.MAPPER_URL || process.env.MAPPER_URL, window.location.origin) - const ownerHome = `/${ownerType}/${owner}` +export const hasMapper = () => Boolean(getMapperURL()) - if(segments.length === 2) - return ownerHome - - if(segments.length === 3 && SHARED_OWNER_PATHS[ownerType].includes(segments[2])) - return fullPath - - if(!['sources', 'collections'].includes(repoType) || !isRouteId(repo)) - return ownerHome - - let repoHome = `${ownerHome}/${repoType}/${repo}` - let rest = segments.slice(4) - let hasVersion = false - - if(rest.length > 0 && !RESERVED_REPO_SEGMENTS.includes(rest[0])) { - if(!isRouteId(rest[0])) - return repoHome - repoHome = `${repoHome}/${rest[0]}` - rest = rest.slice(1) - hasVersion = true - } - - let expansion = '' - if(rest[0] === 'expansions' && isRouteId(rest[1])) { - expansion = rest[1] - rest = rest.slice(2) - } - - const expansionBase = expansion ? `${repoHome}${hasVersion ? '' : '/HEAD'}/expansions/${expansion}` : repoHome - - if(rest.length === 0) - return expansionBase - - if(!SHARED_REPO_TABS.includes(rest[0])) - return repoHome - - const tabPath = `${EXPANSION_SCOPED_TABS.includes(rest[0]) ? expansionBase : repoHome}/${rest[0]}` - - return isRouteId(rest[1]) ? `${tabPath}/${rest[1]}` : tabPath -} - -export const toV2URL = path => { - let url = 'https://app.openconceptlab.org' - if(window.location.host?.includes('localhost')) - url = 'http://localhost:4000' - if(window.location.host.match('app.v3.*.openconceptlab.org')) - url = window.location.origin.replace('//app.v3.', '//app.') +export const toMapperURL = path => { + const url = getMapperURL() let referrerParams = `referrer=${window.location.href}` if(isLoggedIn()) referrerParams += '?auth=true' - return `${url}/#${toV2Path(path)}?${referrerParams}` + return `${url}/#${path || '/'}?${referrerParams}` } export const isMapperURL = url => { diff --git a/src/components/app/App.jsx b/src/components/app/App.jsx index 5e3f35559..c274f6d1d 100644 --- a/src/components/app/App.jsx +++ b/src/components/app/App.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Route, Switch, withRouter } from 'react-router-dom'; import { - isLoggedIn, refreshCurrentUserCache, getCurrentUser, getLoginURL, isOtherOCLClientURL, isRedirectingToLoginViaReferrer + isLoggedIn, refreshCurrentUserCache, getCurrentUser, hasMapper, getLoginURL, isOtherOCLClientURL, isRedirectingToLoginViaReferrer } from '../../common/utils'; import GAService from '../../services/GAService'; import { silentSignIn, isSSOSessionEnabled, settleForcedSignOut } from '../../common/ssoSession'; @@ -39,6 +39,7 @@ import ConceptsComparison from '../concepts/ConceptsComparison' import MappingsComparison from '../mappings/MappingsComparison' import CheckAuth from './CheckAuth' import { loadUsageDashboard } from "../../common/plugins/loader"; +import { usePluginTabs } from "../../hooks/useProfileTabs"; import UserChip from '../users/UserChip' import UserTooltip from '../users/UserTooltip' @@ -186,7 +187,8 @@ const App = props => { }) }, []); - const orgTabs = ['repos'] + const pluginTabs = usePluginTabs() + const orgTabs = ['repos', ...(hasMapper() ? ['map-projects'] : []), ...pluginTabs.map(tab => tab.key)] const orgTabsStr = orgTabs.join('|') return ( diff --git a/src/components/app/ClassicTermBrowserButton.jsx b/src/components/app/ClassicTermBrowserButton.jsx deleted file mode 100644 index a9edccca7..000000000 --- a/src/components/app/ClassicTermBrowserButton.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; -import Chip from '@mui/material/Chip'; -import Tooltip from '@mui/material/Tooltip'; -import { toV2URL } from '../../common/utils'; -import ClassicOCLLogo from '../common/ClassicOCLLogo'; - -const V2_PRIMARY = 'rgb(51, 115, 170)'; - -const ClassicTermBrowserButton = () => { - const { t } = useTranslation(); - const location = useLocation(); - const href = toV2URL(location.pathname + (location.search || '')); - - return ( - - } - label={t('common.classic_termbrowser')} - sx={{ - marginRight: '8px', - paddingLeft: '8px', - verticalAlign: 'middle', - maxWidth: {xs: '150px', sm: 'none'}, - color: V2_PRIMARY, - borderColor: V2_PRIMARY, - '.MuiChip-label': { - paddingLeft: '6px', - paddingRight: '8px' - } - }} - /> - - ); -}; - -export default ClassicTermBrowserButton; diff --git a/src/components/app/Footer.jsx b/src/components/app/Footer.jsx index 9a75cec1e..aecb44b33 100644 --- a/src/components/app/Footer.jsx +++ b/src/components/app/Footer.jsx @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { getAppliedServerConfig } from '../../common/utils'; -import { TERMS_OF_USE_URL, PRIVACY_POLICY_URL } from '../../common/constants'; +import { COMMUNITY_SITE_URL, TERMS_OF_USE_URL, PRIVACY_POLICY_URL } from '../../common/constants'; import AppVersions from './AppVersions'; import './Footer.scss'; @@ -44,6 +44,8 @@ const Footer = () => {