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.49.0",
"version": "7.49.1-fb-sq-detail.0",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ import { Filter, Query } from '@labkey/api';
import { List } from 'immutable';

import { QueryColumn } from '../../../public/QueryColumn';
import { SchemaQuery } from '../../../public/SchemaQuery';

import { LOOKUP_DEFAULT_SIZE } from '../../constants';

import { getContainerFilterForLookups } from '../../query/api';
import { ViewInfo } from '../../ViewInfo';
import { SelectInputChange } from '../forms/input/SelectInput';
import { TextChoiceInput } from '../forms/input/TextChoiceInput';
import { QuerySelect } from '../forms/QuerySelect';
Expand Down Expand Up @@ -80,11 +78,6 @@ const QueryLookupCell: FC<QueryLookupCellProps> = memo(props => {
);
}, [col, filteredLookupKeys, filteredLookupValues, forUpdate, lookupValueFilters]);

const schemaQuery = useMemo(
() => new SchemaQuery(lookup.schemaQuery.schemaName, lookup.schemaQuery.queryName, ViewInfo.DETAIL_NAME),
[lookup]
);

let selectValue = isMultiple ? rawValues : rawValues[0];

// Issue 49502: Some column types have special handling of raw data, i.e. Alias
Expand All @@ -109,7 +102,7 @@ const QueryLookupCell: FC<QueryLookupCellProps> = memo(props => {
onQSChange={onSelectChange}
preLoad
queryFilters={queryFilters}
schemaQuery={schemaQuery}
schemaQuery={lookup.schemaQuery.detailView}
value={selectValue}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
quoteValueWithDelimiters,
splitMultiValueForImport,
} from '../../util/utils';
import { ViewInfo } from '../../ViewInfo';

import { getContainerFilterForLookups } from '../../query/api';

Expand Down Expand Up @@ -306,8 +305,8 @@ const findLookupValues = async (options: FindLookupValuesOptions): Promise<Value
),
includeTotalCount: false,
maxRows: -1,
schemaQuery: lookup.schemaQuery,
viewName: ViewInfo.DETAIL_NAME, // Use the detail view so values that may be filtered out of the default view show up.
// Use the detail view so values that may be filtered out of the default view show up.
schemaQuery: lookup.schemaQuery.detailView,
});

