Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(content-uploader): add onSelection callback (#3839) #3843

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
9 changes: 7 additions & 2 deletions src/elements/content-explorer/ContentExplorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ type Props = {
canUpload: boolean,
className: string,
contentPreviewProps: ContentPreviewProps,
contentUploaderProps: ContentUploaderProps,
/** Props to be forwarded to the ContentUploader UI Element, including onSelection callback for file validation */
contentUploaderProps: ContentUploaderProps & {
onSelection?: (files: FileList) => boolean,
},
currentFolderId?: string,
defaultView: DefaultView,
features: FeatureConfig,
Expand Down Expand Up @@ -224,7 +227,9 @@ class ContentExplorer extends Component<Props, State> {
contentPreviewProps: {
contentSidebarProps: {},
},
contentUploaderProps: {},
contentUploaderProps: {
onSelection: undefined, // Optional callback for file validation
},
};

/**
Expand Down
2 changes: 2 additions & 0 deletions src/elements/content-uploader/ContentUploader.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ import '../common/fonts.scss';
import '../common/base.scss';

type Props = {
/** Callback invoked when files are selected, before upload begins. Return false to prevent upload. */
onSelection?: (files: FileList) => boolean,
apiHost: string,
chunked: boolean,
className: string,
Expand Down
2 changes: 2 additions & 0 deletions src/elements/content-uploader/ContentUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ import '../common/fonts.scss';
import '../common/base.scss';

export interface ContentUploaderProps {
/** Callback invoked when files are selected, before upload begins. Return false to prevent upload. */
onSelection?: (files: FileList) => boolean;
apiHost: string;
chunked: boolean;
className: string;
Expand Down
18 changes: 15 additions & 3 deletions src/elements/content-uploader/UploadInput.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@
import * as React from 'react';

type Props = {
handleChange: Function,
onChange: Function,
/** Optional callback to validate files before selection. Return false to prevent selection. */
onSelection?: (files: FileList) => boolean,
inputLabel?: React.Node,
inputLabelClass?: string,
isFolderUpload?: boolean,
isMultiple?: boolean,
};

const UploadInput = ({
handleChange,
onChange,
inputLabel,
inputLabelClass = '',
isFolderUpload = false,
isMultiple = true,
onSelection,
}: Props) => {
const inputRef = React.useRef(null);

Expand All @@ -39,7 +42,16 @@ const UploadInput = ({
data-testid="upload-input"
directory={isFolderUpload ? '' : undefined}
multiple={isMultiple}
onChange={handleChange}
onChange={(event) => {
const { files } = event.target;
if (onSelection && files) {
const shouldContinue = onSelection(files);
if (!shouldContinue) {
return;
}
}
onChange(event);
}}
ref={inputRef}
type="file"
webkitdirectory={isFolderUpload ? '' : undefined}
Expand Down
14 changes: 13 additions & 1 deletion src/elements/content-uploader/UploadInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface UploadInputProps {
isFolderUpload?: boolean;
isMultiple?: boolean;
onChange: React.ChangeEventHandler<HTMLInputElement>;
/** Optional callback to validate files before selection. Return false to prevent selection. */
onSelection?: (files: FileList) => boolean;
}

const UploadInput = ({
Expand All @@ -23,6 +25,7 @@ const UploadInput = ({
isFolderUpload = false,
isMultiple = true,
onChange,
onSelection,
}: UploadInputProps) => {
const inputRef = React.useRef(null);

Expand All @@ -42,7 +45,16 @@ const UploadInput = ({
data-testid="upload-input"
directory={isFolderUpload ? '' : undefined}
multiple={isMultiple}
onChange={onChange}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const { files } = event.target;
if (onSelection && files) {
const shouldContinue = onSelection(files);
if (!shouldContinue) {
return;
}
}
onChange(event);
}}
ref={inputRef}
type="file"
webkitdirectory={isFolderUpload ? '' : undefined}
Expand Down
6 changes: 3 additions & 3 deletions src/elements/content-uploader/UploadStateContent.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const UploadStateContent = ({ fileInputLabel, folderInputLabel, message, onChang
const inputLabelClass = useButton ? 'btn btn-primary be-input-btn' : 'be-input-link';
const shouldShowFolderUploadInput = !useButton && !!folderInputLabel;

const handleChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
const handleInputChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
if (!onChange) {
return;
}
Expand All @@ -34,11 +34,11 @@ const UploadStateContent = ({ fileInputLabel, folderInputLabel, message, onChang
};

const fileInputContent = (
<UploadInput handleChange={handleChange} inputLabel={fileInputLabel} inputLabelClass={inputLabelClass} />
<UploadInput onChange={handleInputChange} inputLabel={fileInputLabel} inputLabelClass={inputLabelClass} />
);
const folderInputContent = shouldShowFolderUploadInput ? (
<UploadInput
handleChange={handleChange}
onChange={handleInputChange}
inputLabel={folderInputLabel}
inputLabelClass={inputLabelClass}
isFolderUpload
Expand Down
92 changes: 89 additions & 3 deletions src/elements/content-uploader/__tests__/UploadInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import * as React from 'react';

import { render, screen } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';

import UploadInput from '../UploadInput';
import UploadInput, { UploadInputProps } from '../UploadInput';

describe('elements/content-uploader/UploadInput', () => {
const renderComponent = props => render(<UploadInput handleChange={jest.fn()} {...props} />);
const renderComponent = (props: Partial<UploadInputProps>) => {
const defaultProps: UploadInputProps = {
onChange: jest.fn(),
};
return render(<UploadInput {...defaultProps} {...props} />);
};

test('should render correctly when inputLabel is available', () => {
renderComponent({
Expand Down Expand Up @@ -61,4 +66,85 @@ describe('elements/content-uploader/UploadInput', () => {
expect(screen.getByText('yo')).toBeInTheDocument();
expect(screen.getByLabelText('yo')).not.toHaveAttribute('multiple');
});

describe('onSelection callback', () => {
const createMockFileList = (files: File[]) => ({
...files,
item: (i: number) => files[i],
length: files.length,
});

test('should call onSelection with FileList when provided', () => {
const onSelection = jest.fn(() => true);
const onChange = jest.fn();
const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' });
const mockFileList = createMockFileList([mockFile]);

renderComponent({
inputLabel: 'upload',
onSelection,
onChange,
});

const input = screen.getByTestId('upload-input');
fireEvent.change(input, { target: { files: mockFileList } });

expect(onSelection).toHaveBeenCalledWith(mockFileList);
expect(onChange).toHaveBeenCalled();
});

test('should prevent upload when onSelection returns false', () => {
const onSelection = jest.fn(() => false);
const onChange = jest.fn();
const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' });
const mockFileList = createMockFileList([mockFile]);

renderComponent({
inputLabel: 'upload',
onSelection,
onChange,
});

const input = screen.getByTestId('upload-input');
fireEvent.change(input, { target: { files: mockFileList } });

expect(onSelection).toHaveBeenCalledWith(mockFileList);
expect(onChange).not.toHaveBeenCalled();
});

test('should proceed with upload when onSelection returns true', () => {
const onSelection = jest.fn(() => true);
const onChange = jest.fn();
const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' });
const mockFileList = createMockFileList([mockFile]);

renderComponent({
inputLabel: 'upload',
onSelection,
onChange,
});

const input = screen.getByTestId('upload-input');
fireEvent.change(input, { target: { files: mockFileList } });

expect(onSelection).toHaveBeenCalledWith(mockFileList);
expect(onChange).toHaveBeenCalled();
});

test('should proceed with upload when onSelection is not provided', () => {
const onChange = jest.fn();
const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' });
const mockFileList = createMockFileList([mockFile]);

renderComponent({
inputLabel: 'upload',
onChange,
});

const input = screen.getByTestId('upload-input');
fireEvent.change(input, { target: { files: mockFileList } });

expect(onChange).toHaveBeenCalled();
});
});
});
Loading