diff --git a/src/generic/key-utils.ts b/src/generic/key-utils.ts index 7504e362b..cdf9876f4 100644 --- a/src/generic/key-utils.ts +++ b/src/generic/key-utils.ts @@ -13,6 +13,17 @@ export function getBlockType(usageKey: string): string { throw new Error(`Invalid usageKey: ${usageKey}`); } +/** + * Parses a library key and returns the organization and library name as an object. + */ +export function parseLibraryKey(libraryKey: string): { org: string, lib: string } { + const [, org, lib] = libraryKey?.split(':') || []; + if (org && lib) { + return { org, lib }; + } + throw new Error(`Invalid libraryKey: ${libraryKey}`); +} + /** * Given a usage key like `lb:org:lib:html:id`, get the library key * @param usageKey e.g. `lb:org:lib:html:id` diff --git a/src/studio-home/card-item/index.tsx b/src/studio-home/card-item/index.tsx index 9c56c44b2..89a39e292 100644 --- a/src/studio-home/card-item/index.tsx +++ b/src/studio-home/card-item/index.tsx @@ -1,17 +1,20 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; import { Card, Dropdown, + Icon, IconButton, + Stack, } from '@openedx/paragon'; -import { MoreHoriz } from '@openedx/paragon/icons'; +import { AccessTime, ArrowForward, MoreHoriz } from '@openedx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; import { Link } from 'react-router-dom'; -import { useWaffleFlags } from '../../data/apiHooks'; -import { COURSE_CREATOR_STATES } from '../../constants'; +import { useWaffleFlags } from '@src/data/apiHooks'; +import { COURSE_CREATOR_STATES } from '@src/constants'; +import { parseLibraryKey } from '@src/generic/key-utils'; import { getStudioHomeData } from '../data/selectors'; import messages from '../messages'; @@ -24,7 +27,12 @@ interface BaseProps { rerunLink?: string | null; courseKey?: string; isLibraries?: boolean; + isMigrated?: boolean; + migratedToKey?: string; + migratedToTitle?: string; + migratedToCollectionKey?: string; } + type Props = BaseProps & ( /** If we should open this course/library in this MFE, this is the path to the edit page, e.g. '/course/foo' */ { path: string, url?: never } | @@ -35,6 +43,33 @@ type Props = BaseProps & ( { url: string, path?: never } ); +const PrevToNextName = ({ from, to }: { from: React.ReactNode, to?: React.ReactNode }) => ( + + {from} + {to + && ( + <> + + {to} + + )} + +); + +const MakeLinkOrSpan = ({ + when, to, children, className, +}: { + when: boolean, + to: string, + children: React.ReactNode; + className?: string, +}) => { + if (when) { + return {children}; + } + return {children}; +}; + /** * A card on the Studio home page that represents a Course or a Library */ @@ -49,6 +84,10 @@ const CardItem: React.FC = ({ courseKey = '', path, url, + isMigrated = false, + migratedToKey, + migratedToTitle, + migratedToCollectionKey, }) => { const intl = useIntl(); const { @@ -63,29 +102,66 @@ const CardItem: React.FC = ({ ? url : new URL(url, getConfig().STUDIO_BASE_URL).toString() ); - const subtitle = isLibraries ? `${org} / ${number}` : `${org} / ${number} / ${run}`; const readOnlyItem = !(lmsLink || rerunLink || url || path); const showActions = !(readOnlyItem || isLibraries); const isShowRerunLink = allowCourseReruns && rerunCreatorStatus && courseCreatorStatus === COURSE_CREATOR_STATES.granted; - const hasDisplayName = (displayName ?? '').trim().length ? displayName : courseKey; + const title = (displayName ?? '').trim().length ? displayName : courseKey; + + const getSubtitle = useCallback(() => { + let subtitle = isLibraries ? <>{org} / {number} : <>{org} / {number} / {run}; + if (isMigrated && migratedToKey) { + const migratedToKeyObj = parseLibraryKey(migratedToKey); + subtitle = ( + {migratedToKeyObj.org} / {migratedToKeyObj.lib}} + /> + ); + } + return subtitle; + }, [isLibraries, org, number, run, migratedToKey, isMigrated]); + + const collectionLink = () => { + let libUrl = `/library/${migratedToKey}`; + if (migratedToCollectionKey) { + libUrl += `/collection/${migratedToCollectionKey}`; + } + return libUrl; + }; + + const getTitle = useCallback(() => ( + + {title} + + )} + to={ + isMigrated && migratedToTitle && ( + + {migratedToTitle} + + ) + } + /> + ), [readOnlyItem, isMigrated, destinationUrl, migratedToTitle, title]); return ( - {hasDisplayName} - - ) : ( - {displayName} - )} - subtitle={subtitle} + title={getTitle()} + subtitle={getSubtitle()} actions={showActions && ( = ({ )} /> + {isMigrated && migratedToKey + && ( + + + + {intl.formatMessage(messages.libraryMigrationStatusText)} + + + {migratedToTitle} + + + + + )} ); }; diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.ts similarity index 69% rename from src/studio-home/data/api.js rename to src/studio-home/data/api.ts index 00d3b17e6..c4e6c7738 100644 --- a/src/studio-home/data/api.js +++ b/src/studio-home/data/api.ts @@ -9,54 +9,64 @@ export const getCourseNotificationUrl = (url) => new URL(url, getApiBaseUrl()).h /** * Get's studio home data. - * @returns {Promise} */ -export async function getStudioHomeData() { +export async function getStudioHomeData(): Promise { const { data } = await getAuthenticatedHttpClient().get(getStudioHomeApiUrl()); return camelCaseObject(data); } /** Get list of courses from the deprecated non-paginated API */ -export async function getStudioHomeCourses(search) { +export async function getStudioHomeCourses(search: string) { const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/courses${search}`); return camelCaseObject(data); } /** * Get's studio home courses. - * @param {string} search - Query string parameters for filtering the courses. - * TODO: this should be an object with a list of allowed keys and values; not a string. - * @param {object} customParams - Additional custom parameters for the API request. - * @returns {Promise} - A Promise that resolves to the response data containing the studio home courses. * Note: We are changing /api/contentstore/v1 to /api/contentstore/v2 due to upcoming breaking changes. * Features such as pagination, filtering, and ordering are better handled in the new version. * Please refer to this PR for further details: https://github.com/openedx/edx-platform/pull/34173 */ -export async function getStudioHomeCoursesV2(search, customParams) { +export async function getStudioHomeCoursesV2(search: string, customParams: object): Promise { const customParamsFormat = snakeCaseObject(customParams); const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v2/home/courses${search}`, { params: customParamsFormat }); return camelCaseObject(data); } -export async function getStudioHomeLibraries() { - const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/libraries`); +export interface LibraryV1Data { + displayName: string; + libraryKey: string; + url: string; + org: string; + number: string; + canEdit: boolean; + isMigrated: boolean; + migratedToTitle?: string; + migratedToKey?: string; + migratedToCollectionKey?: string | null; + migratedToCollectionTitle?: string | null; +} + +export interface LibrariesV1ListData { + libraries: LibraryV1Data[]; +} + +export async function getStudioHomeLibraries(): Promise { + const { data } = await getAuthenticatedHttpClient().get(`${getStudioHomeApiUrl()}/libraries`); return camelCaseObject(data); } /** * Handle course notification requests. - * @param {string} url - * @returns {Promise} */ -export async function handleCourseNotification(url) { +export async function handleCourseNotification(url: string): Promise { const { data } = await getAuthenticatedHttpClient().delete(getCourseNotificationUrl(url)); return camelCaseObject(data); } /** * Send user request to course creation access for studio home data. - * @returns {Promise} */ -export async function sendRequestForCourseCreator() { +export async function sendRequestForCourseCreator(): Promise { const { data } = await getAuthenticatedHttpClient().post(getRequestCourseCreatorUrl()); return camelCaseObject(data); } diff --git a/src/studio-home/data/apiHooks.ts b/src/studio-home/data/apiHooks.ts new file mode 100644 index 000000000..95435a3a4 --- /dev/null +++ b/src/studio-home/data/apiHooks.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; +import { getStudioHomeLibraries } from './api'; + +export const studioHomeQueryKeys = { + all: ['studioHome'], + /** + * Base key for list of v1/legacy libraries + */ + librariesV1: () => [...studioHomeQueryKeys.all, 'librariesV1'], +}; + +export const useLibrariesV1Data = (enabled: boolean = true) => ( + useQuery({ + queryKey: studioHomeQueryKeys.librariesV1(), + queryFn: () => getStudioHomeLibraries(), + enabled, + }) +); diff --git a/src/studio-home/data/thunks.js b/src/studio-home/data/thunks.js index 728aa60fb..ffc821955 100644 --- a/src/studio-home/data/thunks.js +++ b/src/studio-home/data/thunks.js @@ -3,14 +3,12 @@ import { getStudioHomeData, sendRequestForCourseCreator, handleCourseNotification, - getStudioHomeLibraries, getStudioHomeCoursesV2, } from './api'; import { fetchStudioHomeDataSuccess, updateLoadingStatuses, updateSavingStatuses, - fetchLibraryDataSuccess, fetchCourseDataSuccessV2, } from './slice'; @@ -58,20 +56,6 @@ function fetchOnlyStudioHomeData() { return fetchStudioHomeData('', false, {}, false, false); } -function fetchLibraryData() { - return async (dispatch) => { - dispatch(updateLoadingStatuses({ libraryLoadingStatus: RequestStatus.IN_PROGRESS })); - - try { - const libraryData = await getStudioHomeLibraries(); - dispatch(fetchLibraryDataSuccess(libraryData)); - dispatch(updateLoadingStatuses({ libraryLoadingStatus: RequestStatus.SUCCESSFUL })); - } catch (error) { - dispatch(updateLoadingStatuses({ libraryLoadingStatus: RequestStatus.FAILED })); - } - }; -} - function handleDeleteNotificationQuery(url) { return async (dispatch) => { dispatch(updateSavingStatuses({ deleteNotificationSavingStatus: RequestStatus.PENDING })); @@ -103,7 +87,6 @@ function requestCourseCreatorQuery() { export { fetchStudioHomeData, fetchOnlyStudioHomeData, - fetchLibraryData, requestCourseCreatorQuery, handleDeleteNotificationQuery, }; diff --git a/src/studio-home/factories/mockApiResponses.tsx b/src/studio-home/factories/mockApiResponses.tsx index f8199aca1..7407f5343 100644 --- a/src/studio-home/factories/mockApiResponses.tsx +++ b/src/studio-home/factories/mockApiResponses.tsx @@ -88,6 +88,20 @@ export const generateGetStudioHomeLibrariesApiResponse = () => ({ org: 'Cambridge', number: '123', canEdit: true, + isMigrated: false, + }, + { + displayName: 'Legacy library 1', + libraryKey: 'library-v1:UNIX+LG1', + url: '/library/library-v1:UNIX+LG1', + org: 'unix', + number: 'LG1', + canEdit: true, + isMigrated: true, + migratedToKey: 'lib:UNIX:CS1', + migratedToTitle: 'Imported library', + migratedToCollectionKey: 'imported-content', + migratedToCollectionTitle: 'Imported content', }, ], }); diff --git a/src/studio-home/messages.ts b/src/studio-home/messages.ts index 52978fbee..a4de63ae7 100644 --- a/src/studio-home/messages.ts +++ b/src/studio-home/messages.ts @@ -73,6 +73,11 @@ const messages = defineMessages({ id: 'course-authoring.studio-home.organization.input.no-options', defaultMessage: 'No options', }, + libraryMigrationStatusText: { + id: 'course-authoring.studio-home.library-v1.card.status', + description: 'Status text in v1 library card in studio informing user of its migration status', + defaultMessage: 'Previously migrated library. Any problem bank links were already moved to', + }, }); export default messages; diff --git a/src/studio-home/scss/StudioHome.scss b/src/studio-home/scss/StudioHome.scss index 3f4320cc2..bf0deb79d 100644 --- a/src/studio-home/scss/StudioHome.scss +++ b/src/studio-home/scss/StudioHome.scss @@ -75,7 +75,7 @@ } .card-item-title { - font: normal var(--pgn-typography-font-weight-normal) 1.125rem/1.75rem var(--pgn-typography-font-family-base); + font: normal var(--pgn-typography-font-weight-semi-bold) 1.125rem/1.75rem var(--pgn-typography-font-family-base); color: var(--pgn-color-black); margin-bottom: .1875rem; } diff --git a/src/studio-home/tabs-section/TabsSection.test.tsx b/src/studio-home/tabs-section/TabsSection.test.tsx index 67450dc7f..72a4b16d6 100644 --- a/src/studio-home/tabs-section/TabsSection.test.tsx +++ b/src/studio-home/tabs-section/TabsSection.test.tsx @@ -2,6 +2,17 @@ import { Routes, Route, useLocation } from 'react-router-dom'; import { getConfig, setConfig } from '@edx/frontend-platform'; import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; +import { userEvent } from '@testing-library/user-event'; +import { executeThunk } from '@src/utils'; +import { mockGetContentLibraryV2List } from '@src/library-authoring/data/api.mocks'; +import contentLibrariesListV2 from '@src/library-authoring/__mocks__/contentLibrariesListV2'; +import { + initializeMocks, + render as baseRender, + fireEvent, + screen, + act, +} from '@src/testUtils'; import messages from '../messages'; import tabMessages from './messages'; import TabsSection from '.'; @@ -12,23 +23,14 @@ import { generateGetStudioHomeLibrariesApiResponse, } from '../factories/mockApiResponses'; import { getApiBaseUrl, getStudioHomeApiUrl } from '../data/api'; -import { executeThunk } from '../../utils'; -import { fetchLibraryData, fetchStudioHomeData } from '../data/thunks'; -import { mockGetContentLibraryV2List } from '../../library-authoring/data/api.mocks'; -import contentLibrariesListV2 from '../../library-authoring/__mocks__/contentLibrariesListV2'; -import { - initializeMocks, - render as baseRender, - fireEvent, - screen, -} from '../../testUtils'; +import { fetchStudioHomeData } from '../data/thunks'; const { studioShortName } = studioHomeMock; let axiosMock; let store; const courseApiLinkV2 = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`; -const libraryApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`; +const libraryApiLink = `${getStudioHomeApiUrl()}/libraries`; // The Libraries v2 tab title contains a badge, so we need to use regex to match its tab text. const librariesBetaTabTitle = /Libraries Beta/; @@ -197,12 +199,13 @@ describe('', () => { expect(pagination).not.toBeInTheDocument(); }); - it('should set the url path to "/home" when switching away then back to courses tab', async () => { + it('should set the url path to home when switching away then back to courses tab', async () => { const data = generateGetStudioCoursesApiResponseV2(); data.results.courses = []; - render(); + await axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); await axiosMock.onGet(courseApiLinkV2).reply(200, data); + render(); await executeThunk(fetchStudioHomeData(), store.dispatch); // confirm the url path is initially /home @@ -210,8 +213,6 @@ describe('', () => { expect(firstLocationDisplay).toHaveTextContent('/home'); // switch to libraries tab - await axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); - await executeThunk(fetchLibraryData(), store.dispatch); const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage); fireEvent.click(librariesTab); @@ -269,20 +270,22 @@ describe('', () => { await axiosMock.onGet(courseApiLinkV2).reply(200, generateGetStudioCoursesApiResponseV2()); }); it('should switch to Legacy Libraries tab and render specific v1 library details', async () => { - render(); await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); await axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); - await executeThunk(fetchStudioHomeData(), store.dispatch); - await executeThunk(fetchLibraryData(), store.dispatch); + render(); + const user = userEvent.setup(); + await act(async () => executeThunk(fetchStudioHomeData(), store.dispatch)); const librariesTab = await screen.findByText(tabMessages.legacyLibrariesTabTitle.defaultMessage); - fireEvent.click(librariesTab); + await user.click(librariesTab); expect(librariesTab).toHaveClass('active'); expect(await screen.findByText(studioHomeMock.libraries[0].displayName)).toBeVisible(); - expect(screen.getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible(); + expect( + await screen.findByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`), + ).toBeVisible(); }); it('should switch to Libraries tab and render specific v2 library details', async () => { @@ -308,24 +311,37 @@ describe('', () => { )).toBeVisible(); }); - it('should switch to Libraries tab and render specific v1 library details ("v1 only" mode)', async () => { - render({ librariesV2Enabled: false }); + it('should switch to Libraries tab and render specific v1 library details - v1 only mode', async () => { await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); await axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); - await executeThunk(fetchStudioHomeData(), store.dispatch); - await executeThunk(fetchLibraryData(), store.dispatch); + render({ librariesV2Enabled: false }); + const user = userEvent.setup(); + await act(async () => executeThunk(fetchStudioHomeData(), store.dispatch)); // Libraries v2 tab should not be shown expect(screen.queryByRole('tab', { name: librariesBetaTabTitle })).toBeNull(); const librariesTab = await screen.findByRole('tab', { name: tabMessages.librariesTabTitle.defaultMessage }); - fireEvent.click(librariesTab); + await user.click(librariesTab); expect(librariesTab).toHaveClass('active'); expect(await screen.findByText(studioHomeMock.libraries[0].displayName)).toBeVisible(); - expect(screen.getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible(); + expect( + await screen.findByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`), + ).toBeVisible(); + + // Migration info should be displayed + const migratedContent = generateGetStudioHomeLibrariesApiResponse().libraries[1]; + expect(await screen.findByText(migratedContent.displayName)).toBeVisible(); + const newTitleElement = await screen.findAllByText(migratedContent.migratedToTitle!); + expect(newTitleElement[0]).toBeVisible(); + expect(newTitleElement[0]).toHaveAttribute('href', `/library/${migratedContent.migratedToKey}`); + expect(newTitleElement[1]).toHaveAttribute( + 'href', + `/library/${migratedContent.migratedToKey}/collection/${migratedContent.migratedToCollectionKey}`, + ); }); it('should switch to Libraries tab and render specific v2 library details ("v2 only" mode)', async () => { @@ -383,11 +399,10 @@ describe('', () => { }); it('should render legacy libraries fetch failure alert', async () => { - render(); await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); await axiosMock.onGet(libraryApiLink).reply(404); + render(); await executeThunk(fetchStudioHomeData(), store.dispatch); - await executeThunk(fetchLibraryData(), store.dispatch); const librariesTab = await screen.findByText(tabMessages.legacyLibrariesTabTitle.defaultMessage); fireEvent.click(librariesTab); @@ -399,12 +414,14 @@ describe('', () => { it('should render v2 libraries fetch failure alert', async () => { mockGetContentLibraryV2List.applyMockError(); - render(); + await axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse()); await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); - await executeThunk(fetchStudioHomeData(), store.dispatch); + render(); + const user = userEvent.setup(); + await act(async () => executeThunk(fetchStudioHomeData(), store.dispatch)); const librariesTab = await screen.findByRole('tab', { name: librariesBetaTabTitle }); - fireEvent.click(librariesTab); + await user.click(librariesTab); expect(librariesTab).toHaveClass('active'); diff --git a/src/studio-home/tabs-section/index.tsx b/src/studio-home/tabs-section/index.tsx index ab4eef7af..7996fc95e 100644 --- a/src/studio-home/tabs-section/index.tsx +++ b/src/studio-home/tabs-section/index.tsx @@ -1,5 +1,5 @@ -import React, { useMemo, useState, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useMemo, useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { Badge, @@ -11,13 +11,12 @@ import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; import { useNavigate, useLocation } from 'react-router-dom'; +import { RequestStatus } from '@src/data/constants'; import { getLoadingStatuses, getStudioHomeData } from '../data/selectors'; import messages from './messages'; import LibrariesTab from './libraries-tab'; import LibrariesV2Tab from './libraries-v2-tab/index'; import CoursesTab from './courses-tab'; -import { RequestStatus } from '../../data/constants'; -import { fetchLibraryData } from '../data/thunks'; const TabsSection = ({ showNewCourseContainer, @@ -26,7 +25,6 @@ const TabsSection = ({ librariesV1Enabled, librariesV2Enabled, }) => { - const dispatch = useDispatch(); const intl = useIntl(); const navigate = useNavigate(); const { pathname } = useLocation(); @@ -37,8 +35,9 @@ const TabsSection = ({ archived: 'archived', taxonomies: 'taxonomies', } as const; + type TabKeyType = keyof typeof TABS_LIST; - const initTabKeyState = (pname) => { + const initTabKeyState = (pname: string) => { if (pname.includes('/libraries-v1')) { return TABS_LIST.legacyLibraries; } @@ -53,30 +52,19 @@ const TabsSection = ({ return TABS_LIST.courses; }; - const [tabKey, setTabKey] = useState(initTabKeyState(pathname)); + const [tabKey, setTabKey] = useState(initTabKeyState(pathname)); // This is needed to handle navigating using the back/forward buttons in the browser useEffect(() => { - // Handle special case when navigating directly to /libraries-v1 - // we need to call dispatch to fetch library data - if (pathname.includes('/libraries-v1')) { - dispatch(fetchLibraryData()); - } setTabKey(initTabKeyState(pathname)); }, [pathname]); - const { - courses, libraries, - numPages, coursesCount, - } = useSelector(getStudioHomeData); + const { courses, numPages, coursesCount } = useSelector(getStudioHomeData); const { courseLoadingStatus, - libraryLoadingStatus, } = useSelector(getLoadingStatuses); const isLoadingCourses = courseLoadingStatus === RequestStatus.IN_PROGRESS; const isFailedCoursesPage = courseLoadingStatus === RequestStatus.FAILED; - const isLoadingLibraries = libraryLoadingStatus === RequestStatus.IN_PROGRESS; - const isFailedLibrariesPage = libraryLoadingStatus === RequestStatus.FAILED; // Controlling the visibility of tabs when using conditional rendering is necessary for // the correct operation of iterating over child elements inside the Paragon Tabs component. @@ -129,11 +117,7 @@ const TabsSection = ({ : messages.librariesTabTitle, )} > - + , ); } @@ -149,13 +133,12 @@ const TabsSection = ({ } return tabs; - }, [showNewCourseContainer, isLoadingCourses, isLoadingLibraries]); + }, [showNewCourseContainer, isLoadingCourses]); - const handleSelectTab = (tab) => { + const handleSelectTab = (tab: TabKeyType) => { if (tab === TABS_LIST.courses) { navigate('/home'); } else if (tab === TABS_LIST.legacyLibraries) { - dispatch(fetchLibraryData()); navigate('/libraries-v1'); } else if (tab === TABS_LIST.libraries) { navigate('/libraries'); diff --git a/src/studio-home/tabs-section/libraries-tab/index.tsx b/src/studio-home/tabs-section/libraries-tab/index.tsx index 4a763fca6..b23b6cdf4 100644 --- a/src/studio-home/tabs-section/libraries-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-tab/index.tsx @@ -1,34 +1,20 @@ -import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon, Row } from '@openedx/paragon'; import { Error } from '@openedx/paragon/icons'; import { getConfig } from '@edx/frontend-platform'; -import AlertMessage from '@src/generic/alert-message'; import { LoadingSpinner } from '@src/generic/Loading'; -import CardItem from '../../card-item'; -import { sortAlphabeticallyArray } from '../utils'; +import AlertMessage from '@src/generic/alert-message'; +import { useLibrariesV1Data } from '@src/studio-home/data/apiHooks'; +import CardItem from '@src/studio-home/card-item'; import messages from '../messages'; +import { sortAlphabeticallyArray } from '../utils'; import { MigrateLegacyLibrariesAlert } from './MigrateLegacyLibrariesAlert'; -interface LibrariesTabProps { - libraries: { - displayName: string; - libraryKey: string; - number: string; - org: string; - url: string; - }[]; - isLoading: boolean; - isFailed: boolean; -} - -const LibrariesTab = ({ - libraries, - isLoading, - isFailed, -}: LibrariesTabProps) => { +const LibrariesTab = () => { const intl = useIntl(); + const { isLoading, data, isError } = useLibrariesV1Data(); + if (isLoading) { return ( @@ -37,7 +23,7 @@ const LibrariesTab = ({ ); } return ( - isFailed ? ( + isError ? ( {getConfig().ENABLE_LEGACY_LIBRARY_MIGRATOR === 'true' && ()}
- {sortAlphabeticallyArray(libraries).map(({ - displayName, org, number, url, + {sortAlphabeticallyArray(data?.libraries || []).map(({ + displayName, org, number, url, isMigrated, migratedToKey, migratedToTitle, migratedToCollectionKey, }) => ( ))}