feat: preview library block changes in course unit [FC-0062] (#1506)

Creates a new preview library block modal. Intercepts the message when the block is iframed to open the new modal.
This commit is contained in:
Navin Karkera
2024-11-23 01:48:43 +05:30
committed by GitHub
parent 31f59d6bca
commit 7aa5accdbb
17 changed files with 458 additions and 69 deletions

View File

@@ -597,10 +597,10 @@ describe('<CourseOutline />', () => {
});
it('check whether section, subsection and unit is deleted when corresponding delete button is clicked', async () => {
const { findAllByTestId, findByTestId, queryByText } = render(<RootWrapper />);
render(<RootWrapper />);
// 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('<CourseOutline />', () => {
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('<CourseOutline />', () => {
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();
});
};

View File

@@ -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}
/>
<PreviewLibraryXBlockChanges />
</Layout.Element>
<Layout.Element>
<Stack gap={3}>

View File

@@ -4,6 +4,7 @@
@import "./sidebar/Sidebar";
@import "./header-title/HeaderTitle";
@import "./move-modal";
@import "./preview-changes";
.course-unit__alert {
margin-bottom: 1.75rem;

View File

@@ -102,6 +102,9 @@ jest.mock('@tanstack/react-query', () => ({
useQueryClient: jest.fn(() => ({
setQueryData: jest.fn(),
})),
useMutation: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
const clipboardBroadcastChannelMock = {

View File

@@ -52,6 +52,7 @@ export const messageTypes = {
videoFullScreen: 'plugin.videoFullScreen',
refreshXBlock: 'refreshXBlock',
showMoveXBlockModal: 'showMoveXBlockModal',
showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview',
};
export const IFRAME_FEATURE_POLICY = (

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
.lib-preview-xblock-changes-modal {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}

View File

@@ -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(<PreviewLibraryXBlockChanges />, {
extraWrapper: ({ children }) => <IframeProvider>{ children }</IframeProvider>,
});
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('<PreviewLibraryXBlockChanges />', () => {
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();
});
});

View File

@@ -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<LibraryChangesMessageData | undefined>(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 <Loading />;
}
return (
<CompareChangesWidget
usageKey={blockData.upstreamBlockId}
oldVersion={blockData.upstreamBlockVersionSynced || 'published'}
newVersion="published"
/>
);
}, [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 (
<ModalDialog
isOpen={isModalOpen}
onClose={closeModal}
size="xl"
className="lib-preview-xblock-changes-modal"
hasCloseButton
isFullscreenOnMobile
>
<ModalDialog.Header>
<ModalDialog.Title>
{getTitle()}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
{getBody()}
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<LoadingButton
onClick={() => updateAndRefresh(true)}
label={intl.formatMessage(messages.acceptChangesBtn)}
/>
<Button
variant="tertiary"
onClick={openConfirmModal}
>
<FormattedMessage {...messages.ignoreChangesBtn} />
</Button>
<ModalDialog.CloseButton variant="tertiary">
<FormattedMessage {...messages.cancelBtn} />
</ModalDialog.CloseButton>
</ActionRow>
</ModalDialog.Footer>
<DeleteModal
isOpen={isConfirmModalOpen}
close={closeConfirmModal}
variant="warning"
title={intl.formatMessage(messages.confirmationTitle)}
description={intl.formatMessage(messages.confirmationDescription)}
onDeleteSubmit={() => updateAndRefresh(false)}
btnLabel={intl.formatMessage(messages.confirmationConfirmBtn)}
/>
</ModalDialog>
);
};
export default PreviewLibraryXBlockChanges;

View File

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

View File

@@ -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 (
<AlertModal
@@ -46,18 +43,13 @@ const DeleteModal = ({
>
{intl.formatMessage(messages.cancelButton)}
</Button>
<StatefulButton
data-testid="delete-confirm-button"
state={btnState}
onClick={(e) => {
<LoadingButton
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
onDeleteSubmit();
}}
labels={{
default: defaultBtnLabel,
pending: pendingBtnLabel,
await onDeleteSubmit();
}}
label={defaultBtnLabel}
/>
</ActionRow>
)}
@@ -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;

View File

@@ -22,7 +22,7 @@ interface LibraryBlockProps {
*/
export const LibraryBlock = ({ onBlockNotification, usageKey, version }: LibraryBlockProps) => {
const iframeRef = useRef<HTMLIFrameElement>(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 (
<div style={{
height: `${iFrameHeight}px`,
height: `${iFrameHeight}vh`,
boxSizing: 'content-box',
position: 'relative',
overflow: 'hidden',

View File

@@ -92,7 +92,6 @@ export const ComponentAdvancedAssets: React.FC<Record<never, never>> = () => {
title={intl.formatMessage(messages.advancedDetailsAssetsDeleteFileTitle)}
description={`Are you sure you want to delete ${filePathToDelete}?`}
onDeleteSubmit={deleteFile}
btnState="default"
/>
</>
);

View File

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

View File

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

View File

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