Skip to content

Commit 66dbf5f

Browse files
committed
Merge branch 'develop' into fb_exceptionsCAN256
# Conflicts: # packages/components/package-lock.json # packages/components/package.json # packages/components/releaseNotes/components.md
2 parents 90c2d13 + f9a3985 commit 66dbf5f

7 files changed

Lines changed: 152 additions & 42 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.44.0-exceptionsCAN256.0",
3+
"version": "6.44.1",
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.41.1",
53+
"@labkey/api": "1.41.2",
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ Components, models, actions, and utility functions for LabKey applications and p
55
*Released*: TBD
66
- Issue 53164: AssayPicker.tsx gives JS error if no specialty assay providers available for server
77

8+
### version 6.44.1
9+
*Released*: 28 May 2025
10+
- Introduce `getTransferItemDirectoryEntry` to centralize handling/wrapping of calling `webkitGetAsEntry()` on a `DataTransferItemList` item.
11+
- Address Issue 53149 by updating endpoint wrapper to be tolerant of invalid payload upon success.
12+
813
### version 6.44.0
914
*Released*: 27 May 2025
1015
- QueryModel/QueryConfig change useSavedSettings from boolean to enum SavedSettings

packages/components/src/internal/components/files/FileAttachmentContainer.test.tsx

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
import React from 'react';
1717
import { render } from '@testing-library/react';
1818

19-
import { FileAttachmentContainer } from './FileAttachmentContainer';
19+
import {
20+
FileAttachmentContainer,
21+
getTransferItemDirectoryEntry,
22+
isDirectoryEntry,
23+
isFileEntry,
24+
} from './FileAttachmentContainer';
2025

