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
53 changes: 47 additions & 6 deletions src/elements/content-uploader/ContentUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export interface ContentUploaderProps {
uploadHost: string;
useUploadsManager?: boolean;
enableModernizedUploads?: boolean;
isExpanded?: boolean;
onToggle?: (isExpanded: boolean) => void;
}

type State = {
Expand Down Expand Up @@ -655,7 +657,7 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
*/
addToQueue = (newItems: UploadItem[], itemUpdateCallback: Function) => {
const { fileLimit, useUploadsManager } = this.props;
const { isUploadsManagerExpanded } = this.state;
const isUploadsManagerExpanded = this.getIsExpanded();

let updatedItems = [];
const prevItemsNum = this.itemsRef.current.length;
Expand Down Expand Up @@ -965,8 +967,35 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
});
};

/**
* Whether the component is in controlled-expand mode.
*
* @return {boolean}
*/
isExpandControlled = (): boolean => this.props.isExpanded !== undefined;

/**
* Returns the resolved expanded state. When `isExpanded` prop is provided
* the component is in controlled mode and the prop wins; otherwise the
* internal state is used (uncontrolled mode).
*
* @return {boolean}
*/
getIsExpanded = (): boolean => {
if (this.isExpandControlled()) {
return this.props.isExpanded;
}

return this.state.isUploadsManagerExpanded;
};

