From 45f6ef42a7dbc847315084f25b30906714edbd0e Mon Sep 17 00:00:00 2001 From: Jillian Date: Thu, 16 Jan 2025 04:14:30 +1030 Subject: [PATCH] fix: allow user provided value if can auto-create orgs [FC-0076] (#1582) Allows Content Authors to auto-create an organization when creating a library, if auto-creating orgs is allowed by the platform. --- .../create-library/CreateLibrary.test.tsx | 164 ++++++++++++------ .../create-library/CreateLibrary.tsx | 27 ++- src/studio-home/__mocks__/studioHomeMock.js | 1 + 3 files changed, 132 insertions(+), 60 deletions(-) diff --git a/src/library-authoring/create-library/CreateLibrary.test.tsx b/src/library-authoring/create-library/CreateLibrary.test.tsx index cf323b1aca..d129cfed16 100644 --- a/src/library-authoring/create-library/CreateLibrary.test.tsx +++ b/src/library-authoring/create-library/CreateLibrary.test.tsx @@ -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; @@ -29,66 +32,41 @@ jest.mock('../../generic/data/apiHooks', () => ({ }), })); -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, -}); - -const RootWrapper = () => ( - - - - - - - -); - describe('', () => { 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(); + render(); - 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( @@ -98,41 +76,115 @@ describe('', () => { }); }); + test('cannot create new org unless allowed', async () => { + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); + axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, { + id: 'library-id', + }); + + render(); + + 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(); + + 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(); + render(); - 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(); + render(); - fireEvent.click(getByRole('button', { name: /cancel/i })); + fireEvent.click(await screen.findByRole('button', { name: /cancel/i })); await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith('/libraries'); }); diff --git a/src/library-authoring/create-library/CreateLibrary.tsx b/src/library-authoring/create-library/CreateLibrary.tsx index 1731984ec5..c4f3695c7e 100644 --- a/src/library-authoring/create-library/CreateLibrary.tsx +++ b/src/library-authoring/create-library/CreateLibrary.tsx @@ -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'; @@ -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'); }; @@ -100,12 +114,17 @@ const CreateLibrary = () => { 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) => ( {org} - )) : []} + ))} {intl.formatMessage(messages.orgHelp)} diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.js index dcd313c511..cc135f259a 100644 --- a/src/studio-home/__mocks__/studioHomeMock.js +++ b/src/studio-home/__mocks__/studioHomeMock.js @@ -76,4 +76,5 @@ module.exports = { platformName: 'Your Platform Name Here', userIsActive: true, allowToCreateNewOrg: false, + allowedOrganizationsForLibraries: ['org1'], };