2126
describe('FileAttachmentContainer', () => {
2227
test('with single file', () => {
@@ -29,7 +34,7 @@ describe('FileAttachmentContainer', () => {
2934
);
3035

3136
expect(document.querySelector('.file-upload__container').className).toContain('hidden');
32-
expect(document.querySelector('.attached-file__container').textContent).toBe('file1.txt');
37+
expect(document.querySelector('.attached-file__container')).toHaveTextContent('file1.txt');
3338
});
3439

3540
test('with multiple files', () => {
@@ -46,8 +51,8 @@ describe('FileAttachmentContainer', () => {
4651

4752
expect(document.querySelector('.file-upload__container').className).toContain('block');
4853
expect(document.querySelectorAll('.attached-file__container')).toHaveLength(2);
49-
expect(document.querySelectorAll('.attached-file__container')[0].textContent).toBe('file1.txt');
50-
expect(document.querySelectorAll('.attached-file__container')[1].textContent).toBe('file2.txt');
54+
expect(document.querySelectorAll('.attached-file__container')[0]).toHaveTextContent('file1.txt');
55+
expect(document.querySelectorAll('.attached-file__container')[1]).toHaveTextContent('file2.txt');
5156

5257
expect(document.querySelectorAll('.file-upload__file-entry-listing')).toHaveLength(1);
5358
expect(document.querySelectorAll('.file-upload__scroll-footer')).toHaveLength(0);
@@ -91,7 +96,7 @@ describe('FileAttachmentContainer', () => {
9196
expect(document.querySelector('.file-upload__container').className).toContain('block');
9297
expect(document.querySelectorAll('.attached-file__container')).toHaveLength(2);
9398
expect(document.querySelectorAll('.file-upload__file-entry-listing')).toHaveLength(1);
94-
expect(document.querySelector('.file-upload__scroll-footer').textContent).toBe('2 files will be uploaded.');
99+
expect(document.querySelector('.file-upload__scroll-footer')).toHaveTextContent('2 files will be uploaded.');
95100
});
96101

97102
test('fileCountSuffix with single', () => {
@@ -109,6 +114,92 @@ describe('FileAttachmentContainer', () => {
109114
expect(document.querySelector('.file-upload__container').className).toContain('block');
110115
expect(document.querySelectorAll('.attached-file__container')).toHaveLength(1);
111116
expect(document.querySelectorAll('.file-upload__file-entry-listing')).toHaveLength(1);
112-
expect(document.querySelector('.file-upload__scroll-footer').textContent).toBe('1 file will be uploaded.');
117+
expect(document.querySelector('.file-upload__scroll-footer')).toHaveTextContent('1 file will be uploaded.');
118+
});
119+
});
120+
121+
describe('File System Helper Functions', () => {
122+
function mockFileSystemEntry(isDirectory: boolean, isFile: boolean): FileSystemEntry {
123+
return {
124+
filesystem: undefined,
125+
fullPath: undefined,
126+
isDirectory,
127+
isFile,
128+
name: undefined,
129+
getParent: jest.fn(),
130+
};
131+
}
132+
133+
function mockDataTransferItemList(entry: FileSystemEntry): DataTransferItemList {
134+
return {
135+
0: {
136+
webkitGetAsEntry: jest.fn().mockReturnValue(entry),
137+
},
138+
} as unknown as DataTransferItemList;
139+
}
140+
141+
describe('isDirectoryEntry', () => {
142+
it('should return true when entry is a directory', () => {
143+
const mockDirEntry = mockFileSystemEntry(true, false);
144+
expect(isDirectoryEntry(mockDirEntry)).toBe(true);
145+
});
146+
147+
it('should return false when entry is a file', () => {
148+
const mockFileEntry = mockFileSystemEntry(false, true);
149+
expect(isDirectoryEntry(mockFileEntry)).toBe(false);
150+
});
151+
152+
it('should return false when entry is undefined', () => {
153+
expect(isDirectoryEntry(undefined)).toBe(false);
154+
expect(isDirectoryEntry(null)).toBe(false);
155+
expect(isDirectoryEntry({} as unknown as FileSystemEntry)).toBe(false);
156+
});
157+
});
158+
159+
describe('isFileEntry', () => {
160+
it('should return true when entry is a file', () => {
161+
const mockFileEntry = mockFileSystemEntry(false, true);
162+
expect(isFileEntry(mockFileEntry)).toBe(true);
163+
});
164+
165+
it('should return false when entry is a directory', () => {
166+
const mockDirEntry = mockFileSystemEntry(true, false);
167+
expect(isFileEntry(mockDirEntry)).toBe(false);
168+
});
169+
170+
it('should return false when entry is undefined', () => {
171+
expect(isFileEntry(undefined)).toBe(false);
172+
expect(isFileEntry(null)).toBe(false);
173+
expect(isFileEntry({} as unknown as FileSystemEntry)).toBe(false);
174+
});
175+
});
176+
177+
describe('getTransferItemDirectoryEntry', () => {
178+
it('should return directory entry when item at index is a directory', () => {
179+
const mockDirEntry = mockFileSystemEntry(true, false);
180+
const mockTransferItems = mockDataTransferItemList(mockDirEntry);
181+
182+
const result = getTransferItemDirectoryEntry(mockTransferItems, 0);
183+
expect(result).toBe(mockDirEntry);
184+
expect(mockTransferItems[0].webkitGetAsEntry).toHaveBeenCalledTimes(1);
185+
});
186+
187+
it('should return undefined when item at index is a file', () => {
188+
const mockFileEntry = mockFileSystemEntry(false, true);
189+
const mockTransferItems = mockDataTransferItemList(mockFileEntry);
190+
191+
const result = getTransferItemDirectoryEntry(mockTransferItems, 0);
192+
expect(result).toBeUndefined();
193+
});
194+
195+
it('should return undefined when index is out of bounds', () => {
196+
expect(getTransferItemDirectoryEntry(undefined as DataTransferItemList, 0)).toBeUndefined();
197+
expect(getTransferItemDirectoryEntry({} as DataTransferItemList, 999)).toBeUndefined();
198+
});
199+
200+
it('should return undefined when webkitGetAsEntry returns null', () => {
201+
const mockTransferItems = mockDataTransferItemList(null);
202+
expect(getTransferItemDirectoryEntry(mockTransferItems, 0)).toBeUndefined();
203+
});
113204
});
114205
});

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ import { fileMatchesAcceptedFormat, fileSizeLimitCompare } from './actions';
2929
import { FileAttachmentEntry } from './FileAttachmentEntry';
3030
import { ALL_FILES_LIMIT_KEY } from './models';
3131

32-
const isDirectoryEntry = (entry: FileSystemEntry): entry is FileSystemDirectoryEntry => entry.isDirectory;
33-
const isFileEntry = (entry: FileSystemEntry): entry is FileSystemFileEntry => entry.isFile;
32+
export const isDirectoryEntry = (entry: FileSystemEntry): entry is FileSystemDirectoryEntry => !!entry?.isDirectory;
33+
export const isFileEntry = (entry: FileSystemEntry): entry is FileSystemFileEntry => !!entry?.isFile;
34+
export const getTransferItemDirectoryEntry = (
35+
transferItems: DataTransferItemList,
36+
index: number
37+
): FileSystemDirectoryEntry | undefined => {
38+
const entry = transferItems?.[index]?.webkitGetAsEntry?.();
39+
if (entry && isDirectoryEntry(entry)) return entry;
40+
return undefined;
41+
};
3442

3543
interface Props {
3644
acceptedFormats?: string; // comma separated list of allowed extensions i.e. '.png, .jpg, .jpeg'
@@ -173,7 +181,7 @@ export class FileAttachmentContainer extends React.PureComponent<Props, State> {
173181
const invalidNames = new Set<string>();
174182

175183
Array.from(fileList).forEach((file, index) => {
176-
if (transferItems && transferItems[index].webkitGetAsEntry().isDirectory) {
184+
if (getTransferItemDirectoryEntry(transferItems, index)) {
177185
if (!allowDirectories) {
178186
invalidDirectories.push(file.name);
179187
invalidNames.add(file.name);
@@ -312,7 +320,7 @@ export class FileAttachmentContainer extends React.PureComponent<Props, State> {
312320
newFiles[file.name] = file;
313321
haveValidFiles = true;
314322

315-
if (transferItems && transferItems[index].webkitGetAsEntry().isDirectory) {
323+
if (getTransferItemDirectoryEntry(transferItems, index)) {
316324
hasDirectory = true;
317325
}
318326
}
@@ -325,8 +333,8 @@ export class FileAttachmentContainer extends React.PureComponent<Props, State> {
325333
this.dirCbCount = 0;
326334
this.fileCbCount = 0;
327335
Array.from(fileList).forEach((file, index) => {
328-
const entry = transferItems[index].webkitGetAsEntry();
329-
if (isDirectoryEntry(entry)) {
336+
const entry = getTransferItemDirectoryEntry(transferItems, index);
337+
if (entry) {
330338
delete files[file.name];
331339
this.getFilesFromDirectory(files, entry, this, () => {
332340
this._handleFiles(files);

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { FILELINK_RANGE_URI } from '../../domainproperties/constants';
2929

3030
import { fileMatchesAcceptedFormat } from '../../files/actions';
3131

32+
import { getTransferItemDirectoryEntry } from '../../files/FileAttachmentContainer';
33+
3234
import { DisableableInput, DisableableInputProps, DisableableInputState } from './DisableableInput';
3335

3436
export interface FileInputProps extends DisableableInputProps {
@@ -42,11 +44,11 @@ export interface FileInputProps extends DisableableInputProps {
4244
labelClassName?: string;
4345
maxFileSize?: number;
4446
name?: string;
47+
onChange?: (fileMap: Record<string, File>) => void;
4548
queryColumn?: QueryColumn;
4649
renderFieldLabel?: (queryColumn: QueryColumn, label?: string, description?: string) => ReactNode;
4750
showLabel?: boolean;
4851
toggleDisabledTooltip?: string;
49-
onChange?: (fileMap: Record<string, File>) => void;
5052
}
5153

5254
type FileInputImplProps = FileInputProps & FormsyInjectedProps<any>;
@@ -107,7 +109,7 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
107109
return;
108110
}
109111

110-
if (transferItems && transferItems[0].webkitGetAsEntry().isDirectory) {
112+
if (getTransferItemDirectoryEntry(transferItems, 0)) {
111113
this.setState({ error: 'Folders are not supported, only one file allowed' });
112114
return;
113115
}
@@ -122,7 +124,9 @@ class FileInputImpl extends DisableableInput<FileInputImplProps, State> {
122124
}
123125

124126
if (maxFileSize && file.size > maxFileSize) {
125-
this.setState({ error: `File size must not exceed ${Math.round(maxFileSize / 1024).toLocaleString()} KB.` });
127+
this.setState({
128+
error: `File size must not exceed ${Math.round(maxFileSize / 1024).toLocaleString()} KB.`,
129+
});
126130
return;
127131
}
128132
if (emptyFileNotAllowed && file.size === 0) {

packages/components/src/internal/components/lineage/actions.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -279,30 +279,32 @@ export class ServerLineageAPIWrapper implements LineageAPIWrapper {
279279
return new Promise((resolve, reject) => {
280280
const seed = options.lsid;
281281

282+
function failure(error?: { exception: string; exceptionClass?: string }): void {
283+
let message = `Failed to fetch lineage for seed "${seed}".`;
284+
285+
if (error?.exception) {
286+
message = error.exception;
287+
288+
// When a server exception occurs
289+
if (error.exceptionClass) {
290+
message = `${error.exceptionClass}: ` + error.exception;
291+
}
292+
}
293+
294+
reject({ message, seed });
295+
}
296+
282297
Experiment.lineage({
283298
...options,
284299
success: lineage => {
285-
resolve(LineageResult.create(lineage));
286-
},
287-
failure: error => {
288-
let message = `Failed to fetch lineage for seed "${seed}".`;
289-
290-
if (error) {
291-
if (error.exception) {
292-
message = error.exception;
293-
294-
// When a server exception occurs
295-
if (error.exceptionClass) {
296-
message = `${error.exceptionClass}: ` + error.exception;
297-
}
298-
}
300+
// Issue 53149: Lineage loads undefined object
301+
if (lineage) {
302+
resolve(LineageResult.create(lineage));
303+
} else {
304+
failure();
299305
}
300-
301-
reject({
302-
seed,
303-
message,
304-
});
305306
},
307+
failure,
306308
});
307309
});
308310
};

0 commit comments

Comments
 (0)