From d0b16337e808824ce3655081fb862425f249eeb0 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 6 May 2026 20:22:45 -0700 Subject: [PATCH] Molecule and PS bulk import by file --- packages/components/package-lock.json | 4 +- packages/components/package.json | 2 +- .../components/releaseNotes/components.md | 5 + packages/components/src/index.ts | 4 + packages/components/src/internal/actions.ts | 99 +++++++++++++++---- .../src/internal/util/utils.test.ts | 31 ++++++ .../components/src/internal/util/utils.ts | 10 ++ 7 files changed, 135 insertions(+), 20 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 3dc475b1a7..60d76fd888 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.34.0", + "version": "7.34.1-fb-moleculeImport.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.34.0", + "version": "7.34.1-fb-moleculeImport.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index b5f18ea532..91eea20cbb 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.34.0", + "version": "7.34.1-fb-moleculeImport.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 93374ac1b1..9e6402ee90 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,11 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version 7.X +*Released*: X May 2026 +- Molecule and PS bulk import by file + - TODO + ### version 7.34.0 *Released*: 5 May 2026 - Accessibility improvements for app pages: Colors diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 48919be9ef..931f355343 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -68,6 +68,7 @@ import { isSetEqual, joinMultiValueForExport, makeCommaSeparatedString, + makeDataCountMsg, parseScientificInt, pronoun, quoteValueWithDelimiters, @@ -194,6 +195,7 @@ import { } from './internal/components/editable/actions'; import { clearSelected, + getDataClassesFromTransactionIds, getGridIdsFromTransactionId, getSampleTypesFromTransactionIds, getSelected, @@ -1420,6 +1422,7 @@ export { getSamplesTestAPIWrapper, getSampleTypeDetails, getSampleTypesFromTransactionIds, + getDataClassesFromTransactionIds, getSchemaQuery, getSearchFilterObj, getSearchFilterObjs, @@ -1541,6 +1544,7 @@ export { LOOK_AND_FEEL_METRIC, LookupSelectInput, makeCommaSeparatedString, + makeDataCountMsg, makeQueryInfo, makeTestActions, makeTestISelectRowsResult, diff --git a/packages/components/src/internal/actions.ts b/packages/components/src/internal/actions.ts index 77a10200dd..15c5562923 100644 --- a/packages/components/src/internal/actions.ts +++ b/packages/components/src/internal/actions.ts @@ -43,8 +43,10 @@ import { resolveErrorMessage } from './util/messaging'; import { ViewInfo } from './ViewInfo'; import { createGridModelId } from './models'; -import { SAMPLES_KEY } from './app/constants'; +import { SAMPLES_KEY, SOURCES_KEY } from './app/constants'; import { SCHEMAS } from './schemas'; +import { selectRows } from './query/selectRows'; +import { caseInsensitive } from './util/utils'; export function selectAll( key: string, @@ -62,11 +64,18 @@ export function selectAll( }); } +type DataTypeRowIdsFromTransactionIds = { + rowIds: string[]; + dataTypeIds: Record; // todo rename to count + dataTypes?: string[]; + dataTypeNameIds?: Record; +} + export async function getGridIdsFromTransactionId( transactionAuditId: number | string, dataType: string, containerPath?: string -): Promise { +): Promise { if (!transactionAuditId) return; const failureMsg = `There was a problem retrieving the ${dataType} from the last action.`; @@ -88,7 +97,11 @@ export async function getGridIdsFromTransactionId( } // The server returns numbers, so we coerce to string; If we don't, it can lead to bugs (and has). - return response.rowIds.map(rowId => rowId.toString()); + const rowIds = response.rowIds.map(rowId => rowId.toString()); + return { + rowIds, + dataTypeIds: response['dataTypeIds'] + } } export async function selectGridIdsFromTransactionId( @@ -97,33 +110,85 @@ export async function selectGridIdsFromTransactionId( transactionAuditId: number | string, dataType: string, actions: Actions -): Promise { +): Promise { if (!transactionAuditId) return undefined; const modelId = createGridModelId(gridIdPrefix, schemaQuery); const selected = await getGridIdsFromTransactionId(transactionAuditId, dataType); - actions.replaceSelections(modelId, selected); + actions.replaceSelections(modelId, selected.rowIds); return selected; } -type SampleTypesFromTransactionIds = { rowIds: string[]; sampleTypes: string[] }; -export async function getSampleTypesFromTransactionIds( - transactionAuditId: number | string -): Promise { +async function getDataTypesFromTransactionId( + transactionAuditId: number | string, + auditDataType: string, + schemaName: string, + queryName: string, + typeColumn: string +): Promise { if (!transactionAuditId) return undefined; - const rowIds = await getGridIdsFromTransactionId(transactionAuditId, SAMPLES_KEY); - const sampleTypes = await selectDistinctRows({ - schemaName: SCHEMAS.EXP_TABLES.MATERIALS.schemaName, - queryName: SCHEMAS.EXP_TABLES.MATERIALS.queryName, - column: 'SampleSet/Name', + const { rowIds, dataTypeIds } = await getGridIdsFromTransactionId(transactionAuditId, auditDataType); + const distinct = await selectDistinctRows({ + schemaName, + queryName, + column: typeColumn, filterArray: [Filter.create('RowId', rowIds, Filter.Types.IN)], }); + return { rowIds, dataTypeIds, dataTypes: distinct.values }; +} + +export function getSampleTypesFromTransactionIds( + transactionAuditId: number | string +): Promise { + return getDataTypesFromTransactionId( + transactionAuditId, + SAMPLES_KEY, + SCHEMAS.EXP_TABLES.MATERIALS.schemaName, + SCHEMAS.EXP_TABLES.MATERIALS.queryName, + 'SampleSet/Name' + ); +} + +export async function getDataClassesFromTransactionIds( + transactionAuditId: number | string +): Promise { + const results = await getDataTypesFromTransactionId( + transactionAuditId, + SOURCES_KEY, + SCHEMAS.EXP_TABLES.DATA.schemaName, + SCHEMAS.EXP_TABLES.DATA.queryName, + 'DataClass/Name' + ); + + if (!results) + return undefined; + + const { dataTypeIds, dataTypes } = results; + const dataTypeLcMap = Object.fromEntries((dataTypes ?? []).map(dt => [dt.toLowerCase(), dt])); + + const dataTypeNameIds = {}; + if (dataTypeIds) { + const dataClasses = await selectRows({ + schemaQuery: SCHEMAS.EXP_TABLES.DATA_CLASSES, + columns: ['Name', 'RowId'], + filterArray: [Filter.create('rowId', Object.keys(dataTypeIds), Filter.Types.IN)], + containerFilter: Query.containerFilter.currentPlusProjectAndShared, + }) + + + dataClasses.rows.forEach(row => { + const dataClassLc = caseInsensitive(row, 'Name')?.value?.toLowerCase(); + const rowId = caseInsensitive(row, 'RowId').value; + dataTypeNameIds[dataTypeLcMap[dataClassLc]] = dataTypeIds[rowId]; + }); + } + return { - rowIds, - sampleTypes: sampleTypes.values, - }; + ...results, + dataTypeNameIds + } } export interface ExportOptions { diff --git a/packages/components/src/internal/util/utils.test.ts b/packages/components/src/internal/util/utils.test.ts index 2aba53a654..e5be850161 100644 --- a/packages/components/src/internal/util/utils.test.ts +++ b/packages/components/src/internal/util/utils.test.ts @@ -56,6 +56,7 @@ import { splitMultiValueForImport, stringToHtmlId, styleStringToObj, + makeDataCountMsg, toLowerSafe, uncapitalizeFirstChar, unorderedEqual, @@ -2081,3 +2082,33 @@ describe('stringToHtmlId', () => { expect(stringToHtmlId('my-id')).toBe('my-id'); }); }); + +describe('makeDataCountMsg', () => { + test('empty object', () => { + expect(makeDataCountMsg({})).toBe(''); + }); + + test('all zero or null counts are ignored', () => { + expect(makeDataCountMsg({ Molecule: 0, ProtSequence: null })).toBe(''); + }); + + test('single type, count of 1 — no pluralization', () => { + expect(makeDataCountMsg({ Molecule: 1 })).toBe('1 Molecule'); + }); + + test('single type, count > 1 — pluralized with s', () => { + expect(makeDataCountMsg({ Molecule: 3 })).toBe('3 Molecules'); + }); + + test('multiple types', () => { + expect(makeDataCountMsg({ Molecule: 2, Compound: 1 })).toBe('2 Molecules and 1 Compound'); + }); + + test('multiple types, some zero', () => { + expect(makeDataCountMsg({ Molecule: 2, ProtSequence: 0, Compound: 1 })).toBe('2 Molecules and 1 Compound'); + }); + + test('three types', () => { + expect(makeDataCountMsg({ Molecule: 1, ProtSequence: 4, Compound: 2 })).toBe('1 Molecule, 4 ProtSequences and 2 Compounds'); + }); +}); diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index 480ffd365e..fa251f7704 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -931,3 +931,13 @@ export function hasIdentifiedCol(schemaQuery: SchemaQuery): boolean { const isCompound = schemaQuery.isEqual(SCHEMAS.DATA_CLASSES.COMPOUND, false); return isNucSeq || isProtSeq || isMolecule || isCompound; } + +export function makeDataCountMsg(dataCounts: Record): string { + const parts = []; + for (const [noun, count] of Object.entries(dataCounts)) { + if (!count) continue; + parts.push(`${count} ${count > 1 ? noun + 's' : noun}`); + } + + return makeCommaSeparatedString(parts); +}