diff --git a/src/components/users/UserStatistics.jsx b/src/components/users/UserStatistics.jsx
index 5ca23b57a..8a8ef6458 100644
--- a/src/components/users/UserStatistics.jsx
+++ b/src/components/users/UserStatistics.jsx
@@ -9,12 +9,17 @@ import ListItemText from '@mui/material/ListItemText';
import RepoIcon from '../repos/RepoIcon';
import OrgIcon from '../orgs/OrgIcon'
import About from '../common/About';
+import { getCurrentUser, hasMapper, pluralize } from '../../common/utils';
+import { canSeeMapProjects } from '../../common/mapProjects';
+import useMapProjectCount from '../../hooks/useMapProjectCount';
const UserStatistics = ({ user }) => {
const { t } = useTranslation()
const repos = user.public_sources + user.public_collections
const history = useHistory()
const onRepoStatsClick = () => history.push(user.url + 'repos')
+ const showMapProjects = hasMapper() && canSeeMapProjects(getCurrentUser(), 'users', user.username)
+ const mapProjectCount = useMapProjectCount(user.username, showMapProjects)
return (
{
@@ -41,6 +46,18 @@ const UserStatistics = ({ user }) => {
primary={`${repos} ${t('common.public').toLowerCase()} ${t('repo.repos').toLowerCase()}`}
/>
+ {
+ showMapProjects && mapProjectCount !== null &&
+ history.push(user.url + 'map-projects')}>
+
+
+
+
+
+ }
diff --git a/src/hooks/useDefaultFiltersOpen.js b/src/hooks/useDefaultFiltersOpen.js
index 0148f3907..7a43ed712 100644
--- a/src/hooks/useDefaultFiltersOpen.js
+++ b/src/hooks/useDefaultFiltersOpen.js
@@ -1,16 +1,37 @@
import React from 'react';
import useMediaQuery from '@mui/material/useMediaQuery';
+// The list width at which a profile's repository list opens its filter panel by default
+export const PROFILE_LIST_MIN_WIDTH = 1000
+
// For pages that embed Search (repo pages, org and user repository lists): the filter
-// panel starts open on wide screens and closed on narrow ones. Once the user opens or
-// closes it, that choice is kept while the page stays mounted, including when Search
-// remounts on a tab or version change. A window resize never opens or closes it.
-// Returns [defaultFiltersOpen, onFiltersOpenChange] to pass to Search.
-const useDefaultFiltersOpen = () => {
- // Search reads defaultFiltersOpen once, when it mounts, so the first render needs the real width.
+// panel starts open when the list has room for it and closed when it doesn't. Repo pages
+// go by the window (wide screens). Profiles share their width with the summary panel, so
+// they pass minListWidth and put listRef on the list's container; the hook measures it,
+// and defaultFiltersOpen is null until it has, so render Search once it isn't.
+// The panel follows the window as it is resized until the person opens or closes it.
+// Then that choice is kept while the page stays mounted, including when Search remounts
+// on a tab or version change.
+// Returns [defaultFiltersOpen, onFiltersOpenChange, listRef] to pass to Search.
+const useDefaultFiltersOpen = minListWidth => {
+ // Search reads defaultFiltersOpen when it mounts, so the first render needs the real width.
const isWide = useMediaQuery(theme => theme.breakpoints.up('lg'), {noSsr: true}) // eslint-disable-line spellcheck/spell-checker
+ const [listHasRoom, setListHasRoom] = React.useState(null)
const [choice, setChoice] = React.useState(null)
- return [choice === null ? isWide : choice, setChoice]
+ const observerRef = React.useRef(null)
+ // A callback ref measures before the first paint, so Search mounts with the right default
+ const listRef = React.useCallback(node => {
+ observerRef.current?.disconnect()
+ observerRef.current = null
+ if(!node || !minListWidth)
+ return
+ const update = () => setListHasRoom(node.getBoundingClientRect().width >= minListWidth)
+ update()
+ observerRef.current = new ResizeObserver(update)
+ observerRef.current.observe(node)
+ }, [minListWidth])
+ const hasRoom = minListWidth ? listHasRoom : isWide
+ return [choice === null ? hasRoom : choice, setChoice, listRef]
}
export default useDefaultFiltersOpen;
diff --git a/src/hooks/useMapProjectCount.js b/src/hooks/useMapProjectCount.js
new file mode 100644
index 000000000..b58282e5d
--- /dev/null
+++ b/src/hooks/useMapProjectCount.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import APIService from '../services/APIService';
+import { projectCount } from '../common/mapProjects';
+
+// A user's count includes their organizations' projects, as their Mapping Projects tab does
+const useMapProjectCount = (owner, enabled, ownerType = 'users') => {
+ const [count, setCount] = React.useState(null)
+ React.useEffect(() => {
+ if(!enabled || !owner)
+ return
+ let active = true
+ const requests = [APIService[ownerType](owner).appendToUrl('map-projects/').get(null, null, {limit: 1}, true)]
+ if(ownerType === 'users')
+ requests.push(APIService.users(owner).appendToUrl('orgs/map-projects/').get(null, null, {limit: 1}, true))
+ Promise.all(requests).then(responses => {
+ if(active)
+ setCount(projectCount(...responses))
+ }).catch(() => {})
+ return () => { active = false }
+ }, [owner, enabled, ownerType])
+ return count
+}
+
+export default useMapProjectCount
diff --git a/src/hooks/useProfileTabs.js b/src/hooks/useProfileTabs.js
new file mode 100644
index 000000000..193b052c4
--- /dev/null
+++ b/src/hooks/useProfileTabs.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { loadProfileTabs } from '../common/plugins/loader';
+import { getCurrentUser, hasMapper } from '../common/utils';
+import { canSeeMapProjects } from '../common/mapProjects';
+
+let pluginTabsPromise
+
+const loadPluginTabs = () => {
+ if(!pluginTabsPromise)
+ pluginTabsPromise = loadProfileTabs().then(tabs => Array.isArray(tabs) ? tabs : [])
+ return pluginTabsPromise
+}
+
+export const usePluginTabs = () => {
+ const [tabs, setTabs] = React.useState([])
+ React.useEffect(() => { loadPluginTabs().then(setTabs) }, [])
+ return tabs
+}
+
+const isOwnerTab = (tab, ownerType, owner) => (
+ (!tab.ownerTypes || tab.ownerTypes.includes(ownerType)) &&
+ (!tab.isVisible || tab.isVisible({ownerType, owner, currentUser: getCurrentUser()}))
+)
+
+const useProfileTabs = (ownerType, owner) => {
+ const { t } = useTranslation()
+ const pluginTabs = usePluginTabs()
+ return React.useMemo(() => [
+ ...(hasMapper() && canSeeMapProjects(getCurrentUser(), ownerType, owner) ? [{key: 'map-projects', label: t('map_project.mapping_projects')}] : []),
+ ...pluginTabs.filter(tab => isOwnerTab(tab, ownerType, owner))
+ ], [pluginTabs, ownerType, owner, t])
+}
+
+export default useProfileTabs
diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json
index 2f868b611..eafe15da8 100644
--- a/src/i18n/locales/en/translations.json
+++ b/src/i18n/locales/en/translations.json
@@ -199,8 +199,6 @@
"suggested": "Suggested",
"rest": "Rest",
"click_here": "click here",
- "classic_termbrowser": "View in TBv2",
- "back_to_classic_termbrowser": "Back to the classic TermBrowser",
"already_exists": "Already exists",
"comment": "Comment",
"update_comment": "Update comment",
@@ -1003,6 +1001,10 @@
"hierarchy": "Hierarchy",
"search_all_site": "Search all of OCL for ",
"search_this_repository": "Search this repository",
+ "search_owner_repos": "Search <0>{{owner}}0>'s repositories for \"<1>{{query}}1>\"",
+ "search_owner_map_projects": "Search <0>{{owner}}0>'s mapping projects for \"<1>{{query}}1>\"",
+ "search_repositories": "Search repositories",
+ "search_mapping_projects": "Search mapping projects",
"search_this_url_registry": "Search this URL registry for ",
"search_all_concepts": "Search all concepts",
"url_registry": "entries",
@@ -1198,5 +1200,13 @@
"default": "Clone a narrower concept or fewer cascade levels."
}
}
+ },
+ "map_project": {
+ "mapping_project": "Mapping project",
+ "mapping_projects": "Mapping Projects",
+ "project_name": "Project Name",
+ "no_projects_found": "No mapping projects yet.",
+ "could_not_load_projects": "Could not load mapping projects.",
+ "try_again": "Try again"
}
}
diff --git a/src/i18n/locales/es/translations.json b/src/i18n/locales/es/translations.json
index a616ae63f..854a49f3d 100644
--- a/src/i18n/locales/es/translations.json
+++ b/src/i18n/locales/es/translations.json
@@ -82,8 +82,6 @@
"location": "Ubicación",
"generic_error": "Algo salió mal.",
"click_here": "haz clic aquí",
- "classic_termbrowser": "Ver en TBv2",
- "back_to_classic_termbrowser": "Volver al TermBrowser clásico",
"all": "Todos",
"add": "Agregar",
"already_exists": "Ya existe",
@@ -567,6 +565,7 @@
"user": "Usuario",
"my_profile": "Mi perfil",
"my_repositories": "Mis repositorios",
+ "my_mapping_projects": "Mis proyectos de mapeo",
"my_organizations": "Mis organizaciones",
"my_bookmarks": "Mis marcadores",
"username": "Nombre de usuario",
@@ -608,6 +607,10 @@
"card": "Tarjeta",
"search_all_site": "Buscar en todo OCL para ",
"search_this_repository": "Buscar en este repositorio",
+ "search_owner_repos": "Buscar \"<1>{{query}}1>\" en los repositorios de <0>{{owner}}0>",
+ "search_owner_map_projects": "Buscar \"<1>{{query}}1>\" en los proyectos de mapeo de <0>{{owner}}0>",
+ "search_repositories": "Buscar repositorios",
+ "search_mapping_projects": "Buscar proyectos de mapeo",
"search_all_concepts": "Buscar todos los conceptos",
"save_default_filters": "Guardar como Filtros Predeterminados",
"reset_default_filters": "Restablecer a Filtros Predeterminados",
@@ -724,5 +727,13 @@
"checksums": {
"smart": "Checksum inteligente",
"standard": "Checksum estándar"
+ },
+ "map_project": {
+ "mapping_project": "Proyecto de mapeo",
+ "mapping_projects": "Proyectos de Mapeo",
+ "project_name": "Nombre del Proyecto",
+ "no_projects_found": "Aún no hay proyectos de mapeo.",
+ "could_not_load_projects": "No se pudieron cargar los proyectos de mapeo.",
+ "try_again": "Intentar de nuevo"
}
}
diff --git a/src/i18n/locales/pt-BR/translations.json b/src/i18n/locales/pt-BR/translations.json
index 71490b2c9..b4773e2af 100644
--- a/src/i18n/locales/pt-BR/translations.json
+++ b/src/i18n/locales/pt-BR/translations.json
@@ -199,8 +199,6 @@
"suggested": "Sugeridos",
"rest": "Restante",
"click_here": "clique aqui",
- "classic_termbrowser": "Ver no TBv2",
- "back_to_classic_termbrowser": "Voltar para o TermBrowser clássico",
"already_exists": "Já existe",
"comment": "Comentário",
"update_comment": "Comentário de atualização",
@@ -993,6 +991,10 @@
"hierarchy": "Hierarquia",
"search_all_site": "Buscar em todo o OCL por ",
"search_this_repository": "Buscar neste repositório",
+ "search_owner_repos": "Buscar \"<1>{{query}}1>\" nos repositórios de <0>{{owner}}0>",
+ "search_owner_map_projects": "Buscar \"<1>{{query}}1>\" nos projetos de mapeamento de <0>{{owner}}0>",
+ "search_repositories": "Buscar repositórios",
+ "search_mapping_projects": "Buscar projetos de mapeamento",
"search_this_url_registry": "Buscar neste registro de URL por ",
"search_all_concepts": "Buscar todos os conceitos",
"url_registry": "entradas",
@@ -1187,5 +1189,13 @@
"default": "Clone um conceito mais específico ou menos níveis de cascata."
}
}
+ },
+ "map_project": {
+ "mapping_project": "Projeto de mapeamento",
+ "mapping_projects": "Projetos de Mapeamento",
+ "project_name": "Nome do Projeto",
+ "no_projects_found": "Ainda não há projetos de mapeamento.",
+ "could_not_load_projects": "Não foi possível carregar os projetos de mapeamento.",
+ "try_again": "Tentar novamente"
}
}
diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json
index 5d2f6740f..01862e8ba 100644
--- a/src/i18n/locales/zh/translations.json
+++ b/src/i18n/locales/zh/translations.json
@@ -121,8 +121,6 @@
"something_went_wrong": "出了些问题",
"no_results": "无结果",
"click_here": "点击此处",
- "classic_termbrowser": "在 TBv2 中查看",
- "back_to_classic_termbrowser": "返回经典版 TermBrowser",
"already_exists": "已存在",
"comment": "备注",
"update_comment": "更新说明",
@@ -584,6 +582,7 @@
"user": "用户",
"my_profile": "我的个人资料",
"my_repositories": "我的存储库",
+ "my_mapping_projects": "我的映射项目",
"my_organizations": "我的组织机构",
"my_bookmarks": "我的书签",
"username": "用户名",
@@ -715,6 +714,10 @@
"hierarchy": "层级",
"search_all_site": "搜索全部符合搜索条件的 OCL 内容: ",
"search_this_repository": "搜索该存储库",
+ "search_owner_repos": "在 <0>{{owner}}0> 的存储库中搜索“<1>{{query}}1>”",
+ "search_owner_map_projects": "在 <0>{{owner}}0> 的映射项目中搜索“<1>{{query}}1>”",
+ "search_repositories": "搜索存储库",
+ "search_mapping_projects": "搜索映射项目",
"search_this_url_registry": "搜索该 URL 注册库: ",
"search_all_concepts": "搜索全部的概念",
"url_registry": "条目",
@@ -791,5 +794,13 @@
},
"cloneToSource": {
"clone_to_source": "克隆到来源"
+ },
+ "map_project": {
+ "mapping_project": "映射项目",
+ "mapping_projects": "映射项目",
+ "project_name": "项目名称",
+ "no_projects_found": "还没有映射项目。",
+ "could_not_load_projects": "无法加载映射项目。",
+ "try_again": "重试"
}
}
diff --git a/start-prod.sh b/start-prod.sh
index cb2454f85..190a98afe 100755
--- a/start-prod.sh
+++ b/start-prod.sh
@@ -44,6 +44,9 @@ fi
if [[ ! -z "${AI_ASSISTANT_API_URL}" ]]; then
echo "var AI_ASSISTANT_API_URL = \"${AI_ASSISTANT_API_URL}\";" >> ${ENV_FILE}
fi
+if [[ ! -z "${MAPPER_URL}" ]]; then
+ echo "var MAPPER_URL = \"${MAPPER_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 bb759ce19..c1d1aedd2 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -147,6 +147,7 @@ module.exports = (env) => {
'process.env.OIDC_REALM': JSON.stringify(env.OIDC_REALM || ''),
'process.env.ANALYTICS_API': JSON.stringify(env.ANALYTICS_API || ''),
'process.env.AI_ASSISTANT_API_URL': JSON.stringify(env.AI_ASSISTANT_API_URL || ''),
+ 'process.env.MAPPER_URL': JSON.stringify(env.MAPPER_URL || ''),
'process.env.OCL_ONLINE_API_URL': JSON.stringify(env.OCL_ONLINE_API_URL || ''),
}),
new IgnorePlugin({ resourceRegExp: /moment\/locale\// })