return results.rows.reduce<ValueDescriptor[]>((desc, row) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,34 @@
* in any form or by any electronic or mechanical means without written permission from LabKey Corporation.
*/
import { Map } from 'immutable';
import { Filter } from '@labkey/api';

import { extractEntityTypeOptionFromRow, getChosenParentData, getFieldDisplayValue, sampleGenCellKey } from './actions';
import { SchemaQuery } from '../../../public/SchemaQuery';
import { Row, selectRows, SelectRowsOptions, SelectRowsResponse } from '../../query/selectRows';

import {
extractEntityTypeOptionFromRow,
getChosenParentData,
getFieldDisplayValue,
getSelectedParents,
sampleGenCellKey,
} from './actions';
import { EntityDataType, EntityIdCreationModel } from './models';
import { DataClassDataType, SampleTypeDataType } from './constants';

jest.mock('../../query/selectRows', () => ({
...jest.requireActual('../../query/selectRows'),
selectRows: jest.fn().mockResolvedValue({ rows: [] }),
}));

const mockSelectRows = selectRows as jest.MockedFunction<typeof selectRows>;

const getSelectRowsOptions = (): SelectRowsOptions => mockSelectRows.mock.calls[0][0];

const mockSelectRowsResponse = (rows: Row[]): void => {
mockSelectRows.mockResolvedValue({ rows } as SelectRowsResponse);
};

describe('extractEntityTypeOptionFromRow', () => {
const NAME = 'Test Name';
const ROW = {
Expand All @@ -33,8 +56,8 @@ describe('getChosenParentData', () => {

test('allowParents = false', async () => {
const result = await getChosenParentData(new EntityIdCreationModel(), PARENT_ENTITY_DATA_TYPES, false);
expect(result.originalParents).toBe(undefined);
expect(result.selectionKey).toBe(undefined);
expect(result.originalParents).toBeUndefined();
expect(result.selectionKey).toBeUndefined();
expect(result.entityParents.size).toBe(2);
expect(result.entityParents.get(SampleTypeDataType.typeListingSchemaQuery.queryName).size).toBe(0);
expect(result.entityParents.get(DataClassDataType.typeListingSchemaQuery.queryName).size).toBe(0);
Expand All @@ -50,8 +73,8 @@ describe('getChosenParentData', () => {
PARENT_ENTITY_DATA_TYPES,
true
);
expect(result.originalParents).toBe(undefined);
expect(result.selectionKey).toBe(undefined);
expect(result.originalParents).toBeUndefined();
expect(result.selectionKey).toBeUndefined();
expect(result.entityParents.size).toBe(2);
expect(result.entityParents.get(SampleTypeDataType.typeListingSchemaQuery.queryName).size).toBe(1);
expect(result.entityParents.get(DataClassDataType.typeListingSchemaQuery.queryName).size).toBe(0);
Expand All @@ -67,8 +90,8 @@ describe('getChosenParentData', () => {
PARENT_ENTITY_DATA_TYPES,
true
);
expect(result.originalParents).toBe(undefined);
expect(result.selectionKey).toBe(undefined);
expect(result.originalParents).toBeUndefined();
expect(result.selectionKey).toBeUndefined();
expect(result.entityParents.size).toBe(2);
expect(result.entityParents.get(SampleTypeDataType.typeListingSchemaQuery.queryName).size).toBe(0);
expect(result.entityParents.get(DataClassDataType.typeListingSchemaQuery.queryName).size).toBe(0);
Expand All @@ -93,7 +116,9 @@ describe('sampleGenCellKey', () => {

describe('getFieldDisplayValue', () => {
test('returns formattedValue when available', () => {
expect(getFieldDisplayValue({ formattedValue: 'formatted', displayValue: 'display', value: 'raw' })).toBe('formatted');
expect(getFieldDisplayValue({ formattedValue: 'formatted', displayValue: 'display', value: 'raw' })).toBe(
'formatted'
);
});

test('falls back to displayValue when formattedValue is undefined', () => {
Expand All @@ -120,3 +145,36 @@ describe('getFieldDisplayValue', () => {
expect(getFieldDisplayValue({ value: ['only'] })).toBe('only');
});
});

describe('getSelectedParents', () => {
const SAMPLE_SQ = new SchemaQuery('samples', 'Blood');
const DATA_CLASS_SQ = new SchemaQuery('exp.data', 'Ingredients');
const FILTERS = [Filter.create('RowId', [1, 2], Filter.Types.IN)];

beforeEach(() => {
mockSelectRows.mockClear();
mockSelectRowsResponse([]);
});

// GitHub Issue 1357: the selected parents must be resolved from the detail view so that filters applied to the
// default view (or to whichever view the selection came from) don't drop selected parents from the response.
test('queries the detail view for a sample parent', async () => {
await getSelectedParents(SAMPLE_SQ, FILTERS);

expect(mockSelectRows).toHaveBeenCalledTimes(1);
const { columns, filterArray, schemaQuery } = getSelectRowsOptions();
expect(SAMPLE_SQ.detailView.isEqual(schemaQuery)).toBe(true);
expect(columns).toEqual(['LSID', 'Name', 'RowId', 'SampleSet']);
expect(filterArray).toBe(FILTERS);
});

test('queries the detail view for a data class parent', async () => {
await getSelectedParents(DATA_CLASS_SQ, FILTERS);

expect(mockSelectRows).toHaveBeenCalledTimes(1);
const { columns, filterArray, schemaQuery } = getSelectRowsOptions();
expect(DATA_CLASS_SQ.detailView.isEqual(schemaQuery)).toBe(true);
expect(columns).toEqual(['LSID', 'Name', 'RowId', 'DataClass']);
expect(filterArray).toBe(FILTERS);
});
});
10 changes: 4 additions & 6 deletions packages/components/src/internal/components/entities/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ import { SCHEMAS } from '../../schemas';

import { Row, selectRows, SelectRowsResponse } from '../../query/selectRows';

import { ViewInfo } from '../../ViewInfo';

import { getAppHomeFolderPath, getFolderDataExclusion, hasModule } from '../../app/utils';

import { resolveErrorMessage } from '../../util/messaging';
Expand Down Expand Up @@ -259,7 +257,7 @@ export async function getOperationConfirmationDataForModel(
return getOperationConfirmationData(dataType, model.getSelectedIds(), undefined, undefined, extraParams);
}

async function getSelectedParents(
export async function getSelectedParents(
schemaQuery: SchemaQuery,
filterArray: Filter.IFilter[],
isAliquotParent?: boolean,
Expand All @@ -273,7 +271,8 @@ async function getSelectedParents(
columns.push('DataClass');
}

const response = await selectRows({ columns, filterArray, schemaQuery });
// GitHub Issue 1357: Resolve selected parents from details view to avoid filters applied to default view
const response = await selectRows({ columns, filterArray, schemaQuery: schemaQuery.detailView });

if (isSampleParent) {
return resolveSampleParentTypes(response, isAliquotParent, orderedRowIds);
Expand Down Expand Up @@ -817,8 +816,7 @@ async function getParentRowIdAndDataType(
): Promise<Record<string, ParentIdData>> {
const response = await selectRows({
containerPath,
schemaQuery: parentDataType.listingSchemaQuery,
viewName: ViewInfo.DETAIL_NAME, // use this to avoid filters on the default view
schemaQuery: parentDataType.listingSchemaQuery.detailView, // use this to avoid filters on the default view
columns: 'LSID, RowId, DataClass, SampleSet', // only one of DataClass or SampleSet will exist
filterArray: [Filter.create('LSID', parentIDs, Filter.Types.IN)],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import { DetailsListLineageIO, DetailsListNodes, DetailsListSteps } from './Deta
import { InjectedQueryModels, QueryConfigMap, withQueryModels } from '../../../../public/QueryModel/withQueryModels';
import { Filter } from '@labkey/api';
import { SchemaQuery } from '../../../../public/SchemaQuery';
import { ViewInfo } from '../../../ViewInfo';
import { QueryModel } from '../../../../public/QueryModel/QueryModel';

import { LINEAGE_DETAIL_REQUIRED_COLS } from '../constants';
Expand Down Expand Up @@ -101,7 +100,7 @@ export const LineageNodeDetail: FC<LineageNodeDetailProps> = memo(props => {
baseFilters: node.pkFilters.map(pkFilter => Filter.create(pkFilter.fieldKey, pkFilter.value)),
containerPath: node.containerPath,
// Issue 45028: Display details view columns in lineage
schemaQuery: new SchemaQuery(node.schemaName, node.queryName, ViewInfo.DETAIL_NAME),
schemaQuery: new SchemaQuery(node.schemaName, node.queryName).detailView,
requiredColumns: LINEAGE_DETAIL_REQUIRED_COLS,
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ describe('isAllSamplesSchema', () => {
});
test('exp.materials query', () => {
const sq = SCHEMAS.EXP_TABLES.MATERIALS;
const details = new SchemaQuery(sq.schemaName, sq.queryName, ViewInfo.DETAIL_NAME);
const details = new SchemaQuery(sq.schemaName, sq.queryName).detailView;
const otherView = new SchemaQuery(sq.schemaName.toUpperCase(), sq.queryName.toUpperCase(), 'otherView');
expect(isAllSamplesSchema(sq)).toBeTruthy();
expect(isAllSamplesSchema(details)).toBeTruthy();
Expand Down Expand Up @@ -393,7 +393,7 @@ describe('isWorkflowInputSamplesSchema', () => {
});
test('job input samples', () => {
const sq = SCHEMAS.WORKFLOW.JOB_INPUT_SAMPLES;
const details = new SchemaQuery(sq.schemaName, sq.queryName, ViewInfo.DETAIL_NAME);
const details = new SchemaQuery(sq.schemaName, sq.queryName).detailView;
const otherView = new SchemaQuery(sq.schemaName.toUpperCase(), sq.queryName.toUpperCase(), 'otherView');
expect(isWorkflowInputSamplesSchema(sq)).toBeTruthy();
expect(isWorkflowInputSamplesSchema(details)).toBeTruthy();
Expand Down
4 changes: 0 additions & 4 deletions packages/components/src/internal/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@ import { List } from 'immutable';

import { SchemaQuery } from '../public/SchemaQuery';

import { ViewInfo } from './ViewInfo';

// Created By / Modified By
export const CBMB = List<string>(['Created', 'CreatedBy', 'Modified', 'ModifiedBy']);

// ASSAY
const ASSAY_SCHEMA = 'assay';
export const ASSAY_TABLES = {
ASSAY_LIST: new SchemaQuery(ASSAY_SCHEMA, 'AssayList'),
ASSAY_DETAILS_SQ: new SchemaQuery(ASSAY_SCHEMA, 'AssayList', ViewInfo.DETAIL_NAME),
ASSAY_RUN_COUNTS: new SchemaQuery(ASSAY_SCHEMA, 'AssayRunCounts'),
ASSAY_RUNS: new SchemaQuery(ASSAY_SCHEMA, 'AssayRuns'),
ASSAY_RUNS_PER_SAMPLE: new SchemaQuery(ASSAY_SCHEMA, 'AssayRunsPerSample'),
Expand All @@ -35,7 +32,6 @@ export const EXP_TABLES = {
RUN_GROUPS: new SchemaQuery(EXP_SCHEMA, 'RunGroups'),
SCHEMA: EXP_SCHEMA,
SAMPLE_SETS: new SchemaQuery(EXP_SCHEMA, 'SampleSets'),
SAMPLE_SETS_DETAILS: new SchemaQuery(EXP_SCHEMA, 'SampleSets', ViewInfo.DETAIL_NAME),
SAMPLE_STATUS: new SchemaQuery(EXP_SCHEMA, 'SampleStatus'),
DATA_COLORS: new SchemaQuery(EXP_SCHEMA, 'DataColors'),
};
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/public/QueryModel/QueryModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ export class QueryModel {
// Note: this default may not be appropriate outside of Biologics/SM
if (keyValue !== undefined && schemaQuery.viewName === undefined) {
const { schemaName, queryName } = schemaQuery;
this.schemaQuery = new SchemaQuery(schemaName, queryName, ViewInfo.DETAIL_NAME);
this.schemaQuery = new SchemaQuery(schemaName, queryName).detailView;
this.bindURL = false;
} else {
this.schemaQuery = schemaQuery;
Expand Down
78 changes: 78 additions & 0 deletions packages/components/src/public/SchemaQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Copyright (c) 2020-2026 LabKey Corporation. All rights reserved. No portion of this work may be reproduced
* in any form or by any electronic or mechanical means without written permission from LabKey Corporation.
*/
import { ViewInfo } from '../internal/ViewInfo';

import { getSchemaQuery, resolveKey, resolveKeyFromJson, SchemaQuery } from './SchemaQuery';

describe('SchemaQuery', () => {
Expand Down Expand Up @@ -202,6 +204,82 @@ describe('SchemaQuery', () => {
});
});

describe('detailView', () => {
test('adds the detail view name, preserving schema and query case', () => {
const detailView = new SchemaQuery('Schema.SubSchema', 'Query').detailView;
expect(detailView.schemaName).toEqual('Schema.SubSchema');
expect(detailView.queryName).toEqual('Query');
expect(detailView.viewName).toEqual(ViewInfo.DETAIL_NAME);
});

test('does not mutate the source schema query', () => {
const sq = new SchemaQuery('s', 'q');
expect(sq.detailView).not.toBe(sq);
expect(sq.viewName).toBeUndefined();
});

test('replaces an existing view name', () => {
const sq = new SchemaQuery('s', 'q', 'someOtherView');
expect(sq.detailView).not.toBe(sq);
expect(sq.detailView.viewName).toEqual(ViewInfo.DETAIL_NAME);
expect(sq.viewName).toEqual('someOtherView');
});

test('returns itself when it is already the detail view', () => {
const sq = new SchemaQuery('s', 'q', ViewInfo.DETAIL_NAME);
expect(sq.detailView).toBe(sq);
});

test('is idempotent', () => {
const detailView = new SchemaQuery('s', 'q').detailView;
expect(detailView.detailView).toBe(detailView);
});

test('memoizes the derived instance', () => {
const sq = new SchemaQuery('s', 'q');
expect(sq.detailView).toBe(sq.detailView);
});

test('memoization does not affect structural equality of the source', () => {
const sq = new SchemaQuery('s', 'q');
expect(sq.detailView.viewName).toEqual(ViewInfo.DETAIL_NAME);
expect(sq).toEqual(new SchemaQuery('s', 'q'));
});

test('view name comparison is case-insensitive', () => {
const lowerViewName = ViewInfo.DETAIL_NAME.toLowerCase();
const sq = new SchemaQuery('s', 'q', lowerViewName);
expect(sq.detailView).toBe(sq);
expect(sq.detailView.viewName).toEqual(lowerViewName);
});

test('undefined schema and query names', () => {
const detailView = new SchemaQuery(undefined, undefined).detailView;
expect(detailView.schemaName).toBeUndefined();
expect(detailView.queryName).toBeUndefined();
expect(detailView.viewName).toEqual(ViewInfo.DETAIL_NAME);
});

test('equivalent to explicitly constructing the detail view', () => {
const explicit = new SchemaQuery('schema.subschema', 'query', ViewInfo.DETAIL_NAME);
expect(SQ.detailView.isEqual(explicit)).toEqual(true);
expect(SQ.detailView.isEqual(SQ)).toEqual(false);
expect(SQ.detailView.isEqual(SQ, false)).toEqual(true);
});

test('getKey encodes the detail view name', () => {
const detailView = new SchemaQuery('s', 'q').detailView;
expect(detailView.getKey()).toEqual('s/q/$t$tdetails$t$t');
expect(detailView.getKey(false)).toEqual('s/q');
});

test('toString includes the detail view name', () => {
const detailView = new SchemaQuery('s', 'q').detailView;
expect(detailView.toString()).toEqual('s|q|~~DETAILS~~');
expect(detailView.toString(false)).toEqual('s|q');
});
});

describe('isEqual', () => {
test('null or undefined argument', () => {
expect(SQ.isEqual(undefined)).toEqual(false);
Expand Down
Loading