From 6dbf9efbc2bff8abfafb13e83bc5b9718dadf842 Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Wed, 14 Oct 2026 12:29:24 -0400 Subject: [PATCH 1/2] OpenConceptLab/ocl_issues#2906 | Start a new target on its latest semantic-search release, and mark each version Choosing a target repository now fills Version with its latest release that supports semantic search (match_algorithms includes llm), else its latest release, else HEAD. Before, Version stayed empty unless the repository had a single version or a single release. A saved project, or a copy made from one, keeps its saved version and never gets the default. A saved URL without a version means HEAD, since HEAD's version_url carries none. A versions response that arrives after another repository was chosen is ignored, so it can't set that repository's version. Each version option shows its release date and "Latest" and "Semantic search" badges. OCL keeps no release timestamp, so the date is created_on, which is also what OCL orders versions by. Version turns red only after a save is tried without one, and choosing a version clears it. Save & Close no longer waits for a version, so that try can happen from the form too; the save itself still refuses. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HgMg6pPngQoQJzZToPti27 --- .../map-projects/ConfigurationForm.jsx | 23 +++- src/components/map-projects/MapProject.jsx | 26 +++-- .../__tests__/projectTargetRepo.test.js | 109 +++++++++++++++--- .../map-projects/projectTargetRepo.js | 41 +++++-- .../repos/RepoVersionSearchAutocomplete.jsx | 12 +- src/i18n/locales/en/translations.json | 3 + src/i18n/locales/es/translations.json | 3 + src/i18n/locales/pt-BR/translations.json | 3 + src/i18n/locales/zh/translations.json | 3 + 9 files changed, 183 insertions(+), 40 deletions(-) diff --git a/src/components/map-projects/ConfigurationForm.jsx b/src/components/map-projects/ConfigurationForm.jsx index 94de2a20..83cdf833 100644 --- a/src/components/map-projects/ConfigurationForm.jsx +++ b/src/components/map-projects/ConfigurationForm.jsx @@ -27,7 +27,7 @@ import isEmpty from 'lodash/isEmpty' import omit from 'lodash/omit' import filter from 'lodash/filter' -import { getCurrentUser, toV3URL } from '../../common/utils' +import { formatDate, getCurrentUser, toV3URL } from '../../common/utils' import { MATCHING_ALGORITHMS_DOC_LINK } from '../../common/constants' import { getProjectConfigErrors } from './algorithms' import NamespaceDropdown from '../common/NamespaceDropdown' @@ -43,7 +43,7 @@ import LookupConfig from './LookupConfig' import AdvancedSettings from './AdvancedSettings' import RerankerConfig from './RerankerConfig' import AIAssistantSelectorPanel from './AIAssistantSelectorPanel' -import { hasSelectedTargetRepoVersion } from './projectTargetRepo' +import { getTargetRepoVersionBadges, getTargetRepoVersionReleaseDate, hasSelectedTargetRepoVersion } from './projectTargetRepo' import { getPreviewAlgorithmOptions, getPreviewConfigAccessErrors } from './previewConfig' const VisuallyHiddenInput = styled('input')({ @@ -61,7 +61,12 @@ const VisuallyHiddenInput = styled('input')({ const deriveCanonicalUrl = relativeUrl => relativeUrl ? `https://ns.openconceptlab.org${relativeUrl}` : '' -const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, name, setName, description, setDescription, repo, onRepoChange, repoVersion, setRepoVersion, versions, mappedSources, targetSourcesFromRows, algosSelected, setAlgosSelected, sx, algos, validColumns, columns, isValidColumnValue, updateColumn, configure, setConfigure, columnVisibilityModel, setColumnVisibilityModel, onSave, isSaving, candidatesScore, onScoreChange, includeDefaultFilter, setIncludeDefaultFilter, filters, setFilters, locales, isLoadingLocales, setAIAssistantColumns, AIAssistantColumns, inAIAssistantGroup, lookupConfig, setLookupConfig, encoderModel, setEncoderModel, isCoreUser, requestLimits, canSelectAIModel, canSetAIOutputLocale, canBridge, canScispacy, canUseOrgProjects=true, canUseCustomAlgorithms=true, promptTemplates, promptTemplate, onPromptTemplateChange, AIModels, AIModel, setAIModel, namespace, setNamespace, promptOutputLocale, setPromptOutputLocale, inputLocale, setInputLocale, oclLocales, useLexicalVariants, setUseLexicalVariants }) => { +const VERSION_BADGE_LABELS = { + latest: 'common.latest', + semantic_search: 'map_project.semantic_search', +} + +const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, name, setName, description, setDescription, repo, onRepoChange, repoVersion, setRepoVersion, showTargetRepoVersionError, versions, mappedSources, targetSourcesFromRows, algosSelected, setAlgosSelected, sx, algos, validColumns, columns, isValidColumnValue, updateColumn, configure, setConfigure, columnVisibilityModel, setColumnVisibilityModel, onSave, isSaving, candidatesScore, onScoreChange, includeDefaultFilter, setIncludeDefaultFilter, filters, setFilters, locales, isLoadingLocales, setAIAssistantColumns, AIAssistantColumns, inAIAssistantGroup, lookupConfig, setLookupConfig, encoderModel, setEncoderModel, isCoreUser, requestLimits, canSelectAIModel, canSetAIOutputLocale, canBridge, canScispacy, canUseOrgProjects=true, canUseCustomAlgorithms=true, promptTemplates, promptTemplate, onPromptTemplateChange, AIModels, AIModel, setAIModel, namespace, setNamespace, promptOutputLocale, setPromptOutputLocale, inputLocale, setInputLocale, oclLocales, useLexicalVariants, setUseLexicalVariants }) => { const { t } = useTranslation(); const user = getCurrentUser() const isLLMAlgoNotAllowed = !repoVersion?.match_algorithms?.includes('llm') @@ -112,6 +117,7 @@ const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, n const configErrors = getProjectConfigErrors(algosSelected) const hasConfigErrors = configErrors.length > 0 const hasTargetRepoVersion = hasSelectedTargetRepoVersion(repoVersion) + const isTargetRepoVersionInError = Boolean(showTargetRepoVersionError && !hasTargetRepoVersion) const getAlgos = () => getPreviewAlgorithmOptions({ algos, algosSelected, @@ -242,11 +248,16 @@ const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, n onChange={(id, item) => setRepoVersion(item)} value={repoVersion} sx={{marginTop: '12px'}} - error={Boolean(repo?.url && !hasTargetRepoVersion)} - helperText={repo?.url && !hasTargetRepoVersion ? t( + error={isTargetRepoVersionInError} + helperText={isTargetRepoVersionInError ? t( 'map_project.target_repo_version_required', 'Select a target repository version before saving.' ) : ''} + getOptionBadges={option => getTargetRepoVersionBadges(option, versions).map(badge => t(VERSION_BADGE_LABELS[badge]))} + getOptionRightText={option => { + const releaseDate = getTargetRepoVersionReleaseDate(option) + return releaseDate ? t('map_project.version_released_on', {date: formatDate(releaseDate)}) : '' + }} /> { effectiveTargetCanonical && @@ -537,7 +548,7 @@ const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, n sx={{textTransform: 'none', margin: '20px 5px 5px 0px'}} startIcon={} onClick={onSave} - disabled={!name || !file?.name || !owner || hasConfigErrors || hasAccessErrors || !hasTargetRepoVersion} + disabled={!name || !file?.name || !owner || hasConfigErrors || hasAccessErrors} loading={isSaving} loadingPosition="start" > diff --git a/src/components/map-projects/MapProject.jsx b/src/components/map-projects/MapProject.jsx index 7882629e..248dd197 100644 --- a/src/components/map-projects/MapProject.jsx +++ b/src/components/map-projects/MapProject.jsx @@ -114,7 +114,7 @@ import { getAIAssistantChoices, getModelUsed, getPromptTemplateKey, createLatest import { DEFAULT_ENCODER_MODEL } from './rerankerModels' import { normalizeAlgorithmInvocation, hasSuccessfulAlgorithmResponse, getAlgorithmStagesFromResponses, lookupStatusRank, buildRecommendableConceptEntry, stripConstantClassAndDatatype, buildLookupConceptUrl, serializeCandidates, toStoredRowMatchState } from './normalizers' import { parseConceptKey } from './conceptKey' -import { getDefaultTargetRepoVersion, getProjectTargetRepoVersion, getTargetRepoVersionFromUrl, getTargetRepoVersionId } from './projectTargetRepo' +import { getDefaultTargetRepoVersion, getProjectTargetRepoVersion, getSavedTargetRepoVersion, getTargetRepoVersionFromUrl, getTargetRepoVersionId } from './projectTargetRepo' import { buildBridgeTargetDownloadEntries, buildQualityRowViews, conceptBelongsToTargetRepo, conceptForMapping, formatBridgeTargetDownloadEntry, resolveAICandidateID, getScoreDetails, getAIAnalysisCandidateIDs } from './viewBuilders.js' import { formatClockTime, getCapacityWaitLabel, mergeCapacityWaits } from './rowProgress.js' @@ -388,6 +388,8 @@ const MapProject = () => { // repo state const [repo, setRepo] = React.useState(false) const [repoVersion, setRepoVersion] = React.useState(false) + // Version shows its error only after a save without one. + const [showTargetRepoVersionError, setShowTargetRepoVersionError] = React.useState(false) const [mappedSources, setMappedSources] = React.useState([]) const [bridgeMappedSources, setBridgeMappedSources] = React.useState({}) const [locales, setLocales] = React.useState([]) @@ -1584,6 +1586,7 @@ const MapProject = () => { autosaveSchedulerRef.current.cancel() if(!repoVersion?.version_url || !selectedTargetRepoVersion) { setConfigure(true) + setShowTargetRepoVersionError(true) setAlert({ message: t( 'map_project.target_repo_version_required', @@ -2029,6 +2032,7 @@ const MapProject = () => { const onRepoVersionChange = version => { setRepoVersion(version || false) + setShowTargetRepoVersionError(false) if(version?.version_url) { fetchLocaleDistribution(version.version_url) fetchMappedSources(version.version_url, setMappedSources) @@ -2826,16 +2830,20 @@ const MapProject = () => { const isRunningBulkBridgeCandidates = bridgeCandidatesEndedAt ? moment().isBetween(bridgeCandidatesStartedAt, bridgeCandidatesEndedAt) : Boolean(bridgeCandidatesStartedAt) const isRunningBulkScispacyCandidates = scispacyCandidatesStartedAt ? moment().isBetween(scispacyCandidatesStartedAt, scispacyCandidatesEndedAt) : Boolean(scispacyCandidatesStartedAt) - const fetchVersions = (url, _selectedVersion) => { + // Only the latest request may set the version: a slower response is for a + // repository no longer chosen. + const versionsRequestRef = React.useRef(0) + + // A newly chosen repository gets the default version. A saved project + // passes its own version and keeps it (OpenConceptLab/ocl_issues#2906). + const fetchVersions = (url, savedVersion) => { + const request = ++versionsRequestRef.current APIService.new().overrideURL(dropVersion(url)).appendToUrl('versions/?verbose=true').get().then(response => { + if(request !== versionsRequestRef.current) + return let _versions = response.data setVersions(_versions) - if(_selectedVersion) { - const _version = find(_versions, {id: _selectedVersion}) - onRepoVersionChange(_version || false) - } else { - onRepoVersionChange(getDefaultTargetRepoVersion(_versions)) - } + onRepoVersionChange(savedVersion === undefined ? getDefaultTargetRepoVersion(_versions) : getSavedTargetRepoVersion(_versions, savedVersion)) }) } @@ -2845,6 +2853,7 @@ const MapProject = () => { if(newRepo?.url) { fetchVersions(newRepo.url) } else { + versionsRequestRef.current += 1 setVersions([]) } } @@ -5940,6 +5949,7 @@ const MapProject = () => { onRepoChange={onRepoChange} repoVersion={repoVersion} setRepoVersion={onRepoVersionChange} + showTargetRepoVersionError={showTargetRepoVersionError} mappedSources={mappedSources} targetSourcesFromRows={targetSourcesFromRows} versions={versions} diff --git a/src/components/map-projects/__tests__/projectTargetRepo.test.js b/src/components/map-projects/__tests__/projectTargetRepo.test.js index c35c99b3..0ef0e4b4 100644 --- a/src/components/map-projects/__tests__/projectTargetRepo.test.js +++ b/src/components/map-projects/__tests__/projectTargetRepo.test.js @@ -5,12 +5,25 @@ import { getDefaultBridgeRepoVersion, getDefaultTargetRepoVersion, getLatestReleasedBridgeVersion, + getLatestReleasedTargetRepoVersion, getProjectTargetRepoVersion, + getSavedTargetRepoVersion, + getTargetRepoVersionBadges, getTargetRepoVersionFromUrl, getTargetRepoVersionId, + getTargetRepoVersionReleaseDate, hasSelectedTargetRepoVersion } from '../projectTargetRepo.js' +// LOINC's versions as the API lists them, newest first: only 2.81 supports +// the OCL Semantic Algorithm. +const LOINC_VERSIONS = [ + { id: '2.81', released: true, match_algorithms: ['es', 'llm'], revision_date: '2025-08-12T00:00:00Z', created_on: '2025-09-01T10:00:00Z' }, + { id: '2.71.21AA', released: true, match_algorithms: ['es'], created_on: '2021-11-03T10:00:00Z' }, + { id: '2.70.21AA', released: true, match_algorithms: ['es'], created_on: '2021-06-30T10:00:00Z' }, + { id: 'HEAD', released: false, match_algorithms: ['es'], created_on: '2021-01-15T10:00:00Z' } +] + test('getTargetRepoVersionId prefers the selected version id', () => { assert.equal(getTargetRepoVersionId({ id: '2.81', version_url: '/orgs/OCL/sources/Test/2.81/' }), '2.81') }) @@ -43,32 +56,94 @@ test('getProjectTargetRepoVersion returns empty when the project has no pinned v ) }) -test('getDefaultTargetRepoVersion selects the only version transparently', () => { - const onlyVersion = { id: '2.81', released: false } - assert.deepEqual(getDefaultTargetRepoVersion([onlyVersion]), onlyVersion) +test('getDefaultTargetRepoVersion picks the latest release that supports semantic search', () => { + assert.equal(getDefaultTargetRepoVersion(LOINC_VERSIONS), LOINC_VERSIONS[0]) +}) + +test('getDefaultTargetRepoVersion skips a newer release without semantic search', () => { + const versions = [ + { id: '2.82', released: true, match_algorithms: ['es'] }, + { id: '2.81', released: true, match_algorithms: ['es', 'llm'] }, + { id: 'HEAD', released: false, match_algorithms: ['es', 'llm'] } + ] + assert.equal(getDefaultTargetRepoVersion(versions), versions[1]) +}) + +test('getDefaultTargetRepoVersion falls back to the latest release', () => { + const versions = [ + { id: 'HEAD', released: false, match_algorithms: ['llm'] }, + { id: '2.81', released: false, match_algorithms: ['llm'] }, + { id: '2.80', released: true, match_algorithms: ['es'] }, + { id: '2.79', released: true, match_algorithms: null } + ] + assert.equal(getDefaultTargetRepoVersion(versions), versions[2]) +}) + +test('getDefaultTargetRepoVersion falls back to HEAD when nothing is released', () => { + const versions = [ + { id: '2.81', released: false, match_algorithms: ['llm'] }, + { id: 'HEAD', released: false } + ] + assert.equal(getDefaultTargetRepoVersion(versions), versions[1]) +}) + +test('getDefaultTargetRepoVersion stays unset without a release or HEAD', () => { + assert.equal(getDefaultTargetRepoVersion([]), false) + assert.equal(getDefaultTargetRepoVersion(undefined), false) + assert.equal(getDefaultTargetRepoVersion({ detail: 'Not found.' }), false) + assert.equal(getDefaultTargetRepoVersion([{ id: '2.81', released: false }]), false) +}) + +test('getSavedTargetRepoVersion keeps the saved version over a newer default', () => { + assert.equal(getSavedTargetRepoVersion(LOINC_VERSIONS, '2.70.21AA'), LOINC_VERSIONS[2]) +}) + +test('getSavedTargetRepoVersion reads a blank saved version as HEAD', () => { + // HEAD's version_url carries no version, so a project saved on HEAD + // stores a target_repo_url without one. + assert.equal(getSavedTargetRepoVersion(LOINC_VERSIONS, ''), LOINC_VERSIONS[3]) +}) + +test('getSavedTargetRepoVersion stays unset when the saved version is gone', () => { + assert.equal(getSavedTargetRepoVersion(LOINC_VERSIONS, '2.69'), false) + assert.equal(getSavedTargetRepoVersion(undefined, '2.81'), false) +}) + +test('getLatestReleasedTargetRepoVersion takes the first release the API lists', () => { + assert.equal(getLatestReleasedTargetRepoVersion(LOINC_VERSIONS), LOINC_VERSIONS[0]) + assert.equal(getLatestReleasedTargetRepoVersion([{ id: 'HEAD', released: false }]), null) + assert.equal(getLatestReleasedTargetRepoVersion(undefined), null) }) -test('getDefaultTargetRepoVersion prefers the only released version', () => { - const releasedVersion = { id: '2.81', released: true } +test('getTargetRepoVersionBadges marks the latest release and semantic search', () => { assert.deepEqual( - getDefaultTargetRepoVersion([ - { id: 'HEAD', released: false }, - releasedVersion - ]), - releasedVersion + LOINC_VERSIONS.map(version => getTargetRepoVersionBadges(version, LOINC_VERSIONS)), + [['latest', 'semantic_search'], [], [], []] ) }) -test('getDefaultTargetRepoVersion stays unset when multiple choices remain', () => { - assert.equal( - getDefaultTargetRepoVersion([ - { id: 'HEAD', released: false }, - { id: '2.81', released: false } - ]), - false +test('getTargetRepoVersionBadges marks no version latest when nothing is released', () => { + const versions = [ + { id: '2.81', released: false }, + { id: 'HEAD', released: false, match_algorithms: ['llm'] } + ] + assert.deepEqual( + versions.map(version => getTargetRepoVersionBadges(version, versions)), + [[], ['semantic_search']] ) }) +test('getTargetRepoVersionReleaseDate dates a release by when OCL created it', () => { + // Not revision_date: OCL copies it from HEAD into each new version. + assert.equal(getTargetRepoVersionReleaseDate(LOINC_VERSIONS[0]), '2025-09-01T10:00:00Z') + assert.equal(getTargetRepoVersionReleaseDate(LOINC_VERSIONS[1]), '2021-11-03T10:00:00Z') +}) + +test('getTargetRepoVersionReleaseDate leaves HEAD and unreleased versions undated', () => { + assert.equal(getTargetRepoVersionReleaseDate(LOINC_VERSIONS[3]), null) + assert.equal(getTargetRepoVersionReleaseDate({ id: '2.81', released: false, created_on: '2025-09-01T10:00:00Z' }), null) +}) + test('hasSelectedTargetRepoVersion treats blank selections as invalid', () => { assert.equal(hasSelectedTargetRepoVersion(false), false) assert.equal(hasSelectedTargetRepoVersion({ id: 'HEAD' }), true) diff --git a/src/components/map-projects/projectTargetRepo.js b/src/components/map-projects/projectTargetRepo.js index d654ddb3..360f59f6 100644 --- a/src/components/map-projects/projectTargetRepo.js +++ b/src/components/map-projects/projectTargetRepo.js @@ -6,19 +6,46 @@ export const getTargetRepoVersionFromUrl = url => { export const isLLMBridgeRepoVersion = version => version?.match_algorithms?.includes('llm') +// The OCL Semantic Algorithm runs only on versions vectorized for it. +export const supportsSemanticSearch = version => Boolean(version?.match_algorithms?.includes('llm')) + +// The API lists versions newest first. +export const getLatestReleasedTargetRepoVersion = versions => + (Array.isArray(versions) && versions.find(version => version?.released)) || null + +// A newly chosen repository starts on its latest release that supports +// semantic search, else its latest release, else HEAD. export const getDefaultTargetRepoVersion = versions => { - if(!Array.isArray(versions) || versions.length === 0) + if(!Array.isArray(versions)) return false - if(versions.length === 1) - return versions[0] - const releasedVersions = versions.filter(version => version?.released) - if(releasedVersions.length === 1) - return releasedVersions[0] + return versions.find(version => version?.released && supportsSemanticSearch(version)) || + getLatestReleasedTargetRepoVersion(versions) || + versions.find(version => version?.id === 'HEAD') || + false +} + +// A saved project keeps its version, never the default. HEAD's version_url +// carries no version, so a project saved on HEAD has none in its URL. +export const getSavedTargetRepoVersion = (versions, savedVersion) => + (Array.isArray(versions) && versions.find(version => version?.id === (savedVersion || 'HEAD'))) || false - return false +export const getTargetRepoVersionBadges = (version, versions) => { + const latestReleasedVersion = getLatestReleasedTargetRepoVersion(versions) + const badges = [] + if(latestReleasedVersion && version?.id === latestReleasedVersion.id) + badges.push('latest') + if(supportsSemanticSearch(version)) + badges.push('semantic_search') + return badges } +// OCL keeps no release timestamp: revision_date is copied from HEAD when a +// version is created, and updated_on moves with later edits. created_on is +// what OCL orders versions by, so the latest release shows the newest date. +export const getTargetRepoVersionReleaseDate = version => + (version?.released && version.created_on) || null + export const getDefaultBridgeRepoVersion = versions => { if(!Array.isArray(versions) || versions.length === 0) return false diff --git a/src/components/repos/RepoVersionSearchAutocomplete.jsx b/src/components/repos/RepoVersionSearchAutocomplete.jsx index e7118260..9ae471f4 100644 --- a/src/components/repos/RepoVersionSearchAutocomplete.jsx +++ b/src/components/repos/RepoVersionSearchAutocomplete.jsx @@ -2,6 +2,7 @@ import React from 'react'; import TextField from '@mui/material/TextField'; import Autocomplete from '@mui/material/Autocomplete'; import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; const RepoVersionSearchAutocomplete = ({ @@ -16,7 +17,8 @@ const RepoVersionSearchAutocomplete = ({ error, helperText, getOptionDisabled, - getOptionRightText + getOptionRightText, + getOptionBadges }) => { const [open, setOpen] = React.useState(false) @@ -51,14 +53,20 @@ const RepoVersionSearchAutocomplete = ({ renderOption={(props, option) => { const labelContent = getLabel(option) const rightText = getOptionRightText ? getOptionRightText(option) : '' + const badges = getOptionBadges ? getOptionBadges(option) : [] return (
  • - + {labelContent} + { + badges.map(badge => ( + + )) + } { diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index 1367cf2e..534b4dcb 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -768,6 +768,9 @@ "bridge_version_source_required": "Set a bridge source URL before choosing a version.", "bridge_version_reset": "Bridge repo version changed to {{version}} because the previously selected version ({{previous}}) is no longer available.", "not_vectorised": "Not vectorised", + "semantic_search": "Semantic search", + "version_released_on": "Released {{date}}", + "target_repo_version_required": "Select a target repository version before saving.", "advanced_settings": "Advanced settings", "resolution_namespace": "Resolution Namespace", "resolution_namespace_description": "Namespace passed to $resolveReference. When blank, defaults to {{owner}}. Drives which URL Registry entries apply when resolving canonical URLs.", diff --git a/src/i18n/locales/es/translations.json b/src/i18n/locales/es/translations.json index 41180134..75f09c51 100644 --- a/src/i18n/locales/es/translations.json +++ b/src/i18n/locales/es/translations.json @@ -747,6 +747,9 @@ "bridge_version_source_required": "Defina una URL de fuente de Bridge antes de elegir una versión.", "bridge_version_reset": "La versión del repositorio de Bridge se cambió a {{version}} porque la versión seleccionada anteriormente ({{previous}}) ya no está disponible.", "not_vectorised": "No vectorizado", + "semantic_search": "Búsqueda semántica", + "version_released_on": "Publicada el {{date}}", + "target_repo_version_required": "Seleccione una versión del repositorio objetivo antes de guardar.", "advanced_settings": "Configuración avanzada", "resolution_namespace": "Espacio de nombres de resolución", "resolution_namespace_description": "Espacio de nombres que se pasa a $resolveReference. Si se deja en blanco, usa {{owner}} por defecto. Esto determina qué entradas del Registro de URL se aplican al resolver URL canónicas.", diff --git a/src/i18n/locales/pt-BR/translations.json b/src/i18n/locales/pt-BR/translations.json index 303b8574..5fc3f368 100644 --- a/src/i18n/locales/pt-BR/translations.json +++ b/src/i18n/locales/pt-BR/translations.json @@ -768,6 +768,9 @@ "bridge_version_source_required": "Defina uma URL de Source Bridge antes de escolher uma versão.", "bridge_version_reset": "A versão do repositório Bridge foi alterada para {{version}} porque a versão selecionada anteriormente ({{previous}}) não está mais disponível.", "not_vectorised": "Não vetorizado", + "semantic_search": "Busca semântica", + "version_released_on": "Publicada em {{date}}", + "target_repo_version_required": "Selecione uma versão do repositório de destino antes de salvar.", "advanced_settings": "Configurações avançadas", "resolution_namespace": "Namespace de Resolução", "resolution_namespace_description": "Namespace passado para $resolveReference. Quando em branco, usa {{owner}} como padrão. Determina quais entradas do Registro de URL se aplicam ao resolver URLs canônicas.", diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json index 9953349a..5f585f68 100644 --- a/src/i18n/locales/zh/translations.json +++ b/src/i18n/locales/zh/translations.json @@ -772,6 +772,9 @@ "bridge_version_source_required": "请选择版本前先设置桥接源 URL。", "bridge_version_reset": "桥接仓库版本已更改为 {{version}},因为先前选择的版本({{previous}})已不可用。", "not_vectorised": "未向量化", + "semantic_search": "语义搜索", + "version_released_on": "发布于 {{date}}", + "target_repo_version_required": "保存前请选择目标存储库版本。", "advanced_settings": "高级设置", "resolution_namespace": "解析命名空间", "resolution_namespace_description": "传递给 $resolveReference 的命名空间。留空时默认使用 {{owner}}。它决定解析规范 URL 时适用哪些 URL 注册表条目。", From ee9c85eeff08bf7620ace8c575b58e4b7ceb6e7b Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Wed, 14 Oct 2026 12:44:49 -0400 Subject: [PATCH 2/2] closes OpenConceptLab/ocl_issues#2906 | Reuse the llm check, and let the badges name their i18n keys From the simplification review: supportsSemanticSearch repeated isLLMBridgeRepoVersion, so the target helpers call that instead. getTargetRepoVersionBadges returns the badges' i18n keys, which drops the form's lookup table. Two tests repeated others: the LOINC default test can't tell the semantic preference from taking the latest release, and the latest-release assertions are pinned by the badge tests. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HgMg6pPngQoQJzZToPti27 --- src/components/map-projects/ConfigurationForm.jsx | 7 +------ .../map-projects/__tests__/projectTargetRepo.test.js | 12 +++--------- src/components/map-projects/projectTargetRepo.js | 12 +++++------- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/components/map-projects/ConfigurationForm.jsx b/src/components/map-projects/ConfigurationForm.jsx index 83cdf833..d0457a8f 100644 --- a/src/components/map-projects/ConfigurationForm.jsx +++ b/src/components/map-projects/ConfigurationForm.jsx @@ -61,11 +61,6 @@ const VisuallyHiddenInput = styled('input')({ const deriveCanonicalUrl = relativeUrl => relativeUrl ? `https://ns.openconceptlab.org${relativeUrl}` : '' -const VERSION_BADGE_LABELS = { - latest: 'common.latest', - semantic_search: 'map_project.semantic_search', -} - const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, name, setName, description, setDescription, repo, onRepoChange, repoVersion, setRepoVersion, showTargetRepoVersionError, versions, mappedSources, targetSourcesFromRows, algosSelected, setAlgosSelected, sx, algos, validColumns, columns, isValidColumnValue, updateColumn, configure, setConfigure, columnVisibilityModel, setColumnVisibilityModel, onSave, isSaving, candidatesScore, onScoreChange, includeDefaultFilter, setIncludeDefaultFilter, filters, setFilters, locales, isLoadingLocales, setAIAssistantColumns, AIAssistantColumns, inAIAssistantGroup, lookupConfig, setLookupConfig, encoderModel, setEncoderModel, isCoreUser, requestLimits, canSelectAIModel, canSetAIOutputLocale, canBridge, canScispacy, canUseOrgProjects=true, canUseCustomAlgorithms=true, promptTemplates, promptTemplate, onPromptTemplateChange, AIModels, AIModel, setAIModel, namespace, setNamespace, promptOutputLocale, setPromptOutputLocale, inputLocale, setInputLocale, oclLocales, useLexicalVariants, setUseLexicalVariants }) => { const { t } = useTranslation(); const user = getCurrentUser() @@ -253,7 +248,7 @@ const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, n 'map_project.target_repo_version_required', 'Select a target repository version before saving.' ) : ''} - getOptionBadges={option => getTargetRepoVersionBadges(option, versions).map(badge => t(VERSION_BADGE_LABELS[badge]))} + getOptionBadges={option => getTargetRepoVersionBadges(option, versions).map(key => t(key))} getOptionRightText={option => { const releaseDate = getTargetRepoVersionReleaseDate(option) return releaseDate ? t('map_project.version_released_on', {date: formatDate(releaseDate)}) : '' diff --git a/src/components/map-projects/__tests__/projectTargetRepo.test.js b/src/components/map-projects/__tests__/projectTargetRepo.test.js index 0ef0e4b4..20549109 100644 --- a/src/components/map-projects/__tests__/projectTargetRepo.test.js +++ b/src/components/map-projects/__tests__/projectTargetRepo.test.js @@ -56,10 +56,6 @@ test('getProjectTargetRepoVersion returns empty when the project has no pinned v ) }) -test('getDefaultTargetRepoVersion picks the latest release that supports semantic search', () => { - assert.equal(getDefaultTargetRepoVersion(LOINC_VERSIONS), LOINC_VERSIONS[0]) -}) - test('getDefaultTargetRepoVersion skips a newer release without semantic search', () => { const versions = [ { id: '2.82', released: true, match_algorithms: ['es'] }, @@ -109,16 +105,14 @@ test('getSavedTargetRepoVersion stays unset when the saved version is gone', () assert.equal(getSavedTargetRepoVersion(undefined, '2.81'), false) }) -test('getLatestReleasedTargetRepoVersion takes the first release the API lists', () => { - assert.equal(getLatestReleasedTargetRepoVersion(LOINC_VERSIONS), LOINC_VERSIONS[0]) - assert.equal(getLatestReleasedTargetRepoVersion([{ id: 'HEAD', released: false }]), null) +test('getLatestReleasedTargetRepoVersion is null without a versions list', () => { assert.equal(getLatestReleasedTargetRepoVersion(undefined), null) }) test('getTargetRepoVersionBadges marks the latest release and semantic search', () => { assert.deepEqual( LOINC_VERSIONS.map(version => getTargetRepoVersionBadges(version, LOINC_VERSIONS)), - [['latest', 'semantic_search'], [], [], []] + [['common.latest', 'map_project.semantic_search'], [], [], []] ) }) @@ -129,7 +123,7 @@ test('getTargetRepoVersionBadges marks no version latest when nothing is release ] assert.deepEqual( versions.map(version => getTargetRepoVersionBadges(version, versions)), - [[], ['semantic_search']] + [[], ['map_project.semantic_search']] ) }) diff --git a/src/components/map-projects/projectTargetRepo.js b/src/components/map-projects/projectTargetRepo.js index 360f59f6..5ed190d2 100644 --- a/src/components/map-projects/projectTargetRepo.js +++ b/src/components/map-projects/projectTargetRepo.js @@ -4,10 +4,8 @@ export const getTargetRepoVersionFromUrl = url => { return parts[4] || '' } -export const isLLMBridgeRepoVersion = version => version?.match_algorithms?.includes('llm') - // The OCL Semantic Algorithm runs only on versions vectorized for it. -export const supportsSemanticSearch = version => Boolean(version?.match_algorithms?.includes('llm')) +export const isLLMBridgeRepoVersion = version => version?.match_algorithms?.includes('llm') // The API lists versions newest first. export const getLatestReleasedTargetRepoVersion = versions => @@ -19,7 +17,7 @@ export const getDefaultTargetRepoVersion = versions => { if(!Array.isArray(versions)) return false - return versions.find(version => version?.released && supportsSemanticSearch(version)) || + return versions.find(version => version?.released && isLLMBridgeRepoVersion(version)) || getLatestReleasedTargetRepoVersion(versions) || versions.find(version => version?.id === 'HEAD') || false @@ -34,9 +32,9 @@ export const getTargetRepoVersionBadges = (version, versions) => { const latestReleasedVersion = getLatestReleasedTargetRepoVersion(versions) const badges = [] if(latestReleasedVersion && version?.id === latestReleasedVersion.id) - badges.push('latest') - if(supportsSemanticSearch(version)) - badges.push('semantic_search') + badges.push('common.latest') + if(isLLMBridgeRepoVersion(version)) + badges.push('map_project.semantic_search') return badges }