From 635845a51de49c7a5f988d3ec54907293dbb8dd7 Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Wed, 3 Jun 2026 15:10:39 +0500 Subject: [PATCH] feat: revert ability to create Legacy Libraries Co-Authored-By: Claude Sonnet 4.6 --- src/index.jsx | 2 - .../CreateLegacyLibrary.test.tsx | 202 ---------------- .../CreateLegacyLibrary.tsx | 219 ------------------ .../create-legacy-library/data/api.ts | 27 --- .../create-legacy-library/data/apiHooks.ts | 21 -- .../create-legacy-library/index.ts | 1 - .../create-legacy-library/messages.ts | 41 ---- src/library-authoring/index.tsx | 1 - src/studio-home/StudioHome.test.tsx | 8 +- src/studio-home/StudioHome.tsx | 3 +- 10 files changed, 8 insertions(+), 517 deletions(-) delete mode 100644 src/library-authoring/create-legacy-library/CreateLegacyLibrary.test.tsx delete mode 100644 src/library-authoring/create-legacy-library/CreateLegacyLibrary.tsx delete mode 100644 src/library-authoring/create-legacy-library/data/api.ts delete mode 100644 src/library-authoring/create-legacy-library/data/apiHooks.ts delete mode 100644 src/library-authoring/create-legacy-library/index.ts delete mode 100644 src/library-authoring/create-legacy-library/messages.ts diff --git a/src/index.jsx b/src/index.jsx index b0247846bd..3b50076d34 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -28,7 +28,6 @@ import messages from './i18n'; import { LibraryAndComponentPicker, CreateLibrary, - CreateLegacyLibrary, LibraryLayout, PreviewChangesEmbed, } from './library-authoring'; @@ -77,7 +76,6 @@ const App = () => { } /> } /> } /> - } /> } /> } /> ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => mockNavigate, -})); - -jest.mock('@src/generic/data/apiHooks', () => ({ - ...jest.requireActual('@src/generic/data/apiHooks'), - useOrganizationListData: () => ({ - data: ['org1', 'org2', 'org3', 'org4', 'org5'], - isLoading: false, - }), -})); - -describe('', () => { - beforeEach(() => { - axiosMock = initializeMocks().axiosMock; - axiosMock - .onGet(getApiWaffleFlagsUrl(undefined)) - .reply(200, {}); - Object.defineProperty(window, 'location', { - value: { assign: jest.fn() }, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - axiosMock.restore(); - window.location.assign = realWindowLocationAssign; - }); - - test('call api data with correct data', async () => { - const user = userEvent.setup(); - axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); - axiosMock.onPost(getContentLibraryV1CreateApiUrl()).reply(200, { - id: 'library-id', - url: '/library/library-id', - }); - - render(); - - const titleInput = await screen.findByRole('textbox', { name: /library name/i }); - await user.click(titleInput); - await user.type(titleInput, 'Test Library Name'); - - const orgInput = await screen.findByRole('combobox', { name: /organization/i }); - await user.click(orgInput); - await user.type(orgInput, 'org1'); - await user.tab(); - - const slugInput = await screen.findByRole('textbox', { name: /library id/i }); - await user.click(slugInput); - await user.type(slugInput, 'test_library_slug'); - - await user.click(await screen.findByRole('button', { name: /create/i })); - await waitFor(() => { - expect(axiosMock.history.post.length).toBe(1); - expect(axiosMock.history.post[0].data).toBe( - '{"display_name":"Test Library Name","org":"org1","number":"test_library_slug"}', - ); - expect(window.location.assign).toHaveBeenCalledWith('http://localhost:18010/library/library-id'); - }); - }); - - test('cannot create new org unless allowed', async () => { - const user = userEvent.setup(); - axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); - axiosMock.onPost(getContentLibraryV1CreateApiUrl()).reply(200, { - id: 'library-id', - url: '/library/library-id', - }); - - render(); - - const titleInput = await screen.findByRole('textbox', { name: /library name/i }); - await user.click(titleInput); - await user.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'); - await user.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 }); - await user.click(orgInput); - await user.type(orgInput, 'NewOrg'); - await user.tab(); - - const slugInput = await screen.findByRole('textbox', { name: /library id/i }); - await user.click(slugInput); - await user.type(slugInput, 'test_library_slug'); - - await user.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 () => { - const user = userEvent.setup(); - axiosMock.onGet(getStudioHomeApiUrl()).reply(200, { - ...studioHomeMock, - allow_to_create_new_org: true, - }); - axiosMock.onPost(getContentLibraryV1CreateApiUrl()).reply(200, { - id: 'library-id', - url: '/library/library-id', - }); - - render(); - - const titleInput = await screen.findByRole('textbox', { name: /library name/i }); - await user.click(titleInput); - await user.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'); - await user.click(orgOptions); - ['org1', 'org2', 'org3', 'org4', 'org5'].forEach((org) => expect(screen.queryByText(org)).toBeInTheDocument()); - - const orgInput = await screen.findByRole('combobox', { name: /organization/i }); - await user.click(orgInput); - await user.type(orgInput, 'NewOrg'); - await user.tab(); - - const slugInput = await screen.findByRole('textbox', { name: /library id/i }); - await user.click(slugInput); - await user.type(slugInput, 'test_library_slug'); - - await user.click(await screen.findByRole('button', { name: /create/i })); - await waitFor(() => { - expect(axiosMock.history.post.length).toBe(1); - expect(axiosMock.history.post[0].data).toBe( - '{"display_name":"Test Library Name","org":"NewOrg","number":"test_library_slug"}', - ); - expect(window.location.assign).toHaveBeenCalledWith('http://localhost:18010/library/library-id'); - }); - }); - - test('show api error', async () => { - const user = userEvent.setup(); - axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); - axiosMock.onPost(getContentLibraryV1CreateApiUrl()).reply(400, { - field: 'Error message', - }); - render(); - - const titleInput = await screen.findByRole('textbox', { name: /library name/i }); - await user.click(titleInput); - await user.type(titleInput, 'Test Library Name'); - - const orgInput = await screen.findByRole('combobox', { name: /organization/i }); - await user.click(orgInput); - await user.type(orgInput, 'org1'); - await user.tab(); - - const slugInput = await screen.findByRole('textbox', { name: /library id/i }); - await user.click(slugInput); - await user.type(slugInput, 'test_library_slug'); - - await user.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( - '{"display_name":"Test Library Name","org":"org1","number":"test_library_slug"}', - ); - expect(mockNavigate).not.toHaveBeenCalled(); - }); - await screen.findByText('Request failed with status code 400'); - }); - - test('cancel creating library navigates to libraries page', async () => { - const user = userEvent.setup(); - render(); - - await user.click(await screen.findByRole('button', { name: /cancel/i })); - await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith('/libraries-v1'); - }); - }); -}); diff --git a/src/library-authoring/create-legacy-library/CreateLegacyLibrary.tsx b/src/library-authoring/create-legacy-library/CreateLegacyLibrary.tsx deleted file mode 100644 index 373c38f90a..0000000000 --- a/src/library-authoring/create-legacy-library/CreateLegacyLibrary.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import { StudioFooterSlot } from '@edx/frontend-component-footer'; -import { getConfig } from '@edx/frontend-platform'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import { - Alert, - Container, - Form, - Button, - StatefulButton, - ActionRow, -} from '@openedx/paragon'; -import { Warning } from '@openedx/paragon/icons'; -import { Formik } from 'formik'; -import { useNavigate, Link } from 'react-router-dom'; -import * as Yup from 'yup'; -import classNames from 'classnames'; - -import { REGEX_RULES } from '@src/constants'; -import { useOrganizationListData } from '@src/generic/data/apiHooks'; -import { useStudioHome } from '@src/studio-home/hooks'; -import Header from '@src/header'; -import SubHeader from '@src/generic/sub-header/SubHeader'; -import FormikControl from '@src/generic/FormikControl'; -import FormikErrorFeedback from '@src/generic/FormikErrorFeedback'; -import AlertError from '@src/generic/alert-error'; - -import messages from '@src/library-authoring/create-library/messages'; -import type { LibraryV1Data } from '@src/studio-home/data/api'; -import legacyMessages from './messages'; -import { useCreateLibraryV1 } from './data/apiHooks'; - -/** - * Renders the form and logic to create a new library. - * - * Use `showInModal` to render this component in a way that can be - * used in a modal. Currently this component is used in a modal in the - * legacy libraries migration flow. - */ -export const CreateLegacyLibrary = ({ - showInModal = false, - handleCancel, - handlePostCreate, -}: { - showInModal?: boolean; - handleCancel?: () => void; - handlePostCreate?: (library: LibraryV1Data) => void; -}) => { - const intl = useIntl(); - const navigate = useNavigate(); - - const { noSpaceRule, specialCharsRule } = REGEX_RULES; - const validSlugIdRegex = /^[a-zA-Z\d]+(?:[\w-]*[a-zA-Z\d]+)*$/; - - const { - mutate, - data, - isPending, - isError, - error, - } = useCreateLibraryV1(); - - const { - data: allOrganizations, - isLoading: isOrganizationListLoading, - } = useOrganizationListData(); - - const { - studioHomeData: { - allowedOrganizationsForLibraries, - allowToCreateNewOrg, - }, - } = useStudioHome(); - - const organizations = ( - allowToCreateNewOrg - ? allOrganizations - : allowedOrganizationsForLibraries - ) || []; - - const handleOnClickCancel = () => { - if (handleCancel) { - handleCancel(); - } else { - navigate('/libraries-v1'); - } - }; - - if (data) { - if (handlePostCreate) { - handlePostCreate(data); - } else { - window.location.assign(`${getConfig().STUDIO_BASE_URL}${data.url}`); - } - } - - return ( - <> - {!showInModal &&
} - - {!showInModal && ( - - )} - - {intl.formatMessage(legacyMessages.warningTitle)} - {intl.formatMessage(legacyMessages.warningBody, { - libraryLink: ( - - {intl.formatMessage(legacyMessages.warningLibraryFeature)} - - ), - })} - - mutate(values)} - > - {(formikProps) => ( -
- {intl.formatMessage(legacyMessages.titleLabel)}} - value={formikProps.values.displayName} - placeholder={intl.formatMessage(messages.titlePlaceholder)} - help={intl.formatMessage(messages.titleHelp)} - className="" - controlClasses="pb-2" - /> - - {intl.formatMessage(messages.orgLabel)} - - formikProps.setFieldValue( - 'org', - allowToCreateNewOrg - ? (event.selectionId || event.userProvidedText) - : event.selectionId, - )} - placeholder={intl.formatMessage(messages.orgPlaceholder)} - > - {organizations.map((org) => {org} - )} - - - {intl.formatMessage(messages.orgHelp)} - - - {intl.formatMessage(messages.slugLabel)}} - value={formikProps.values.number} - placeholder={intl.formatMessage(messages.slugPlaceholder)} - help={intl.formatMessage(messages.slugHelp)} - className="" - controlClasses="pb-2" - /> - - - - - - )} -
- {isError && } -
- {!showInModal && } - - ); -}; diff --git a/src/library-authoring/create-legacy-library/data/api.ts b/src/library-authoring/create-legacy-library/data/api.ts deleted file mode 100644 index 2ba9425fdb..0000000000 --- a/src/library-authoring/create-legacy-library/data/api.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { camelCaseObject, snakeCaseObject, getConfig } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; - -import type { LibraryV1Data } from '@src/studio-home/data/api'; - -/** - * Get the URL for creating a new library. - */ -export const getContentLibraryV1CreateApiUrl = () => `${getConfig().STUDIO_BASE_URL}/library/`; - -export interface CreateContentLibraryV1Args { - displayName: string; - org: string; - number: string; -} - -/** - * Create a new library - */ -export async function createLibraryV1(data: CreateContentLibraryV1Args): Promise { - const client = getAuthenticatedHttpClient(); - const url = getContentLibraryV1CreateApiUrl(); - - const { data: newLibrary } = await client.post(url, { ...snakeCaseObject(data) }); - - return camelCaseObject(newLibrary); -} diff --git a/src/library-authoring/create-legacy-library/data/apiHooks.ts b/src/library-authoring/create-legacy-library/data/apiHooks.ts deleted file mode 100644 index 1d10760b6f..0000000000 --- a/src/library-authoring/create-legacy-library/data/apiHooks.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - useMutation, - useQueryClient, -} from '@tanstack/react-query'; - -import { studioHomeQueryKeys } from '@src/studio-home/data/apiHooks'; -import { createLibraryV1 } from './api'; - -/** - * Hook that provides a "mutation" that can be used to create a new content library. - */ -export const useCreateLibraryV1 = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: createLibraryV1, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: studioHomeQueryKeys.librariesV1() }); - }, - }); -}; diff --git a/src/library-authoring/create-legacy-library/index.ts b/src/library-authoring/create-legacy-library/index.ts deleted file mode 100644 index b31a5c4243..0000000000 --- a/src/library-authoring/create-legacy-library/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CreateLegacyLibrary } from './CreateLegacyLibrary'; diff --git a/src/library-authoring/create-legacy-library/messages.ts b/src/library-authoring/create-legacy-library/messages.ts deleted file mode 100644 index c6e7549766..0000000000 --- a/src/library-authoring/create-legacy-library/messages.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - createLibrary: { - id: 'course-authoring.library-authoring.create-legacy-library', - defaultMessage: 'Create new legacy library', - description: 'Header for the create legacy library form', - }, - titleLabel: { - id: 'course-authoring.library-authoring.create-legacy-library.form.title.label', - defaultMessage: 'Legacy library name', - description: 'Label for the title field when creating a legacy library.', - }, - warningTitle: { - id: 'course-authoring.library-authoring.create-legacy-library.warning.title', - defaultMessage: 'You are creating content in a deprecated format', - description: 'Warning to discourage users from creating a new Legacy Library', - }, - warningBody: { - id: 'course-authoring.library-authoring.create-legacy-library.warning.body', - defaultMessage: 'Legacy libraries will be unsupported in Willow. Any content you create in a legacy library will soon need to be migrated. Consider using the {libraryLink} instead.', - description: 'Warning to discourage users from creating a new Legacy Library', - }, - warningLibraryFeature: { - id: 'course-authoring.library-authoring.create-legacy-library.warning.library-feature', - defaultMessage: 'Library feature', - description: 'Link to the Libraries feature page', - }, - createLibraryButton: { - id: 'course-authoring.library-authoring.create-legacy-library.form.create-library.button', - defaultMessage: 'Create legacy library', - description: 'Button text for creating a new legacy library.', - }, - createLibraryButtonPending: { - id: 'course-authoring.library-authoring.create-legacy-library.form.create-library.button.pending', - defaultMessage: 'Creating legacy library..', - description: 'Button text while the legacy library is being created.', - }, -}); - -export default messages; diff --git a/src/library-authoring/index.tsx b/src/library-authoring/index.tsx index 6c9ecd97bd..e40fdf27fe 100644 --- a/src/library-authoring/index.tsx +++ b/src/library-authoring/index.tsx @@ -1,6 +1,5 @@ export { type SelectedComponent } from './common/context/ComponentPickerContext'; export { LibraryAndComponentPicker, ComponentPicker } from './component-picker'; -export { CreateLegacyLibrary } from './create-legacy-library'; export { CreateLibrary, CreateLibraryModal } from './create-library'; export { libraryAuthoringQueryKeys, useContentLibraryV2List } from './data/apiHooks'; export { default as PreviewChangesEmbed } from './legacy-integration/PreviewChangesEmbed'; diff --git a/src/studio-home/StudioHome.test.tsx b/src/studio-home/StudioHome.test.tsx index ec3d195e76..09e894e533 100644 --- a/src/studio-home/StudioHome.test.tsx +++ b/src/studio-home/StudioHome.test.tsx @@ -126,18 +126,22 @@ describe('', () => { }); describe('render new library button', () => { - it('should navigate to legacy library creation when libraries-v2 disabled', async () => { + it('should navigate to home_library when libraries-v2 disabled', async () => { mockUseSelector.mockReturnValue({ ...studioHomeMock, courseCreatorStatus: COURSE_CREATOR_STATES.granted, librariesV2Enabled: false, }); + const studioBaseUrl = 'http://localhost:18010'; + render(, { path: '/home' }); await waitFor(() => { const createNewLibraryButton = screen.getByRole('button', { name: 'New library' }); + const mockWindowOpen = jest.spyOn(window, 'open'); fireEvent.click(createNewLibraryButton); - expect(mockNavigate).toHaveBeenCalledWith('/libraries-v1/create'); + expect(mockWindowOpen).toHaveBeenCalledWith(`${studioBaseUrl}/home_library`); + mockWindowOpen.mockRestore(); }); }); diff --git a/src/studio-home/StudioHome.tsx b/src/studio-home/StudioHome.tsx index a2be1984da..8fb453a589 100644 --- a/src/studio-home/StudioHome.tsx +++ b/src/studio-home/StudioHome.tsx @@ -108,7 +108,8 @@ const StudioHome = () => { if (showV2LibraryURL) { navigate('/library/create'); } else { - navigate('/libraries-v1/create'); + // Studio home library for legacy libraries + window.open(`${getConfig().STUDIO_BASE_URL}/home_library`); } };