Skip to content
Merged
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
12 changes: 6 additions & 6 deletions packages/components/package-lock.json

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

4 changes: 2 additions & 2 deletions packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@labkey/components",
"version": "6.58.8",
"version": "6.59.0",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down Expand Up @@ -50,7 +50,7 @@
"homepage": "https://github.com/LabKey/labkey-ui-components#readme",
"dependencies": {
"@hello-pangea/dnd": "18.0.1",
"@labkey/api": "1.42.1",
"@labkey/api": "1.43.0",
"@testing-library/dom": "~10.4.0",
"@testing-library/jest-dom": "~6.6.3",
"@testing-library/react": "~16.3.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# @labkey/components
Components, models, actions, and utility functions for LabKey applications and pages

### version 6.59.0
*Released*: 29 August 2025
- Support string values for `FileInput` so that values can be round-tripped.
- Removed unused `QueryInfo.getColumnFieldKeys()`
- Remove unused `QueryModel.getRow()` property `flattenValues`

### version 6.58.8
*Released*: 29 August 2025
- 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Map } from 'immutable';

import { initializeValue } from './FileInput';

describe('FileInput', () => {
test('initializeValue', () => {
expect(initializeValue(undefined)).toEqual({ data: undefined, formValue: undefined });
expect(initializeValue(null)).toEqual({ data: undefined, formValue: undefined });
expect(initializeValue('')).toEqual({ data: undefined, formValue: undefined });
expect(initializeValue(' ')).toEqual({ data: undefined, formValue: undefined });
expect(initializeValue(' some/file/path1 ')).toEqual({
data: 'some/file/path1',
formValue: 'some/file/path1',
});
expect(initializeValue(Map())).toEqual({ data: Map(), formValue: undefined });
expect(initializeValue(Map({ path: 'some/file/path' }))).toEqual({
data: Map({ path: 'some/file/path' }),
formValue: undefined,
});
expect(initializeValue(Map({ value: 'some/file/path' }))).toEqual({
data: Map({ value: 'some/file/path' }),
formValue: 'some/file/path',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ import { getTransferItemDirectoryEntry } from '../../files/FileAttachmentContain

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

type FileInputData = Map<string, any> | string | undefined;

export function initializeValue(initialValue: any): { data: FileInputData; formValue: string | undefined } {
let data: Map<string, any> | string | undefined;
let formValue: string;
if (Map.isMap(initialValue)) {
data = initialValue;
formValue = initialValue.get('value');
} else if (typeof initialValue === 'string') {
const trimmedValue = initialValue.trim();
if (trimmedValue !== '') {
data = trimmedValue;
formValue = trimmedValue;
}
}

return { data, formValue };
}

export interface FileInputProps extends DisableableInputProps {
acceptedFormats?: string;
addLabelAsterisk?: boolean;
Expand All @@ -54,7 +73,7 @@ export interface FileInputProps extends DisableableInputProps {
type FileInputImplProps = FileInputProps & FormsyInjectedProps<any>;

interface State extends DisableableInputState {
data: any;
data: FileInputData;
error: string;
file: File;
isHover: boolean;
Expand All @@ -74,30 +93,21 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {

constructor(props: FileInputImplProps) {
super(props);
this.processFiles = this.processFiles.bind(this);
this.onChange = this.onChange.bind(this);
this.onDrag = this.onDrag.bind(this);
this.onDragLeave = this.onDragLeave.bind(this);
this.onDrop = this.onDrop.bind(this);
this.onRemove = this.onRemove.bind(this);
this.setFormValue = this.setFormValue.bind(this);
this.toggleDisabled = this.toggleDisabled.bind(this);

this.fileInput = React.createRef<HTMLInputElement>();
const { data, formValue } = initializeValue(props.initialValue);

this.state = {
// FileInput only accepts query-shaped row data as the initialValue
// as that is what is accepted by FileColumnRenderer. Without this there is likely insufficient
// metadata to render and act on the associated file value.
data: Map.isMap(props.initialValue) ? props.initialValue : undefined,
isHover: false,
data,
file: null,
error: '',
isDisabled: props.initiallyDisabled,
isHover: false,
};

if (Map.isMap(props.initialValue)) {
// call setValue so to populate form data (for diff compare)
props.setValue?.(props.initialValue.get('value'));
if (formValue) {
props.setValue?.(formValue);
}
}

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

processFiles(fileList: FileList, transferItems?: DataTransferItemList): void {
processFiles = (fileList: FileList, transferItems?: DataTransferItemList): void => {
const { acceptedFormats, maxFileSize, emptyFileNotAllowed } = this.props;
if (fileList.length > 1) {
this.setState({ error: 'Only one file allowed' });
Expand Down Expand Up @@ -139,52 +149,52 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
return;
}
this.setFormValue(file);
}
};

setFormValue(file: File): void {
setFormValue = (file: File): void => {
const { formsy, onChange, setValue } = this.props;
this.setState({ data: undefined, file, error: '' });
onChange?.({ [this.getInputName()]: file });

if (formsy) {
setValue?.(file);
}
}
};

onChange(event: React.FormEvent<HTMLInputElement>): void {
onChange = (event: React.FormEvent<HTMLInputElement>): void => {
cancelEvent(event);
this.processFiles(this.fileInput.current.files);
}
};

onDrag(event: React.DragEvent<HTMLElement>): void {
onDrag = (event: React.DragEvent<HTMLElement>): void => {
cancelEvent(event);

if (!this.state.isHover) {
this.setState({ isHover: true });
}
}
};

onDragLeave(event: React.DragEvent<HTMLElement>): void {
onDragLeave = (event: React.DragEvent<HTMLElement>): void => {
cancelEvent(event);

if (this.state.isHover) {
this.setState({ isHover: false });
}
}
};

onDrop(event: React.DragEvent<HTMLElement>): void {
onDrop = (event: React.DragEvent<HTMLElement>): void => {
cancelEvent(event);

if (event.dataTransfer && event.dataTransfer.files) {
this.processFiles(event.dataTransfer.files, event.dataTransfer.items);
this.setState({ isHover: false });
}
}
};

onRemove(): void {
onRemove = (): void => {
// A value of null is supported by server APIs to clear/remove a file field's value.
this.setFormValue(null);
}
};

render() {
const {
Expand All @@ -197,31 +207,28 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
showLabel,
toggleDisabledTooltip,
} = this.props;
const { data, file, isDisabled, isHover } = this.state;
const { data, error, file, isDisabled, isHover } = this.state;

const name = this.getInputName();
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
let body;

if (file) {
const attachedFileClass = classNames('attached-file__inline-container', {
'file-upload__is-hover': isHover,
});
if (file || typeof data === 'string') {
body = (
<div
className={attachedFileClass}
className={classNames('attached-file__inline-container', { 'file-upload__is-hover': isHover })}
onDragEnter={this.onDrag}
onDragLeave={this.onDragLeave}
onDragOver={this.onDrag}
onDrop={this.onDrop}
>
<span className="fa fa-times-circle attached-file__remove-icon" onClick={this.onRemove} />
<span className="fa fa-file-text attached-file--icon" />
<span>{file.name}</span>
<div className="file-upload__error-message">{this.state.error}</div>
<span>{file ? file.name : (data as string)}</span>
<div className="file-upload__error-message">{error}</div>
</div>
);
} else if (data?.get('value')) {
} else if (Map.isMap(data) && data.get('value')) {
body = (
<FileColumnRenderer
data={data}
Expand All @@ -234,7 +241,7 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
<>
<input
className="file-upload__input" // This class makes the file input hidden
disabled={this.state.isDisabled}
disabled={isDisabled}
id={inputId}
multiple={false}
name={name}
Expand All @@ -258,7 +265,7 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
<i aria-hidden="true" className="fa fa-cloud-upload" />
&nbsp;
<span>Select file or drag and drop here.</span>
<div className="file-upload__error-message">{this.state.error}</div>
<div className="file-upload__error-message">{error}</div>
</label>
</>
);
Expand Down
23 changes: 1 addition & 22 deletions packages/components/src/public/QueryInfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,27 +69,6 @@ const QUERY_INFO_WITH_ID_VIEW_NAME_ONLY = QueryInfo.fromJsonForTests(
true
);

describe('getColumnFieldKeys', () => {
test('missing params', () => {
const queryInfo = new QueryInfo({});

expect(JSON.stringify(queryInfo.getColumnFieldKeys(undefined))).toBe('[]');
expect(JSON.stringify(queryInfo.getColumnFieldKeys(['test']))).toBe('[]');
});

test('queryInfo with columns', () => {
const queryInfo = QueryInfo.fromJsonForTests({
columns: [{ fieldKey: 'test1' }, { fieldKey: 'test2' }, { fieldKey: 'test3' }],
});

expect(JSON.stringify(queryInfo.getColumnFieldKeys(undefined))).toBe('["test1","test2","test3"]');
expect(JSON.stringify(queryInfo.getColumnFieldKeys(['test0']))).toBe('[]');
expect(JSON.stringify(queryInfo.getColumnFieldKeys(['test1']))).toBe('["test1"]');
expect(JSON.stringify(queryInfo.getColumnFieldKeys(['test1', 'test2']))).toBe('["test1","test2"]');
expect(JSON.stringify(queryInfo.getColumnFieldKeys(['test1', 'test2', 'test4']))).toBe('["test1","test2"]');
});
});

describe('QueryInfo', () => {
const queryInfo = QueryInfo.fromJsonForTests(sampleSetQueryInfo);

Expand Down Expand Up @@ -305,7 +284,7 @@ describe('QueryInfo', () => {
);

test('with disabledSystemFields and addToSystemView fields', () => {
let added: Set<string> = new Set();
let added = new Set<string>();
let extras = queryInfoWithAddAndDisabledSystemFields.getExtraDisplayColumns(added, []);
expect(extras.length).toBe(1);
expect(extras[0].fieldKey).toBe('test2');
Expand Down
15 changes: 0 additions & 15 deletions packages/components/src/public/QueryInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,21 +487,6 @@ export class QueryInfo {
return this.iconURL;
}

/**
* Get an array of fieldKeys for the column keys provided.
* Default to getting all column fieldKeys if no parameter provided
* @param keys The column keys to filter by
*/
getColumnFieldKeys(keys?: string[]): string[] {
if (this.columns) {
return this.columns
.filter((col, key) => !keys || keys.indexOf(key) > -1)
.valueArray.map(col => col.fieldKey);
}

return [];
}

getColumnIndex(fieldKey: string): number {
if (!fieldKey) return -1;

Expand Down
Loading