Shows an alert in course outline and review tab of course libraries page when the course contains legacy library content blocks that depend on libraries that are already migrated to library v2, i.e. the blocks are ready to be converted into item banks that can make use of these new v2 libraries. Authors can click on a single button to convert all references in a single go. The button launches a background task which is then polled by the frontend and the status is presented to the Author.
246 lines
7.2 KiB
TypeScript
246 lines
7.2 KiB
TypeScript
import React, {
|
|
useCallback, useEffect, useMemo, useState,
|
|
} from 'react';
|
|
import { Helmet } from 'react-helmet';
|
|
import { getConfig } from '@edx/frontend-platform';
|
|
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
|
import {
|
|
Alert,
|
|
ActionRow,
|
|
Button,
|
|
Card,
|
|
Container,
|
|
Hyperlink,
|
|
Icon,
|
|
Stack,
|
|
Tab,
|
|
Tabs,
|
|
} from '@openedx/paragon';
|
|
import {
|
|
Cached, CheckCircle, Launch, Loop,
|
|
} from '@openedx/paragon/icons';
|
|
|
|
import sumBy from 'lodash/sumBy';
|
|
import { useSearchParams } from 'react-router-dom';
|
|
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
|
import { useStudioHome } from '@src/studio-home/hooks';
|
|
import NewsstandIcon from '@src/generic/NewsstandIcon';
|
|
import getPageHeadTitle from '@src/generic/utils';
|
|
import SubHeader from '@src/generic/sub-header/SubHeader';
|
|
import Loading from '@src/generic/Loading';
|
|
import messages from './messages';
|
|
import { useEntityLinksSummaryByDownstreamContext } from './data/apiHooks';
|
|
import type { PublishableEntityLinkSummary } from './data/api';
|
|
import ReviewTabContent from './ReviewTabContent';
|
|
import { OutOfSyncAlert } from './OutOfSyncAlert';
|
|
import LegacyLibContentBlockAlert from './LegacyLibContentBlockAlert';
|
|
|
|
interface LibraryCardProps {
|
|
linkSummary: PublishableEntityLinkSummary;
|
|
}
|
|
|
|
export enum CourseLibraryTabs {
|
|
all = 'all',
|
|
review = 'review',
|
|
}
|
|
|
|
const LibraryCard = ({ linkSummary }: LibraryCardProps) => {
|
|
const intl = useIntl();
|
|
|
|
return (
|
|
<Card className="my-3 border-light-500 border shadow-none">
|
|
<Card.Header
|
|
title={(
|
|
<Stack direction="horizontal" gap={2}>
|
|
<Icon src={NewsstandIcon} />
|
|
{linkSummary.upstreamContextTitle}
|
|
</Stack>
|
|
)}
|
|
actions={(
|
|
<ActionRow>
|
|
<Button
|
|
destination={`${getConfig().PUBLIC_PATH}library/${linkSummary.upstreamContextKey}`}
|
|
target="_blank"
|
|
className="border border-light-300"
|
|
variant="tertiary"
|
|
as={Hyperlink}
|
|
size="sm"
|
|
showLaunchIcon={false}
|
|
iconAfter={Launch}
|
|
>
|
|
View Library
|
|
</Button>
|
|
</ActionRow>
|
|
)}
|
|
size="sm"
|
|
/>
|
|
<Card.Section>
|
|
<Stack
|
|
direction="horizontal"
|
|
gap={4}
|
|
className="x-small"
|
|
>
|
|
<span>
|
|
{intl.formatMessage(messages.totalComponentLabel, { totalComponents: linkSummary.totalCount })}
|
|
</span>
|
|
{linkSummary.readyToSyncCount > 0 && (
|
|
<Stack direction="horizontal" gap={1}>
|
|
<Icon src={Loop} size="xs" />
|
|
<span>
|
|
{intl.formatMessage(messages.outOfSyncCountLabel, { outOfSyncCount: linkSummary.readyToSyncCount })}
|
|
</span>
|
|
</Stack>
|
|
)}
|
|
</Stack>
|
|
</Card.Section>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export const CourseLibraries = () => {
|
|
const intl = useIntl();
|
|
const { courseId, courseDetails } = useCourseAuthoringContext();
|
|
const [searchParams] = useSearchParams();
|
|
const [tabKey, setTabKey] = useState<CourseLibraryTabs>(
|
|
() => searchParams.get('tab') as CourseLibraryTabs,
|
|
);
|
|
const [showReviewAlert, setShowReviewAlert] = useState(false);
|
|
const { data: libraries, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId);
|
|
const outOfSyncCount = useMemo(() => sumBy(libraries, (lib) => lib.readyToSyncCount), [libraries]);
|
|
const {
|
|
isLoadingPage: isLoadingStudioHome,
|
|
isFailedLoadingPage: isFailedLoadingStudioHome,
|
|
librariesV2Enabled,
|
|
} = useStudioHome();
|
|
|
|
const onAlertReview = () => {
|
|
setTabKey(CourseLibraryTabs.review);
|
|
};
|
|
|
|
const tabChange = useCallback((selectedTab: CourseLibraryTabs) => {
|
|
setTabKey(selectedTab);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setTabKey((prev) => {
|
|
if (outOfSyncCount > 0) {
|
|
return CourseLibraryTabs.review;
|
|
}
|
|
if (prev) {
|
|
return prev;
|
|
}
|
|
/* istanbul ignore next */
|
|
return CourseLibraryTabs.all;
|
|
});
|
|
}, [outOfSyncCount]);
|
|
|
|
const renderLibrariesTabContent = useCallback(() => {
|
|
if (isLoading) {
|
|
return <Loading />;
|
|
}
|
|
if (libraries?.length === 0) {
|
|
return <small><FormattedMessage {...messages.homeTabDescriptionEmpty} /></small>;
|
|
}
|
|
return (
|
|
<>
|
|
<small><FormattedMessage {...messages.homeTabDescription} /></small>
|
|
{libraries?.map((library) => (
|
|
<LibraryCard
|
|
linkSummary={library}
|
|
key={library.upstreamContextKey}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}, [libraries, isLoading]);
|
|
|
|
const renderReviewTabContent = useCallback(() => {
|
|
if (isLoading) {
|
|
return <Loading />;
|
|
}
|
|
if (tabKey !== CourseLibraryTabs.review) {
|
|
return null;
|
|
}
|
|
if (!outOfSyncCount) {
|
|
return (
|
|
<Stack direction="horizontal" gap={2}>
|
|
<Icon src={CheckCircle} size="xs" />
|
|
<small>
|
|
<FormattedMessage {...messages.reviewTabDescriptionEmpty} />
|
|
</small>
|
|
</Stack>
|
|
);
|
|
}
|
|
return <ReviewTabContent courseId={courseId} />;
|
|
}, [outOfSyncCount, isLoading, tabKey]);
|
|
|
|
if (!isLoadingStudioHome && (!librariesV2Enabled || isFailedLoadingStudioHome)) {
|
|
return (
|
|
<Alert variant="danger">
|
|
{intl.formatMessage(messages.librariesV2DisabledError)}
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Helmet>
|
|
<title>
|
|
{getPageHeadTitle(courseDetails?.name || '', intl.formatMessage(messages.headingTitle))}
|
|
</title>
|
|
</Helmet>
|
|
<Container size="xl" className="px-4 pt-4 mt-3">
|
|
<OutOfSyncAlert
|
|
courseId={courseId}
|
|
onReview={onAlertReview}
|
|
showAlert={showReviewAlert && tabKey === CourseLibraryTabs.all}
|
|
setShowAlert={setShowReviewAlert}
|
|
/>
|
|
<SubHeader
|
|
title={intl.formatMessage(messages.headingTitle)}
|
|
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
|
headerActions={(!showReviewAlert && outOfSyncCount > 0 && tabKey === CourseLibraryTabs.all) ? (
|
|
<Button
|
|
variant="primary"
|
|
onClick={onAlertReview}
|
|
iconBefore={Cached}
|
|
>
|
|
{intl.formatMessage(messages.reviewUpdatesBtn)}
|
|
</Button>
|
|
) : null}
|
|
hideBorder
|
|
/>
|
|
<section className="mb-4">
|
|
<Tabs
|
|
id="course-library-tabs"
|
|
activeKey={tabKey}
|
|
onSelect={tabChange}
|
|
>
|
|
<Tab
|
|
eventKey={CourseLibraryTabs.all}
|
|
title={intl.formatMessage(messages.homeTabTitle)}
|
|
className="px-2 mt-3"
|
|
>
|
|
{renderLibrariesTabContent()}
|
|
</Tab>
|
|
<Tab
|
|
eventKey={CourseLibraryTabs.review}
|
|
title={(
|
|
<Stack direction="horizontal" gap={1}>
|
|
<Icon src={Loop} />
|
|
{intl.formatMessage(messages.reviewTabTitle)}
|
|
</Stack>
|
|
)}
|
|
notification={outOfSyncCount}
|
|
className="px-2 mt-3"
|
|
>
|
|
<LegacyLibContentBlockAlert courseId={courseId} />
|
|
{renderReviewTabContent()}
|
|
</Tab>
|
|
</Tabs>
|
|
</section>
|
|
</Container>
|
|
</>
|
|
);
|
|
};
|