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

fix: Update error messages when adding user to library [FC-0062] #1543

Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ describe('<ProcessingNotification />', () => {
expect(screen.getByText('Undo')).toBeInTheDocument();
expect(screen.getByRole('alert').querySelector('.processing-notification-hide-close-button')).not.toBeInTheDocument();
userEvent.click(screen.getByText('Undo'));
expect(mockUndo).toBeCalled();
expect(mockUndo).toHaveBeenCalled();
});

it('renders with `disableCapitalize`', () => {
const title = 'ThIs IS a Test. OK?';
render(<ProcessingNotification {...props} close={() => {}} title={title} disableCapitalize />);
expect(screen.getByText(title)).toBeInTheDocument();
expect(screen.getByText('Undo')).toBeInTheDocument();
expect(screen.getByRole('alert').querySelector('.processing-notification-hide-close-button')).not.toBeInTheDocument();
});

it('add hide-close-button class if no close action is passed', () => {
Expand Down
8 changes: 6 additions & 2 deletions src/generic/processing-notification/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { capitalize } from 'lodash';
import classNames from 'classnames';

const ProcessingNotification = ({
isShow, title, action, close,
isShow, title, action, close, disableCapitalize,
}) => (
<Toast
className={classNames({ 'processing-notification-hide-close-button': !close })}
Expand All @@ -18,13 +18,16 @@ const ProcessingNotification = ({
>
<span className="d-flex align-items-center">
<Icon className="processing-notification-icon mb-0 mr-2" src={IconSettings} />
<span className="font-weight-bold h4 mb-0 text-white">{capitalize(title)}</span>
<span className="font-weight-bold h4 mb-0 text-white">
{!disableCapitalize ? capitalize(title) : title}
</span>
</span>
</Toast>
);

ProcessingNotification.defaultProps = {
close: null,
disableCapitalize: false,
};

ProcessingNotification.propTypes = {
Expand All @@ -35,6 +38,7 @@ ProcessingNotification.propTypes = {
onClick: PropTypes.func,
}),
close: PropTypes.func,
disableCapitalize: PropTypes.bool,
};

export default ProcessingNotification;
18 changes: 12 additions & 6 deletions src/generic/toast-context/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ export interface WraperProps {
children: React.ReactNode;
}

const TestComponentToShow = () => {
// eslint-disable-next-line react/prop-types
const TestComponentToShow = ({ capitilize = false }) => {
const { showToast } = React.useContext(ToastContext);

React.useEffect(() => {
showToast('This is the toast!');
showToast('This is the Toast!', undefined, capitilize);
}, [showToast]);

return <div>Content</div>;
Expand All @@ -23,7 +24,7 @@ const TestComponentToClose = () => {
const { showToast, closeToast } = React.useContext(ToastContext);

React.useEffect(() => {
showToast('This is the toast!');
showToast('This is the Toast!');
closeToast();
}, [showToast]);

Expand Down Expand Up @@ -59,19 +60,24 @@ describe('<ToastProvider />', () => {

it('should show toast', async () => {
render(<RootWrapper><TestComponentToShow /></RootWrapper>);
expect(await screen.findByText('This is the Toast!')).toBeInTheDocument();
});

it('should capitilize toast', async () => {
render(<RootWrapper><TestComponentToShow capitilize /></RootWrapper>);
expect(await screen.findByText('This is the toast!')).toBeInTheDocument();
});

it('should close toast after 5000ms', async () => {
render(<RootWrapper><TestComponentToShow /></RootWrapper>);
expect(await screen.findByText('This is the toast!')).toBeInTheDocument();
expect(await screen.findByText('This is the Toast!')).toBeInTheDocument();
jest.advanceTimersByTime(6000);
expect(screen.queryByText('This is the toast!')).not.toBeInTheDocument();
expect(screen.queryByText('This is the Toast!')).not.toBeInTheDocument();
});

it('should close toast', async () => {
render(<RootWrapper><TestComponentToClose /></RootWrapper>);
expect(await screen.findByText('Content')).toBeInTheDocument();
expect(screen.queryByText('This is the toast!')).not.toBeInTheDocument();
expect(screen.queryByText('This is the Toast!')).not.toBeInTheDocument();
});
});
15 changes: 11 additions & 4 deletions src/generic/toast-context/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export interface ToastActionData {
export interface ToastContextData {
toastMessage: string | null;
toastAction?: ToastActionData;
showToast: (message: string, action?: ToastActionData) => void;
capitilize?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this new parameter? I don't think "capitalization" should be a feature of our toasts. Do we really need it?

Also, the spelling is wrong in several places. It should be "Capitalize" with two As.

Copy link
Contributor Author

@ChrisChV ChrisChV Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed here d92ea86

showToast: (message: string, action?: ToastActionData, capitilize?: boolean) => void;
closeToast: () => void;
}

Expand All @@ -25,6 +26,7 @@ export interface ToastProviderProps {
export const ToastContext = React.createContext<ToastContextData>({
toastMessage: null,
toastAction: undefined,
capitilize: false,
showToast: () => {},
closeToast: () => {},
});
Expand All @@ -38,29 +40,33 @@ export const ToastProvider = (props: ToastProviderProps) => {

const [toastMessage, setToastMessage] = React.useState<string | null>(null);
const [toastAction, setToastAction] = React.useState<ToastActionData | undefined>(undefined);
const [capitilize, setCapitilize] = React.useState<boolean>(false);

const resetState = React.useCallback(() => {
setToastMessage(null);
setToastAction(undefined);
setCapitilize(false);
}, []);

React.useEffect(() => () => {
// Cleanup function to avoid updating state on unmounted component
resetState();
}, []);

const showToast = React.useCallback((message, action?: ToastActionData) => {
const showToast = React.useCallback((message, action?: ToastActionData, isCapitilize?: boolean) => {
setToastMessage(message);
setToastAction(action);
setCapitilize(isCapitilize ?? false);
}, [setToastMessage, setToastAction]);
const closeToast = React.useCallback(() => resetState(), [setToastMessage, setToastAction]);
const closeToast = React.useCallback(() => resetState(), [setToastMessage, setToastAction, setCapitilize]);

const context = React.useMemo(() => ({
toastMessage,
toastAction,
capitilize,
showToast,
closeToast,
}), [toastMessage, toastAction, showToast, closeToast]);
}), [toastMessage, toastAction, capitilize, showToast, closeToast]);

return (
<ToastContext.Provider value={context}>
Expand All @@ -71,6 +77,7 @@ export const ToastProvider = (props: ToastProviderProps) => {
title={toastMessage}
action={toastAction}
close={closeToast}
disableCapitalize={!capitilize}
/>
)}
</ToastContext.Provider>
Expand Down
59 changes: 56 additions & 3 deletions src/library-authoring/library-team/LibraryTeam.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getLibraryTeamMemberApiUrl,
} from '../data/api';
import { LibraryProvider } from '../common/context';
import { ToastProvider } from '../../generic/toast-context';
import LibraryTeam from './LibraryTeam';

mockContentLibrary.applyMock();
Expand All @@ -28,9 +29,11 @@ describe('<LibraryTeam />', () => {
const { libraryId } = mockContentLibrary;
const renderLibraryTeam = async () => {
render(
<LibraryProvider libraryId={libraryId}>
<LibraryTeam />
</LibraryProvider>,
<ToastProvider>
<LibraryProvider libraryId={libraryId}>
<LibraryTeam />
</LibraryProvider>
</ToastProvider>,
);

await waitFor(() => {
Expand Down Expand Up @@ -176,6 +179,56 @@ describe('<LibraryTeam />', () => {
`{"library_id":"${libraryId}","email":"[email protected]","access_level":"read"}`,
);
});

expect(await screen.findByText('Team Member added')).toBeInTheDocument();
});

it('shows error when user do not exist', async () => {
const url = getLibraryTeamApiUrl(libraryId);
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onPost(url).reply(400, { email: 'Error' });

await renderLibraryTeam();

const addButton = screen.getByRole('button', { name: 'New team member' });
userEvent.click(addButton);
const emailInput = screen.getByRole('textbox', { name: 'User\'s email address' });
userEvent.click(emailInput);
userEvent.type(emailInput, '[email protected]');

const saveButton = screen.getByRole('button', { name: /add member/i });
userEvent.click(saveButton);

await waitFor(() => {
expect(axiosMock.history.post.length).toEqual(1);
});

expect(await screen.findByText(
'Error adding Team Member. Please verify that the email is correct and belongs to a registered user.',
)).toBeInTheDocument();
});

it('shows error', async () => {
const url = getLibraryTeamApiUrl(libraryId);
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onPost(url).reply(400, { error: 'Error' });

await renderLibraryTeam();

const addButton = screen.getByRole('button', { name: 'New team member' });
userEvent.click(addButton);
const emailInput = screen.getByRole('textbox', { name: 'User\'s email address' });
userEvent.click(emailInput);
userEvent.type(emailInput, '[email protected]');

const saveButton = screen.getByRole('button', { name: /add member/i });
userEvent.click(saveButton);

await waitFor(() => {
expect(axiosMock.history.post.length).toEqual(1);
});

expect(await screen.findByText('Error adding Team Member')).toBeInTheDocument();
});

it('allows library team member roles to be changed', async () => {
Expand Down
9 changes: 7 additions & 2 deletions src/library-authoring/library-team/LibraryTeam.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,13 @@ const LibraryTeam: React.FC<Record<never, never>> = () => {
accessLevel: LibraryRole.Reader.toString() as LibraryAccessLevel,
}).then(() => {
showToast(intl.formatMessage(messages.addMemberSuccess));
}).catch(() => {
showToast(intl.formatMessage(messages.addMemberError));
}).catch((addMemberError) => {
const errorData = addMemberError.response.data;
if ('email' in errorData) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is fragile, if a non-Axios error is raised:
Screenshot 2024-12-04 at 10 12 56 AM

Try something like:

Suggested change
const errorData = addMemberError.response.data;
if ('email' in errorData) {
const errorData = typeof addMemberError === 'object' ? addMemberError.response?.data : undefined;
if (errorData && 'email' in errorData) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated bb90f0b

showToast(intl.formatMessage(messages.addMemberEmailError));
} else {
showToast(intl.formatMessage(messages.addMemberError));
}
});
closeAddLibraryTeamMember();
},
Expand Down
5 changes: 5 additions & 0 deletions src/library-authoring/library-team/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ const messages = defineMessages({
defaultMessage: 'Error adding Team Member',
description: 'Message shown when an error occurs while adding a Library Team member',
},
addMemberEmailError: {
id: 'course-authoring.library-authoring.library-team.add-member-email-error',
defaultMessage: 'Error adding Team Member. Please verify that the email is correct and belongs to a registered user.',
description: 'Message shown when an error occurs with email while adding a Library Team member.',
},
deleteMemberSuccess: {
id: 'course-authoring.library-authoring.library-team.delete-member-success',
defaultMessage: 'Team Member deleted',
Expand Down
Loading