Skip to content

Commit 039ebb1

Browse files
committed
Merge branch 'develop' into fb_issue52925
# Conflicts: # packages/components/package-lock.json # packages/components/package.json # packages/components/releaseNotes/components.md
2 parents 95093ab + 37cd778 commit 039ebb1

9 files changed

Lines changed: 201 additions & 45 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.1-fb-issue52925.1",
3+
"version": "6.44.3",
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: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4-
### version 6.X
5-
*Released*: X May 2025
4+
### version 6.44.3
5+
*Released*: 28 May 2025
66
- Issue 52925: App export to csv/tsv ignores filter with column containing double quote
77
- Add `encodeFormDataQuote` util to encode `"` and its encoded form `%22`
88
- Encode form params for exportRows actions
99

10+
### version 6.44.2
11+
*Released*: 28 May 2025
12+
- Issue 53164: AssayPicker.tsx gives JS error if no specialty assay providers available for server
13+
14+
### version 6.44.1
15+
*Released*: 28 May 2025
16+
- Introduce `getTransferItemDirectoryEntry` to centralize handling/wrapping of calling `webkitGetAsEntry()` on a `DataTransferItemList` item.
17+
- Address Issue 53149 by updating endpoint wrapper to be tolerant of invalid payload upon success.
18+
1019
### version 6.44.0
1120
*Released*: 27 May 2025
1221
- QueryModel/QueryConfig change useSavedSettings from boolean to enum SavedSettings

packages/components/src/internal/components/assay/AssayPicker.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@ const load = (): Promise<any> => {
1414
return Promise.resolve(getAssayDesignSectionOptions);
1515
};
1616

17+
const loadGeneralOnly = (): Promise<any> => {
18+
return Promise.resolve({
19+
locations: {
20+
'ea7c355b-0b2e-1039-8359-225b19643884': 'Current Folder (BiologicsAssayTest Project)',
21+
'ce3fb429-92f9-1038-8523-225b19648208': 'Shared Folder',
22+
},
23+
providers: [
24+
{
25+
name: 'General',
26+
description: 'Imports data from simple Excel or TSV files.',
27+
fileTypes: ['.tsv', '.csv', '.xls', '.xlsx', '.txt', '.fna', '.fasta'],
28+
},
29+
],
30+
defaultLocation: 'ea7c355b-0b2e-1039-8359-225b19643884',
31+
});
32+
};
33+
1734
describe('AssayPicker', () => {
1835
test('AssayPicker', async () => {
1936
renderWithAppContext(
@@ -60,4 +77,29 @@ describe('AssayPicker', () => {
6077
expect(document.querySelectorAll('#assay-type-select-container')).toHaveLength(0);
6178
expect(document.querySelectorAll('.alert-info')).toHaveLength(1);
6279
});
80+
81+
test('AssayPicker General provider only', async () => {
82+
renderWithAppContext(
83+
<AssayPicker
84+
defaultTab={AssayPickerTabs.SPECIALTY_ASSAY_TAB}
85+
hasPremium={false}
86+
loadOptions={loadGeneralOnly}
87+
onChange={jest.fn()}
88+
showContainerSelect={false}
89+
showImport={false}
90+
/>
91+
);
92+
93+
await waitFor(() => {
94+
expect(document.querySelectorAll('.nav-tabs li')).toHaveLength(2);
95+
});
96+
97+
// Verify only two tabs and specialty tab selected
98+
expect(document.querySelectorAll('.nav-tabs li')).toHaveLength(2);
99+
expect(document.querySelectorAll('.nav-tabs li')[0].textContent).toEqual('Standard Assay');
100+
expect(document.querySelectorAll('.nav-tabs li')[1].textContent).toEqual('Specialty Assays');
101+
expect(document.querySelectorAll('.nav-tabs li')[1].getAttribute('class')).toEqual('active');
102+
expect(document.querySelectorAll('#assay-type-select-container')).toHaveLength(0);
103+
expect(document.querySelectorAll('.alert-info')).toHaveLength(1);
104+
});
63105
});

packages/components/src/internal/components/assay/AssayPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export const AssayPicker: FC<AssayPickerProps> = memo(props => {
140140
if (tab_ === AssayPickerTabs.STANDARD_ASSAY_TAB) {
141141
selectProvider(GENERAL_ASSAY_PROVIDER_NAME);
142142
} else if (tab_ === AssayPickerTabs.SPECIALTY_ASSAY_TAB) {
143-
if (providers.length > 0 && (!provider || provider.name === GENERAL_ASSAY_PROVIDER_NAME)) {
143+
if (providers.length > 1 && (!provider || provider.name === GENERAL_ASSAY_PROVIDER_NAME)) {
144144
selectProvider(providers.filter(p => p.name !== GENERAL_ASSAY_PROVIDER_NAME)?.[0].name);
145145
}
146146
}

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) {

0 commit comments

Comments
 (0)