Skip to content

Commit

Permalink
fix: allow user provided value if can auto-create orgs [FC-0076] (#1582)
Browse files Browse the repository at this point in the history
Allows Content Authors to auto-create an organization when creating a library, if auto-creating orgs is allowed by the platform.
  • Loading branch information
pomegranited authored Jan 15, 2025
1 parent 8385c4e commit 45f6ef4
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 60 deletions.
164 changes: 108 additions & 56 deletions src/library-authoring/create-library/CreateLibrary.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React from 'react';
import MockAdapter from 'axios-mock-adapter';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render, waitFor } from '@testing-library/react';
import type MockAdapter from 'axios-mock-adapter';
import userEvent from '@testing-library/user-event';

import initializeStore from '../../store';
import {
act,
fireEvent,
initializeMocks,
render,
screen,
waitFor,
} from '../../testUtils';
import { studioHomeMock } from '../../studio-home/__mocks__';
import { getStudioHomeApiUrl } from '../../studio-home/data/api';
import { getApiWaffleFlagsUrl } from '../../data/api';
import { CreateLibrary } from '.';
import { getContentLibraryV2CreateApiUrl } from './data/api';

let store;
const mockNavigate = jest.fn();
let axiosMock: MockAdapter;

Expand All @@ -29,66 +32,41 @@ jest.mock('../../generic/data/apiHooks', () => ({
}),
}));

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<QueryClientProvider client={queryClient}>
<CreateLibrary />
</QueryClientProvider>
</IntlProvider>
</AppProvider>
);

describe('<CreateLibrary />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();

axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock = initializeMocks().axiosMock;
axiosMock
.onGet(getApiWaffleFlagsUrl(undefined))
.reply(200, {});
});

afterEach(() => {
jest.clearAllMocks();
axiosMock.restore();
queryClient.clear();
});

test('call api data with correct data', async () => {
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, {
id: 'library-id',
});

const { getByRole } = render(<RootWrapper />);
render(<CreateLibrary />);

const titleInput = getByRole('textbox', { name: /library name/i });
const titleInput = await screen.findByRole('textbox', { name: /library name/i });
userEvent.click(titleInput);
userEvent.type(titleInput, 'Test Library Name');

const orgInput = getByRole('combobox', { name: /organization/i });
const orgInput = await screen.findByRole('combobox', { name: /organization/i });
userEvent.click(orgInput);
userEvent.type(orgInput, 'org1');
userEvent.tab();
act(() => userEvent.tab());

const slugInput = getByRole('textbox', { name: /library id/i });
const slugInput = await screen.findByRole('textbox', { name: /library id/i });
userEvent.click(slugInput);
userEvent.type(slugInput, 'test_library_slug');

fireEvent.click(getByRole('button', { name: /create/i }));
fireEvent.click(await screen.findByRole('button', { name: /create/i }));
await waitFor(() => {
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(
Expand All @@ -98,41 +76,115 @@ describe('<CreateLibrary />', () => {
});
});

test('cannot create new org unless allowed', async () => {
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, {
id: 'library-id',
});

render(<CreateLibrary />);

const titleInput = await screen.findByRole('textbox', { name: /library name/i });
userEvent.click(titleInput);
userEvent.type(titleInput, 'Test Library Name');

// We cannot create a new org, and so we're restricted to the allowed list
const orgOptions = screen.getByTestId('autosuggest-iconbutton');
userEvent.click(orgOptions);
expect(screen.getByText('org1')).toBeInTheDocument();
['org2', 'org3', 'org4', 'org5'].forEach((org) => expect(screen.queryByText(org)).not.toBeInTheDocument());

const orgInput = await screen.findByRole('combobox', { name: /organization/i });
userEvent.click(orgInput);
userEvent.type(orgInput, 'NewOrg');
act(() => userEvent.tab());

const slugInput = await screen.findByRole('textbox', { name: /library id/i });
userEvent.click(slugInput);
userEvent.type(slugInput, 'test_library_slug');

fireEvent.click(await screen.findByRole('button', { name: /create/i }));
await waitFor(() => {
expect(axiosMock.history.post.length).toBe(0);
});
expect(await screen.findByText('Required field.')).toBeInTheDocument();
});

test('can create new org if allowed', async () => {
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, {
...studioHomeMock,
allow_to_create_new_org: true,
});
axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, {
id: 'library-id',
});

render(<CreateLibrary />);

const titleInput = await screen.findByRole('textbox', { name: /library name/i });
userEvent.click(titleInput);
userEvent.type(titleInput, 'Test Library Name');

// We can create a new org, so we're also allowed to use any existing org
const orgOptions = screen.getByTestId('autosuggest-iconbutton');
userEvent.click(orgOptions);
['org1', 'org2', 'org3', 'org4', 'org5'].forEach((org) => expect(screen.queryByText(org)).toBeInTheDocument());

const orgInput = await screen.findByRole('combobox', { name: /organization/i });
userEvent.click(orgInput);
userEvent.type(orgInput, 'NewOrg');
act(() => userEvent.tab());

const slugInput = await screen.findByRole('textbox', { name: /library id/i });
userEvent.click(slugInput);
userEvent.type(slugInput, 'test_library_slug');

fireEvent.click(await screen.findByRole('button', { name: /create/i }));
await waitFor(() => {
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(
'{"description":"","title":"Test Library Name","org":"NewOrg","slug":"test_library_slug"}',
);
expect(mockNavigate).toHaveBeenCalledWith('/library/library-id');
});
});

