feat: Adding loading and done states to the publish library button [FC-0097] (#2237)

Adds loading and done states to the publish library button.
This commit is contained in:
Chris Chávez
2025-09-26 10:15:23 -05:00
committed by GitHub
parent c5de944d72
commit 56d3eede64
4 changed files with 85 additions and 19 deletions

View File

@@ -4,9 +4,15 @@ import {
FormattedTime,
useIntl,
} from '@edx/frontend-platform/i18n';
import { Button, Container, Stack } from '@openedx/paragon';
import {
Button,
Container,
Icon,
Stack,
StatefulButton,
} from '@openedx/paragon';
import { SpinnerSimple } from '@openedx/paragon/icons';
import classNames from 'classnames';
import messages from './messages';
const CustomFormattedDate = ({ date }: { date: string }) => (
@@ -84,8 +90,9 @@ type StatusWidgedProps = {
created: string | null;
publishedBy: string | null;
numBlocks?: number;
onCommit?: () => void;
onCommit?: () => Promise<void>;
onCommitLabel?: string;
onCommitStatus?: 'pending' | 'error' | 'idle' | 'success';
onRevert?: () => void;
};
@@ -116,6 +123,7 @@ const StatusWidget = ({
numBlocks,
onCommit,
onCommitLabel,
onCommitStatus,
onRevert,
}: StatusWidgedProps) => {
const intl = useIntl();
@@ -125,6 +133,7 @@ const StatusWidget = ({
let statusMessage: string;
let extraStatusMessage: string | undefined;
let bodyMessage: React.ReactNode | undefined;
let publishButtonState: 'pending' | 'complete' | 'default' = 'default';
if (!lastPublished) {
// Entity is never published (new)
@@ -167,6 +176,12 @@ const StatusWidget = ({
}
}
if (onCommitStatus === 'pending') {
publishButtonState = 'pending';
} else if (isPublished) {
publishButtonState = 'complete';
}
return (
<Stack>
<Container className={classNames('status-widget', {
@@ -189,9 +204,21 @@ const StatusWidget = ({
{bodyMessage}
</span>
{onCommit && (
<Button disabled={isPublished} onClick={onCommit}>
{onCommitLabel || intl.formatMessage(messages.publishButtonLabel)}
</Button>
<StatefulButton
variant="primary"
state={publishButtonState}
labels={{
default: onCommitLabel || intl.formatMessage(messages.publishButtonLabel),
complete: intl.formatMessage(messages.publishedStatusLabel),
pending: intl.formatMessage(messages.publishingButtonLabelState),
}}
icons={{
complete: null,
pending: <Icon src={SpinnerSimple} className="icon-spin" />,
}}
disabledStates={['pending', 'complete']}
onClick={onCommit}
/>
)}
{onRevert && (
<div className="d-flex justify-content-end">

View File

@@ -46,6 +46,11 @@ const messages = defineMessages({
defaultMessage: 'Publish',
description: 'Label of publish button for an entity.',
},
publishingButtonLabelState: {
id: 'course-authoring.library-authoring.generic.status-widget.publishing-button',
defaultMessage: 'Publishing',
description: 'Label of publish button for an entity in the publishing state.',
},
discardChangesButtonLabel: {
id: 'course-authoring.library-authoring.generic.status-widget.discard-button',
defaultMessage: 'Discard Changes',

View File

@@ -6,11 +6,12 @@ import {
screen,
waitFor,
initializeMocks,
} from '../../testUtils';
} from '@src/testUtils';
import { mockContentLibrary } from '../data/api.mocks';
import { getCommitLibraryChangesUrl } from '../data/api';
import { LibraryProvider } from '../common/context/LibraryContext';
import LibraryInfo from './LibraryInfo';
import * as apiHooks from '../data/apiHooks';
const {
libraryId: mockLibraryId,
@@ -105,7 +106,10 @@ describe('<LibraryInfo />', () => {
expect(await screen.findByText(libraryData.org)).toBeInTheDocument();
expect(screen.getByText('Published')).toBeInTheDocument();
// First 'Published' from the state
expect(screen.getAllByText('Published')[0]).toBeInTheDocument();
// Second 'Published' from the published button
expect(screen.getAllByText('Published')[1]).toBeInTheDocument();
expect(screen.getByText('July 26, 2024')).toBeInTheDocument();
expect(screen.getByText('staff')).toBeInTheDocument();
});
@@ -115,7 +119,10 @@ describe('<LibraryInfo />', () => {
expect(await screen.findByText(libraryData.org)).toBeInTheDocument();
expect(screen.getByText('Published')).toBeInTheDocument();
// First 'Published' from the state
expect(screen.getAllByText('Published')[0]).toBeInTheDocument();
// Second 'Published' from the published button
expect(screen.getAllByText('Published')[1]).toBeInTheDocument();
expect(screen.getByText('July 26, 2024')).toBeInTheDocument();
expect(screen.queryByText('staff')).not.toBeInTheDocument();
});
@@ -136,6 +143,31 @@ describe('<LibraryInfo />', () => {
});
});
it('should publish library 2', async () => {
const useCommitLibraryChangesSpy = jest
.spyOn(apiHooks, 'useCommitLibraryChanges')
.mockReturnValue(
// @ts-ignore
{
mutate: jest.fn(),
mutateAsync: jest.fn(),
status: 'pending',
},
);
render();
expect(await screen.findByText(libraryData.org)).toBeInTheDocument();
const publishButton = screen.getByRole('button', { name: /publish/i });
fireEvent.click(publishButton);
await waitFor(() => {
expect(screen.getByText(/publishing/i)).toBeInTheDocument();
});
useCommitLibraryChangesSpy.mockRestore();
});
it('should show error on publish library', async () => {
const url = getCommitLibraryChangesUrl(libraryData.id);
axiosMock.onPost(url).reply(500);

View File

@@ -1,13 +1,14 @@
import { useCallback, useContext } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle } from '@openedx/paragon';
import { ToastContext } from '../../generic/toast-context';
import { ToastContext } from '@src/generic/toast-context';
import DeleteModal from '@src/generic/delete-modal/DeleteModal';
import { useLibraryContext } from '../common/context/LibraryContext';
import { useCommitLibraryChanges, useRevertLibraryChanges } from '../data/apiHooks';
import StatusWidget from '../generic/status-widget';
import messages from './messages';
import DeleteModal from '../../generic/delete-modal/DeleteModal';
const LibraryPublishStatus = () => {
const intl = useIntl();
@@ -18,14 +19,14 @@ const LibraryPublishStatus = () => {
const revertLibraryChanges = useRevertLibraryChanges();
const { showToast } = useContext(ToastContext);
const commit = useCallback(() => {
const commit = useCallback(async () => {
if (libraryData) {
commitLibraryChanges.mutateAsync(libraryData.id)
.then(() => {
showToast(intl.formatMessage(messages.publishSuccessMsg));
}).catch(() => {
showToast(intl.formatMessage(messages.publishErrorMsg));
});
try {
await commitLibraryChanges.mutateAsync(libraryData.id);
showToast(intl.formatMessage(messages.publishSuccessMsg));
} catch (e) {
showToast(intl.formatMessage(messages.publishErrorMsg));
}
}
}, [libraryData]);
@@ -51,6 +52,7 @@ const LibraryPublishStatus = () => {
<StatusWidget
{...libraryData}
onCommit={!readOnly ? commit : undefined}
onCommitStatus={commitLibraryChanges.status}
onCommitLabel={intl.formatMessage(messages.publishLibraryButtonLabel)}
onRevert={!readOnly ? openConfirmModal : undefined}
/>