From e8fb43a00bf37b104d40ff16f6d629b06b1c678a Mon Sep 17 00:00:00 2001 From: XingY Date: Sun, 29 Mar 2026 16:38:20 -0700 Subject: [PATCH 1/5] GitHub Issue 925: Not providing a MVTC column in an assay import throws error --- api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java b/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java index ea3243e7a5f..7060ae26d9e 100644 --- a/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java +++ b/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java @@ -867,6 +867,7 @@ else if (entry.getKey().equalsIgnoreCase(ProvenanceService.PROVENANCE_INPUT_PROP if (PropertyType.MULTI_CHOICE == pd.getPropertyType()) { o = MultiChoice.Converter.getInstance().convert(MultiChoice.Array.class, o); + map.put(pd.getName(), o); } else if (o instanceof String) { From b93a7b943c60811cea33da97ac047aecbee93595 Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 30 Mar 2026 08:43:00 -0700 Subject: [PATCH 2/5] GitHub Issue 949 & 1014 --- .../labkey/api/exp/property/DomainUtil.java | 97 +++++++++++++++++-- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/api/src/org/labkey/api/exp/property/DomainUtil.java b/api/src/org/labkey/api/exp/property/DomainUtil.java index 010fee00f0c..51c75b2c2d7 100644 --- a/api/src/org/labkey/api/exp/property/DomainUtil.java +++ b/api/src/org/labkey/api/exp/property/DomainUtil.java @@ -30,17 +30,21 @@ import org.labkey.api.collections.LongHashMap; import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.ColumnRenderPropertiesImpl; +import org.labkey.api.data.CompareType; import org.labkey.api.data.ConditionalFormat; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerFilter; import org.labkey.api.data.ContainerManager; import org.labkey.api.data.ContainerService; import org.labkey.api.data.CoreSchema; +import org.labkey.api.data.MultiChoice; import org.labkey.api.data.NameGenerator; import org.labkey.api.data.PHI; import org.labkey.api.data.PropertyStorageSpec; +import org.labkey.api.data.SQLFragment; import org.labkey.api.data.SchemaTableInfo; import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.SqlSelector; import org.labkey.api.data.TableInfo; import org.labkey.api.data.TableInfo.IndexDefinition; import org.labkey.api.data.TableSelector; @@ -58,6 +62,7 @@ import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.exp.api.SampleTypeDomainKind; import org.labkey.api.exp.api.StorageProvisioner; +import org.labkey.api.exp.query.ExpSchema; import org.labkey.api.gwt.client.AuditBehaviorType; import org.labkey.api.gwt.client.DefaultScaleType; import org.labkey.api.gwt.client.FacetingBehaviorType; @@ -87,6 +92,7 @@ import org.labkey.api.util.JdbcUtil; import org.labkey.api.util.JsonUtil; import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.Pair; import org.labkey.api.util.StringExpression; import org.labkey.api.util.StringUtilsLabKey; import org.labkey.api.view.UnauthorizedException; @@ -909,6 +915,8 @@ public static ValidationException updateDomainDescriptor(GWTDomain defaultValues = new HashMap<>(); Map>> textChoiceValueUpdates = new HashMap<>(); + TableInfo domainTable = null; + // and now update properties for (GWTPropertyDescriptor pd : update.getFields()) { @@ -936,7 +944,9 @@ public static ValidationException updateDomainDescriptor(GWTDomain> propTextChoiceValueUpdates = updatePropertyValidators(p, old, pd); + var textChoiceUpdates = updatePropertyValidators(p, old, pd); + List> propTextChoiceValueUpdates = textChoiceUpdates.first; + List deletedValues = textChoiceUpdates.second; if (propTextChoiceValueUpdates != null && !propTextChoiceValueUpdates.isEmpty()) { if (PropertyType.MULTI_CHOICE.getTypeUri().equals(old.getRangeURI()) || PropertyType.MULTI_CHOICE.getTypeUri().equals(pd.getRangeURI())) @@ -947,6 +957,59 @@ public static ValidationException updateDomainDescriptor(GWTDomain>> entry : textChoiceValueUpdates.entrySet()) { for (Map valueUpdate : entry.getValue()) - updateTextChoiceValueRows(d, user, entry.getKey().getName(), valueUpdate, validationException); + updateTextChoiceValueRows(d, user, entry.getKey(), valueUpdate, validationException); } // update indices - add missing and drop those that aren't included in domain info @@ -1279,10 +1342,12 @@ private static void _copyProperties(DomainProperty to, GWTPropertyDescriptor fro to.setDerivationDataScope(from.getDerivationDataScope()); } - private static List> updatePropertyValidators(DomainProperty dp, @Nullable GWTPropertyDescriptor oldPd, GWTPropertyDescriptor newPd) + // Returns list of value updates and list of deleted values for text choice validators. Only returns if we have an oldPd to compare to, otherwise we don't know what the deleted values are. + private static @NotNull Pair>, List> updatePropertyValidators(DomainProperty dp, @Nullable GWTPropertyDescriptor oldPd, GWTPropertyDescriptor newPd) { Map newProps = new LongHashMap<>(); List> valueUpdates = new ArrayList<>(); + List deletedValues = new ArrayList<>(); PropertyDescriptor oldPropertyDescriptor = dp.getPropertyDescriptor().clone(); boolean hasChange = false; @@ -1325,7 +1390,21 @@ else if (prop == null) // update any new or changed for (IPropertyValidator pv : dp.getValidators()) { - boolean change = _copyValidator(pv, newProps.get(pv.getRowId())); + var gpv = newProps.get(pv.getRowId()); + boolean hasExpressionChange = !Objects.equals(pv.getExpressionValue(), gpv.getExpression());// + if (hasExpressionChange && PropertyValidatorType.TextChoice.equals(gpv.getType())) + { + List oldValidValues = PropertyService.get().getTextChoiceValidatorOptions(pv); + + List newValidValues = PageFlowUtil.splitStringToValues(gpv.getExpression(), '|'); + deletedValues = new ArrayList<>(oldValidValues); + deletedValues.removeAll(newValidValues); + // Exclude renamed oldValidValues from deletedValues — their keys in valueUpdates are the old names + for (Map update : valueUpdates) + deletedValues.removeAll(update.keySet()); + } + boolean change = _copyValidator(pv, gpv); + hasChange = hasChange || change; } @@ -1337,13 +1416,17 @@ else if (prop == null) if (hasChange) dp.setOldPropertyDescriptor(oldPropertyDescriptor); // mark dirty as needed - return oldPd != null ? valueUpdates : null; + return Pair.of(valueUpdates, deletedValues); } - private static void updateTextChoiceValueRows(Domain domain, User user, String propName, Map valueUpdates, ValidationException errors) + private static void updateTextChoiceValueRows(Domain domain, User user, DomainProperty prop, Map valueUpdates, ValidationException errors) { if (domain != null && domain.getDomainKind() != null) { + String propName = prop.getName(); + // GitHub Issue 1014: Text choice value update doesn't update aliquot's aliquot-specific field values + boolean isParentOnlyField = StringUtils.isEmpty(prop.getDerivationDataScope()) + || ExpSchema.DerivationDataScopeType.ParentOnly.name().equalsIgnoreCase(prop.getDerivationDataScope()); // using ContainerFilter.EVERYTHING to account for /Shared domains TableInfo domainTable = domain.getDomainKind().getTableInfo(user, domain.getContainer(), domain, ContainerFilter.getUnsafeEverythingFilter()); if (domainTable != null && domainTable.getUpdateService() != null) @@ -1358,7 +1441,7 @@ private static void updateTextChoiceValueRows(Domain domain, User user, String p // query for the row PKs of domain rows that have the original text choice value SimpleFilter filter = new SimpleFilter(FieldKey.fromParts(propName), entry.getKey()); // filter out aliquots for sample type domain - if (domain.getDomainKind() instanceof SampleTypeDomainKind) + if (domain.getDomainKind() instanceof SampleTypeDomainKind && isParentOnlyField) filter.addCondition(FieldKey.fromParts("IsAliquot"), false); List columns = new ArrayList<>(domainTable.getPkColumns()); if (domainTable.getContainerFieldKey() != null) From 392a75a2b6694d3139cb1d021a546c9b617f1398 Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 30 Mar 2026 11:45:53 -0700 Subject: [PATCH 3/5] null check --- api/src/org/labkey/api/exp/property/DomainUtil.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/org/labkey/api/exp/property/DomainUtil.java b/api/src/org/labkey/api/exp/property/DomainUtil.java index 51c75b2c2d7..d4730287e23 100644 --- a/api/src/org/labkey/api/exp/property/DomainUtil.java +++ b/api/src/org/labkey/api/exp/property/DomainUtil.java @@ -1391,7 +1391,10 @@ else if (prop == null) for (IPropertyValidator pv : dp.getValidators()) { var gpv = newProps.get(pv.getRowId()); - boolean hasExpressionChange = !Objects.equals(pv.getExpressionValue(), gpv.getExpression());// + if (gpv == null) + continue; + + boolean hasExpressionChange = !Objects.equals(pv.getExpressionValue(), gpv.getExpression()); if (hasExpressionChange && PropertyValidatorType.TextChoice.equals(gpv.getType())) { List oldValidValues = PropertyService.get().getTextChoiceValidatorOptions(pv); From d52152cfc79a76d11ad889f28864fcdfba68a763 Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 30 Mar 2026 16:06:27 -0700 Subject: [PATCH 4/5] code review changes --- .../labkey/api/exp/property/DomainUtil.java | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/api/src/org/labkey/api/exp/property/DomainUtil.java b/api/src/org/labkey/api/exp/property/DomainUtil.java index d4730287e23..c5e1c6e6b78 100644 --- a/api/src/org/labkey/api/exp/property/DomainUtil.java +++ b/api/src/org/labkey/api/exp/property/DomainUtil.java @@ -37,7 +37,6 @@ import org.labkey.api.data.ContainerManager; import org.labkey.api.data.ContainerService; import org.labkey.api.data.CoreSchema; -import org.labkey.api.data.MultiChoice; import org.labkey.api.data.NameGenerator; import org.labkey.api.data.PHI; import org.labkey.api.data.PropertyStorageSpec; @@ -983,26 +982,19 @@ else if (PropertyType.MULTI_CHOICE.getTypeUri().equals(old.getRangeURI())) if (column != null) { - SQLFragment sql = new SQLFragment("SELECT DISTINCT ") - .appendIdentifier(column.getAlias()) - .append(" FROM ") - .append(domainTable); - var choiceResults = new SqlSelector(domainTable.getSchema().getScope(), sql).getArrayList(String.class); - for (String resultArrayStr : choiceResults) + var dialect = domainTable.getSchema().getSqlDialect(); + SQLFragment deletedArray = new SQLFragment("CAST(? AS TEXT[])").add(deletedValues.toArray(new String[0])); + SQLFragment columnFrag = new SQLFragment().appendIdentifier(column.getAlias()); + + SQLFragment sql = new SQLFragment("SELECT 1 FROM ") + .append(domainTable) + .append(" WHERE ") + .append(dialect.array_some_in_array(columnFrag, deletedArray)); + + if (new SqlSelector(domainTable.getSchema().getScope(), sql).exists()) { - var valueArray = MultiChoice.Array.parsePgArray(resultArrayStr); - // if valueArray contains any of deletedValues, add validationException - if (valueArray != null) - { - for (String deletedValue : deletedValues) - { - if (valueArray.contains(deletedValue)) - { - validationException.addError(new SimpleValidationError("One or more values cannot be removed from the multi-choice list because they are in use: " + deletedValue)); - return validationException; - } - } - } + validationException.addError(new SimpleValidationError("One or more values cannot be removed from the multi-choice list because they are in use: " + StringUtils.join(deletedValues, ", "))); + return validationException; } } } From 4417dcf0b5783177dc568048da96d601cf1cf92d Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 30 Mar 2026 16:08:00 -0700 Subject: [PATCH 5/5] api tests --- .../integration/AssayImportRunAction.ispec.ts | 258 +++++++++++++++++- .../test/integration/DataClassCrud.ispec.ts | 26 +- .../src/client/test/integration/utils.ts | 20 ++ 3 files changed, 277 insertions(+), 27 deletions(-) diff --git a/experiment/src/client/test/integration/AssayImportRunAction.ispec.ts b/experiment/src/client/test/integration/AssayImportRunAction.ispec.ts index 04f1105f12e..8c13782acc4 100644 --- a/experiment/src/client/test/integration/AssayImportRunAction.ispec.ts +++ b/experiment/src/client/test/integration/AssayImportRunAction.ispec.ts @@ -10,9 +10,18 @@ import { ImportRunOptions, importRunToServer, initProject, + MVTC_FIELD_PROP, options, + TC_FIELD_PROP, } from './utils'; -import { ASSAY_DESIGNER_ROLE, caseInsensitive, EXPERIMENT_AUDIT_EVENT, RANGE_URIS, Row } from '@labkey/components'; +import { + ASSAY_DESIGNER_ROLE, + caseInsensitive, + EXPERIMENT_AUDIT_EVENT, + IDomainField, + RANGE_URIS, + Row +} from '@labkey/components'; // @ts-expect-error process is not available in a browser environment const server = hookServer(process.env); @@ -21,16 +30,47 @@ const BATCH_FILE_FIELD_NAME = 'batchFileField'; const BATCH_FILE_FIELD_TWO_NAME = 'batchFile2Field'; const RUN_FILE_FIELD_NAME = 'runFileField'; const RUN_FILE_FIELD_TWO_NAME = 'runFile2Field'; +const RUN_TEXT_CHOICE_FIELD_NAME = 'runTextChoiceField'; const RESULT_FIELD_NAME = 'resultStringField'; const RESULT_FILE_FIELD_NAME = 'resultFileField'; +const RESULT_TC_FIELD_NAME = 'resultTextChoiceField'; +const RESULT_MVTC_FIELD_NAME = 'resultMultiChoiceField'; + let context; let ASSAY_A_ID: number; +let ASSAY_A_DESIGN: any; const ASSAY_A_NAME = 'AssayA'; +let supportMultiChoice = false; + beforeAll(async () => { context = await initProject(server, PROJECT_NAME, ASSAY_DESIGNER_ROLE, ['assay', 'experiment']); - const assayFields: AssayDesignFieldOptions = { + let supportMultiChoice = false; + const createTestPayload = { + kind: 'DataClass', + domainDesign: { name: 'Test_mvtc_support_check', fields: [{ name: 'Prop' }] }, + options: { + name: "Test_mvtc_support_check", + } + }; + + await server.post('property', 'createDomain', createTestPayload).expect((result) => { + const domain = JSON.parse(result.text); + supportMultiChoice = domain.allowMultiChoiceProperties; + return true; + }); + + let resultFields = [ + createDomainField({ name: RESULT_FIELD_NAME }), + createDomainField({ name: RESULT_FILE_FIELD_NAME, rangeURI: RANGE_URIS.FILELINK }), + createDomainField({name: RESULT_TC_FIELD_NAME, ...TC_FIELD_PROP} as Partial) + ]; + + if (supportMultiChoice) + resultFields.push(createDomainField({name: RESULT_MVTC_FIELD_NAME, ...MVTC_FIELD_PROP} as Partial)); + + let assayFields: AssayDesignFieldOptions = { batchFields: [ createDomainField({ name: BATCH_FILE_FIELD_NAME, rangeURI: RANGE_URIS.FILELINK }), createDomainField({ name: BATCH_FILE_FIELD_TWO_NAME, rangeURI: RANGE_URIS.FILELINK }), @@ -38,14 +78,14 @@ beforeAll(async () => { runFields: [ createDomainField({ name: RUN_FILE_FIELD_NAME, rangeURI: RANGE_URIS.FILELINK }), createDomainField({ name: RUN_FILE_FIELD_TWO_NAME, rangeURI: RANGE_URIS.FILELINK }), + createDomainField({name: RUN_TEXT_CHOICE_FIELD_NAME, ...TC_FIELD_PROP} as Partial) ], - resultFields: [ - createDomainField({ name: RESULT_FIELD_NAME }), - createDomainField({ name: RESULT_FILE_FIELD_NAME, rangeURI: RANGE_URIS.FILELINK }), - ], + resultFields, }; + const assayA = await createAssayDesign(server, ASSAY_A_NAME, assayFields, context.topFolderOptions); ASSAY_A_ID = assayA.protocolId; + ASSAY_A_DESIGN = assayA; }); afterAll(async () => { @@ -530,4 +570,210 @@ describe('assay-importRun.api', () => { expect(auditLogs.length).toBe(0); }); }); + + describe('text choice fields', () => { + function getUpdateField(domainFieldFull: any) { + // only keep the following properties for the update payload since the saveProtocol.api doesn't expect the other properties that come through in the assay design response + const propertiesToKeep = [ + 'name', + 'label', + 'type', + 'propertyValidators', + 'required', + 'mvEnabled', + 'multiValued', + 'propertyId', + 'container', + 'conceptURI', + 'rangeURI', + 'format', + 'propertyURI' + ]; + + return Object.fromEntries(Object.entries(domainFieldFull).filter(([key]) => propertiesToKeep.includes(key))); + } + + // Build a saveProtocol.api payload that modifies the text choice validator expression + // for a specific field in the given domain (0=batch, 1=run, 2=data/results). + function buildAssayUpdatePayload( + domainIndex: number, + fieldName: string, + newExpression: string + ): any { + const domains = ASSAY_A_DESIGN.domains.map((domain: any, i: number) => ({ + domainId: domain.domainId, + domainURI: domain.domainURI, + name: domain.name, + fields: domain.fields.map((field: any) => { + const f = getUpdateField(field); + if (i === domainIndex && f.name === fieldName) { + const validators = f.propertyValidators[0] || {}; + return { + ...f, + propertyValidators: [{ + ...validators, + type: 'TextChoice', + name: 'Text Choice Validator', + new: true, + expression: newExpression, + }], + }; + } + return f; + }), + })); + + return { + protocolId: ASSAY_A_DESIGN.protocolId, + name: ASSAY_A_NAME, + providerName: 'General', + allowEditableResults: true, + editableResults: true, + editableRuns: true, + status: 'Active', + domains, + }; + } + + // GitHub Issue 949: Text choice value can be deleted if usage is added after loading designer + it('blocks deleting in-use run text choice value', async () => { + const { topFolderOptions } = context; + + // Import a run with 'Abnormal' as the run text choice value + const importPayload: ImportRunOptions = { + assayId: ASSAY_A_ID, + properties: { [RUN_TEXT_CHOICE_FIELD_NAME]: 'Abnormal' }, + dataRows: [{ [RESULT_FIELD_NAME]: 'tc-run-test' }], + }; + const importResponse = await importRunToServer(server, importPayload, topFolderOptions); + expect(importResponse.body.success).toEqual(true); + + // Try to remove 'Abnormal' from the run TC field's valid values + const updatePayload = buildAssayUpdatePayload(1, RUN_TEXT_CHOICE_FIELD_NAME, 'agent|cDNA|Plasma'); + const response = await server.post('assay', 'saveProtocol.api', updatePayload, topFolderOptions); + expect(response.text).toContain('One or more values cannot be removed from the text choice list'); + }); + + it('blocks deleting in-use result text choice value', async () => { + const { topFolderOptions } = context; + + // Import a run with 'agent' as the result text choice value + const importPayload: ImportRunOptions = { + assayId: ASSAY_A_ID, + dataRows: [{ [RESULT_FIELD_NAME]: 'tc-result-test', [RESULT_TC_FIELD_NAME]: 'agent' }], + }; + const importResponse = await importRunToServer(server, importPayload, topFolderOptions); + expect(importResponse.body.success).toEqual(true); + + // Try to remove 'agent' from the result TC field's valid values + const updatePayload = buildAssayUpdatePayload(2, RESULT_TC_FIELD_NAME, 'Abnormal|cDNA|Plasma'); + const response = await server.post('assay', 'saveProtocol.api', updatePayload, topFolderOptions); + expect(response.text).toContain('One or more values cannot be removed from the text choice list'); + }); + + it('blocks deleting in-use multi-choice value used as single value', async () => { + if (!supportMultiChoice) { + console.warn('Multi-choice properties are not supported in this environment, skipping multi-choice field tests'); + return; + } + + const { topFolderOptions } = context; + + // Import a run with 'Plasma' as a single multi-choice value + const importPayload: ImportRunOptions = { + assayId: ASSAY_A_ID, + dataRows: [{ [RESULT_FIELD_NAME]: 'mc-single-test', [RESULT_MVTC_FIELD_NAME]: ['Plasma'] }], + }; + const importResponse = await importRunToServer(server, importPayload, topFolderOptions); + expect(importResponse.body.success).toEqual(true); + + // Try to remove 'Plasma' from the multi-choice field's valid values + const updatePayload = buildAssayUpdatePayload(2, RESULT_MVTC_FIELD_NAME, 'Abnormal|agent|cDNA'); + const response = await server.post('assay', 'saveProtocol.api', updatePayload, topFolderOptions); + expect(response.text).toContain('One or more values cannot be removed from the multi-choice list'); + }); + + it('blocks deleting in-use multi-choice value used as part of an array value', async () => { + if (!supportMultiChoice) { + console.warn('Multi-choice properties are not supported in this environment, skipping multi-choice field tests'); + return; + } + + const { topFolderOptions } = context; + + // Import a run with ['Abnormal', 'cDNA'] as multi-choice values + const importPayload: ImportRunOptions = { + assayId: ASSAY_A_ID, + dataRows: [{ [RESULT_FIELD_NAME]: 'mc-array-test', [RESULT_MVTC_FIELD_NAME]: ['Abnormal', 'cDNA'] }], + }; + const importResponse = await importRunToServer(server, importPayload, topFolderOptions); + expect(importResponse.body.success).toEqual(true); + + // Try to remove 'cDNA' (used as part of a multi-value array) from the valid values + const updatePayload = buildAssayUpdatePayload(2, RESULT_MVTC_FIELD_NAME, 'Abnormal|agent|Plasma'); + const response = await server.post('assay', 'saveProtocol.api', updatePayload, topFolderOptions); + expect(response.text).toContain('One or more values cannot be removed from the multi-choice list'); + }); + + // GitHub Issue 925: Not providing a MVTC value in an assay result throws error + it('errors when required MVTC column not provided in assay import', async () => { + if (!supportMultiChoice) { + console.warn('Multi-choice properties are not supported in this environment, skipping multi-choice field tests'); + return; + } + + const { topFolderOptions } = context; + + // Create a separate assay with a required MVTC result field + const reqAssayFields: AssayDesignFieldOptions = { + resultFields: [ + createDomainField({ name: RESULT_FIELD_NAME }), + createDomainField({ name: RESULT_MVTC_FIELD_NAME, ...MVTC_FIELD_PROP, required: true } as Partial), + ], + }; + const reqAssay = await createAssayDesign(server, 'AssayRequiredMVTC', reqAssayFields, topFolderOptions); + + // Import without providing the required MVTC column in data rows + const importPayloadNoCol: ImportRunOptions = { + assayId: reqAssay.protocolId, + dataRows: [{ [RESULT_FIELD_NAME]: 'no-mvtc-column' }], + }; + let response = await importRunToServer(server, importPayloadNoCol, topFolderOptions); + expect(response.body.success).toBeFalsy(); + expect(response.body.exception).toContain(RESULT_MVTC_FIELD_NAME); + + // Import without blank required MVTC column in data rows + const importPayloadNull = { + assayId: reqAssay.protocolId, + dataRows: [{ [RESULT_FIELD_NAME]: 'mvtc-column-null', [RESULT_MVTC_FIELD_NAME]: null }], + }; + response = await importRunToServer(server, importPayloadNull, topFolderOptions); + expect(response.body.success).toBeFalsy(); + expect(response.body.exception).toContain(RESULT_MVTC_FIELD_NAME); + + const importPayloadBlank = { + assayId: reqAssay.protocolId, + dataRows: [{ [RESULT_FIELD_NAME]: 'mvtc-column-blank', [RESULT_MVTC_FIELD_NAME]: '' }], + }; + response = await importRunToServer(server, importPayloadBlank, topFolderOptions); + expect(response.body.success).toBeFalsy(); + expect(response.body.exception).toContain(RESULT_MVTC_FIELD_NAME); + + const importPayloadEmpty: ImportRunOptions = { + assayId: reqAssay.protocolId, + dataRows: [{ [RESULT_FIELD_NAME]: 'mvtc-column-empty-array', [RESULT_MVTC_FIELD_NAME]: [] }], + }; + response = await importRunToServer(server, importPayloadEmpty, topFolderOptions); + expect(response.body.success).toBeFalsy(); + expect(response.body.exception).toContain(RESULT_MVTC_FIELD_NAME); + + // Import with provided required MVTC column in data rows + const goodImportPayload = { + assayId: reqAssay.protocolId, + dataRows: [{ [RESULT_FIELD_NAME]: 'with-mvtc-column', [RESULT_MVTC_FIELD_NAME]: ['Abnormal', 'cDNA'] }], + }; + response = await importRunToServer(server, goodImportPayload, topFolderOptions); + expect(response.body.success).toBeTruthy(); + }); + }); }); diff --git a/experiment/src/client/test/integration/DataClassCrud.ispec.ts b/experiment/src/client/test/integration/DataClassCrud.ispec.ts index 6f0d615f647..dae73a5380c 100644 --- a/experiment/src/client/test/integration/DataClassCrud.ispec.ts +++ b/experiment/src/client/test/integration/DataClassCrud.ispec.ts @@ -15,6 +15,8 @@ import { generateFieldNameForImport, getDataClassRowIdByName, initProject, + MVTC_FIELD_PROP, + TC_FIELD_PROP, verifyRequiredLineageInsertUpdate, } from './utils'; import { caseInsensitive, DATA_CLASS_DESIGNER_ROLE } from '@labkey/components'; @@ -481,24 +483,6 @@ describe('Duplicate IDs', () => { describe('Multi Value Text Choice', () => { - const mvtcFieldProp = { - "propertyId": -1, - "propertyValidators": [ - { - "type": "TextChoice", - "name": "Text Choice Validator", - "new": true, - "expression": "Abnormal|agent|cDNA|Plasma" - } - ], - "rangeURI": "http://cpas.fhcrc.org/exp/xml#multiChoice", - }; - - const tcFieldProp = { - ...mvtcFieldProp, - rangeURI: 'http://www.w3.org/2001/XMLSchema#string', - conceptURI: 'http://www.labkey.org/types#textChoice', - } it("MVTC CRUD", async () => { @@ -525,7 +509,7 @@ describe('Multi Value Text Choice', () => { const fields = [ { - ...mvtcFieldProp, + ...MVTC_FIELD_PROP, name: fieldName } ]; @@ -750,7 +734,7 @@ describe('Multi Value Text Choice', () => { name: dataType, fields: [ { - ...mvtcFieldProp, + ...MVTC_FIELD_PROP, name: fieldName, required: true } @@ -774,7 +758,7 @@ describe('Multi Value Text Choice', () => { name: dataType, fields: [ { - ...tcFieldProp, + ...TC_FIELD_PROP, name: fieldName, propertyId, propertyURI diff --git a/experiment/src/client/test/integration/utils.ts b/experiment/src/client/test/integration/utils.ts index 13ccc830350..6acf022954c 100644 --- a/experiment/src/client/test/integration/utils.ts +++ b/experiment/src/client/test/integration/utils.ts @@ -20,6 +20,26 @@ export const ATTACHMENT_FIELD_2_NAME = 'SourceFile2'; const SAMPLE_TYPE_DOMAIN_KIND = 'SampleSet'; const DATA_CLASS_DOMAIN_KIND = 'DataClass'; +export const MVTC_FIELD_PROP = { + "propertyId": -1, + "propertyValidators": [ + { + "type": "TextChoice", + "name": "Text Choice Validator", + "new": true, + "expression": "Abnormal|agent|cDNA|Plasma" + } + ], + "rangeURI": "http://cpas.fhcrc.org/exp/xml#multiChoice", +}; + +export const TC_FIELD_PROP = { + ...MVTC_FIELD_PROP, + rangeURI: 'http://www.w3.org/2001/XMLSchema#string', + conceptURI: 'http://www.labkey.org/types#textChoice', +}; + + export function options(folderOptions?: RequestOptions, userOptions?: RequestOptions): RequestOptions { return folderOptions || userOptions ? { ...folderOptions, ...userOptions } : undefined; }