test('show api error', async () => {
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(400, {
field: 'Error message',
});
const { getByRole, getByTestId } = render(<RootWrapper />);
render(<CreateLibrary />);

const titleInput = getByRole('textbox', { name: /library name/i });
const titleInput = await screen.findByRole('textbox', { name: /library name/i });
userEvent.click(titleInput);
userEvent.type(titleInput, 'Test Library Name');

const orgInput = getByTestId('autosuggest-textbox-input');
const orgInput = await screen.findByTestId('autosuggest-textbox-input');
userEvent.click(orgInput);
userEvent.type(orgInput, 'org1');
userEvent.tab();
act(() => userEvent.tab());

const slugInput = getByRole('textbox', { name: /library id/i });
const slugInput = await screen.findByRole('textbox', { name: /library id/i });
userEvent.click(slugInput);
userEvent.type(slugInput, 'test_library_slug');

fireEvent.click(getByRole('button', { name: /create/i }));
await waitFor(() => {
fireEvent.click(await screen.findByRole('button', { name: /create/i }));
await waitFor(async () => {
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(
'{"description":"","title":"Test Library Name","org":"org1","slug":"test_library_slug"}',
);
expect(mockNavigate).not.toHaveBeenCalled();
expect(getByRole('alert')).toHaveTextContent('Request failed with status code 400');
expect(getByRole('alert')).toHaveTextContent('{"field":"Error message"}');
expect(await screen.findByRole('alert')).toHaveTextContent('Request failed with status code 400');
expect(await screen.findByRole('alert')).toHaveTextContent('{"field":"Error message"}');
});
});

test('cancel creating library navigates to libraries page', async () => {
const { getByRole } = render(<RootWrapper />);
render(<CreateLibrary />);

fireEvent.click(getByRole('button', { name: /cancel/i }));
fireEvent.click(await screen.findByRole('button', { name: /cancel/i }));
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/libraries');
});
Expand Down
27 changes: 23 additions & 4 deletions src/library-authoring/create-library/CreateLibrary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import FormikErrorFeedback from '../../generic/FormikErrorFeedback';
import AlertError from '../../generic/alert-error';
import { useOrganizationListData } from '../../generic/data/apiHooks';
import SubHeader from '../../generic/sub-header/SubHeader';
import { useStudioHome } from '../../studio-home/hooks';
import { useCreateLibraryV2 } from './data/apiHooks';
import messages from './messages';

Expand All @@ -38,10 +39,23 @@ const CreateLibrary = () => {
} = useCreateLibraryV2();

const {
data: organizationListData,
data: allOrganizations,
isLoading: isOrganizationListLoading,
} = useOrganizationListData();

const {
studioHomeData: {
allowedOrganizationsForLibraries,
allowToCreateNewOrg,
},
} = useStudioHome();

const organizations = (
allowToCreateNewOrg
? allOrganizations
: allowedOrganizationsForLibraries
) || [];

const handleOnClickCancel = () => {
navigate('/libraries');
};
Expand Down Expand Up @@ -100,12 +114,17 @@ const CreateLibrary = () => {
<Form.Autosuggest
name="org"
isLoading={isOrganizationListLoading}
onChange={(event) => formikProps.setFieldValue('org', event.selectionId)}
onChange={(event) => formikProps.setFieldValue(
'org',
allowToCreateNewOrg
? (event.selectionId || event.userProvidedText)
: event.selectionId,
)}
placeholder={intl.formatMessage(messages.orgPlaceholder)}
>
{organizationListData ? organizationListData.map((org) => (
{organizations.map((org) => (
<Form.AutosuggestOption key={org} id={org}>{org}</Form.AutosuggestOption>
)) : []}
))}
</Form.Autosuggest>
<FormikErrorFeedback name="org">
<Form.Text>{intl.formatMessage(messages.orgHelp)}</Form.Text>
Expand Down
1 change: 1 addition & 0 deletions src/studio-home/__mocks__/studioHomeMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,5 @@ module.exports = {
platformName: 'Your Platform Name Here',
userIsActive: true,
allowToCreateNewOrg: false,
allowedOrganizationsForLibraries: ['org1'],
};

0 comments on commit 45f6ef4

Please sign in to comment.