From bce75e28e1307078a4445cb8c1ddf1068ae41aa8 Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Fri, 30 Oct 2026 09:23:35 +0530 Subject: [PATCH 01/14] OpenConceptLab/ocl_online#418 | Unified navigation: shared tray, + menu and Mapping Projects profile tab --- Dockerfile | 2 +- docker-compose.yml | 2 +- package.json | 4 +- src/common/navigation.js | 12 ++ src/common/utils.js | 19 +-- src/components/app/App.jsx | 6 +- src/components/app/DocumentTitle.jsx | 2 +- src/components/app/Header.jsx | 6 +- src/components/app/HeaderControls.jsx | 20 ++- src/components/app/HeaderSearch.jsx | 31 +++++ src/components/app/LeftMenu.jsx | 126 +++++++----------- src/components/common/OCLLogo.jsx | 5 +- src/components/map-projects/MapProject.jsx | 14 +- .../map-projects/MapProjectBreadcrumb.jsx | 21 +++ .../map-projects/MapProjectsRedirect.jsx | 18 +++ src/components/users/AddMenuList.jsx | 46 +++++++ src/i18n/locales/en/translations.json | 15 ++- src/i18n/locales/es/translations.json | 15 ++- src/i18n/locales/pt-BR/translations.json | 15 ++- src/i18n/locales/zh/translations.json | 12 +- start-prod.sh | 6 +- webpack.config.js | 2 +- 22 files changed, 270 insertions(+), 129 deletions(-) create mode 100644 src/common/navigation.js create mode 100644 src/components/app/HeaderSearch.jsx create mode 100644 src/components/map-projects/MapProjectBreadcrumb.jsx create mode 100644 src/components/map-projects/MapProjectsRedirect.jsx create mode 100644 src/components/users/AddMenuList.jsx diff --git a/Dockerfile b/Dockerfile index ba941e20..cc9fddf4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,6 @@ FROM node:22 as build ARG NODE_ENV=production ARG NODE_OPTIONS=--max_old_space_size=700 -ENV OCL_ONLINE=${OCL_ONLINE} ENV BRIDGE_MATCH_URL=${BRIDGE_MATCH_URL:-''} ENV NPM_CONFIG_LOGLEVEL warn ENV WEB_PORT=${WEB_PORT:-4004} @@ -16,6 +15,7 @@ ENV OIDC_RP_CLIENT_ID=${OIDC_RP_CLIENT_ID} ENV OIDC_SERVER_URL=${OIDC_SERVER_URL} ENV OIDC_REALM=${OIDC_REALM} ENV OCL_ONLINE_API_URL=${OCL_ONLINE_API_URL} +ENV TERMBROWSER_URL=${TERMBROWSER_URL} ENV SCISPACY_LOINC_API_URL=${SCISPACY_LOINC_API_URL} ENV REPO_SUGGESTIONS=${REPO_SUGGESTIONS} RUN mkdir /app diff --git a/docker-compose.yml b/docker-compose.yml index 514ea2ac..1d7215af 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,9 +19,9 @@ services: - ERRBIT_URL - ERRBIT_KEY - HOTJAR_ID - - OCL_ONLINE=${OCL_ONLINE-false} - BRIDGE_MATCH_URL - AI_ASSISTANT_API_URL - SCISPACY_LOINC_API_URL - OCL_ONLINE_API_URL + - TERMBROWSER_URL=${TERMBROWSER_URL-http://localhost:4002} - REPO_SUGGESTIONS=${REPO_SUGGESTIONS} diff --git a/package.json b/package.json index 5ecc9de9..6e92156e 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,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 OCL_ONLINE=${OCL_ONLINE} --env BRIDGE_MATCH_URL=${BRIDGE_MATCH_URL} --env AI_ASSISTANT_API_URL=${AI_ASSISTANT_API_URL} --env SCISPACY_LOINC_API_URL=${SCISPACY_LOINC_API_URL} --env OCL_ONLINE_API_URL=${OCL_ONLINE_API_URL} --env REPO_SUGGESTIONS=${REPO_SUGGESTIONS} --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 OCL_ONLINE=${OCL_ONLINE} --env BRIDGE_MATCH_URL=${BRIDGE_MATCH_URL} --env AI_ASSISTANT_API_URL=${AI_ASSISTANT_API_URL} --env SCISPACY_LOINC_API_URL=${SCISPACY_LOINC_API_URL} --env OCL_ONLINE_API_URL=${OCL_ONLINE_API_URL} --env REPO_SUGGESTIONS=${REPO_SUGGESTIONS} --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 BRIDGE_MATCH_URL=${BRIDGE_MATCH_URL} --env AI_ASSISTANT_API_URL=${AI_ASSISTANT_API_URL} --env SCISPACY_LOINC_API_URL=${SCISPACY_LOINC_API_URL} --env OCL_ONLINE_API_URL=${OCL_ONLINE_API_URL} --env TERMBROWSER_URL=${TERMBROWSER_URL} --env REPO_SUGGESTIONS=${REPO_SUGGESTIONS} --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 BRIDGE_MATCH_URL=${BRIDGE_MATCH_URL} --env AI_ASSISTANT_API_URL=${AI_ASSISTANT_API_URL} --env SCISPACY_LOINC_API_URL=${SCISPACY_LOINC_API_URL} --env OCL_ONLINE_API_URL=${OCL_ONLINE_API_URL} --env TERMBROWSER_URL=${TERMBROWSER_URL} --env REPO_SUGGESTIONS=${REPO_SUGGESTIONS} --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'", "test:integration": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --import ./test-integration-setup.js --test 'src/**/__integration__/**/*.test.js'", diff --git a/src/common/navigation.js b/src/common/navigation.js new file mode 100644 index 00000000..bf88e7bb --- /dev/null +++ b/src/common/navigation.js @@ -0,0 +1,12 @@ +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} +] diff --git a/src/common/utils.js b/src/common/utils.js index c4a72eab..914070f5 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -1093,20 +1093,23 @@ export const highlightTexts = (items, texts, unmark=false, additionalTexts) => { export const pluralize = (count, singular, plural) => `${count?.toLocaleString()} ${count === 1 ? singular : plural}`; -export const toV3URL = path => { - let url = 'https://app.v3.openconceptlab.org' +export const getV3URL = () => { + const configured = (window.TERMBROWSER_URL || process.env.TERMBROWSER_URL || '').trim().replace(/\/+$/, '') + if(configured) + return configured if(window.location.host?.includes('localhost')) - url = 'http://localhost:4002' - if(['map.dev.openconceptlab.org', 'map.demo.openconceptlab.org'].includes(window.location.host)) - url = 'https://app.v3.qa.openconceptlab.org' - if(window.location.host.match('map.*.openconceptlab.org')) - url = window.location.origin.replace('//map.', '//app.v3.') + return 'http://localhost:4002' + if(/^https:\/\/map\.([a-z]+\.)*openconceptlab\.org$/.test(window.location.origin)) + return window.location.origin.replace('//map.', '//app.v3.') + return 'https://app.v3.openconceptlab.org' +} +export const toV3URL = path => { let referrerParams = `referrer=${window.location.href}` if(isLoggedIn()) referrerParams += '?auth=true' - return `${url}/#${path || '/'}?${referrerParams}` + return `${getV3URL()}/#${path || '/'}?${referrerParams}` } export const isV3URL = url => { diff --git a/src/components/app/App.jsx b/src/components/app/App.jsx index 977140ea..adc65901 100644 --- a/src/components/app/App.jsx +++ b/src/components/app/App.jsx @@ -26,7 +26,7 @@ import OIDLoginCallback from '../users/OIDLoginCallback'; import { OperationsContext } from './LayoutContext'; import Alert from '../common/Alert'; import MapProjectRoute from '../map-projects/MapProjectRoute' -import MapProjects from '../map-projects/MapProjects' +import MapProjectsRedirect from '../map-projects/MapProjectsRedirect' import { useTranslation } from 'react-i18next'; const AuthenticationRequiredRoute = ({component: Component, ...rest}) => { @@ -164,8 +164,8 @@ const App = props => { - - + + diff --git a/src/components/app/DocumentTitle.jsx b/src/components/app/DocumentTitle.jsx index 70ec0dd0..cd351987 100644 --- a/src/components/app/DocumentTitle.jsx +++ b/src/components/app/DocumentTitle.jsx @@ -5,7 +5,7 @@ import { getSiteTitle } from '../../common/utils'; const SITE_TITLE = getSiteTitle() -export const getAppTitle = () => `${getSiteTitle()} Mapper` +export const getAppTitle = () => getSiteTitle() /*eslint no-useless-escape: 0*/ const TITLES = { diff --git a/src/components/app/Header.jsx b/src/components/app/Header.jsx index 2489e1c7..ae761c86 100644 --- a/src/components/app/Header.jsx +++ b/src/components/app/Header.jsx @@ -12,6 +12,7 @@ import { getCurrentUser } from '../../common/utils'; import './Header.scss'; import HeaderControls from './HeaderControls'; import LeftMenu from './LeftMenu' +import HeaderSearch from './HeaderSearch' import AnnouncementBanner from './AnnouncementBanner'; @@ -64,7 +65,10 @@ const Header = props => { } -
+
+
+ {Boolean(user?.url) && } +
diff --git a/src/components/app/HeaderControls.jsx b/src/components/app/HeaderControls.jsx index ce0d1f73..64e43496 100644 --- a/src/components/app/HeaderControls.jsx +++ b/src/components/app/HeaderControls.jsx @@ -1,14 +1,32 @@ import React from 'react'; +import IconButton from '@mui/material/IconButton' +import AddIcon from '@mui/icons-material/Add' import UserMenu from '../users/UserMenu'; import UserProfileButton from '../users/UserProfileButton'; +import AddMenuList from '../users/AddMenuList' +import { isLoggedIn } from '../../common/utils'; const HeaderControls = () => { const [userMenu, setUserMenu] = React.useState(false) + const [addMenuAnchorEl, setAddMenuAnchorEl] = React.useState(null) + + const authenticated = isLoggedIn() + return ( -
+
+ { + authenticated && + setAddMenuAnchorEl(event.currentTarget)}> + + + } setUserMenu(true)} /> setUserMenu(false)} /> + { + authenticated && + setAddMenuAnchorEl(null)} anchorEl={addMenuAnchorEl} /> + }
) } diff --git a/src/components/app/HeaderSearch.jsx b/src/components/app/HeaderSearch.jsx new file mode 100644 index 00000000..c690b457 --- /dev/null +++ b/src/components/app/HeaderSearch.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next' +import InputAdornment from '@mui/material/InputAdornment'; +import TextField from '@mui/material/TextField'; +import SearchIcon from '@mui/icons-material/Search'; +import { getV3URL } from '../../common/utils'; + +const HeaderSearch = () => { + const { t } = useTranslation() + const [value, setValue] = React.useState('') + + const onKeyDown = event => { + const query = value.trim() + if(event.key === 'Enter' && query) + window.location.assign(`${getV3URL()}/search/?q=${encodeURIComponent(query)}&type=concepts`) + } + + return ( + setValue(event.target.value)} + onKeyDown={onKeyDown} + InputProps={{startAdornment: , sx: {borderRadius: '100px'}}} + /> + ) +} + +export default HeaderSearch diff --git a/src/components/app/LeftMenu.jsx b/src/components/app/LeftMenu.jsx index 56d0120f..5fe32a24 100644 --- a/src/components/app/LeftMenu.jsx +++ b/src/components/app/LeftMenu.jsx @@ -17,9 +17,16 @@ import { getCurrentUser, refreshCurrentUserCache, getCurrentUserOrgs, toV3URL } import Drawer from '../common/Drawer'; import OrgIcon from '../orgs/OrgIcon'; import EntityIcon from '../common/EntityIcon' -import MapperQuotaChip from '../map-projects/MapperQuotaChip' +import { trayItems } from '../../common/navigation' +import Button from '@mui/material/Button'; +const TRAY_ICONS = { + dashboard: selected => , + repos: selected => , + 'map-projects': selected => +} + const DrawerHeader = styled('div')(({ theme }) => ({ display: 'flex', alignItems: 'center', @@ -60,88 +67,53 @@ const LeftMenu = ({ isOpen, onClose }) => { - - - - - - - - - - - - - - - {((user?.collections || 0) + (user?.sources + 0)).toLocaleString()} - - - - - - - - - - - + { + trayItems(user, true).map(item => { + const selected = location.pathname === item.path + return ( + + + + {TRAY_ICONS[item.key](selected)} + + + { + item.key === 'repos' && + {((user?.collections || 0) + (user?.sources + 0)).toLocaleString()} + } + + + ) + }) + } + {t('common.create')} + + } > { return ( - - + + diff --git a/src/components/map-projects/MapProject.jsx b/src/components/map-projects/MapProject.jsx index 61c8a1e8..4ec0ce7e 100644 --- a/src/components/map-projects/MapProject.jsx +++ b/src/components/map-projects/MapProject.jsx @@ -114,6 +114,7 @@ import PreviewLimitDialog from './PreviewLimitDialog' import QuotaDialog from '../common/QuotaDialog' import { getQuotaError } from '../common/quotaErrors' import MapperQuotaChip from './MapperQuotaChip' +import MapProjectBreadcrumb from './MapProjectBreadcrumb' import { getPreviewLimitError, isAIPreviewLimitError } from './previewLimits' import { formatRowNumbers, rowNumberOf, runMatchBatch, requestSingleMatch, runWithConcurrency, INTERACTIVE_WAIT_CAP_MS } from './matchBatch' import { getAIAssistantChoices, getModelUsed, getPromptTemplateKey, createLatestRequestGate } from './aiVisibility' @@ -779,13 +780,6 @@ const MapProject = () => { setColumns(prev => resuggestColumns(prev, headers)) }, [headersKey]) - React.useEffect(() => { - if(!isEmpty(decisions)) { - window.addEventListener("beforeunload", alertUser); - return () => window.removeEventListener("beforeunload", alertUser); - } - }, [decisions]); - React.useEffect(() => { fetchMapTypes() fetchOclLocales() @@ -1049,11 +1043,6 @@ const MapProject = () => { setOclLocales(sorted) }) - const alertUser = (e) => { - e.preventDefault(); - e.returnValue = ""; - }; - const rowIndex = row?.__index const getColumns = row => { @@ -6278,6 +6267,7 @@ const MapProject = () => { { !isConfigFormInline && + { name && diff --git a/src/components/map-projects/MapProjectBreadcrumb.jsx b/src/components/map-projects/MapProjectBreadcrumb.jsx new file mode 100644 index 00000000..eefbd6b4 --- /dev/null +++ b/src/components/map-projects/MapProjectBreadcrumb.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Link from '@mui/material/Link'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import { toV3URL } from '../../common/utils'; + +const MapProjectBreadcrumb = ({ ownerType, owner }) => { + const { t } = useTranslation() + if(!ownerType || !owner) + return null + const ownerURL = `/${ownerType}/${owner}/` + return ( + + {owner} + + {t('map_project.mapping_projects')} + + ) +} + +export default MapProjectBreadcrumb diff --git a/src/components/map-projects/MapProjectsRedirect.jsx b/src/components/map-projects/MapProjectsRedirect.jsx new file mode 100644 index 00000000..700bb82c --- /dev/null +++ b/src/components/map-projects/MapProjectsRedirect.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { getCurrentUser, toV3URL } from '../../common/utils'; +import MapProjects from './MapProjects'; + +const MapProjectsRedirect = props => { + const user = getCurrentUser() + const blockedNewProject = Boolean(props.location?.state?.newProjectBlock) + const redirect = Boolean(user?.url) && !blockedNewProject + + React.useEffect(() => { + if(redirect) + window.location.replace(toV3URL(`${user.url}map-projects`)) + }, [redirect]) + + return redirect ? null : +} + +export default MapProjectsRedirect diff --git a/src/components/users/AddMenuList.jsx b/src/components/users/AddMenuList.jsx new file mode 100644 index 00000000..8d0ee2ac --- /dev/null +++ b/src/components/users/AddMenuList.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next' +import { Menu, ListItemButton, ListItemText, ListItemIcon, Divider} from '@mui/material' +import ImportIcon from '@mui/icons-material/Publish'; +import { getCurrentUser, toV3URL } from '../../common/utils'; +import { createItems } from '../../common/navigation'; +import OrgIcon from '../orgs/OrgIcon' +import RepoIcon from '../repos/RepoIcon' + +const ITEM_SX = {padding: '8px 12px', '&:hover': {color: 'inherit'}, '&:focus': {outline: 'none', textDecoration: 'none', color: 'inherit'}} + +const CREATE_ITEM_IDS = {org: 'addOrg', repo: 'addRepo', 'map-project': 'addMapProject', import: 'bulkImport'} +const CREATE_ITEM_LABELS = {org: 'org.org', repo: 'repo.repo', 'map-project': 'map_project.mapping_project', import: 'import.bulk_import'} +const CREATE_ITEM_ICONS = { + org: , + repo: , + 'map-project': , + import: +} + +const AddMenuList = ({ anchorEl, open, onClose }) => { + const { t } = useTranslation() + const user = getCurrentUser() + return ( + + { + createItems(user, true).flatMap(item => [ + item.divider && , + + + {CREATE_ITEM_ICONS[item.key]} + + + + ]) + } + + ) +} +export default AddMenuList; diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index 39068698..191b92b6 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -62,7 +62,8 @@ "advance_settings": "Advanced Settings", "remove": "Remove", "at": "at", - "none": "None" + "none": "None", + "create": "Create" }, "errors": { "401": "Login is required", @@ -175,7 +176,8 @@ "org": { "my": "My Organizations", "orgs": "Organizations", - "members": "Members" + "members": "Members", + "org": "Organization" }, "user": { "my_profile": "My Profile", @@ -196,7 +198,8 @@ "no_match_metrics": "No algorithm match metrics available for this concept.", "list_view": "List View", "table_view": "Table View", - "select_all_rows": "Select all rows" + "select_all_rows": "Select all rows", + "input_placeholder": "Search OCL..." }, "url_registry": { "global_registry": "Global registry" @@ -552,7 +555,8 @@ "ai_recommendation_failed_retry": "OCL AI Assistant recommendation failed — click to retry", "ai_recommendation_running": "OCL AI Assistant recommendation running", "repo_url": "Repo URL", - "repo_version": "Repo Version" + "repo_version": "Repo Version", + "mapping_project": "Mapping project" }, "app": { "web_version": "Web Version", @@ -593,5 +597,8 @@ "import": "Rows past your limit stay in the project but aren't matched." } } + }, + "import": { + "bulk_import": "Bulk Import" } } diff --git a/src/i18n/locales/es/translations.json b/src/i18n/locales/es/translations.json index d8cbc4db..e46e7abb 100644 --- a/src/i18n/locales/es/translations.json +++ b/src/i18n/locales/es/translations.json @@ -62,7 +62,8 @@ "checking_auth": "Verificando autenticación...", "remove": "Eliminar", "at": "a las", - "none": "Ninguno" + "none": "Ninguno", + "create": "Crear" }, "errors": { "401": "Se requiere iniciar sesión", @@ -166,7 +167,8 @@ "org": { "orgs": "Organizaciones", "my": "Mis organizaciones", - "members": "Miembros" + "members": "Miembros", + "org": "Organización" }, "user": { "my_profile": "Mi perfil", @@ -187,7 +189,8 @@ "no_match_metrics": "No hay métricas de coincidencia de algoritmos disponibles para este concepto.", "list_view": "Vista de lista", "table_view": "Vista de tabla", - "select_all_rows": "Seleccionar todas las filas" + "select_all_rows": "Seleccionar todas las filas", + "input_placeholder": "Buscar en OCL..." }, "locale": { "add_custom_language_codes": "Agregar código(s) de idioma personalizado(s)", @@ -540,7 +543,8 @@ "ai_recommendation_failed_retry": "Error en la recomendación del asistente de IA de OCL — haga clic para intentar de nuevo", "ai_recommendation_running": "Recomendación del asistente de IA de OCL en curso", "repo_url": "URL del repositorio", - "repo_version": "Versión del repositorio" + "repo_version": "Versión del repositorio", + "mapping_project": "Proyecto de mapeo" }, "app": { "web_version": "Versión Web", @@ -593,5 +597,8 @@ "import": "Las filas que superan su límite permanecen en el proyecto pero no se emparejan." } } + }, + "import": { + "bulk_import": "Importación masiva" } } diff --git a/src/i18n/locales/pt-BR/translations.json b/src/i18n/locales/pt-BR/translations.json index 3540ef24..e154cd35 100644 --- a/src/i18n/locales/pt-BR/translations.json +++ b/src/i18n/locales/pt-BR/translations.json @@ -62,7 +62,8 @@ "advance_settings": "Configurações avançadas", "remove": "Remover", "at": "em", - "none": "Nenhum" + "none": "Nenhum", + "create": "Criar" }, "errors": { "401": "Login é necessário", @@ -175,7 +176,8 @@ "org": { "my": "Minhas Organizações", "orgs": "Organizações", - "members": "Membros" + "members": "Membros", + "org": "Organização" }, "user": { "my_profile": "Meu Perfil", @@ -196,7 +198,8 @@ "no_match_metrics": "Nenhuma métrica de correspondência de algoritmo disponível para este conceito.", "list_view": "Visualização em Lista", "table_view": "Visualização em Tabela", - "select_all_rows": "Selecionar todas as linhas" + "select_all_rows": "Selecionar todas as linhas", + "input_placeholder": "Buscar no OCL..." }, "url_registry": { "global_registry": "Registro Global" @@ -552,7 +555,8 @@ "ai_recommendation_failed_retry": "Recomendação do Assistente de IA do OCL falhou — clique para tentar novamente", "ai_recommendation_running": "Recomendação do Assistente de IA do OCL em execução", "repo_url": "URL do Repo", - "repo_version": "Versão do Repo" + "repo_version": "Versão do Repo", + "mapping_project": "Projeto de mapeamento" }, "app": { "web_version": "Versão Web", @@ -593,5 +597,8 @@ "import": "Linhas além do seu limite permanecem no projeto mas não são correspondidas." } } + }, + "import": { + "bulk_import": "Importação em massa" } } diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json index 3852a6c6..809378bd 100644 --- a/src/i18n/locales/zh/translations.json +++ b/src/i18n/locales/zh/translations.json @@ -62,7 +62,8 @@ "action": "操作", "remove": "移除", "at": "于", - "none": "无" + "none": "无", + "create": "创建" }, "errors": { "401": "需要登录", @@ -175,7 +176,8 @@ "org": { "my": "我的组织机构", "orgs": "组织机构", - "members": "成员" + "members": "成员", + "org": "组织机构" }, "user": { "my_profile": "我的个人资料", @@ -196,7 +198,8 @@ "no_match_metrics": "此概念没有可用的算法匹配指标。", "list_view": "列表视图", "table_view": "表格视图", - "select_all_rows": "选择全部行" + "select_all_rows": "选择全部行", + "input_placeholder": "搜索 OCL..." }, "url_registry": { "global_registry": "全球注册库" @@ -551,7 +554,8 @@ "ai_recommendation_failed_retry": "OCL AI 助手推荐失败 — 点击重试", "ai_recommendation_running": "OCL AI 助手推荐进行中", "repo_url": "仓库 URL", - "repo_version": "仓库版本" + "repo_version": "仓库版本", + "mapping_project": "映射项目" }, "app": { "web_version": "Web 版本", diff --git a/start-prod.sh b/start-prod.sh index 4abcc950..3de99246 100755 --- a/start-prod.sh +++ b/start-prod.sh @@ -38,9 +38,6 @@ fi if [[ ! -z "${OIDC_REALM}" ]]; then echo "var OIDC_REALM = \"${OIDC_REALM}\";" >> ${ENV_FILE} fi -if [[ ! -z "${OCL_ONLINE}" ]]; then - echo "var OCL_ONLINE = \"${OCL_ONLINE}\";" >> ${ENV_FILE} -fi if [[ ! -z "${BRIDGE_MATCH_URL}" ]]; then echo "var BRIDGE_MATCH_URL = \"${BRIDGE_MATCH_URL}\";" >> ${ENV_FILE} fi @@ -50,6 +47,9 @@ fi if [[ ! -z "${SCISPACY_LOINC_API_URL}" ]]; then echo "var SCISPACY_LOINC_API_URL = \"${SCISPACY_LOINC_API_URL}\";" >> ${ENV_FILE} fi +if [[ ! -z "${TERMBROWSER_URL}" ]]; then + echo "var TERMBROWSER_URL = \"${TERMBROWSER_URL}\";" >> ${ENV_FILE} +fi if [[ ! -z "${OCL_ONLINE_API_URL}" ]]; then echo "var OCL_ONLINE_API_URL = \"${OCL_ONLINE_API_URL}\";" >> ${ENV_FILE} fi diff --git a/webpack.config.js b/webpack.config.js index 3d6f11b1..f85cdf93 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -149,10 +149,10 @@ module.exports = (env) => { 'process.env.OIDC_RP_CLIENT_ID': JSON.stringify(env.OIDC_RP_CLIENT_ID), 'process.env.OIDC_SERVER_URL': JSON.stringify(env.OIDC_SERVER_URL || ''), 'process.env.OIDC_REALM': JSON.stringify(env.OIDC_REALM || ''), - 'process.env.OCL_ONLINE': JSON.stringify(env.OCL_ONLINE) || 'false', 'process.env.BRIDGE_MATCH_URL': JSON.stringify(env.BRIDGE_MATCH_URL || '') || '', 'process.env.AI_ASSISTANT_API_URL': JSON.stringify(env.AI_ASSISTANT_API_URL || '') || '', 'process.env.SCISPACY_LOINC_API_URL': JSON.stringify(env.SCISPACY_LOINC_API_URL || '') || '', + 'process.env.TERMBROWSER_URL': JSON.stringify(env.TERMBROWSER_URL || ''), 'process.env.OCL_ONLINE_API_URL': JSON.stringify(env.OCL_ONLINE_API_URL || '') || '', 'process.env.REPO_SUGGESTIONS': JSON.stringify(env.REPO_SUGGESTIONS || '') }), From 9cca2abf52cfa2e7663395cc7876787a34faa5ef Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Fri, 30 Oct 2026 10:57:06 +0530 Subject: [PATCH 02/14] OpenConceptLab/ocl_online#418 | Review fixes: organization projects, counts, selection, configurable URLs, tests --- src/common/__tests__/appUrls.test.js | 23 +++++++++++++ src/common/__tests__/mapProjects.test.js | 9 ++++++ src/common/__tests__/navigation.test.js | 41 ++++++++++++++++++++++++ src/common/appUrls.js | 14 ++++++++ src/common/mapProjects.js | 4 +++ src/common/navigation.js | 9 ++++++ src/common/utils.js | 12 ++----- src/components/app/LeftMenu.jsx | 25 +++++++++++++-- 8 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 src/common/__tests__/appUrls.test.js create mode 100644 src/common/__tests__/mapProjects.test.js create mode 100644 src/common/__tests__/navigation.test.js create mode 100644 src/common/appUrls.js create mode 100644 src/common/mapProjects.js diff --git a/src/common/__tests__/appUrls.test.js b/src/common/__tests__/appUrls.test.js new file mode 100644 index 00000000..e1a42448 --- /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 { resolveV3URL, PRODUCTION_TERMBROWSER_URL } = await import('../appUrls.js') + +test('configured URL wins and loses its trailing slash', () => { + assert.equal(resolveV3URL('https://tb.example.org/', 'https://map.qa.openconceptlab.org', 'map.qa.openconceptlab.org'), 'https://tb.example.org') +}) + +test('localhost points to the local TermBrowser', () => { + assert.equal(resolveV3URL('', 'http://localhost:4004', 'localhost:4004'), 'http://localhost:4002') +}) + +test('official Mapper hosts map to the matching TermBrowser environment', () => { + assert.equal(resolveV3URL('', 'https://map.openconceptlab.org', 'map.openconceptlab.org'), 'https://app.v3.openconceptlab.org') + assert.equal(resolveV3URL('', 'https://map.qa.openconceptlab.org', 'map.qa.openconceptlab.org'), 'https://app.v3.qa.openconceptlab.org') + assert.equal(resolveV3URL(undefined, 'https://map.staging.who.openconceptlab.org', 'map.staging.who.openconceptlab.org'), 'https://app.v3.staging.who.openconceptlab.org') +}) + +test('custom hosts without configuration point to production', () => { + assert.equal(resolveV3URL('', 'https://mapper.example.org', 'mapper.example.org'), PRODUCTION_TERMBROWSER_URL) + assert.equal(resolveV3URL('', undefined, undefined), PRODUCTION_TERMBROWSER_URL) +}) diff --git a/src/common/__tests__/mapProjects.test.js b/src/common/__tests__/mapProjects.test.js new file mode 100644 index 00000000..f15897ff --- /dev/null +++ b/src/common/__tests__/mapProjects.test.js @@ -0,0 +1,9 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +const { projectCount } = await import('../mapProjects.js') + +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) +}) diff --git a/src/common/__tests__/navigation.test.js b/src/common/__tests__/navigation.test.js new file mode 100644 index 00000000..7b5fd72e --- /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 00000000..4ffedd67 --- /dev/null +++ b/src/common/appUrls.js @@ -0,0 +1,14 @@ +export const PRODUCTION_TERMBROWSER_URL = 'https://app.v3.openconceptlab.org' + +const OFFICIAL_MAPPER_ORIGIN = /^https:\/\/map\.([a-z]+\.)*openconceptlab\.org$/ + +export const resolveV3URL = (configured, origin, host) => { + const url = (configured || '').trim().replace(/\/+$/, '') + if(url) + return url + if(host?.includes('localhost')) + return 'http://localhost:4002' + if(OFFICIAL_MAPPER_ORIGIN.test(origin || '')) + return origin.replace('//map.', '//app.v3.') + return PRODUCTION_TERMBROWSER_URL +} diff --git a/src/common/mapProjects.js b/src/common/mapProjects.js new file mode 100644 index 00000000..822cd1ee --- /dev/null +++ b/src/common/mapProjects.js @@ -0,0 +1,4 @@ +export const projectCount = (...responses) => responses.reduce( + (total, response) => total + (parseInt(response?.headers?.num_found) || 0), + 0 +) diff --git a/src/common/navigation.js b/src/common/navigation.js index bf88e7bb..9eec4cac 100644 --- a/src/common/navigation.js +++ b/src/common/navigation.js @@ -10,3 +10,12 @@ export const createItems = (user, hasMapper) => [ ...(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/utils.js b/src/common/utils.js index 914070f5..e63d6781 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -18,6 +18,7 @@ import GAService from '../services/GAService'; import { ensureIdToken, prepareSignOut, settleForcedSignOut, renewSessionSilently } from './ssoSession'; import { SERVER_CONFIGS } from './serverConfigs'; import { legacyHashRoute, isSameSitePath } from './history'; +import { resolveV3URL } from './appUrls'; export const currentPath = () => window.location.pathname; @@ -1093,16 +1094,7 @@ export const highlightTexts = (items, texts, unmark=false, additionalTexts) => { export const pluralize = (count, singular, plural) => `${count?.toLocaleString()} ${count === 1 ? singular : plural}`; -export const getV3URL = () => { - const configured = (window.TERMBROWSER_URL || process.env.TERMBROWSER_URL || '').trim().replace(/\/+$/, '') - if(configured) - return configured - if(window.location.host?.includes('localhost')) - return 'http://localhost:4002' - if(/^https:\/\/map\.([a-z]+\.)*openconceptlab\.org$/.test(window.location.origin)) - return window.location.origin.replace('//map.', '//app.v3.') - return 'https://app.v3.openconceptlab.org' -} +export const getV3URL = () => resolveV3URL(window.TERMBROWSER_URL || process.env.TERMBROWSER_URL, window.location.origin, window.location.host) export const toV3URL = path => { let referrerParams = `referrer=${window.location.href}` diff --git a/src/components/app/LeftMenu.jsx b/src/components/app/LeftMenu.jsx index 5fe32a24..daf5e7aa 100644 --- a/src/components/app/LeftMenu.jsx +++ b/src/components/app/LeftMenu.jsx @@ -17,7 +17,9 @@ import { getCurrentUser, refreshCurrentUserCache, getCurrentUserOrgs, toV3URL } import Drawer from '../common/Drawer'; import OrgIcon from '../orgs/OrgIcon'; import EntityIcon from '../common/EntityIcon' -import { trayItems } from '../../common/navigation' +import { trayItems, isTrayItemSelected } from '../../common/navigation' +import { projectCount } from '../../common/mapProjects' +import APIService from '../../services/APIService' import Button from '@mui/material/Button'; @@ -43,6 +45,7 @@ const LeftMenu = ({ isOpen, onClose }) => { const formatFollowing = following => map(following, 'object') || [] + const [mapProjectCount, setMapProjectCount] = React.useState(null) const [orgs, setOrgs] = React.useState(getCurrentUserOrgs()) const [following, setFollowing] = React.useState(formatFollowing(user?.following)) @@ -59,6 +62,20 @@ const LeftMenu = ({ isOpen, onClose }) => { fetchData() }, [isOpen && user?.url]) + React.useEffect(() => { + if(!isOpen || !user?.username) + return + let active = true + Promise.all([ + APIService.users(user.username).appendToUrl('map-projects/').get(null, null, {limit: 1}, true), + APIService.users(user.username).appendToUrl('orgs/map-projects/').get(null, null, {limit: 1}, true), + ]).then(responses => { + if(active) + setMapProjectCount(projectCount(...responses)) + }).catch(() => {}) + return () => { active = false } + }, [isOpen && user?.username]) + return ( @@ -69,7 +86,7 @@ const LeftMenu = ({ isOpen, onClose }) => { { trayItems(user, true).map(item => { - const selected = location.pathname === item.path + const selected = isTrayItemSelected(item, location.pathname) return ( { item.key === 'repos' && {((user?.collections || 0) + (user?.sources + 0)).toLocaleString()} } + { + item.key === 'map-projects' && mapProjectCount !== null && + {mapProjectCount.toLocaleString()} + } ) From 538a4f668d8311bc83f978b6bdf64d782d197e3c Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Fri, 30 Oct 2026 11:12:38 +0530 Subject: [PATCH 03/14] OpenConceptLab/ocl_online#418 | Hide sibling-app navigation unless configured or on a recognized OCL host --- src/common/__tests__/appUrls.test.js | 10 +++++++++- src/common/appUrls.js | 6 ++++-- src/common/utils.js | 4 +++- src/components/app/Header.jsx | 4 ++-- src/components/app/LeftMenu.jsx | 12 +++++++++--- src/components/common/OCLLogo.jsx | 4 ++-- src/components/map-projects/MapProjectsRedirect.jsx | 4 ++-- src/components/users/AddMenuList.jsx | 4 ++-- 8 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/common/__tests__/appUrls.test.js b/src/common/__tests__/appUrls.test.js index e1a42448..7ed6353f 100644 --- a/src/common/__tests__/appUrls.test.js +++ b/src/common/__tests__/appUrls.test.js @@ -1,7 +1,7 @@ import test from 'node:test' import assert from 'node:assert/strict' -const { resolveV3URL, PRODUCTION_TERMBROWSER_URL } = await import('../appUrls.js') +const { resolveV3URL, resolveIntegratedV3URL, PRODUCTION_TERMBROWSER_URL } = await import('../appUrls.js') test('configured URL wins and loses its trailing slash', () => { assert.equal(resolveV3URL('https://tb.example.org/', 'https://map.qa.openconceptlab.org', 'map.qa.openconceptlab.org'), 'https://tb.example.org') @@ -21,3 +21,11 @@ test('custom hosts without configuration point to production', () => { assert.equal(resolveV3URL('', 'https://mapper.example.org', 'mapper.example.org'), PRODUCTION_TERMBROWSER_URL) assert.equal(resolveV3URL('', undefined, undefined), PRODUCTION_TERMBROWSER_URL) }) + +test('an integrated TermBrowser needs configuration, localhost or an official Mapper host', () => { + assert.equal(resolveIntegratedV3URL('https://tb.example.org/', 'https://mapper.example.org', 'mapper.example.org'), 'https://tb.example.org') + assert.equal(resolveIntegratedV3URL('', 'http://localhost:4004', 'localhost:4004'), 'http://localhost:4002') + assert.equal(resolveIntegratedV3URL('', 'https://map.qa.openconceptlab.org', 'map.qa.openconceptlab.org'), 'https://app.v3.qa.openconceptlab.org') + assert.equal(resolveIntegratedV3URL('', 'https://mapper.example.org', 'mapper.example.org'), '') + assert.equal(resolveIntegratedV3URL('', undefined, undefined), '') +}) diff --git a/src/common/appUrls.js b/src/common/appUrls.js index 4ffedd67..d3add84f 100644 --- a/src/common/appUrls.js +++ b/src/common/appUrls.js @@ -2,7 +2,7 @@ export const PRODUCTION_TERMBROWSER_URL = 'https://app.v3.openconceptlab.org' const OFFICIAL_MAPPER_ORIGIN = /^https:\/\/map\.([a-z]+\.)*openconceptlab\.org$/ -export const resolveV3URL = (configured, origin, host) => { +export const resolveIntegratedV3URL = (configured, origin, host) => { const url = (configured || '').trim().replace(/\/+$/, '') if(url) return url @@ -10,5 +10,7 @@ export const resolveV3URL = (configured, origin, host) => { return 'http://localhost:4002' if(OFFICIAL_MAPPER_ORIGIN.test(origin || '')) return origin.replace('//map.', '//app.v3.') - return PRODUCTION_TERMBROWSER_URL + return '' } + +export const resolveV3URL = (configured, origin, host) => resolveIntegratedV3URL(configured, origin, host) || PRODUCTION_TERMBROWSER_URL diff --git a/src/common/utils.js b/src/common/utils.js index e63d6781..db615898 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -18,7 +18,7 @@ import GAService from '../services/GAService'; import { ensureIdToken, prepareSignOut, settleForcedSignOut, renewSessionSilently } from './ssoSession'; import { SERVER_CONFIGS } from './serverConfigs'; import { legacyHashRoute, isSameSitePath } from './history'; -import { resolveV3URL } from './appUrls'; +import { resolveV3URL, resolveIntegratedV3URL } from './appUrls'; export const currentPath = () => window.location.pathname; @@ -1096,6 +1096,8 @@ export const pluralize = (count, singular, plural) => `${count?.toLocaleString() export const getV3URL = () => resolveV3URL(window.TERMBROWSER_URL || process.env.TERMBROWSER_URL, window.location.origin, window.location.host) +export const hasTermBrowser = () => Boolean(resolveIntegratedV3URL(window.TERMBROWSER_URL || process.env.TERMBROWSER_URL, window.location.origin, window.location.host)) + export const toV3URL = path => { let referrerParams = `referrer=${window.location.href}` if(isLoggedIn()) diff --git a/src/components/app/Header.jsx b/src/components/app/Header.jsx index ae761c86..e6bcd030 100644 --- a/src/components/app/Header.jsx +++ b/src/components/app/Header.jsx @@ -8,7 +8,7 @@ import CssBaseline from '@mui/material/CssBaseline'; import MenuIcon from '@mui/icons-material/Menu'; import { COLORS } from '../../common/colors'; import OCLLogo from '../common/OCLLogo'; -import { getCurrentUser } from '../../common/utils'; +import { getCurrentUser, hasTermBrowser } from '../../common/utils'; import './Header.scss'; import HeaderControls from './HeaderControls'; import LeftMenu from './LeftMenu' @@ -67,7 +67,7 @@ const Header = props => {
- {Boolean(user?.url) && } + {Boolean(user?.url) && hasTermBrowser() && }
diff --git a/src/components/app/LeftMenu.jsx b/src/components/app/LeftMenu.jsx index daf5e7aa..dd57f45e 100644 --- a/src/components/app/LeftMenu.jsx +++ b/src/components/app/LeftMenu.jsx @@ -13,7 +13,7 @@ import FolderOpenIcon from '@mui/icons-material/FolderOutlined'; import Divider from '@mui/material/Divider'; import map from 'lodash/map' import { PRIMARY_COLORS } from '../../common/colors' -import { getCurrentUser, refreshCurrentUserCache, getCurrentUserOrgs, toV3URL } from '../../common/utils'; +import { getCurrentUser, refreshCurrentUserCache, getCurrentUserOrgs, hasTermBrowser, toV3URL } from '../../common/utils'; import Drawer from '../common/Drawer'; import OrgIcon from '../orgs/OrgIcon'; import EntityIcon from '../common/EntityIcon' @@ -42,6 +42,7 @@ const LeftMenu = ({ isOpen, onClose }) => { const { t } = useTranslation() const location = useLocation() const user = getCurrentUser() + const integrated = hasTermBrowser() const formatFollowing = following => map(following, 'object') || [] @@ -85,13 +86,14 @@ const LeftMenu = ({ isOpen, onClose }) => { { - trayItems(user, true).map(item => { + trayItems(user, true).filter(item => integrated || item.key === 'map-projects').map(tray => { + const item = integrated ? tray : {...tray, path: '/map-projects'} const selected = isTrayItemSelected(item, location.pathname) return ( { }) } + {integrated && ( + <> { )) } + + )} ) } diff --git a/src/components/common/OCLLogo.jsx b/src/components/common/OCLLogo.jsx index 4260f9cd..8935e339 100644 --- a/src/components/common/OCLLogo.jsx +++ b/src/components/common/OCLLogo.jsx @@ -1,11 +1,11 @@ import React from 'react'; import SvgIcon from '@mui/material/SvgIcon'; import { PRIMARY_COLORS } from '../../common/colors' -import { toV3URL } from '../../common/utils' +import { hasTermBrowser, toV3URL } from '../../common/utils' const OCLLogo = ({ color, width, height, }) => { return ( -
+ diff --git a/src/components/map-projects/MapProjectsRedirect.jsx b/src/components/map-projects/MapProjectsRedirect.jsx index 700bb82c..13d700bc 100644 --- a/src/components/map-projects/MapProjectsRedirect.jsx +++ b/src/components/map-projects/MapProjectsRedirect.jsx @@ -1,11 +1,11 @@ import React from 'react'; -import { getCurrentUser, toV3URL } from '../../common/utils'; +import { getCurrentUser, hasTermBrowser, toV3URL } from '../../common/utils'; import MapProjects from './MapProjects'; const MapProjectsRedirect = props => { const user = getCurrentUser() const blockedNewProject = Boolean(props.location?.state?.newProjectBlock) - const redirect = Boolean(user?.url) && !blockedNewProject + const redirect = Boolean(user?.url) && !blockedNewProject && hasTermBrowser() React.useEffect(() => { if(redirect) diff --git a/src/components/users/AddMenuList.jsx b/src/components/users/AddMenuList.jsx index 8d0ee2ac..62759ee5 100644 --- a/src/components/users/AddMenuList.jsx +++ b/src/components/users/AddMenuList.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next' import { Menu, ListItemButton, ListItemText, ListItemIcon, Divider} from '@mui/material' import ImportIcon from '@mui/icons-material/Publish'; -import { getCurrentUser, toV3URL } from '../../common/utils'; +import { getCurrentUser, hasTermBrowser, toV3URL } from '../../common/utils'; import { createItems } from '../../common/navigation'; import OrgIcon from '../orgs/OrgIcon' import RepoIcon from '../repos/RepoIcon' @@ -30,7 +30,7 @@ const AddMenuList = ({ anchorEl, open, onClose }) => { sx={{'.MuiPaper-root': {backgroundColor: 'surface.n94', minWidth: '200px'}}} > { - createItems(user, true).flatMap(item => [ + createItems(user, true).filter(item => hasTermBrowser() || item.app !== 'v3').flatMap(item => [ item.divider && , From db31c5fede873d45bc24125c95a13930b8ea9c27 Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Fri, 30 Oct 2026 11:28:01 +0530 Subject: [PATCH 04/14] OpenConceptLab/ocl_online#418 | Keep project breadcrumb local without a configured TermBrowser --- package-lock.json | 142 +++++++++--------- src/common/__tests__/breadcrumb.test.js | 20 +++ src/common/breadcrumb.js | 4 + .../map-projects/MapProjectBreadcrumb.jsx | 9 +- 4 files changed, 102 insertions(+), 73 deletions(-) create mode 100644 src/common/__tests__/breadcrumb.test.js create mode 100644 src/common/breadcrumb.js diff --git a/package-lock.json b/package-lock.json index 9b8b2f2c..cd4515cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ocl-map", - "version": "1.0.37-beta", + "version": "1.0.43-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ocl-map", - "version": "1.0.37-beta", + "version": "1.0.43-beta", "dependencies": { "@airbrake/browser": "^2.1.9", "@emotion/react": "^11.14.0", @@ -159,13 +159,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.12.tgz", + "integrity": "sha512-2W76mhjrpQFO1lK5e8IvGYB2D0YZ7S2R1Z2BjO2ZUYuW+amcpK/z60U6ZZmzskwoa7sJ6XohP/Wg8H5KxXI/IA==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", + "@babel/parser": "^7.29.12", + "@babel/types": "^7.29.11", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -205,9 +205,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", - "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.12.tgz", + "integrity": "sha512-ACxGoWMV866q/QlZHeCOaQZAR5QclKsIvkAmlDemqjm6+6Bynijs1mH068BzH2N2e+wSuDmBWY87fqKfvhi1Lw==", "dev": true, "license": "MIT", "dependencies": { @@ -216,7 +216,7 @@ "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", - "@babel/traverse": "^7.29.7", + "@babel/traverse": "^7.29.12", "semver": "^6.3.1" }, "engines": { @@ -446,9 +446,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", - "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", + "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.11" @@ -760,9 +760,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.11.tgz", - "integrity": "sha512-iARF4jjHSM0v8DImCACPXrwEltBP3DgqDpXTvjAMBAVpyEY0LiGss78wvI+KtmW+IJNEvZ5sOhPDCsOsXsIybA==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.12.tgz", + "integrity": "sha512-nEreZBofjfd6FG621xe9Rk3+tMGBXf1nAuFrR6NUGF67fkQbSQf3NrbRuhgWRn+zb0qcy9XVok1GVFI/t9wWVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1783,17 +1783,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", - "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.12.tgz", + "integrity": "sha512-MscXe/NAZcOkYEsRajHZFt3ynsb22PUi6CTrp5U7+RYycR9By84mn9mFN54YgfITiqK4hFx3Ju3tZSyzfpS5ew==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", + "@babel/generator": "^7.29.12", "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.9", + "@babel/parser": "^7.29.12", "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", + "@babel/types": "^7.29.11", "debug": "^4.3.1" }, "engines": { @@ -3680,9 +3680,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -3790,9 +3790,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", - "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "version": "26.6.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz", + "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", "license": "MIT", "dependencies": { "undici-types": "~8.9.0" @@ -4684,9 +4684,9 @@ } }, "node_modules/axios": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", - "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.21.0.tgz", + "integrity": "sha512-YCzXMB53Q9PZBsh7yN7wtapit4WepwcHS2fP8QhGsBxSz5YtgYobV7GGlWlAJi+RxfU77GQTQi2DSLUma42EYA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", @@ -4867,9 +4867,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.33", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.33.tgz", - "integrity": "sha512-yiVKDX+qCMRKBE084lXvq9EcwFUhKZ2nU8TK1EQQuOgPNR/lmbGxXOeDdlGUqC2Zz11Cs9rk1d/8NfgBmlkt0w==", + "version": "2.11.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.34.tgz", + "integrity": "sha512-Aqr8mA1kIvyJ3UY5YAY7ib8LSo1DmDMbMnVqGPtsKW1LxaFQFwn9Eb3yvq2fUKJsRvaXjnAV7R5osqETapzNiA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5007,9 +5007,9 @@ } }, "node_modules/browserslist": { - "version": "4.29.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", - "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", "dev": true, "funding": [ { @@ -5027,10 +5027,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.29", - "caniuse-lite": "^1.0.30001816", - "electron-to-chromium": "^1.5.459", - "node-releases": "^2.0.59", + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", "update-browserslist-db": "^1.3.4" }, "bin": { @@ -5221,16 +5221,16 @@ } }, "node_modules/caniuse-db": { - "version": "1.0.30001819", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30001819.tgz", - "integrity": "sha512-mLpSoyGOxULIZqnuoPKJl/Z8CCpgmaRotGeoGCJawSf649JqQ75QzR+bqM2tUSlwCnCGPRiNguITesfTJbQvUg==", + "version": "1.0.30001820", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30001820.tgz", + "integrity": "sha512-zQcHHXW1JBaCQIrIB9GSljW1WpxaFOntxRIJB+AiRKrVIKRoxocsOJ1ofzekZrr3uCH9OJXjvxAOgdH0qtGzSQ==", "dev": true, "license": "CC-BY-4.0" }, "node_modules/caniuse-lite": { - "version": "1.0.30001819", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", - "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "version": "1.0.30001820", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001820.tgz", + "integrity": "sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==", "dev": true, "funding": [ { @@ -6616,9 +6616,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.464", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz", - "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==", + "version": "1.5.468", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.468.tgz", + "integrity": "sha512-8XdEp3sIqO+PzmVPxhzqHq/3+yCxp44m27cm2WY1sRn3ZhwKyKmqSi2hx6gEhmnOXmh8NUuYE00z6Onzn5m6DA==", "dev": true, "license": "ISC" }, @@ -6640,13 +6640,17 @@ } }, "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.1.tgz", + "integrity": "sha512-Ev/hIyePLZtpL98x562lvPtenrUfGr0lfaf44HFZ+hDEwso7ccV9QAZrEVLg0hxNAzygaAFxy2dYaAKKM0G06A==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/end-of-stream": { @@ -10191,9 +10195,9 @@ } }, "node_modules/mdast-util-to-markdown": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.0.tgz", - "integrity": "sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.1.tgz", + "integrity": "sha512-ThatlCBxG2GE+mljhNSfZPv56d3QrGiRh97jjYgIk80IzS4uU6d7cHAaexCYSAmUCCcOs3grJ07kX3oTGsFUOw==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -11189,9 +11193,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.60", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", - "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "version": "2.0.61", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.61.tgz", + "integrity": "sha512-5NUN6QCxNFrS9oEA9RPS+d0AP+ooGBzzovY2JhIpDRHacPvOhHjXjUsUMX9qp4nLHZ+5jVgXB2jj/P4arGOE/g==", "dev": true, "license": "MIT", "engines": { @@ -11248,9 +11252,9 @@ } }, "node_modules/num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.3.0.tgz", + "integrity": "sha512-rclir6MW4M4Hinz/AnripNvEbz8HRxJ9Qkrtj0V8ivgEsfpxGWGbXDkfCT4IJLEWegu4pvO9qibWpeHkODkGqg==", "dev": true, "license": "MIT" }, @@ -12843,9 +12847,9 @@ } }, "node_modules/qs": { - "version": "6.16.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", - "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.2.tgz", + "integrity": "sha512-VYdnB5S7/0IeXzBacUtwNAaM6d7Y/PgYnNC7bJCI6meqA/OgnsQbZMtVW8Dt2D1PHgbHGtYmhSoEIgvt7xZ2ZA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13636,9 +13640,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.106.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.106.1.tgz", - "integrity": "sha512-kD+Gi66vycZcbJt+42NC4z4PXNMcCajVrJPhqoKKXy5cOBMmJQt052CxIBcRjwYXeImHYOpu/ydWWGZQIenl7A==", + "version": "1.107.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.107.0.tgz", + "integrity": "sha512-+whPilvVNoAD57xvdIPLQT0gXo4RD1lHYw+Uw8zYHlojaeaMDxK14tc0JmEsz/XMns48h3lpypisMP+3E8eHQA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/common/__tests__/breadcrumb.test.js b/src/common/__tests__/breadcrumb.test.js new file mode 100644 index 00000000..714f9b89 --- /dev/null +++ b/src/common/__tests__/breadcrumb.test.js @@ -0,0 +1,20 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +const { projectCrumbs } = await import('../breadcrumb.js') + +const toV3URL = path => `https://tb.example.org/#${path}` + +test('integrated deployments link both crumbs to TermBrowser', () => { + assert.deepEqual(projectCrumbs('/users/test/', true, toV3URL).map(c => c.href), [ + 'https://tb.example.org/#/users/test/', + 'https://tb.example.org/#/users/test/map-projects' + ]) +}) + +test('standalone deployments keep the owner as text and the projects crumb local', () => { + assert.deepEqual(projectCrumbs('/users/test/', false, () => { throw new Error('no TermBrowser link') }).map(c => c.href), [ + null, + '#/map-projects' + ]) +}) diff --git a/src/common/breadcrumb.js b/src/common/breadcrumb.js new file mode 100644 index 00000000..cd03ffb7 --- /dev/null +++ b/src/common/breadcrumb.js @@ -0,0 +1,4 @@ +export const projectCrumbs = (ownerURL, integrated, toV3URL) => [ + {key: 'owner', href: integrated ? toV3URL(ownerURL) : null}, + {key: 'projects', href: integrated ? toV3URL(`${ownerURL}map-projects`) : '#/map-projects'} +] diff --git a/src/components/map-projects/MapProjectBreadcrumb.jsx b/src/components/map-projects/MapProjectBreadcrumb.jsx index eefbd6b4..532928a5 100644 --- a/src/components/map-projects/MapProjectBreadcrumb.jsx +++ b/src/components/map-projects/MapProjectBreadcrumb.jsx @@ -2,18 +2,19 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import Link from '@mui/material/Link'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import { toV3URL } from '../../common/utils'; +import { hasTermBrowser, toV3URL } from '../../common/utils'; +import { projectCrumbs } from '../../common/breadcrumb'; const MapProjectBreadcrumb = ({ ownerType, owner }) => { const { t } = useTranslation() if(!ownerType || !owner) return null - const ownerURL = `/${ownerType}/${owner}/` + const [ownerCrumb, projectsCrumb] = projectCrumbs(`/${ownerType}/${owner}/`, hasTermBrowser(), toV3URL) return ( - {owner} + {ownerCrumb.href ? {owner} : {owner}} - {t('map_project.mapping_projects')} + {t('map_project.mapping_projects')} ) } From fe0965b5f1997aae83ddf98ddedc0d08cec4e40e Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Fri, 30 Oct 2026 10:54:12 -0400 Subject: [PATCH 05/14] OpenConceptLab/ocl_online#418 | Drop unrelated lock-file changes; Chinese Bulk Import label Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 142 +++++++++++++------------- src/i18n/locales/zh/translations.json | 3 + 2 files changed, 72 insertions(+), 73 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd4515cd..9b8b2f2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ocl-map", - "version": "1.0.43-beta", + "version": "1.0.37-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ocl-map", - "version": "1.0.43-beta", + "version": "1.0.37-beta", "dependencies": { "@airbrake/browser": "^2.1.9", "@emotion/react": "^11.14.0", @@ -159,13 +159,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.12.tgz", - "integrity": "sha512-2W76mhjrpQFO1lK5e8IvGYB2D0YZ7S2R1Z2BjO2ZUYuW+amcpK/z60U6ZZmzskwoa7sJ6XohP/Wg8H5KxXI/IA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.12", - "@babel/types": "^7.29.11", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -205,9 +205,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.12.tgz", - "integrity": "sha512-ACxGoWMV866q/QlZHeCOaQZAR5QclKsIvkAmlDemqjm6+6Bynijs1mH068BzH2N2e+wSuDmBWY87fqKfvhi1Lw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "dev": true, "license": "MIT", "dependencies": { @@ -216,7 +216,7 @@ "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", - "@babel/traverse": "^7.29.12", + "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "engines": { @@ -446,9 +446,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", - "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.11" @@ -760,9 +760,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.12.tgz", - "integrity": "sha512-nEreZBofjfd6FG621xe9Rk3+tMGBXf1nAuFrR6NUGF67fkQbSQf3NrbRuhgWRn+zb0qcy9XVok1GVFI/t9wWVQ==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.11.tgz", + "integrity": "sha512-iARF4jjHSM0v8DImCACPXrwEltBP3DgqDpXTvjAMBAVpyEY0LiGss78wvI+KtmW+IJNEvZ5sOhPDCsOsXsIybA==", "dev": true, "license": "MIT", "dependencies": { @@ -1783,17 +1783,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.12.tgz", - "integrity": "sha512-MscXe/NAZcOkYEsRajHZFt3ynsb22PUi6CTrp5U7+RYycR9By84mn9mFN54YgfITiqK4hFx3Ju3tZSyzfpS5ew==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.12", + "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.12", + "@babel/parser": "^7.29.9", "@babel/template": "^7.29.7", - "@babel/types": "^7.29.11", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -3680,9 +3680,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", - "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -3790,9 +3790,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz", - "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", "license": "MIT", "dependencies": { "undici-types": "~8.9.0" @@ -4684,9 +4684,9 @@ } }, "node_modules/axios": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.21.0.tgz", - "integrity": "sha512-YCzXMB53Q9PZBsh7yN7wtapit4WepwcHS2fP8QhGsBxSz5YtgYobV7GGlWlAJi+RxfU77GQTQi2DSLUma42EYA==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", @@ -4867,9 +4867,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.34", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.34.tgz", - "integrity": "sha512-Aqr8mA1kIvyJ3UY5YAY7ib8LSo1DmDMbMnVqGPtsKW1LxaFQFwn9Eb3yvq2fUKJsRvaXjnAV7R5osqETapzNiA==", + "version": "2.11.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.33.tgz", + "integrity": "sha512-yiVKDX+qCMRKBE084lXvq9EcwFUhKZ2nU8TK1EQQuOgPNR/lmbGxXOeDdlGUqC2Zz11Cs9rk1d/8NfgBmlkt0w==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5007,9 +5007,9 @@ } }, "node_modules/browserslist": { - "version": "4.29.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", - "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", + "version": "4.29.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", "dev": true, "funding": [ { @@ -5027,10 +5027,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.33", - "caniuse-lite": "^1.0.30001819", - "electron-to-chromium": "^1.5.464", - "node-releases": "^2.0.60", + "baseline-browser-mapping": "^2.11.29", + "caniuse-lite": "^1.0.30001816", + "electron-to-chromium": "^1.5.459", + "node-releases": "^2.0.59", "update-browserslist-db": "^1.3.4" }, "bin": { @@ -5221,16 +5221,16 @@ } }, "node_modules/caniuse-db": { - "version": "1.0.30001820", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30001820.tgz", - "integrity": "sha512-zQcHHXW1JBaCQIrIB9GSljW1WpxaFOntxRIJB+AiRKrVIKRoxocsOJ1ofzekZrr3uCH9OJXjvxAOgdH0qtGzSQ==", + "version": "1.0.30001819", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30001819.tgz", + "integrity": "sha512-mLpSoyGOxULIZqnuoPKJl/Z8CCpgmaRotGeoGCJawSf649JqQ75QzR+bqM2tUSlwCnCGPRiNguITesfTJbQvUg==", "dev": true, "license": "CC-BY-4.0" }, "node_modules/caniuse-lite": { - "version": "1.0.30001820", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001820.tgz", - "integrity": "sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==", + "version": "1.0.30001819", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", + "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", "dev": true, "funding": [ { @@ -6616,9 +6616,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.468", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.468.tgz", - "integrity": "sha512-8XdEp3sIqO+PzmVPxhzqHq/3+yCxp44m27cm2WY1sRn3ZhwKyKmqSi2hx6gEhmnOXmh8NUuYE00z6Onzn5m6DA==", + "version": "1.5.464", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz", + "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==", "dev": true, "license": "ISC" }, @@ -6640,17 +6640,13 @@ } }, "node_modules/encodeurl": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.1.tgz", - "integrity": "sha512-Ev/hIyePLZtpL98x562lvPtenrUfGr0lfaf44HFZ+hDEwso7ccV9QAZrEVLg0hxNAzygaAFxy2dYaAKKM0G06A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, "node_modules/end-of-stream": { @@ -10195,9 +10191,9 @@ } }, "node_modules/mdast-util-to-markdown": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.1.tgz", - "integrity": "sha512-ThatlCBxG2GE+mljhNSfZPv56d3QrGiRh97jjYgIk80IzS4uU6d7cHAaexCYSAmUCCcOs3grJ07kX3oTGsFUOw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.0.tgz", + "integrity": "sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -11193,9 +11189,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.61", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.61.tgz", - "integrity": "sha512-5NUN6QCxNFrS9oEA9RPS+d0AP+ooGBzzovY2JhIpDRHacPvOhHjXjUsUMX9qp4nLHZ+5jVgXB2jj/P4arGOE/g==", + "version": "2.0.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", + "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", "dev": true, "license": "MIT", "engines": { @@ -11252,9 +11248,9 @@ } }, "node_modules/num2fraction": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.3.0.tgz", - "integrity": "sha512-rclir6MW4M4Hinz/AnripNvEbz8HRxJ9Qkrtj0V8ivgEsfpxGWGbXDkfCT4IJLEWegu4pvO9qibWpeHkODkGqg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==", "dev": true, "license": "MIT" }, @@ -12847,9 +12843,9 @@ } }, "node_modules/qs": { - "version": "6.16.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.2.tgz", - "integrity": "sha512-VYdnB5S7/0IeXzBacUtwNAaM6d7Y/PgYnNC7bJCI6meqA/OgnsQbZMtVW8Dt2D1PHgbHGtYmhSoEIgvt7xZ2ZA==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13640,9 +13636,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.107.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.107.0.tgz", - "integrity": "sha512-+whPilvVNoAD57xvdIPLQT0gXo4RD1lHYw+Uw8zYHlojaeaMDxK14tc0JmEsz/XMns48h3lpypisMP+3E8eHQA==", + "version": "1.106.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.106.1.tgz", + "integrity": "sha512-kD+Gi66vycZcbJt+42NC4z4PXNMcCajVrJPhqoKKXy5cOBMmJQt052CxIBcRjwYXeImHYOpu/ydWWGZQIenl7A==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json index 809378bd..afdf2dee 100644 --- a/src/i18n/locales/zh/translations.json +++ b/src/i18n/locales/zh/translations.json @@ -596,5 +596,8 @@ "import": "超出限制的行会保留在项目中,但不会被匹配。" } } + }, + "import": { + "bulk_import": "批量导入" } } From e2f6730e257d8fcc45c9e1e8e8f37cb64617149d Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Fri, 30 Oct 2026 10:54:25 -0400 Subject: [PATCH 06/14] OpenConceptLab/ocl_online#418 | Tray lists what you follow Co-Authored-By: Claude Opus 5.5 --- src/common/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/utils.js b/src/common/utils.js index db615898..85f38a7a 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -246,7 +246,7 @@ export const arrayToCSV = objArray => { } export const refreshCurrentUserCache = (callback, onFailure) => { - APIService.user().get(null, null, {includeAuthGroups: true, includeSubscribedOrgs: true, includeCapabilities: true}).then(response => { + APIService.user().get(null, null, {includeAuthGroups: true, includeSubscribedOrgs: true, includeFollowing: true, includeCapabilities: true}).then(response => { if(response?.status === 200) { localStorage.setItem('user', JSON.stringify(response.data)); if(callback) callback(response); From 3ef5b364eae0a021921abe443d6fc2d642930acd Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Fri, 30 Oct 2026 10:55:07 -0400 Subject: [PATCH 07/14] OpenConceptLab/ocl_online#418 | Tray and footer link to the OCL Community website Co-Authored-By: Claude Opus 5.5 --- src/common/constants.js | 1 + src/components/app/Footer.jsx | 4 ++- src/components/app/LeftMenu.jsx | 31 +++++++++++++++++++++ src/components/app/__tests__/footer.test.js | 4 ++- src/components/users/UserMenu.jsx | 4 +-- 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/common/constants.js b/src/common/constants.js index 62afee14..b97fd17a 100644 --- a/src/common/constants.js +++ b/src/common/constants.js @@ -76,5 +76,6 @@ export const MATCHING_ALGORITHMS_DOC_LINK = 'https://docs.openconceptlab.org/en/ export const MAPPER_DOCS_LINK = 'https://docs.openconceptlab.org/en/latest/oclmap/overview.html' export const MAPPER_ANNOUNCEMENT_LETTER_URL = 'https://openconceptlab.org/blog/letter-to-the-ocl-community?utm_source=oclmap&utm_medium=banner&utm_campaign=subscription_dates' export const MAPPER_ANNOUNCEMENT_PRICING_URL = 'https://openconceptlab.org/pricing?utm_source=oclmap&utm_medium=banner&utm_campaign=subscription_dates' +export const COMMUNITY_SITE_URL = 'https://openconceptlab.org' export const TERMS_OF_USE_URL = 'https://openconceptlab.org/terms-of-use' export const PRIVACY_POLICY_URL = 'https://openconceptlab.org/privacy-policy' diff --git a/src/components/app/Footer.jsx b/src/components/app/Footer.jsx index bb59b7e8..1e2ef8cc 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 = () => {