Skip to content

Commit 35e651b

Browse files
authored
Provide warnings for unknown fields for cross sample type import (#1930)
### version 7.16.0 *Released*: 5 February 2026 - File import warnings for cross type sample import case - InferDomain API call and response to include distinctValues for specified column keys - EntityIdCreationModel.getSchemaQuery to include optional targetQueryName param
1 parent 882f1d2 commit 35e651b

12 files changed

Lines changed: 129 additions & 22 deletions

File tree

packages/components/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/components",
3-
"version": "7.15.0",
3+
"version": "7.16.0",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [

packages/components/releaseNotes/components.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 7.16.0
5+
*Released*: 5 February 2026
6+
- File import warnings for cross type sample import case
7+
- InferDomain API call and response to include distinctValues for specified column keys
8+
- EntityIdCreationModel.getSchemaQuery to include optional targetQueryName param
9+
410
### version 7.15.0
511
*Released*: 4 February 2026
612
- Package updates

packages/components/src/internal/components/entities/models.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ describe('EntityIdCreationModel', () => {
200200
});
201201
expect(sq.getSchemaQuery().schemaName).toBe('samples');
202202
expect(sq.getSchemaQuery().queryName).toBe('a');
203+
204+
expect(sq.getSchemaQuery('altTarget').schemaName).toBe('samples');
205+
expect(sq.getSchemaQuery('altTarget').queryName).toBe('altTarget');
203206
});
204207
});
205208

packages/components/src/internal/components/entities/models.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,8 +360,8 @@ export class EntityIdCreationModel extends Record({
360360
};
361361
}
362362

