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:
@@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@import "./sidebar/Sidebar";
|
||||
@import "./header-title/HeaderTitle";
|
||||
@import "./move-modal";
|
||||
@import "./preview-changes";
|
||||
|
||||
.course-unit__alert {
|
||||
margin-bottom: 1.75rem;
|
||||
|
||||
@@ -102,6 +102,9 @@ jest.mock('@tanstack/react-query', () => ({
|
||||
useQueryClient: jest.fn(() => ({
|
||||
setQueryData: jest.fn(),
|
||||
})),
|
||||
useMutation: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
|
||||
@@ -52,6 +52,7 @@ export const messageTypes = {
|
||||
videoFullScreen: 'plugin.videoFullScreen',
|
||||
refreshXBlock: 'refreshXBlock',
|
||||
showMoveXBlockModal: 'showMoveXBlockModal',
|
||||
showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview',
|
||||
};
|
||||
|
||||
export const IFRAME_FEATURE_POLICY = (
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
19
src/course-unit/data/apiHooks.ts
Normal file
19
src/course-unit/data/apiHooks.ts
Normal 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,
|
||||
});
|
||||
4
src/course-unit/preview-changes/index.scss
Normal file
4
src/course-unit/preview-changes/index.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.lib-preview-xblock-changes-modal {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
138
src/course-unit/preview-changes/index.test.tsx
Normal file
138
src/course-unit/preview-changes/index.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
153
src/course-unit/preview-changes/index.tsx
Normal file
153
src/course-unit/preview-changes/index.tsx
Normal 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;
|
||||
66
src/course-unit/preview-changes/messages.ts
Normal file
66
src/course-unit/preview-changes/messages.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user