feat: refined ux update a taxonomy by downloading and uploading [FC-0036] (#732)

This PR improves the import tags functionality for existing taxonomies implemented at #675.

Co-authored-by: Jillian <jill@opencraft.com>
Co-authored-by: Braden MacDonald <mail@bradenm.com>
This commit is contained in:
Rômulo Penido
2024-01-16 03:30:15 -03:00
committed by GitHub
parent 1fef358f55
commit b59ecafc83
26 changed files with 1190 additions and 460 deletions

View File

@@ -0,0 +1,76 @@
import React from 'react';
import {
act,
fireEvent,
render,
} from '@testing-library/react';
import LoadingButton from '.';
const buttonTitle = 'Button Title';
const RootWrapper = (onClick) => (
<LoadingButton label={buttonTitle} onClick={onClick} />
);
describe('<LoadingButton />', () => {
it('renders the title and doesnt handle the spinner initially', () => {
const { container, getByText } = render(RootWrapper(() => { }));
expect(getByText(buttonTitle)).toBeInTheDocument();
expect(container.getElementsByClassName('icon-spin').length).toBe(0);
});
it('doesnt render the spinner without onClick function', () => {
const { container, getByRole, getByText } = render(RootWrapper());
const titleElement = getByText(buttonTitle);
expect(titleElement).toBeInTheDocument();
expect(container.getElementsByClassName('icon-spin').length).toBe(0);
fireEvent.click(getByRole('button'));
expect(container.getElementsByClassName('icon-spin').length).toBe(0);
});
it('renders the spinner correctly', async () => {
let resolver;
const longFunction = () => new Promise((resolve) => {
resolver = resolve;
});
const { container, getByRole, getByText } = render(RootWrapper(longFunction));
const buttonElement = getByRole('button');
fireEvent.click(buttonElement);
expect(container.getElementsByClassName('icon-spin').length).toBe(1);
expect(getByText(buttonTitle)).toBeInTheDocument();
// StatefulButton only sets aria-disabled (not disabled) when the state is pending
// expect(buttonElement).toBeDisabled();
expect(buttonElement).toHaveAttribute('aria-disabled', 'true');
await act(async () => { resolver(); });
expect(buttonElement).not.toHaveAttribute('aria-disabled', 'true');
expect(container.getElementsByClassName('icon-spin').length).toBe(0);
});
it('renders the spinner correctly even with error', async () => {
let rejecter;
const longFunction = () => new Promise((_resolve, reject) => {
rejecter = reject;
});
const { container, getByRole, getByText } = render(RootWrapper(longFunction));
const buttonElement = getByRole('button');
fireEvent.click(buttonElement);
expect(container.getElementsByClassName('icon-spin').length).toBe(1);
expect(getByText(buttonTitle)).toBeInTheDocument();
// StatefulButton only sets aria-disabled (not disabled) when the state is pending
// expect(buttonElement).toBeDisabled();
expect(buttonElement).toHaveAttribute('aria-disabled', 'true');
await act(async () => { rejecter(new Error('error')); });
// StatefulButton only sets aria-disabled (not disabled) when the state is pending
// expect(buttonElement).toBeEnabled();
expect(buttonElement).not.toHaveAttribute('aria-disabled', 'true');
expect(container.getElementsByClassName('icon-spin').length).toBe(0);
});
});

View File

@@ -0,0 +1,72 @@
// @ts-check
import React, {
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import {
StatefulButton,
} from '@edx/paragon';
import PropTypes from 'prop-types';
/**
* A button that shows a loading spinner when clicked.
* @param {object} props
* @param {string} props.label
* @param {function=} props.onClick
* @param {boolean=} props.disabled
* @returns {JSX.Element}
*/
const LoadingButton = ({
label,
onClick,
disabled,
}) => {
const [state, setState] = useState('');
// This is used to prevent setting the isLoading state after the component has been unmounted.
const componentMounted = useRef(true);
useEffect(() => () => {
componentMounted.current = false;
}, []);
const loadingOnClick = useCallback(async (e) => {
if (!onClick) {
return;
}
setState('pending');
try {
await onClick(e);
} catch (err) {
// Do nothing
} finally {
if (componentMounted.current) {
setState('');
}
}
}, [componentMounted, onClick]);
return (
<StatefulButton
disabledStates={disabled ? [state] : ['pending'] /* StatefulButton doesn't support disabled prop */}
onClick={loadingOnClick}
labels={{ default: label }}
state={state}
/>
);
};
LoadingButton.propTypes = {
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
disabled: PropTypes.bool,
};
LoadingButton.defaultProps = {
onClick: undefined,
disabled: undefined,
};
export default LoadingButton;