diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx index bfe6c07705..ac8a4d8e1e 100644 --- a/src/course-outline/CourseOutline.test.jsx +++ b/src/course-outline/CourseOutline.test.jsx @@ -597,10 +597,10 @@ describe('', () => { }); it('check whether section, subsection and unit is deleted when corresponding delete button is clicked', async () => { - const { findAllByTestId, findByTestId, queryByText } = render(); + render(); // get section, subsection and unit const [section] = courseOutlineIndexMock.courseStructure.childInfo.children; - const [sectionElement] = await findAllByTestId('section-card'); + const [sectionElement] = await screen.findAllByTestId('section-card'); const [subsection] = section.childInfo.children; const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); @@ -610,7 +610,7 @@ describe('', () => { const checkDeleteBtn = async (item, element, elementName) => { await waitFor(() => { - expect(queryByText(item.displayName), `Failed for ${elementName}!`).toBeInTheDocument(); + expect(screen.queryByText(item.displayName), `Failed for ${elementName}!`).toBeInTheDocument(); }); axiosMock.onDelete(getCourseItemApiUrl(item.id)).reply(200); @@ -619,11 +619,11 @@ describe('', () => { fireEvent.click(menu); const deleteButton = await within(element).findByTestId(`${elementName}-card-header__menu-delete-button`); fireEvent.click(deleteButton); - const confirmButton = await findByTestId('delete-confirm-button'); - await act(async () => fireEvent.click(confirmButton)); + const confirmButton = await screen.findByRole('button', { name: 'Delete' }); + fireEvent.click(confirmButton); await waitFor(() => { - expect(queryByText(item.displayName), `Failed for ${elementName}!`).not.toBeInTheDocument(); + expect(screen.queryByText(item.displayName), `Failed for ${elementName}!`).not.toBeInTheDocument(); }); }; diff --git a/src/course-unit/CourseUnit.jsx b/src/course-unit/CourseUnit.jsx index b235e832b9..4c54d6775e 100644 --- a/src/course-unit/CourseUnit.jsx +++ b/src/course-unit/CourseUnit.jsx @@ -36,6 +36,7 @@ import TagsSidebarControls from '../content-tags-drawer/tags-sidebar-controls'; import { PasteNotificationAlert } from './clipboard'; import XBlockContainerIframe from './xblock-container-iframe'; import MoveModal from './move-modal'; +import PreviewLibraryXBlockChanges from './preview-changes'; const CourseUnit = ({ courseId }) => { const { blockId } = useParams(); @@ -200,6 +201,7 @@ const CourseUnit = ({ courseId }) => { closeModal={closeMoveModal} courseId={courseId} /> + diff --git a/src/course-unit/CourseUnit.scss b/src/course-unit/CourseUnit.scss index 3ada01ca2f..a2d6124ba3 100644 --- a/src/course-unit/CourseUnit.scss +++ b/src/course-unit/CourseUnit.scss @@ -4,6 +4,7 @@ @import "./sidebar/Sidebar"; @import "./header-title/HeaderTitle"; @import "./move-modal"; +@import "./preview-changes"; .course-unit__alert { margin-bottom: 1.75rem; diff --git a/src/course-unit/CourseUnit.test.jsx b/src/course-unit/CourseUnit.test.jsx index 25e0274708..63fb3bf1d1 100644 --- a/src/course-unit/CourseUnit.test.jsx +++ b/src/course-unit/CourseUnit.test.jsx @@ -102,6 +102,9 @@ jest.mock('@tanstack/react-query', () => ({ useQueryClient: jest.fn(() => ({ setQueryData: jest.fn(), })), + useMutation: jest.fn(() => ({ + mutateAsync: jest.fn(), + })), })); const clipboardBroadcastChannelMock = { diff --git a/src/course-unit/constants.js b/src/course-unit/constants.js index ebadb310b4..f63490530b 100644 --- a/src/course-unit/constants.js +++ b/src/course-unit/constants.js @@ -52,6 +52,7 @@ export const messageTypes = { videoFullScreen: 'plugin.videoFullScreen', refreshXBlock: 'refreshXBlock', showMoveXBlockModal: 'showMoveXBlockModal', + showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview', }; export const IFRAME_FEATURE_POLICY = ( diff --git a/src/course-unit/data/api.js b/src/course-unit/data/api.js index 7ede6e0236..039285dcf4 100644 --- a/src/course-unit/data/api.js +++ b/src/course-unit/data/api.js @@ -13,6 +13,7 @@ export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()} export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`; export const getCourseOutlineInfoUrl = (courseId) => `${getStudioBaseUrl()}/course/${courseId}?format=concise`; export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`; +export const libraryBlockChangesUrl = (blockId) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`; /** * Get course unit. @@ -206,3 +207,21 @@ export async function patchUnitItem(sourceLocator, targetParentLocator) { return camelCaseObject(data); } + +/** + * Accept the changes from upstream library block in course + * @param {string} blockId - The ID of the item to be updated from library. + */ +export async function acceptLibraryBlockChanges(blockId) { + await getAuthenticatedHttpClient() + .post(libraryBlockChangesUrl(blockId)); +} + +/** + * Ignore the changes from upstream library block in course + * @param {string} blockId - The ID of the item to be updated from library. + */ +export async function ignoreLibraryBlockChanges(blockId) { + await getAuthenticatedHttpClient() + .delete(libraryBlockChangesUrl(blockId)); +} diff --git a/src/course-unit/data/apiHooks.ts b/src/course-unit/data/apiHooks.ts new file mode 100644 index 0000000000..e07fd10004 --- /dev/null +++ b/src/course-unit/data/apiHooks.ts @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; + +import { acceptLibraryBlockChanges, ignoreLibraryBlockChanges } from './api'; + +/** + * Hook that provides a "mutation" that can be used to accept library block changes. + */ +// eslint-disable-next-line import/prefer-default-export +export const useAcceptLibraryBlockChanges = () => useMutation({ + mutationFn: acceptLibraryBlockChanges, +}); + +/** + * Hook that provides a "mutation" that can be used to ignore library block changes. + */ +// eslint-disable-next-line import/prefer-default-export +export const useIgnoreLibraryBlockChanges = () => useMutation({ + mutationFn: ignoreLibraryBlockChanges, +}); diff --git a/src/course-unit/preview-changes/index.scss b/src/course-unit/preview-changes/index.scss new file mode 100644 index 0000000000..b6749c9214 --- /dev/null +++ b/src/course-unit/preview-changes/index.scss @@ -0,0 +1,4 @@ +.lib-preview-xblock-changes-modal { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} diff --git a/src/course-unit/preview-changes/index.test.tsx b/src/course-unit/preview-changes/index.test.tsx new file mode 100644 index 0000000000..235560820f --- /dev/null +++ b/src/course-unit/preview-changes/index.test.tsx @@ -0,0 +1,138 @@ +import userEvent from '@testing-library/user-event'; +import MockAdapter from 'axios-mock-adapter/types'; +import { + act, + render as baseRender, + screen, + initializeMocks, + waitFor, +} from '../../testUtils'; + +import PreviewLibraryXBlockChanges, { LibraryChangesMessageData } from '.'; +import { messageTypes } from '../constants'; +import { IframeProvider } from '../context/iFrameContext'; +import { libraryBlockChangesUrl } from '../data/api'; +import { ToastActionData } from '../../generic/toast-context'; +import { getLibraryBlockMetadataUrl } from '../../library-authoring/data/api'; + +const usageKey = 'some-id'; +const defaultEventData: LibraryChangesMessageData = { + displayName: 'Test block', + downstreamBlockId: usageKey, + upstreamBlockId: 'some-lib-id', + upstreamBlockVersionSynced: 1, + isVertical: false, +}; + +const mockSendMessageToIframe = jest.fn(); +jest.mock('../context/hooks', () => ({ + useIframe: () => ({ + sendMessageToIframe: mockSendMessageToIframe, + }), +})); +const render = (eventData?: LibraryChangesMessageData) => { + baseRender(, { + extraWrapper: ({ children }) => { children }, + }); + const message = { + data: { + type: messageTypes.showXBlockLibraryChangesPreview, + payload: eventData || defaultEventData, + }, + }; + // Dispatch showXBlockLibraryChangesPreview message event to open the preivew modal. + act(() => { + window.dispatchEvent(new MessageEvent('message', message)); + }); +}; + +let axiosMock: MockAdapter; +let mockShowToast: (message: string, action?: ToastActionData | undefined) => void; + +describe('', () => { + beforeEach(() => { + const mocks = initializeMocks(); + axiosMock = mocks.axiosMock; + mockShowToast = mocks.mockShowToast; + }); + + it('renders modal', async () => { + render(); + + expect(await screen.findByText('Preview changes: Test block')).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: 'Accept changes' })).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: 'Ignore changes' })).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + expect(await screen.findByRole('tab', { name: 'New version' })).toBeInTheDocument(); + expect(await screen.findByRole('tab', { name: 'Old version' })).toBeInTheDocument(); + }); + + it('renders displayName for units', async () => { + render({ ...defaultEventData, isVertical: true, displayName: '' }); + + expect(await screen.findByText('Preview changes: Unit')).toBeInTheDocument(); + }); + + it('renders default displayName for components with no displayName', async () => { + render({ ...defaultEventData, displayName: '' }); + + expect(await screen.findByText('Preview changes: Component')).toBeInTheDocument(); + }); + + it('renders both new and old title if they are different', async () => { + axiosMock.onGet(getLibraryBlockMetadataUrl(defaultEventData.upstreamBlockId)).reply(200, { + displayName: 'New test block', + }); + render(); + + expect(await screen.findByText('Preview changes: Test block -> New test block')).toBeInTheDocument(); + }); + + it('accept changes works', async () => { + axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {}); + render(); + + expect(await screen.findByText('Preview changes: Test block')).toBeInTheDocument(); + const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' }); + userEvent.click(acceptBtn); + await waitFor(() => { + expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.refreshXBlock, null); + expect(axiosMock.history.post.length).toEqual(1); + expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey)); + }); + expect(screen.queryByText('Preview changes: Test block')).not.toBeInTheDocument(); + }); + + it('shows toast if accept changes fails', async () => { + axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(500, {}); + render(); + + expect(await screen.findByText('Preview changes: Test block')).toBeInTheDocument(); + const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' }); + userEvent.click(acceptBtn); + await waitFor(() => { + expect(mockSendMessageToIframe).not.toHaveBeenCalledWith(messageTypes.refreshXBlock, null); + expect(axiosMock.history.post.length).toEqual(1); + expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey)); + }); + expect(screen.queryByText('Preview changes: Test block')).not.toBeInTheDocument(); + expect(mockShowToast).toHaveBeenCalledWith('Failed to update component'); + }); + + it('ignore changes works', async () => { + axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(200, {}); + render(); + + expect(await screen.findByText('Preview changes: Test block')).toBeInTheDocument(); + const ignoreBtn = await screen.findByRole('button', { name: 'Ignore changes' }); + userEvent.click(ignoreBtn); + const ignoreConfirmBtn = await screen.findByRole('button', { name: 'Ignore' }); + userEvent.click(ignoreConfirmBtn); + await waitFor(() => { + expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.refreshXBlock, null); + expect(axiosMock.history.delete.length).toEqual(1); + expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey)); + }); + expect(screen.queryByText('Preview changes: Test block')).not.toBeInTheDocument(); + }); +}); diff --git a/src/course-unit/preview-changes/index.tsx b/src/course-unit/preview-changes/index.tsx new file mode 100644 index 0000000000..dc39755183 --- /dev/null +++ b/src/course-unit/preview-changes/index.tsx @@ -0,0 +1,153 @@ +import { useCallback, useContext, useState } from 'react'; +import { + ActionRow, Button, ModalDialog, useToggle, +} from '@openedx/paragon'; +import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n'; + +import { useEventListener } from '../../generic/hooks'; +import { messageTypes } from '../constants'; +import CompareChangesWidget from '../../library-authoring/component-comparison/CompareChangesWidget'; +import { useAcceptLibraryBlockChanges, useIgnoreLibraryBlockChanges } from '../data/apiHooks'; +import { useIframe } from '../context/hooks'; +import DeleteModal from '../../generic/delete-modal/DeleteModal'; +import messages from './messages'; +import { ToastContext } from '../../generic/toast-context'; +import LoadingButton from '../../generic/loading-button'; +import Loading from '../../generic/Loading'; +import { useLibraryBlockMetadata } from '../../library-authoring/data/apiHooks'; + +export interface LibraryChangesMessageData { + displayName: string, + downstreamBlockId: string, + upstreamBlockId: string, + upstreamBlockVersionSynced: number, + isVertical: boolean, +} + +const PreviewLibraryXBlockChanges = () => { + const { showToast } = useContext(ToastContext); + const intl = useIntl(); + + const [blockData, setBlockData] = useState(undefined); + + // Main preview library modal toggle. + const [isModalOpen, openModal, closeModal] = useToggle(false); + // ignore changes confirmation modal toggle. + const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false); + + const acceptChangesMutation = useAcceptLibraryBlockChanges(); + const ignoreChangesMutation = useIgnoreLibraryBlockChanges(); + const { data: componentMetadata } = useLibraryBlockMetadata(blockData?.upstreamBlockId); + + const { sendMessageToIframe } = useIframe(); + + const receiveMessage = useCallback(({ data }: { data: { + payload: LibraryChangesMessageData; + type: string; + } }) => { + const { payload, type } = data; + + if (type === messageTypes.showXBlockLibraryChangesPreview) { + setBlockData(payload); + openModal(); + } + }, [openModal]); + + useEventListener('message', receiveMessage); + + const getTitle = useCallback(() => { + const oldName = blockData?.displayName; + const newName = componentMetadata?.displayName; + + if (!oldName) { + if (blockData?.isVertical) { + return intl.formatMessage(messages.defaultUnitTitle); + } + return intl.formatMessage(messages.defaultComponentTitle); + } + if (oldName === newName || !newName) { + return intl.formatMessage(messages.title, { blockTitle: oldName }); + } + return intl.formatMessage(messages.diffTitle, { oldName, newName }); + }, [blockData, componentMetadata]); + + const getBody = useCallback(() => { + if (!blockData) { + return ; + } + return ( + + ); + }, [blockData]); + + const updateAndRefresh = useCallback(async (accept: boolean) => { + // istanbul ignore if: this should never happen + if (!blockData) { + return; + } + + const mutation = accept ? acceptChangesMutation : ignoreChangesMutation; + const failureMsg = accept ? messages.acceptChangesFailure : messages.ignoreChangesFailure; + + try { + await mutation.mutateAsync(blockData.downstreamBlockId); + sendMessageToIframe(messageTypes.refreshXBlock, null); + } catch (e) { + showToast(intl.formatMessage(failureMsg)); + } finally { + closeModal(); + } + }, [blockData]); + + return ( + + + + {getTitle()} + + + + {getBody()} + + + + updateAndRefresh(true)} + label={intl.formatMessage(messages.acceptChangesBtn)} + /> + + + + + + + updateAndRefresh(false)} + btnLabel={intl.formatMessage(messages.confirmationConfirmBtn)} + /> + + ); +}; + +export default PreviewLibraryXBlockChanges; diff --git a/src/course-unit/preview-changes/messages.ts b/src/course-unit/preview-changes/messages.ts new file mode 100644 index 0000000000..6b0d778c26 --- /dev/null +++ b/src/course-unit/preview-changes/messages.ts @@ -0,0 +1,66 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + title: { + id: 'authoring.course-unit.preview-changes.modal-title', + defaultMessage: 'Preview changes: {blockTitle}', + description: 'Preview changes modal title text', + }, + diffTitle: { + id: 'authoring.course-unit.preview-changes.modal-diff-title', + defaultMessage: 'Preview changes: {oldName} -> {newName}', + description: 'Preview changes modal title text', + }, + defaultUnitTitle: { + id: 'authoring.course-unit.preview-changes.modal-default-unit-title', + defaultMessage: 'Preview changes: Unit', + description: 'Preview changes modal default title text for units', + }, + defaultComponentTitle: { + id: 'authoring.course-unit.preview-changes.modal-default-component-title', + defaultMessage: 'Preview changes: Component', + description: 'Preview changes modal default title text for components', + }, + acceptChangesBtn: { + id: 'authoring.course-unit.preview-changes.accept-changes-btn', + defaultMessage: 'Accept changes', + description: 'Preview changes modal accept button text.', + }, + acceptChangesFailure: { + id: 'authoring.course-unit.preview-changes.accept-changes-failure', + defaultMessage: 'Failed to update component', + description: 'Toast message to display when accepting changes call fails', + }, + ignoreChangesBtn: { + id: 'authoring.course-unit.preview-changes.accept-ignore-btn', + defaultMessage: 'Ignore changes', + description: 'Preview changes modal ignore button text.', + }, + ignoreChangesFailure: { + id: 'authoring.course-unit.preview-changes.ignore-changes-failure', + defaultMessage: 'Failed to ignore changes', + description: 'Toast message to display when ignore changes call fails', + }, + cancelBtn: { + id: 'authoring.course-unit.preview-changes.cancel-btn', + defaultMessage: 'Cancel', + description: 'Preview changes modal cancel button text.', + }, + confirmationTitle: { + id: 'authoring.course-unit.preview-changes.confirmation-dialog-title', + defaultMessage: 'Ignore these changes?', + description: 'Preview changes confirmation dialog title when user clicks on ignore changes.', + }, + confirmationDescription: { + id: 'authoring.course-unit.preview-changes.confirmation-dialog-description', + defaultMessage: 'Would you like to permanently ignore this updated version? If so, you won\'t be able to update this until a newer version is published (in the library).', + description: 'Preview changes confirmation dialog description text when user clicks on ignore changes.', + }, + confirmationConfirmBtn: { + id: 'authoring.course-unit.preview-changes.confirmation-dialog-confirm-btn', + defaultMessage: 'Ignore', + description: 'Preview changes confirmation dialog confirm button text when user clicks on ignore changes.', + }, +}); + +export default messages; diff --git a/src/generic/delete-modal/DeleteModal.jsx b/src/generic/delete-modal/DeleteModal.jsx index 52bbedcc1b..c7a22c5d9e 100644 --- a/src/generic/delete-modal/DeleteModal.jsx +++ b/src/generic/delete-modal/DeleteModal.jsx @@ -3,11 +3,11 @@ import { ActionRow, Button, AlertModal, - StatefulButton, } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; +import LoadingButton from '../loading-button'; const DeleteModal = ({ category, @@ -17,16 +17,13 @@ const DeleteModal = ({ title, description, variant, - btnState, - btnDefaultLabel, - btnPendingLabel, + btnLabel, }) => { const intl = useIntl(); const modalTitle = title || intl.formatMessage(messages.title, { category }); const modalDescription = description || intl.formatMessage(messages.description, { category }); - const defaultBtnLabel = btnDefaultLabel || intl.formatMessage(messages.deleteButton); - const pendingBtnLabel = btnPendingLabel || intl.formatMessage(messages.pendingDeleteButton); + const defaultBtnLabel = btnLabel || intl.formatMessage(messages.deleteButton); return ( {intl.formatMessage(messages.cancelButton)} - { + { e.preventDefault(); e.stopPropagation(); - onDeleteSubmit(); - }} - labels={{ - default: defaultBtnLabel, - pending: pendingBtnLabel, + await onDeleteSubmit(); }} + label={defaultBtnLabel} /> )} @@ -72,9 +64,7 @@ DeleteModal.defaultProps = { title: '', description: '', variant: 'default', - btnState: 'default', - btnDefaultLabel: '', - btnPendingLabel: '', + btnLabel: '', }; DeleteModal.propTypes = { @@ -85,9 +75,7 @@ DeleteModal.propTypes = { title: PropTypes.string, description: PropTypes.string, variant: PropTypes.string, - btnState: PropTypes.string, - btnDefaultLabel: PropTypes.string, - btnPendingLabel: PropTypes.string, + btnLabel: PropTypes.string, }; export default DeleteModal; diff --git a/src/library-authoring/LibraryBlock/LibraryBlock.tsx b/src/library-authoring/LibraryBlock/LibraryBlock.tsx index b76e2b1aea..4397090edd 100644 --- a/src/library-authoring/LibraryBlock/LibraryBlock.tsx +++ b/src/library-authoring/LibraryBlock/LibraryBlock.tsx @@ -22,7 +22,7 @@ interface LibraryBlockProps { */ export const LibraryBlock = ({ onBlockNotification, usageKey, version }: LibraryBlockProps) => { const iframeRef = useRef(null); - const [iFrameHeight, setIFrameHeight] = useState(600); + const [iFrameHeight, setIFrameHeight] = useState(50); const studioBaseUrl = getConfig().STUDIO_BASE_URL; const intl = useIntl(); @@ -59,6 +59,10 @@ export const LibraryBlock = ({ onBlockNotification, usageKey, version }: Library // Messages are the only way that the code in the IFrame can communicate // with the surrounding UI. window.addEventListener('message', receivedWindowMessage); + if (window.self !== window.top) { + // This component is loaded inside an iframe. + setIFrameHeight(86); + } return () => { window.removeEventListener('message', receivedWindowMessage); @@ -69,7 +73,7 @@ export const LibraryBlock = ({ onBlockNotification, usageKey, version }: Library return (
> = () => { title={intl.formatMessage(messages.advancedDetailsAssetsDeleteFileTitle)} description={`Are you sure you want to delete ${filePathToDelete}?`} onDeleteSubmit={deleteFile} - btnState="default" /> ); diff --git a/src/library-authoring/components/CollectionCard.tsx b/src/library-authoring/components/CollectionCard.tsx index c7c5164f62..67f97d3b8b 100644 --- a/src/library-authoring/components/CollectionCard.tsx +++ b/src/library-authoring/components/CollectionCard.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useState } from 'react'; +import { useCallback, useContext } from 'react'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { ActionRow, @@ -26,7 +26,6 @@ const CollectionMenu = ({ collectionHit } : CollectionMenuProps) => { const intl = useIntl(); const { showToast } = useContext(ToastContext); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false); - const [confirmBtnState, setConfirmBtnState] = useState('default'); const { closeLibrarySidebar, sidebarComponentInfo } = useLibraryContext(); const restoreCollectionMutation = useRestoreCollection(collectionHit.contextKey, collectionHit.blockId); @@ -40,28 +39,26 @@ const CollectionMenu = ({ collectionHit } : CollectionMenuProps) => { }, []); const deleteCollectionMutation = useDeleteCollection(collectionHit.contextKey, collectionHit.blockId); - const deleteCollection = useCallback(() => { - setConfirmBtnState('pending'); + const deleteCollection = useCallback(async () => { if (sidebarComponentInfo?.id === collectionHit.blockId) { // Close sidebar if current collection is open to avoid displaying // deleted collection in sidebar closeLibrarySidebar(); } - deleteCollectionMutation.mutateAsync() - .then(() => { - showToast( - intl.formatMessage(messages.deleteCollectionSuccess), - { - label: intl.formatMessage(messages.undoDeleteCollectionToastAction), - onClick: restoreCollection, - }, - ); - }).catch(() => { - showToast(intl.formatMessage(messages.deleteCollectionFailed)); - }).finally(() => { - setConfirmBtnState('default'); - closeDeleteModal(); - }); + try { + await deleteCollectionMutation.mutateAsync(); + showToast( + intl.formatMessage(messages.deleteCollectionSuccess), + { + label: intl.formatMessage(messages.undoDeleteCollectionToastAction), + onClick: restoreCollection, + }, + ); + } catch (e) { + showToast(intl.formatMessage(messages.deleteCollectionFailed)); + } finally { + closeDeleteModal(); + } }, [sidebarComponentInfo?.id]); return ( @@ -97,7 +94,6 @@ const CollectionMenu = ({ collectionHit } : CollectionMenuProps) => { collectionTitle: collectionHit.displayName, })} onDeleteSubmit={deleteCollection} - btnState={confirmBtnState} /> ); diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index e1ab38f1f5..09151a4602 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -285,10 +285,11 @@ export const useLibraryPasteClipboard = () => { }); }; -export const useLibraryBlockMetadata = (usageId: string) => ( +export const useLibraryBlockMetadata = (usageId: string | undefined) => ( useQuery({ - queryKey: xblockQueryKeys.componentMetadata(usageId), - queryFn: () => getLibraryBlockMetadata(usageId), + queryKey: xblockQueryKeys.componentMetadata(usageId!), + queryFn: () => getLibraryBlockMetadata(usageId!), + enabled: !!usageId, }) ); diff --git a/src/library-authoring/library-info/LibraryPublishStatus.tsx b/src/library-authoring/library-info/LibraryPublishStatus.tsx index 2498a10241..1bd707958c 100644 --- a/src/library-authoring/library-info/LibraryPublishStatus.tsx +++ b/src/library-authoring/library-info/LibraryPublishStatus.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useState } from 'react'; +import { useCallback, useContext } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { useToggle } from '@openedx/paragon'; @@ -13,7 +13,6 @@ const LibraryPublishStatus = () => { const intl = useIntl(); const { libraryData, readOnly } = useLibraryContext(); const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false); - const [confirmBtnState, setConfirmBtnState] = useState('default'); const commitLibraryChanges = useCommitLibraryChanges(); const revertLibraryChanges = useRevertLibraryChanges(); @@ -30,18 +29,16 @@ const LibraryPublishStatus = () => { } }, [libraryData]); - const revert = useCallback(() => { + const revert = useCallback(async () => { if (libraryData) { - setConfirmBtnState('pending'); - revertLibraryChanges.mutateAsync(libraryData.id) - .then(() => { - showToast(intl.formatMessage(messages.revertSuccessMsg)); - }).catch(() => { - showToast(intl.formatMessage(messages.revertErrorMsg)); - }).finally(() => { - setConfirmBtnState('default'); - closeConfirmModal(); - }); + try { + await revertLibraryChanges.mutateAsync(libraryData.id); + showToast(intl.formatMessage(messages.revertSuccessMsg)); + } catch (e) { + showToast(intl.formatMessage(messages.revertErrorMsg)); + } finally { + closeConfirmModal(); + } } }, [libraryData]); @@ -63,9 +60,7 @@ const LibraryPublishStatus = () => { title={intl.formatMessage(messages.discardChangesTitle)} description={intl.formatMessage(messages.discardChangesDescription)} onDeleteSubmit={revert} - btnState={confirmBtnState} - btnDefaultLabel={intl.formatMessage(messages.discardChangesDefaultBtnLabel)} - btnPendingLabel={intl.formatMessage(messages.discardChangesDefaultBtnLabel)} + btnLabel={intl.formatMessage(messages.discardChangesDefaultBtnLabel)} /> );