From 56d3eede6496c3feb2202876d168ec152a1fc443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Fri, 26 Sep 2025 10:15:23 -0500 Subject: [PATCH] feat: Adding loading and done states to the publish library button [FC-0097] (#2237) Adds loading and done states to the publish library button. --- .../generic/status-widget/index.tsx | 39 ++++++++++++++++--- .../generic/status-widget/messages.ts | 5 +++ .../library-info/LibraryInfo.test.tsx | 38 ++++++++++++++++-- .../library-info/LibraryPublishStatus.tsx | 22 ++++++----- 4 files changed, 85 insertions(+), 19 deletions(-) diff --git a/src/library-authoring/generic/status-widget/index.tsx b/src/library-authoring/generic/status-widget/index.tsx index 3694e2836..f2be49bbf 100644 --- a/src/library-authoring/generic/status-widget/index.tsx +++ b/src/library-authoring/generic/status-widget/index.tsx @@ -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; 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 ( {onCommit && ( - + , + }} + disabledStates={['pending', 'complete']} + onClick={onCommit} + /> )} {onRevert && (
diff --git a/src/library-authoring/generic/status-widget/messages.ts b/src/library-authoring/generic/status-widget/messages.ts index a9af92755..b4c481ddc 100644 --- a/src/library-authoring/generic/status-widget/messages.ts +++ b/src/library-authoring/generic/status-widget/messages.ts @@ -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', diff --git a/src/library-authoring/library-info/LibraryInfo.test.tsx b/src/library-authoring/library-info/LibraryInfo.test.tsx index 69aa4c14f..28f82007b 100644 --- a/src/library-authoring/library-info/LibraryInfo.test.tsx +++ b/src/library-authoring/library-info/LibraryInfo.test.tsx @@ -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('', () => { 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('', () => { 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('', () => { }); }); + 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); diff --git a/src/library-authoring/library-info/LibraryPublishStatus.tsx b/src/library-authoring/library-info/LibraryPublishStatus.tsx index b6ee64738..02f45fac0 100644 --- a/src/library-authoring/library-info/LibraryPublishStatus.tsx +++ b/src/library-authoring/library-info/LibraryPublishStatus.tsx @@ -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 = () => {