Skip to content

Commit c187ba3

Browse files
authored
Assay: support file field values for batch and run properties via importRun API (#1845)
1 parent 88e9a7e commit c187ba3

8 files changed

Lines changed: 98 additions & 99 deletions

File tree

packages/components/package-lock.json

Lines changed: 6 additions & 6 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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/components",
3-
"version": "6.58.8",
3+
"version": "6.59.0",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [
@@ -50,7 +50,7 @@
5050
"homepage": "https://github.com/LabKey/labkey-ui-components#readme",
5151
"dependencies": {
5252
"@hello-pangea/dnd": "18.0.1",
53-
"@labkey/api": "1.42.1",
53+
"@labkey/api": "1.43.0",
5454
"@testing-library/dom": "~10.4.0",
5555
"@testing-library/jest-dom": "~6.6.3",
5656
"@testing-library/react": "~16.3.0",

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 6.59.0
5+
*Released*: 29 August 2025
6+
- Support string values for `FileInput` so that values can be round-tripped.
7+
- Removed unused `QueryInfo.getColumnFieldKeys()`
8+
- Remove unused `QueryModel.getRow()` property `flattenValues`
9+
410
### version 6.58.8
511
*Released*: 29 August 2025
612
- Issue 53773: Updating a field whose name contains a space via file will silently be ignored if the space is not included in the file
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Map } from 'immutable';
2+
3+
import { initializeValue } from './FileInput';
4+
5+
describe('FileInput', () => {
6+
test('initializeValue', () => {
7+
expect(initializeValue(undefined)).toEqual({ data: undefined, formValue: undefined });
8+
expect(initializeValue(null)).toEqual({ data: undefined, formValue: undefined });
9+
expect(initializeValue('')).toEqual({ data: undefined, formValue: undefined });
10+
expect(initializeValue(' ')).toEqual({ data: undefined, formValue: undefined });
11+
expect(initializeValue(' some/file/path1 ')).toEqual({
12+
data: 'some/file/path1',
13+
formValue: 'some/file/path1',
14+
});
15+
expect(initializeValue(Map())).toEqual({ data: Map(), formValue: undefined });
16+
expect(initializeValue(Map({ path: 'some/file/path' }))).toEqual({
17+
data: Map({ path: 'some/file/path' }),
18+
formValue: undefined,
19+
});
20+
expect(initializeValue(Map({ value: 'some/file/path' }))).toEqual({
21+
data: Map({ value: 'some/file/path' }),
22+
formValue: 'some/file/path',
23+
});
24+
});
25+
});

packages/components/src/internal/components/forms/input/FileInput.tsx

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,25 @@ import { getTransferItemDirectoryEntry } from '../../files/FileAttachmentContain
3333

3434
import { DisableableInput, DisableableInputProps, DisableableInputState } from './DisableableInput';
3535

36+
type FileInputData = Map<string, any> | string | undefined;
37+
38+
export function initializeValue(initialValue: any): { data: FileInputData; formValue: string | undefined } {
39+
let data: Map<string, any> | string | undefined;
40+
let formValue: string;
41+
if (Map.isMap(initialValue)) {
42+
data = initialValue;
43+
formValue = initialValue.get('value');
44+
} else if (typeof initialValue === 'string') {
45+
const trimmedValue = initialValue.trim();
46+
if (trimmedValue !== '') {
47+
data = trimmedValue;
48+
formValue = trimmedValue;
49+
}
50+
}
51+
52+
return { data, formValue };
53+
}
54+
3655
export interface FileInputProps extends DisableableInputProps {
3756
acceptedFormats?: string;
3857
addLabelAsterisk?: boolean;
@@ -54,7 +73,7 @@ export interface FileInputProps extends DisableableInputProps {
5473
type FileInputImplProps = FileInputProps & FormsyInjectedProps<any>;
5574

5675
interface State extends DisableableInputState {
57-
data: any;
76+
data: FileInputData;
5877
error: string;
5978
file: File;
6079
isHover: boolean;
@@ -74,30 +93,21 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
7493

7594
constructor(props: FileInputImplProps) {
7695
super(props);
77-
this.processFiles = this.processFiles.bind(this);
78-
this.onChange = this.onChange.bind(this);
79-
this.onDrag = this.onDrag.bind(this);
80-
this.onDragLeave = this.onDragLeave.bind(this);
81-
this.onDrop = this.onDrop.bind(this);
82-
this.onRemove = this.onRemove.bind(this);
83-
this.setFormValue = this.setFormValue.bind(this);
8496
this.toggleDisabled = this.toggleDisabled.bind(this);
8597

8698
this.fileInput = React.createRef<HTMLInputElement>();
99+
const { data, formValue } = initializeValue(props.initialValue);
100+
87101
this.state = {
88-
// FileInput only accepts query-shaped row data as the initialValue
89-
// as that is what is accepted by FileColumnRenderer. Without this there is likely insufficient
90-
// metadata to render and act on the associated file value.
91-
data: Map.isMap(props.initialValue) ? props.initialValue : undefined,
92-
isHover: false,
102+
data,
93103
file: null,
94104
error: '',
95105
isDisabled: props.initiallyDisabled,
106+
isHover: false,
96107
};
97108

98-
if (Map.isMap(props.initialValue)) {
99-
// call setValue so to populate form data (for diff compare)
100-
props.setValue?.(props.initialValue.get('value'));
109+
if (formValue) {
110+
props.setValue?.(formValue);
101111
}
102112
}
103113

@@ -107,7 +117,7 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
107117
return this.props.name ?? this.props.queryColumn.fieldKey;
108118
}
109119

110-
processFiles(fileList: FileList, transferItems?: DataTransferItemList): void {
120+
processFiles = (fileList: FileList, transferItems?: DataTransferItemList): void => {
111121
const { acceptedFormats, maxFileSize, emptyFileNotAllowed } = this.props;
112122
if (fileList.length > 1) {
113123
this.setState({ error: 'Only one file allowed' });
@@ -139,52 +149,52 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
139149
return;
140150
}
141151
this.setFormValue(file);
142-
}
152+
};
143153

144-
setFormValue(file: File): void {
154+
setFormValue = (file: File): void => {
145155
const { formsy, onChange, setValue } = this.props;
146156
this.setState({ data: undefined, file, error: '' });
147157
onChange?.({ [this.getInputName()]: file });
148158

149159
if (formsy) {
150160
setValue?.(file);
151161
}
152-
}
162+
};
153163

154-
onChange(event: React.FormEvent<HTMLInputElement>): void {
164+
onChange = (event: React.FormEvent<HTMLInputElement>): void => {
155165
cancelEvent(event);
156166
this.processFiles(this.fileInput.current.files);
157-
}
167+
};
158168

159-
onDrag(event: React.DragEvent<HTMLElement>): void {
169+
onDrag = (event: React.DragEvent<HTMLElement>): void => {
160170
cancelEvent(event);
161171

162172
if (!this.state.isHover) {
163173
this.setState({ isHover: true });
164174
}
165-
}
175+
};
166176

167-
onDragLeave(event: React.DragEvent<HTMLElement>): void {
177+
onDragLeave = (event: React.DragEvent<HTMLElement>): void => {
168178
cancelEvent(event);
169179

170180
if (this.state.isHover) {
171181
this.setState({ isHover: false });
172182
}
173-
}
183+
};
174184

175-
onDrop(event: React.DragEvent<HTMLElement>): void {
185+
onDrop = (event: React.DragEvent<HTMLElement>): void => {
176186
cancelEvent(event);
177187

178188
if (event.dataTransfer && event.dataTransfer.files) {
179189
this.processFiles(event.dataTransfer.files, event.dataTransfer.items);
180190
this.setState({ isHover: false });
181191
}
182-
}
192+
};
183193

184-
onRemove(): void {
194+
onRemove = (): void => {
185195
// A value of null is supported by server APIs to clear/remove a file field's value.
186196
this.setFormValue(null);
187-
}
197+
};
188198

189199
render() {
190200
const {
@@ -197,31 +207,28 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
197207
showLabel,
198208
toggleDisabledTooltip,
199209
} = this.props;
200-
const { data, file, isDisabled, isHover } = this.state;
210+
const { data, error, file, isDisabled, isHover } = this.state;
201211

202212
const name = this.getInputName();
203213
const inputId = `${name}-fileUpload`; // Issue 53394: needs to be a distinct input id so it doesn't collide with other elements on the page for this fieldKey
204214
let body;
205215

206-
if (file) {
207-
const attachedFileClass = classNames('attached-file__inline-container', {
208-
'file-upload__is-hover': isHover,
209-
});
216+
if (file || typeof data === 'string') {
210217
body = (
211218
<div
212-
className={attachedFileClass}
219+
className={classNames('attached-file__inline-container', { 'file-upload__is-hover': isHover })}
213220
onDragEnter={this.onDrag}
214221
onDragLeave={this.onDragLeave}
215222
onDragOver={this.onDrag}
216223
onDrop={this.onDrop}
217224
>
218225
<span className="fa fa-times-circle attached-file__remove-icon" onClick={this.onRemove} />
219226
<span className="fa fa-file-text attached-file--icon" />
220-
<span>{file.name}</span>
221-
<div className="file-upload__error-message">{this.state.error}</div>
227+
<span>{file ? file.name : (data as string)}</span>
228+
<div className="file-upload__error-message">{error}</div>
222229
</div>
223230
);
224-
} else if (data?.get('value')) {
231+
} else if (Map.isMap(data) && data.get('value')) {
225232
body = (
226233
<FileColumnRenderer
227234
data={data}
@@ -234,7 +241,7 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
234241
<>
235242
<input
236243
className="file-upload__input" // This class makes the file input hidden
237-
disabled={this.state.isDisabled}
244+
disabled={isDisabled}
238245
id={inputId}
239246
multiple={false}
240247
name={name}
@@ -258,7 +265,7 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
258265
<i aria-hidden="true" className="fa fa-cloud-upload" />
259266
&nbsp;
260267
<span>Select file or drag and drop here.</span>
261-
<div className="file-upload__error-message">{this.state.error}</div>
268+
<div className="file-upload__error-message">{error}</div>
262269
</label>
263270
</>
264271
);

packages/components/src/public/QueryInfo.test.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -69,27 +69,6 @@ const QUERY_INFO_WITH_ID_VIEW_NAME_ONLY = QueryInfo.fromJsonForTests(
6969
true
7070
);
7171

72-
describe('getColumnFieldKeys', () => {
73-
test('missing params', () => {
74-
const queryInfo = new QueryInfo({});
75-
76-
expect(JSON.stringify(queryInfo.getColumnFieldKeys(undefined))).toBe('[]');
77-
expect(JSON.stringify(queryInfo.getColumnFieldKeys(['test']))).toBe('[]');
78-
});
79-
80-
test('queryInfo with columns', () => {
81-
const queryInfo = QueryInfo.fromJsonForTests({
82-
columns: [{ fieldKey: 'test1' }, { fieldKey: 'test2' }, { fieldKey: 'test3' }],
83-
});
84-
85-
expect(JSON.stringify(queryInfo.getColumnFieldKeys(undefined))).toBe('["test1","test2","test3"]');
86-
expect(JSON.stringify(queryInfo.getColumnFieldKeys(['test0']))).toBe('[]');
87-
expect(JSON.stringify(queryInfo.getColumnFieldKeys(['test1']))).toBe('["test1"]');
88-
expect(JSON.stringify(queryInfo.getColumnFieldKeys(['test1', 'test2']))).toBe('["test1","test2"]');
89-
expect(JSON.stringify(queryInfo.getColumnFieldKeys(['test1', 'test2', 'test4']))).toBe('["test1","test2"]');
90-
});
91-
});
92-
9372
describe('QueryInfo', () => {
9473
const queryInfo = QueryInfo.fromJsonForTests(sampleSetQueryInfo);
9574

@@ -305,7 +284,7 @@ describe('QueryInfo', () => {
305284
);
306285

307286
test('with disabledSystemFields and addToSystemView fields', () => {
308-
let added: Set<string> = new Set();
287+
let added = new Set<string>();
309288
let extras = queryInfoWithAddAndDisabledSystemFields.getExtraDisplayColumns(added, []);
310289
expect(extras.length).toBe(1);
311290
expect(extras[0].fieldKey).toBe('test2');

packages/components/src/public/QueryInfo.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -487,21 +487,6 @@ export class QueryInfo {
487487
return this.iconURL;
488488
}
489489

490-
/**
491-
* Get an array of fieldKeys for the column keys provided.
492-
* Default to getting all column fieldKeys if no parameter provided
493-
* @param keys The column keys to filter by
494-
*/
495-
getColumnFieldKeys(keys?: string[]): string[] {
496-
if (this.columns) {
497-
return this.columns
498-
.filter((col, key) => !keys || keys.indexOf(key) > -1)
499-
.valueArray.map(col => col.fieldKey);
500-
}
501-
502-
return [];
503-
}
504-
505490
getColumnIndex(fieldKey: string): number {
506491
if (!fieldKey) return -1;
507492

0 commit comments

Comments
 (0)