feat: show migration status in libraries list [FC-0097] (#2417)

Adds migration status to library cards in legacy libraries tab in studio home.
Also converts javascript files to typescript and replaces redux with react query for related api calls.
This commit is contained in:
Navin Karkera
2025-09-25 21:19:13 +05:30
committed by GitHub
parent 39e5f89b45
commit d63680083d
11 changed files with 257 additions and 132 deletions

View File

@@ -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`

View File

@@ -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 }) => (
<Stack direction="horizontal" gap={2}>
<span>{from}</span>
{to
&& (
<>
<Icon src={ArrowForward} size="xs" className="mb-1" />
<span>{to}</span>
</>
)}
</Stack>
);
const MakeLinkOrSpan = ({
when, to, children, className,
}: {
when: boolean,
to: string,
children: React.ReactNode;
className?: string,
}) => {
if (when) {
return <Link className={className} to={to}>{children}</Link>;
}
return <span className={className}>{children}</span>;
};
/**
* A card on the Studio home page that represents a Course or a Library
*/
@@ -49,6 +84,10 @@ const CardItem: React.FC<Props> = ({
courseKey = '',
path,
url,
isMigrated = false,
migratedToKey,
migratedToTitle,
migratedToCollectionKey,
}) => {
const intl = useIntl();
const {
@@ -63,29 +102,66 @@ const CardItem: React.FC<Props> = ({
? 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 = (
<PrevToNextName
from={subtitle}
to={<>{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(() => (
<PrevToNextName
from={(
<MakeLinkOrSpan
when={!readOnlyItem}
to={destinationUrl}
className="card-item-title"
>
{title}
</MakeLinkOrSpan>
)}
to={
isMigrated && migratedToTitle && (
<MakeLinkOrSpan
when={!readOnlyItem}
to={`/library/${migratedToKey}`}
className="card-item-title"
>
{migratedToTitle}
</MakeLinkOrSpan>
)
}
/>
), [readOnlyItem, isMigrated, destinationUrl, migratedToTitle, title]);
return (
<Card className="card-item">
<Card.Header
size="sm"
title={!readOnlyItem ? (
<Link
className="card-item-title"
to={destinationUrl}
>
{hasDisplayName}
</Link>
) : (
<span className="card-item-title">{displayName}</span>
)}
subtitle={subtitle}
title={getTitle()}
subtitle={getSubtitle()}
actions={showActions && (
<Dropdown>
<Dropdown.Toggle
@@ -110,6 +186,24 @@ const CardItem: React.FC<Props> = ({
</Dropdown>
)}
/>
{isMigrated && migratedToKey
&& (
<Card.Status className="bg-white pt-0 text-gray-500">
<Stack direction="horizontal" gap={2}>
<Icon src={AccessTime} size="sm" className="mb-1" />
{intl.formatMessage(messages.libraryMigrationStatusText)}
<b>
<MakeLinkOrSpan
when={!readOnlyItem}
to={collectionLink()}
className="text-info-500"
>
{migratedToTitle}
</MakeLinkOrSpan>
</b>
</Stack>
</Card.Status>
)}
</Card>
);
};

View File

@@ -9,54 +9,64 @@ export const getCourseNotificationUrl = (url) => new URL(url, getApiBaseUrl()).h
/**
* Get's studio home data.
* @returns {Promise<Object>}
*/
export async function getStudioHomeData() {
export async function getStudioHomeData(): Promise<object> {
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<Object>} - 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<object> {
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<LibrariesV1ListData> {
const { data } = await getAuthenticatedHttpClient().get(`${getStudioHomeApiUrl()}/libraries`);
return camelCaseObject(data);
}
/**
* Handle course notification requests.
* @param {string} url
* @returns {Promise<Object>}
*/
export async function handleCourseNotification(url) {
export async function handleCourseNotification(url: string): Promise<object> {
const { data } = await getAuthenticatedHttpClient().delete(getCourseNotificationUrl(url));
return camelCaseObject(data);
}
/**
* Send user request to course creation access for studio home data.
* @returns {Promise<Object>}
*/
export async function sendRequestForCourseCreator() {
export async function sendRequestForCourseCreator(): Promise<object> {
const { data } = await getAuthenticatedHttpClient().post(getRequestCourseCreatorUrl());
return camelCaseObject(data);
}

View File

@@ -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,
})
);

View File

@@ -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,
};

View File

@@ -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',
},
],
});

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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('<TabsSection />', () => {
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('<TabsSection />', () => {
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('<TabsSection />', () => {
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('<TabsSection />', () => {
)).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('<TabsSection />', () => {
});
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('<TabsSection />', () => {
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');

View File

@@ -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<TabKeyType>(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,
)}
>
<LibrariesTab
libraries={libraries}
isLoading={isLoadingLibraries}
isFailed={isFailedLibrariesPage}
/>
<LibrariesTab />
</Tab>,
);
}
@@ -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');

View File

@@ -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 (
<Row className="m-0 mt-4 justify-content-center">
@@ -37,7 +23,7 @@ const LibrariesTab = ({
);
}
return (
isFailed ? (
isError ? (
<AlertMessage
variant="danger"
description={(
@@ -51,8 +37,8 @@ const LibrariesTab = ({
<>
{getConfig().ENABLE_LEGACY_LIBRARY_MIGRATOR === 'true' && (<MigrateLegacyLibrariesAlert />)}
<div className="courses-tab">
{sortAlphabeticallyArray(libraries).map(({
displayName, org, number, url,
{sortAlphabeticallyArray(data?.libraries || []).map(({
displayName, org, number, url, isMigrated, migratedToKey, migratedToTitle, migratedToCollectionKey,
}) => (
<CardItem
key={`${org}+${number}`}
@@ -61,6 +47,10 @@ const LibrariesTab = ({
org={org}
number={number}
url={url}
isMigrated={isMigrated}
migratedToKey={migratedToKey}
migratedToTitle={migratedToTitle}
migratedToCollectionKey={migratedToCollectionKey}
/>
))}
</div>