363-
getSchemaQuery(): SchemaQuery {
364-
const entityTypeName = this.getTargetEntityTypeValue();
363+
getSchemaQuery(targetQueryName?: string): SchemaQuery {
364+
const entityTypeName = targetQueryName ?? this.getTargetEntityTypeValue();
365365
return entityTypeName ? new SchemaQuery(this.entityDataType.instanceSchemaName, entityTypeName) : undefined;
366366
}
367367

packages/components/src/internal/components/files/FilePreviewGrid.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class FilePreviewGrid extends React.Component<Props, any> {
3636
<>
3737
<strong>{header}</strong>
3838
{(warningMsg || previewData?.warningMsg) && (
39-
<Alert bsStyle="warning" className="margin-top">
39+
<Alert bsStyle="warning" className="margin-top file-preview-grid__warning">
4040
{warningMsg}
4141
{previewData?.warningMsg}
4242
</Alert>

packages/components/src/internal/components/files/__snapshots__/FilePreviewGrid.test.tsx.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ exports[`<FilePreviewGrid/> previewData.warningMsg 1`] = `
465465
File preview:
466466
</strong>
467467
<div
468-
class="margin-top alert alert-warning"
468+
class="margin-top file-preview-grid__warning alert alert-warning"
469469
role="alert"
470470
>
471471
Duplicate fields
@@ -799,7 +799,7 @@ exports[`<FilePreviewGrid/> warning message 1`] = `
799799
File preview:
800800
</strong>
801801
<div
802-
class="margin-top alert alert-warning"
802+
class="margin-top file-preview-grid__warning alert alert-warning"
803803
role="alert"
804804
>
805805
Testing warning message
@@ -969,7 +969,7 @@ exports[`<FilePreviewGrid/> warning message and previewData.warningMsg 1`] = `
969969
File preview:
970970
</strong>
971971
<div
972-
class="margin-top alert alert-warning"
972+
class="margin-top file-preview-grid__warning alert alert-warning"
973973
role="alert"
974974
>
975975
Testing warning message
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { List } from 'immutable';
2+
import { InferDomainResponse } from './InferDomainResponse';
3+
import { QueryColumn } from './QueryColumn';
4+
5+
describe('InferDomainResponse.create', () => {
6+
test('returns empty collections when rawModel is undefined', () => {
7+
const res = InferDomainResponse.create({});
8+
expect(res.data.size).toBe(0);
9+
expect(res.fields.size).toBe(0);
10+
expect(res.reservedFields.size).toBe(0);
11+
expect(res.distinctValues.size).toBe(0);
12+
expect(res.commentLineCount).toBeUndefined();
13+
});
14+
15+
test('converts plain JS data to immutable structures and preserves commentLineCount', () => {
16+
const raw = {
17+
data: [
18+
{ id: 1, name: 'a' },
19+
{ id: 2, name: 'b' },
20+
],
21+
commentLineCount: 3,
22+
};
23+
const res = InferDomainResponse.create(raw);
24+
expect(res.data.size).toBe(2);
25+
// items are immutable Maps after fromJS
26+
expect((res.data.get(0) as any).get('id')).toBe(1);
27+
expect((res.data.get(1) as any).get('name')).toBe('b');
28+
expect(res.commentLineCount).toBe(3);
29+
});
30+
31+
test('maps fields and reservedFields to QueryColumn instances', () => {
32+
const raw = {
33+
fields: [
34+
{ name: 'col1', type: 'string' },
35+
{ name: 'col2', type: 'int' },
36+
],
37+
reservedFields: [{ name: 'reserved', type: 'string' }],
38+
};
39+
const res = InferDomainResponse.create(raw);
40+
expect(res.fields.size).toBe(2);
41+
expect(res.reservedFields.size).toBe(1);
42+
43+
expect(res.fields.get(0) instanceof QueryColumn).toBe(true);
44+
expect(res.fields.get(1) instanceof QueryColumn).toBe(true);
45+
expect(res.reservedFields.get(0) instanceof QueryColumn).toBe(true);
46+
});
47+
48+
test('builds distinctValues map and getDistinctValuesForColumn returns a List', () => {
49+
const raw = {
50+
distinctValues: {
51+
col1: [null, 'a', 'b'],
52+
colEmpty: [],
53+
},
54+
};
55+
const res = InferDomainResponse.create(raw);
56+
57+
const col1List = res.getDistinctValuesForColumn('col1') as List<string>;
58+
expect(col1List.toArray()).toEqual([null, 'a', 'b']);
59+
60+
expect(res.getDistinctValuesForColumn('missing').size).toBe(0);
61+
expect(res.getDistinctValuesForColumn('colEmpty').size).toBe(0);
62+
});
63+
});

packages/components/src/public/InferDomainResponse.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fromJS, List, Record } from 'immutable';
1+
import { fromJS, List, Map, Record } from 'immutable';
22

33
import { ActionURL, Ajax, Utils } from '@labkey/api';
44

@@ -9,16 +9,19 @@ export class InferDomainResponse extends Record({
99
fields: List<QueryColumn>(),
1010
reservedFields: List<QueryColumn>(),
1111
commentLineCount: undefined,
12+
distinctValues: Map<string, List<string>>(),
1213
}) {
1314
declare data: List<any>;
1415
declare fields: List<QueryColumn>;
1516
declare reservedFields: List<QueryColumn>;
1617
declare commentLineCount?: number;
18+
declare distinctValues: Map<string, List<string>>;
1719

1820
static create(rawModel): InferDomainResponse {
1921
let data = List<any>();
2022
let fields = List<QueryColumn>();
2123
let reservedFields = List<QueryColumn>();
24+
let distinctValues = Map<string, List<string>>();
2225

2326
if (rawModel) {
2427
if (rawModel.data) {
@@ -32,21 +35,34 @@ export class InferDomainResponse extends Record({
3235
if (rawModel.reservedFields) {
3336
reservedFields = List(rawModel.reservedFields.map(field => new QueryColumn(field)));
3437
}
38+
39+
if (rawModel.distinctValues) {
40+
distinctValues = Map<string, List<string>>(
41+
Object.entries(rawModel.distinctValues).map(([key, value]) => [key, List<string>(value)])
42+
);
43+
}
3544
}
3645

3746
return new InferDomainResponse({
3847
data,
3948
fields,
4049
reservedFields,
4150
commentLineCount: rawModel.commentLineCount,
51+
distinctValues,
4252
});
4353
}
54+
55+
// get the distinct values for a specific column, return empty list if the column doesn't exist
56+
getDistinctValuesForColumn(columnName: string): List<string> {
57+
return this.distinctValues.get(columnName, List<string>());
58+
}
4459
}
4560

4661
export function inferDomainFromFile(
4762
file: File | string, // file or webdav url path
4863
numLinesToInclude: number,
49-
domainKindName?: string
64+
domainKindName?: string,
65+
distinctValueColumns?: string[]
5066
): Promise<InferDomainResponse> {
5167
return new Promise((resolve, reject) => {
5268
const form = new FormData();
@@ -56,6 +72,10 @@ export function inferDomainFromFile(
5672
if (domainKindName) {
5773
form.append('domainKindName', domainKindName);
5874
}
75+
if (distinctValueColumns && distinctValueColumns.length > 0) {
76+
// append each value separately so they are received as an array on the server
77+
distinctValueColumns.forEach(col => form.append('distinctValueColumns', col));
78+
}
5979

6080
Ajax.request({
6181
url: ActionURL.buildURL('property', 'inferDomain.api'),

packages/components/src/public/files/FileAttachmentForm.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { inferDomainFromFile } from '../InferDomainResponse';
3636
import { SchemaQuery } from '../SchemaQuery';
3737

3838
import { TemplateDownloadButton } from './TemplateDownloadButton';
39-
import { FileSizeLimitProps, FileGridPreviewProps } from './models';
39+
import { FileGridPreviewProps, FileSizeLimitProps } from './models';
4040

4141
export interface FileAttachmentFormProps {
4242
acceptedFormats?: string; // comma-separated list of allowed extensions i.e. '.png, .jpg, .jpeg'
@@ -266,7 +266,7 @@ export class FileAttachmentForm extends PureComponent<FileAttachmentFormProps, S
266266

267267
if (this.shouldShowPreviewGrid()) {
268268
if (previewData || errorMessage) {
269-
return <FilePreviewGrid {...previewGridProps} previewData={previewData} errorMsg={errorMessage} />;
269+
return <FilePreviewGrid {...previewGridProps} errorMsg={errorMessage} previewData={previewData} />;
270270
} else if (previewStatus) {
271271
return (
272272
<div className="margin-top">
@@ -306,9 +306,13 @@ export class FileAttachmentForm extends PureComponent<FileAttachmentFormProps, S
306306
}
307307
}
308308

309-
this.updatePreviewStatus('Uploading file...');
310-
311-
inferDomainFromFile(file, previewGridProps.previewCount, previewGridProps.domainKindName)
309+
this.updatePreviewStatus('Loading file preview...');
310+
inferDomainFromFile(
311+
file,
312+
previewGridProps.previewCount,
313+
previewGridProps.domainKindName,
314+
previewGridProps.distinctValueColumns
315+
)
312316
.then(response => {
313317
this.updatePreviewStatus(null);
314318

@@ -374,21 +378,21 @@ export class FileAttachmentForm extends PureComponent<FileAttachmentFormProps, S
374378
<FormSection iconSpacer={false} label={label} showLabel={showLabel}>
375379
<div className={classNames({ 'file-upload--one-row': compact })}>
376380
<FileAttachmentContainer
377-
ref={this.fileAttachmentContainerRef}
378-
index={index}
379381
acceptedFormats={acceptedFormats}
380382
allowDirectories={allowDirectories}
381-
includeDirectoryFiles={includeDirectoryFiles}
383+
allowMultiple={allowMultiple}
384+
compact={compact}
382385
fileCountSuffix={fileCountSuffix}
383386
handleChange={this.handleFileChange}
384387
handleRemoval={this.handleFileRemoval}
388+
includeDirectoryFiles={includeDirectoryFiles}
389+
index={index}
385390
initialFileNames={initialFileNames}
386391
initialFiles={initialFiles}
387-
allowMultiple={allowMultiple}
392+
labelLong={labelLong}
393+
ref={this.fileAttachmentContainerRef}
388394
sizeLimits={sizeLimits}
389395
sizeLimitsHelpText={sizeLimitsHelpText}
390-
labelLong={labelLong}
391-
compact={compact}
392396
/>
393397
{compact && showButtons && this.renderButtons()}
394398
</div>

0 commit comments

Comments
 (0)