resetUploadManagerExpandState = () => {
this.isAutoExpanded = false;
// In controlled mode the consumer owns the expanded state. Auto-collapse
// (post-completion timeout, cancel-all) must not mutate that preference.
if (this.isExpandControlled()) {
return;
}
this.setState({
isUploadsManagerExpanded: false,
});
Expand Down Expand Up @@ -1297,6 +1326,12 @@ class ContentUploader extends Component<ContentUploaderProps, State> {

clearTimeout(this.resetItemsTimeout);

// In controlled mode the consumer owns the expanded state. Auto-expand
// (file-count threshold) must not mutate that preference.
if (this.isExpandControlled()) {
return;
}

this.setState({ isUploadsManagerExpanded: true });
};

Expand Down Expand Up @@ -1337,13 +1372,17 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
* @return {void}
*/
toggleUploadsManager = (): void => {
const { isUploadsManagerExpanded } = this.state;
const { onToggle } = this.props;
const isExpanded = this.getIsExpanded();
const nextExpanded = !isExpanded;

if (isUploadsManagerExpanded) {
if (isExpanded) {
this.minimizeUploadsManager();
} else {
} else if (!this.isExpandControlled()) {
this.expandUploadsManager();
}

onToggle?.(nextExpanded);
};

/**
Expand Down Expand Up @@ -1397,7 +1436,8 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
*/
resetUploadsManagerItemsWhenUploadsComplete = (): void => {
const { onCancel, useUploadsManager } = this.props;
const { isUploadsManagerExpanded, view } = this.state;
const { view } = this.state;
const isUploadsManagerExpanded = this.getIsExpanded();

// Do not reset items when upload manger is expanded or there're uploads in progress
if (
Expand Down Expand Up @@ -1459,7 +1499,8 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
theme,
useUploadsManager,
}: ContentUploaderProps = this.props;
const { view, items, errorCode, isCancelAllModalOpen, isUploadsManagerExpanded }: State = this.state;
const { view, items, errorCode, isCancelAllModalOpen }: State = this.state;
const isUploadsManagerExpanded = this.getIsExpanded();
const isEmpty = items.length === 0;
const isVisible = !isEmpty || !!isDraggingItemsToUploadsManager;

Expand Down
117 changes: 117 additions & 0 deletions src/elements/content-uploader/__tests__/ContentUploader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,123 @@ describe('elements/content-uploader/ContentUploader', () => {
});
});

describe('controlled isExpanded / onToggle', () => {
test('uses isExpanded prop value when in controlled mode', () => {
const wrapper = getWrapper({ enableModernizedUploads: true, isExpanded: true, onToggle: jest.fn() });
expect(wrapper.find(UploadsManagerBP).prop('isExpanded')).toBe(true);

wrapper.setProps({ isExpanded: false });
expect(wrapper.find(UploadsManagerBP).prop('isExpanded')).toBe(false);
});

test('falls back to internal state when isExpanded prop is not provided', () => {
const wrapper = getWrapper({ enableModernizedUploads: true });
expect(wrapper.find(UploadsManagerBP).prop('isExpanded')).toBe(false);

wrapper.setState({ isUploadsManagerExpanded: true });
expect(wrapper.find(UploadsManagerBP).prop('isExpanded')).toBe(true);
});

test('toggleUploadsManager calls onToggle with next value in controlled mode and does not mutate internal state', () => {
const onToggle = jest.fn();
const wrapper = getWrapper({
enableModernizedUploads: true,
useUploadsManager: true,
isExpanded: false,
onToggle,
});

wrapper.instance().toggleUploadsManager();

expect(onToggle).toHaveBeenCalledTimes(1);
expect(onToggle).toHaveBeenCalledWith(true);
expect(wrapper.state().isUploadsManagerExpanded).toBe(false);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test('toggleUploadsManager flips internal state in uncontrolled mode and fires onToggle', () => {
const onToggle = jest.fn();
const wrapper = getWrapper({ useUploadsManager: true, onToggle });

wrapper.instance().toggleUploadsManager();

expect(wrapper.state().isUploadsManagerExpanded).toBe(true);
expect(onToggle).toHaveBeenCalledTimes(1);
expect(onToggle).toHaveBeenCalledWith(true);
});

test('toggleUploadsManager does not require onToggle in uncontrolled mode', () => {
const wrapper = getWrapper({ useUploadsManager: true });

expect(() => wrapper.instance().toggleUploadsManager()).not.toThrow();
expect(wrapper.state().isUploadsManagerExpanded).toBe(true);
});

test('auto-expand on file-count threshold does not mutate state in controlled mode', () => {
const onToggle = jest.fn();
const wrapper = getWrapper({
useUploadsManager: true,
isExpanded: false,
onToggle,
});
const instance = wrapper.instance();
const mockAPI = { upload: () => {} };
instance.createAPIFactory = jest.fn().mockReturnValue({
getPlainUploadAPI: () => mockAPI,
getChunkedUploadAPI: () => mockAPI,
});

const files = createMockFiles(EXPAND_UPLOADS_MANAGER_ITEMS_NUM_THRESHOLD + 1);
wrapper.setProps({ files });

expect(wrapper.state().isUploadsManagerExpanded).toBe(false);
// Auto-expand must not leak into the persisted preference via onToggle.
expect(onToggle).not.toHaveBeenCalled();
});

test('controlled-mode collapse runs minimizeUploadsManager side effects (onMinimize, cleanup timer, isAutoExpanded reset)', () => {
jest.useFakeTimers();
const onToggle = jest.fn();
const onMinimize = jest.fn();
const wrapper = getWrapper({
enableModernizedUploads: true,
useUploadsManager: true,
isExpanded: true,
onToggle,
onMinimize,
});
const instance = wrapper.instance();
instance.isAutoExpanded = true;
instance.checkClearUploadItems = jest.fn();

instance.toggleUploadsManager();

expect(onToggle).toHaveBeenCalledWith(false);
expect(onMinimize).toHaveBeenCalledTimes(1);
expect(instance.isAutoExpanded).toBe(false);
expect(instance.checkClearUploadItems).toHaveBeenCalledTimes(1);
// Internal state still owned by consumer — untouched.
expect(wrapper.state().isUploadsManagerExpanded).toBe(false);
jest.useRealTimers();
});

test('auto-collapse via resetUploadManagerExpandState is a no-op in controlled mode', () => {
const onToggle = jest.fn();
const wrapper = getWrapper({
useUploadsManager: true,
isExpanded: true,
onToggle,
});
const instance = wrapper.instance();
instance.isAutoExpanded = true;

instance.resetUploadManagerExpandState();

expect(instance.isAutoExpanded).toBe(false);
expect(wrapper.state().isUploadsManagerExpanded).toBe(false); // initial default
expect(onToggle).not.toHaveBeenCalled();
});
});

describe('componentDidMount()', () => {
test('adds files to upload queue if isPrepopulateFilesEnabled is true and files are provided', () => {
const files = createMockFiles(3);
Expand Down
Loading