Skip to content

Commit

Permalink
Merge branch 'master' into rpenido/fal-3983-split-up-library-context
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido committed Dec 10, 2024
2 parents 8f89646 + c7e2bf9 commit a4b53a3
Show file tree
Hide file tree
Showing 17 changed files with 128 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { capitalize } from 'lodash';
import userEvent from '@testing-library/user-event';
import { initializeMocks, render, screen } from '../../testUtils';
import { NOTIFICATION_MESSAGES } from '../../constants';
import ProcessingNotification from '.';

const mockUndo = jest.fn();

const props = {
title: NOTIFICATION_MESSAGES.saving,
title: 'ThIs IS a Test. OK?',
isShow: true,
action: {
label: 'Undo',
Expand All @@ -22,16 +20,16 @@ describe('<ProcessingNotification />', () => {

it('renders successfully', () => {
render(<ProcessingNotification {...props} close={() => {}} />);
expect(screen.getByText(capitalize(props.title))).toBeInTheDocument();
expect(screen.getByText(props.title)).toBeInTheDocument();
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('add hide-close-button class if no close action is passed', () => {
render(<ProcessingNotification {...props} />);
expect(screen.getByText(capitalize(props.title))).toBeInTheDocument();
expect(screen.getByText(props.title)).toBeInTheDocument();
expect(screen.getByRole('alert').querySelector('.processing-notification-hide-close-button')).toBeInTheDocument();
});
});
3 changes: 1 addition & 2 deletions src/generic/processing-notification/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Icon, Toast,
} from '@openedx/paragon';
import { Settings as IconSettings } from '@openedx/paragon/icons';
import { capitalize } from 'lodash';
import classNames from 'classnames';

const ProcessingNotification = ({
Expand All @@ -18,7 +17,7 @@ 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">{title}</span>
</span>
</Toast>
);
Expand Down
12 changes: 6 additions & 6 deletions src/generic/toast-context/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const TestComponentToShow = () => {
const { showToast } = React.useContext(ToastContext);

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

return <div>Content</div>;
Expand All @@ -23,7 +23,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 +59,19 @@ describe('<ToastProvider />', () => {

it('should show toast', async () => {
render(<RootWrapper><TestComponentToShow /></RootWrapper>);
expect(await screen.findByText('This is the toast!')).toBeInTheDocument();
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();
});
});
2 changes: 1 addition & 1 deletion src/library-authoring/LibraryAuthoringPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
} else if (currentPath && currentPath in ContentType) {
setActiveKey(ContentType[currentPath]);
}
}, [location.pathname]);
}, []);

