Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
5 changes: 5 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
isSetEqual,
joinMultiValueForExport,
makeCommaSeparatedString,
makeDataCountMsg,
parseScientificInt,
pronoun,
quoteValueWithDelimiters,
Expand Down Expand Up @@ -194,6 +195,7 @@ import {
} from './internal/components/editable/actions';
import {
clearSelected,
getDataClassesFromTransactionIds,
getGridIdsFromTransactionId,
getSampleTypesFromTransactionIds,
getSelected,
Expand Down Expand Up @@ -1420,6 +1422,7 @@ export {
getSamplesTestAPIWrapper,
getSampleTypeDetails,
getSampleTypesFromTransactionIds,
getDataClassesFromTransactionIds,
getSchemaQuery,
getSearchFilterObj,
getSearchFilterObjs,
Expand Down Expand Up @@ -1541,6 +1544,7 @@ export {
LOOK_AND_FEEL_METRIC,
LookupSelectInput,
makeCommaSeparatedString,
makeDataCountMsg,
makeQueryInfo,
makeTestActions,
makeTestISelectRowsResult,
Expand Down
99 changes: 82 additions & 17 deletions packages/components/src/internal/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -62,11 +64,18 @@ export function selectAll(
});
}

type DataTypeRowIdsFromTransactionIds = {
rowIds: string[];
dataTypeIds: Record<number, number>; // todo rename to count
dataTypes?: string[];
dataTypeNameIds?: Record<string, number>;
}

export async function getGridIdsFromTransactionId(
transactionAuditId: number | string,
dataType: string,
containerPath?: string
): Promise<string[]> {
): Promise<DataTypeRowIdsFromTransactionIds> {
if (!transactionAuditId) return;

const failureMsg = `There was a problem retrieving the ${dataType} from the last action.`;
Expand All @@ -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(
Expand All @@ -97,33 +110,85 @@ export async function selectGridIdsFromTransactionId(
transactionAuditId: number | string,
dataType: string,
actions: Actions
): Promise<string[]> {
): Promise<DataTypeRowIdsFromTransactionIds> {
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<SampleTypesFromTransactionIds> {
async function getDataTypesFromTransactionId(
transactionAuditId: number | string,
auditDataType: string,
schemaName: string,
queryName: string,
typeColumn: string
): Promise<DataTypeRowIdsFromTransactionIds> {
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<DataTypeRowIdsFromTransactionIds> {
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<DataTypeRowIdsFromTransactionIds> {
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 {
Expand Down
31 changes: 31 additions & 0 deletions packages/components/src/internal/util/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
splitMultiValueForImport,
stringToHtmlId,
styleStringToObj,
makeDataCountMsg,
toLowerSafe,
uncapitalizeFirstChar,
unorderedEqual,
Expand Down Expand Up @@ -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');
});
});
10 changes: 10 additions & 0 deletions packages/components/src/internal/util/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, number>): 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);
}
Loading