From 6413c3fb4be0a3cb9ad56d6c6af1ff8f2b6c0ea9 Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Thu, 5 Nov 2026 11:55:08 +0530 Subject: [PATCH 1/3] OpenConceptLab/ocl_issues#3010 | Collection version create asks which expansion to use --- src/components/repos/VersionForm.jsx | 103 +++++++++++++++++------ src/i18n/locales/en/translations.json | 9 ++ src/i18n/locales/es/translations.json | 9 ++ src/i18n/locales/pt-BR/translations.json | 9 ++ src/i18n/locales/zh/translations.json | 9 ++ 5 files changed, 111 insertions(+), 28 deletions(-) diff --git a/src/components/repos/VersionForm.jsx b/src/components/repos/VersionForm.jsx index 30c211e31..5816f4f1f 100644 --- a/src/components/repos/VersionForm.jsx +++ b/src/components/repos/VersionForm.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { withTranslation } from 'react-i18next'; import moment from 'moment'; -import { TextField, FormControlLabel, Checkbox, Autocomplete } from '@mui/material'; +import { TextField, FormControlLabel, Checkbox, Autocomplete, DialogContent, RadioGroup, Radio, Typography } from '@mui/material'; import { set, get, startCase, isBoolean, isObject, values, map } from 'lodash'; import APIService from '../../services/APIService'; import GAService from '../../services/GAService'; @@ -13,6 +13,8 @@ import { } from '../../common/validators'; import Button from '../common/Button' import CloseIconButton from '../common/CloseIconButton'; +import Dialog from '../common/Dialog' +import DialogTitle from '../common/DialogTitle' import { ID_REGEX } from '../../common/constants' @@ -35,6 +37,8 @@ class VersionForm extends FormComponent { expansion_url: {...fieldStruct}, revision_date: {...fieldStruct}, }, + expansionDialog: false, + expansion: null, } } @@ -57,37 +61,56 @@ class VersionForm extends FormComponent { onCheckboxChange = event => this.setFieldValue(event.target.name, event.target.checked) + isCollectionVersion = () => (this.props.resource || this.props.resourceType) === 'collection' + + canCopyHeadExpansion = () => Boolean(this.context?.toggles?.EXPANSION_LOCKED_REPO_VERSION_TOGGLE && this.props.version?.expansion_url) + onSubmit = event => { event.preventDefault(); event.stopPropagation(); - const isValid = this.setAllFieldsErrors() - if(isValid) { - const { version, edit, resource, resourceType } = this.props - if(version?.url) { - const isCollectionVersion = (resource || resourceType) === 'collection' - const payload = this.getValues() - - if(isBoolean(this.state.fields.released.value)) - payload.released = this.state.fields.released.value - if(this.state.fields.revision_date.value) - payload.revision_date = moment(payload.revision_date).utc().format('YYYY-MM-DD HH:mm:ss') - else - delete payload.revision_date - - if(isCollectionVersion) { - payload.autoexpand = isBoolean(this.state.fields.autoexpand.value) ? this.state.fields.autoexpand.value : false - payload.expansion_url = this.state.fields.expansion_url.value || null - } else { - delete payload.autoexpand - delete payload.expansion_url - } + if(this.setAllFieldsErrors()) { + if(!this.props.edit && this.isCollectionVersion()) + this.setState({expansionDialog: true, expansion: this.canCopyHeadExpansion() ? 'copy' : 'generate'}) + else + this.submit() + } + } + + onExpansionSubmit = () => { + this.setState({expansionDialog: false}) + this.submit(this.state.expansion) + } - const url = edit ? version.version_url : version.url - GAService.recordUpsertEvent(isCollectionVersion ? 'Collection Version' : 'Source Version', edit) - let service = APIService.new().overrideURL(url) - service = edit ? service.put(payload) : service.appendToUrl('versions/').post(payload) - service.then(response => this.handleSubmitResponse(response)) + submit = expansion => { + const { version, edit } = this.props + if(version?.url) { + const isCollectionVersion = this.isCollectionVersion() + const payload = this.getValues() + + if(isBoolean(this.state.fields.released.value)) + payload.released = this.state.fields.released.value + if(this.state.fields.revision_date.value) + payload.revision_date = moment(payload.revision_date).utc().format('YYYY-MM-DD HH:mm:ss') + else + delete payload.revision_date + + if(isCollectionVersion) { + payload.autoexpand = isBoolean(this.state.fields.autoexpand.value) ? this.state.fields.autoexpand.value : false + payload.expansion_url = this.state.fields.expansion_url.value || null + if(expansion) { + payload.expansion = expansion + payload.autoexpand = expansion !== 'none' + } + } else { + delete payload.autoexpand + delete payload.expansion_url } + + const url = edit ? version.version_url : version.url + GAService.recordUpsertEvent(isCollectionVersion ? 'Collection Version' : 'Source Version', edit) + let service = APIService.new().overrideURL(url) + service = edit ? service.put(payload) : service.appendToUrl('versions/').post(payload) + service.then(response => this.handleSubmitResponse(response)) } } @@ -206,7 +229,7 @@ class VersionForm extends FormComponent { /> { - resource === 'collection' && + resource === 'collection' && edit &&
} @@ -220,6 +243,30 @@ class VersionForm extends FormComponent {
+ this.setState({expansionDialog: false})} fullWidth maxWidth='xs'> + {t('repo.version.expansion_dialog.title')} + + this.setState({expansion: event.target.value})}> + { + map(this.canCopyHeadExpansion() ? ['copy', 'generate', 'none'] : ['generate', 'none'], choice => ( + } + sx={{alignItems: 'flex-start', marginBottom: '8px', '& .MuiRadio-root': {paddingTop: '2px'}}} + label={ + + {t(`repo.version.expansion_dialog.${choice}`)} + {t(`repo.version.expansion_dialog.${choice}_help`)} + + } + /> + )) + } + + ) } diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index 651147233..a664e2cbe 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -770,6 +770,15 @@ "new_title": "New {{resourceType}} Version: {{version}}", "edit_title": "Edit {{resourceType}} Version: {{version}}" }, + "expansion_dialog": { + "title": "Expansion for this version", + "copy": "Copy HEAD's expansion", + "copy_help": "Keeps what HEAD's expansion holds now: the same repository versions and members.", + "generate": "Generate a new expansion", + "generate_help": "Builds a new expansion from this version's references, resolving repository versions as they are now.", + "none": "No expansion", + "none_help": "Creates the version without an expansion. You can add one later." + }, "update_dialog": { "title": "Update {{resourceType}}: {{resourceId}}", "message": "Are you sure you want to {{action}} this {{resourceType}} {{repoId}}?" diff --git a/src/i18n/locales/es/translations.json b/src/i18n/locales/es/translations.json index 1cfc61a4c..9e565cd57 100644 --- a/src/i18n/locales/es/translations.json +++ b/src/i18n/locales/es/translations.json @@ -524,6 +524,15 @@ "form": { "new_title": "Nueva versión de {{resourceType}}: {{version}}", "edit_title": "Editar versión de {{resourceType}}: {{version}}" + }, + "expansion_dialog": { + "title": "Expansión para esta versión", + "copy": "Copiar la expansión de HEAD", + "copy_help": "Conserva lo que contiene ahora la expansión de HEAD: las mismas versiones de repositorio y los mismos miembros.", + "generate": "Generar una nueva expansión", + "generate_help": "Crea una nueva expansión a partir de las referencias de esta versión, resolviendo las versiones de repositorio tal como están ahora.", + "none": "Sin expansión", + "none_help": "Crea la versión sin expansión. Puede agregar una más tarde." } }, "repo_version": "Versión del repositorio" diff --git a/src/i18n/locales/pt-BR/translations.json b/src/i18n/locales/pt-BR/translations.json index 4bc9965f8..0299de152 100644 --- a/src/i18n/locales/pt-BR/translations.json +++ b/src/i18n/locales/pt-BR/translations.json @@ -759,6 +759,15 @@ "new_title": "Nova versão de {{resourceType}}: {{version}}", "edit_title": "Editar versão de {{resourceType}}: {{version}}" }, + "expansion_dialog": { + "title": "Expansão para esta versão", + "copy": "Copiar a expansão do HEAD", + "copy_help": "Mantém o que a expansão do HEAD contém agora: as mesmas versões de repositório e os mesmos membros.", + "generate": "Gerar uma nova expansão", + "generate_help": "Cria uma nova expansão a partir das referências desta versão, resolvendo as versões de repositório como estão agora.", + "none": "Sem expansão", + "none_help": "Cria a versão sem expansão. Você pode adicionar uma depois." + }, "update_dialog": { "title": "Atualizar {{resourceType}}: {{resourceId}}", "message": "Tem certeza de que deseja {{action}} este {{resourceType}} {{repoId}}?" diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json index 682ba6442..d71eca4d0 100644 --- a/src/i18n/locales/zh/translations.json +++ b/src/i18n/locales/zh/translations.json @@ -553,6 +553,15 @@ "form": { "new_title": "新建{{resourceType}}版本:{{version}}", "edit_title": "编辑{{resourceType}}版本:{{version}}" + }, + "expansion_dialog": { + "title": "此版本的扩展", + "copy": "复制 HEAD 的扩展", + "copy_help": "保留 HEAD 扩展当前的内容:相同的存储库版本和成员。", + "generate": "生成新的扩展", + "generate_help": "根据此版本的引用生成新的扩展,并按当前状态解析存储库版本。", + "none": "不扩展", + "none_help": "创建不带扩展的版本。之后可以再添加。" } }, "repo_version": "仓库版本" From 1670100df381e7a48913c7f931ebb08057bf271e Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Fri, 6 Nov 2026 11:56:16 +0530 Subject: [PATCH 2/3] OpenConceptLab/ocl_issues#3010 | Version popup offers lock, evaluate from scratch or none, only with the lock toggle on --- src/components/repos/VersionForm.jsx | 14 +++++++++----- src/i18n/locales/en/translations.json | 8 ++++---- src/i18n/locales/es/translations.json | 8 ++++---- src/i18n/locales/pt-BR/translations.json | 8 ++++---- src/i18n/locales/zh/translations.json | 8 ++++---- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/components/repos/VersionForm.jsx b/src/components/repos/VersionForm.jsx index 5816f4f1f..60d643a52 100644 --- a/src/components/repos/VersionForm.jsx +++ b/src/components/repos/VersionForm.jsx @@ -63,14 +63,18 @@ class VersionForm extends FormComponent { isCollectionVersion = () => (this.props.resource || this.props.resourceType) === 'collection' - canCopyHeadExpansion = () => Boolean(this.context?.toggles?.EXPANSION_LOCKED_REPO_VERSION_TOGGLE && this.props.version?.expansion_url) + isLockToggleOn = () => Boolean(this.context?.toggles?.EXPANSION_LOCKED_REPO_VERSION_TOGGLE) + + canLockRepoVersions = () => Boolean(this.isLockToggleOn() && this.props.version?.expansion_url) + + getExpansionChoices = () => this.canLockRepoVersions() ? ['lock', 'latest', 'none'] : ['latest', 'none'] onSubmit = event => { event.preventDefault(); event.stopPropagation(); if(this.setAllFieldsErrors()) { - if(!this.props.edit && this.isCollectionVersion()) - this.setState({expansionDialog: true, expansion: this.canCopyHeadExpansion() ? 'copy' : 'generate'}) + if(!this.props.edit && this.isCollectionVersion() && this.isLockToggleOn()) + this.setState({expansionDialog: true, expansion: this.canLockRepoVersions() ? 'lock' : 'latest'}) else this.submit() } @@ -229,7 +233,7 @@ class VersionForm extends FormComponent { /> { - resource === 'collection' && edit && + resource === 'collection' && (edit || !this.isLockToggleOn()) &&
} @@ -248,7 +252,7 @@ class VersionForm extends FormComponent { this.setState({expansion: event.target.value})}> { - map(this.canCopyHeadExpansion() ? ['copy', 'generate', 'none'] : ['generate', 'none'], choice => ( + map(this.getExpansionChoices(), choice => ( Date: Fri, 6 Nov 2026 12:18:28 +0530 Subject: [PATCH 3/3] OpenConceptLab/ocl_issues#3010 | Lock help names the value set limit; expansion choice logic tested --- src/components/repos/VersionForm.jsx | 27 +++++----- .../repos/__tests__/versionExpansion.test.js | 50 +++++++++++++++++++ src/components/repos/versionExpansion.js | 11 ++++ src/i18n/locales/en/translations.json | 2 +- src/i18n/locales/es/translations.json | 2 +- src/i18n/locales/pt-BR/translations.json | 2 +- src/i18n/locales/zh/translations.json | 2 +- 7 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 src/components/repos/__tests__/versionExpansion.test.js create mode 100644 src/components/repos/versionExpansion.js diff --git a/src/components/repos/VersionForm.jsx b/src/components/repos/VersionForm.jsx index 60d643a52..cad30db42 100644 --- a/src/components/repos/VersionForm.jsx +++ b/src/components/repos/VersionForm.jsx @@ -16,6 +16,9 @@ import CloseIconButton from '../common/CloseIconButton'; import Dialog from '../common/Dialog' import DialogTitle from '../common/DialogTitle' import { ID_REGEX } from '../../common/constants' +import { + shouldAskForExpansion, shouldShowAutoexpand, getExpansionChoices, getDefaultExpansion, getExpansionPayload +} from './versionExpansion' class VersionForm extends FormComponent { @@ -63,18 +66,19 @@ class VersionForm extends FormComponent { isCollectionVersion = () => (this.props.resource || this.props.resourceType) === 'collection' - isLockToggleOn = () => Boolean(this.context?.toggles?.EXPANSION_LOCKED_REPO_VERSION_TOGGLE) - - canLockRepoVersions = () => Boolean(this.isLockToggleOn() && this.props.version?.expansion_url) - - getExpansionChoices = () => this.canLockRepoVersions() ? ['lock', 'latest', 'none'] : ['latest', 'none'] + getExpansionArgs = () => ({ + edit: Boolean(this.props.edit), + isCollection: this.isCollectionVersion(), + lockToggle: Boolean(this.context?.toggles?.EXPANSION_LOCKED_REPO_VERSION_TOGGLE), + headExpansionURL: this.props.version?.expansion_url, + }) onSubmit = event => { event.preventDefault(); event.stopPropagation(); if(this.setAllFieldsErrors()) { - if(!this.props.edit && this.isCollectionVersion() && this.isLockToggleOn()) - this.setState({expansionDialog: true, expansion: this.canLockRepoVersions() ? 'lock' : 'latest'}) + if(shouldAskForExpansion(this.getExpansionArgs())) + this.setState({expansionDialog: true, expansion: getDefaultExpansion(this.getExpansionArgs())}) else this.submit() } @@ -101,10 +105,7 @@ class VersionForm extends FormComponent { if(isCollectionVersion) { payload.autoexpand = isBoolean(this.state.fields.autoexpand.value) ? this.state.fields.autoexpand.value : false payload.expansion_url = this.state.fields.expansion_url.value || null - if(expansion) { - payload.expansion = expansion - payload.autoexpand = expansion !== 'none' - } + Object.assign(payload, getExpansionPayload(expansion)) } else { delete payload.autoexpand delete payload.expansion_url @@ -233,7 +234,7 @@ class VersionForm extends FormComponent { />
{ - resource === 'collection' && (edit || !this.isLockToggleOn()) && + resource === 'collection' && shouldShowAutoexpand(this.getExpansionArgs()) &&
} @@ -252,7 +253,7 @@ class VersionForm extends FormComponent { this.setState({expansion: event.target.value})}> { - map(this.getExpansionChoices(), choice => ( + map(getExpansionChoices(this.getExpansionArgs()), choice => ( { + const args = {...create, lockToggle: false, headExpansionURL: head} + assert.equal(shouldAskForExpansion(args), false) + assert.equal(shouldShowAutoexpand(args), true) + assert.deepEqual(getExpansionPayload(undefined), {}) +}) + +test('with the lock toggle on and no HEAD expansion, the choices are latest and none, latest first', () => { + const args = {...create, lockToggle: true, headExpansionURL: null} + assert.equal(shouldAskForExpansion(args), true) + assert.equal(shouldShowAutoexpand(args), false) + assert.deepEqual(getExpansionChoices(args), ['latest', 'none']) + assert.equal(getDefaultExpansion(args), 'latest') +}) + +test('with the lock toggle on and a HEAD expansion, lock is offered and is the default', () => { + const args = {...create, lockToggle: true, headExpansionURL: head} + assert.deepEqual(getExpansionChoices(args), ['lock', 'latest', 'none']) + assert.equal(getDefaultExpansion(args), 'lock') +}) + +test('editing a version asks nothing and shows the checkbox, whatever the toggle', () => { + for(const lockToggle of [true, false]) { + const args = {edit: true, isCollection: true, lockToggle, headExpansionURL: head} + assert.equal(shouldAskForExpansion(args), false) + assert.equal(shouldShowAutoexpand(args), true) + } +}) + +test('source versions get neither the popup nor the checkbox', () => { + const args = {edit: false, isCollection: false, lockToggle: true, headExpansionURL: head} + assert.equal(shouldAskForExpansion(args), false) + assert.equal(shouldShowAutoexpand(args), false) +}) + +test('each choice maps to its expansion and autoexpand pair', () => { + assert.deepEqual(getExpansionPayload('lock'), {expansion: 'lock', autoexpand: true}) + assert.deepEqual(getExpansionPayload('latest'), {expansion: 'latest', autoexpand: true}) + assert.deepEqual(getExpansionPayload('none'), {expansion: 'none', autoexpand: false}) +}) diff --git a/src/components/repos/versionExpansion.js b/src/components/repos/versionExpansion.js new file mode 100644 index 000000000..28ad2c786 --- /dev/null +++ b/src/components/repos/versionExpansion.js @@ -0,0 +1,11 @@ +// How a new collection version's expansion is chosen. The popup is asked only with the lock toggle on. +export const shouldAskForExpansion = ({edit, isCollection, lockToggle}) => Boolean(!edit && isCollection && lockToggle) + +export const shouldShowAutoexpand = ({edit, isCollection, lockToggle}) => Boolean(isCollection && (edit || !lockToggle)) + +export const getExpansionChoices = ({lockToggle, headExpansionURL}) => + lockToggle && headExpansionURL ? ['lock', 'latest', 'none'] : ['latest', 'none'] + +export const getDefaultExpansion = args => getExpansionChoices(args)[0] + +export const getExpansionPayload = expansion => expansion ? {expansion, autoexpand: expansion !== 'none'} : {} diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index 31789a314..31def3043 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -773,7 +773,7 @@ "expansion_dialog": { "title": "Expansion for this version", "lock": "Lock repo versions", - "lock_help": "Uses the same repository versions as HEAD's expansion. They're saved in the new expansion's parameters, so a rebuild gives the same result.", + "lock_help": "Uses the same repository versions as HEAD's expansion. Source versions are saved in the new expansion's parameters, so a rebuild keeps them. Value sets referenced without a version can resolve again on a rebuild.", "latest": "Evaluate from scratch", "latest_help": "Evaluates every reference again. References without a version use each repository's latest released version, or its HEAD if it has no release.", "none": "No expansion", diff --git a/src/i18n/locales/es/translations.json b/src/i18n/locales/es/translations.json index 193e0ddb3..e8636647e 100644 --- a/src/i18n/locales/es/translations.json +++ b/src/i18n/locales/es/translations.json @@ -528,7 +528,7 @@ "expansion_dialog": { "title": "Expansión para esta versión", "lock": "Fijar versiones de repositorio", - "lock_help": "Usa las mismas versiones de repositorio que la expansión de HEAD. Se guardan en los parámetros de la nueva expansión, así que una reconstrucción da el mismo resultado.", + "lock_help": "Usa las mismas versiones de repositorio que la expansión de HEAD. Las versiones de fuentes se guardan en los parámetros de la nueva expansión, así que una reconstrucción las conserva. Los conjuntos de valores referenciados sin versión pueden resolverse de nuevo al reconstruir.", "latest": "Evaluar desde cero", "latest_help": "Evalúa de nuevo todas las referencias. Las referencias sin versión usan la última versión publicada de cada repositorio, o su HEAD si no tiene ninguna.", "none": "Sin expansión", diff --git a/src/i18n/locales/pt-BR/translations.json b/src/i18n/locales/pt-BR/translations.json index 5cf675dab..99ec7a721 100644 --- a/src/i18n/locales/pt-BR/translations.json +++ b/src/i18n/locales/pt-BR/translations.json @@ -762,7 +762,7 @@ "expansion_dialog": { "title": "Expansão para esta versão", "lock": "Fixar versões de repositório", - "lock_help": "Usa as mesmas versões de repositório da expansão do HEAD. Elas ficam salvas nos parâmetros da nova expansão, então uma reconstrução dá o mesmo resultado.", + "lock_help": "Usa as mesmas versões de repositório da expansão do HEAD. As versões de fontes ficam salvas nos parâmetros da nova expansão, então uma reconstrução as mantém. Conjuntos de valores referenciados sem versão podem ser resolvidos de novo ao reconstruir.", "latest": "Avaliar do zero", "latest_help": "Avalia todas as referências novamente. Referências sem versão usam a versão publicada mais recente de cada repositório, ou o HEAD se não houver nenhuma.", "none": "Sem expansão", diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json index df25caee1..1a0700afd 100644 --- a/src/i18n/locales/zh/translations.json +++ b/src/i18n/locales/zh/translations.json @@ -557,7 +557,7 @@ "expansion_dialog": { "title": "此版本的扩展", "lock": "锁定存储库版本", - "lock_help": "使用与 HEAD 扩展相同的存储库版本。这些版本保存在新扩展的参数中,因此重建会得到相同的结果。", + "lock_help": "使用与 HEAD 扩展相同的存储库版本。源版本保存在新扩展的参数中,因此重建会保留它们。未指定版本引用的值集在重建时可能会重新解析。", "latest": "从头评估", "latest_help": "重新评估每个引用。没有版本的引用使用每个存储库最新发布的版本;如果没有发布版本,则使用其 HEAD。", "none": "不扩展",