useEffect(() => {
if (!componentPickerMode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../data/api.mocks';
import { LibraryProvider } from '../common/context/LibraryContext';
import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext';
import * as apiHooks from '../data/apiHooks';
import { ComponentAdvancedInfo } from './ComponentAdvancedInfo';
import { getXBlockAssetsApiUrl } from '../data/api';

Expand All @@ -26,11 +27,12 @@ const setOLXspy = mockSetXBlockOLX.applyMock();
const render = (
usageKey: string = mockLibraryBlockMetadata.usageKeyPublished,
libraryId: string = mockContentLibrary.libraryId,
showOnlyPublished: boolean = false,
) => baseRender(
<ComponentAdvancedInfo />,
{
extraWrapper: ({ children }: { children: React.ReactNode }) => (
<LibraryProvider libraryId={libraryId}>
<LibraryProvider libraryId={libraryId}> showOnlyPublished={showOnlyPublished}
<SidebarProvider
initialSidebarComponentInfo={{
id: usageKey,
Expand Down Expand Up @@ -126,13 +128,31 @@ describe('<ComponentAdvancedInfo />', () => {
});

it('should display the OLX source of the block (when expanded)', async () => {
const usageKey = mockXBlockOLX.usageKeyHtml;
const spy = jest.spyOn(apiHooks, 'useXBlockOLX');

render(mockXBlockOLX.usageKeyHtml);
const expandButton = await screen.findByRole('button', { name: /Advanced details/ });
fireEvent.click(expandButton);
// Because of syntax highlighting, the OLX will be borken up by many different tags so we need to search for
// Because of syntax highlighting, the OLX will be broken up by many different tags so we need to search for
// just a substring:
const olxPart = /This is a text component which uses/;
await waitFor(() => expect(screen.getByText(olxPart)).toBeInTheDocument());
expect(spy).toHaveBeenCalledWith(usageKey, 'draft');
});

it('should display the published OLX source of the block (when expanded)', async () => {
const usageKey = mockXBlockOLX.usageKeyHtml;
const spy = jest.spyOn(apiHooks, 'useXBlockOLX');

render(usageKey, undefined, true);
const expandButton = await screen.findByRole('button', { name: /Advanced details/ });
fireEvent.click(expandButton);
// Because of syntax highlighting, the OLX will be broken up by many different tags so we need to search for
// just a substring:
const olxPart = /This is a text component which uses/;
await waitFor(() => expect(screen.getByText(olxPart)).toBeInTheDocument());
expect(spy).toHaveBeenCalledWith(usageKey, 'published');
});

it('does not display "Edit OLX" button and assets dropzone when the library is read-only', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { ComponentAdvancedAssets } from './ComponentAdvancedAssets';

const ComponentAdvancedInfoInner: React.FC<Record<never, never>> = () => {
const intl = useIntl();
const { readOnly } = useLibraryContext();
const { readOnly, showOnlyPublished } = useLibraryContext();
const { sidebarComponentInfo } = useSidebarContext();

const usageKey = sidebarComponentInfo?.id;
Expand All @@ -31,7 +31,10 @@ const ComponentAdvancedInfoInner: React.FC<Record<never, never>> = () => {
throw new Error('sidebarComponentUsageKey is required to render ComponentAdvancedInfo');
}

const { data: olx, isLoading: isOLXLoading } = useXBlockOLX(usageKey);
const { data: olx, isLoading: isOLXLoading } = useXBlockOLX(
usageKey,
showOnlyPublished ? 'published' : 'draft',
);
const editorRef = React.useRef<EditorAccessor | undefined>(undefined);
const [isEditingOLX, setEditingOLX] = React.useState(false);
const olxUpdater = useUpdateXBlockOLX(usageKey);
Expand Down
11 changes: 7 additions & 4 deletions src/library-authoring/data/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { camelCaseObject, getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { VersionSpec } from '../LibraryBlock';

const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;

Expand Down Expand Up @@ -52,12 +53,14 @@ export const getLibraryPasteClipboardUrl = (libraryId: string) => `${getApiBaseU
* Get the URL for the xblock fields/metadata API.
*/
export const getXBlockFieldsApiUrl = (usageKey: string) => `${getApiBaseUrl()}/api/xblock/v2/xblocks/${usageKey}/fields/`;
export const getXBlockFieldsVersionApiUrl = (usageKey: string, version: string) => `${getApiBaseUrl()}/api/xblock/v2/xblocks/${usageKey}@${version}/fields/`;
export const getXBlockFieldsVersionApiUrl = (usageKey: string, version: VersionSpec) => `${getApiBaseUrl()}/api/xblock/v2/xblocks/${usageKey}@${version}/fields/`;

/**
* Get the URL for the xblock OLX API
*/
export const getXBlockOLXApiUrl = (usageKey: string) => `${getLibraryBlockMetadataUrl(usageKey)}olx/`;
export const getXBlockOLXVersionApiUrl = (usageKey: string, version: VersionSpec) => `${getApiBaseUrl()}/api/xblock/v2/xblocks/${usageKey}@${version}/olx/`;

/**
* Get the URL for the xblock Publish API
*/
Expand Down Expand Up @@ -391,7 +394,7 @@ export async function getLibraryBlockMetadata(usageKey: string): Promise<Library
/**
* Fetch xblock fields.
*/
export async function getXBlockFields(usageKey: string, version: string = 'draft'): Promise<XBlockFields> {
export async function getXBlockFields(usageKey: string, version: VersionSpec = 'draft'): Promise<XBlockFields> {
const { data } = await getAuthenticatedHttpClient().get(getXBlockFieldsVersionApiUrl(usageKey, version));
return camelCaseObject(data);
}
Expand All @@ -418,8 +421,8 @@ export async function createCollection(libraryId: string, collectionData: Create
* Fetch the OLX for the given XBlock.
*/
// istanbul ignore next
export async function getXBlockOLX(usageKey: string): Promise<string> {
const { data } = await getAuthenticatedHttpClient().get(getXBlockOLXApiUrl(usageKey));
export async function getXBlockOLX(usageKey: string, version: VersionSpec = 'draft'): Promise<string> {
const { data } = await getAuthenticatedHttpClient().get(getXBlockOLXVersionApiUrl(usageKey, version));
return data.olx;
}

Expand Down
9 changes: 5 additions & 4 deletions src/library-authoring/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
publishXBlock,
deleteXBlockAsset,
} from './api';
import { VersionSpec } from '../LibraryBlock';

export const libraryQueryPredicate = (query: Query, libraryId: string): boolean => {
// Invalidate all content queries related to this library.
Expand Down Expand Up @@ -91,7 +92,7 @@ export const xblockQueryKeys = {
*/
xblock: (usageKey?: string) => [...xblockQueryKeys.all, usageKey],
/** Fields (i.e. the content, display name, etc.) of an XBlock */
xblockFields: (usageKey: string, version: string = 'draft') => [...xblockQueryKeys.xblock(usageKey), 'fields', version],
xblockFields: (usageKey: string, version: VersionSpec = 'draft') => [...xblockQueryKeys.xblock(usageKey), 'fields', version],
/** OLX (XML representation of the fields/content) */
xblockOLX: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'OLX'],
/** assets (static files) */
Expand Down Expand Up @@ -293,7 +294,7 @@ export const useLibraryBlockMetadata = (usageId: string | undefined) => (
})
);

export const useXBlockFields = (usageKey: string, version: string = 'draft') => (
export const useXBlockFields = (usageKey: string, version: VersionSpec = 'draft') => (
useQuery({
queryKey: xblockQueryKeys.xblockFields(usageKey, version),
queryFn: () => getXBlockFields(usageKey, version),
Expand Down Expand Up @@ -350,10 +351,10 @@ export const useCreateLibraryCollection = (libraryId: string) => {
};

/** Get the OLX source of a library component */
export const useXBlockOLX = (usageKey: string) => (
export const useXBlockOLX = (usageKey: string, version: VersionSpec) => (
useQuery({
queryKey: xblockQueryKeys.xblockOLX(usageKey),
queryFn: () => getXBlockOLX(usageKey),
queryFn: () => getXBlockOLX(usageKey, version),
enabled: !!usageKey,
})
);
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/LibraryContext';
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, {});

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 = typeof addMemberError === 'object' ? addMemberError.response?.data : undefined;
if (errorData && 'email' in errorData) {
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
4 changes: 3 additions & 1 deletion src/search-manager/SearchManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ export const SearchContextProvider: React.FC<{
setBlockTypesFilter([]);
setTagsFilter([]);
setProblemTypesFilter([]);
setUsageKey('');
if (usageKey !== '') {
setUsageKey('');
}
}, []);

// Initialize a connection to Meilisearch:
Expand Down
Loading

0 comments on commit a4b53a3

Please sign in to comment.