Skip to content

Display Container Publish status and confirm before publish #2186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions src/generic/Loading.jsx → src/generic/Loading.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Spinner } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

export const LoadingSpinner = ({ size }) => (
interface LoadingSpinnerProps {
size?: string;
}

export const LoadingSpinner = ({ size }: LoadingSpinnerProps) => (
<Spinner
animation="border"
role="status"
Expand All @@ -19,14 +21,6 @@ export const LoadingSpinner = ({ size }) => (
/>
);

LoadingSpinner.defaultProps = {
size: undefined,
};

LoadingSpinner.propTypes = {
size: PropTypes.string,
};

const Loading = () => (
<div className="d-flex justify-content-center align-items-center flex-column vh-100">
<LoadingSpinner />
Expand Down
193 changes: 182 additions & 11 deletions src/library-authoring/containers/ContainerInfo.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import type MockAdapter from 'axios-mock-adapter';
import {
initializeMocks, render as baseRender, screen, waitFor,
fireEvent,
} from '../../testUtils';
} from '@src/testUtils';
import { PublishStatus } from '@src/search-manager';
import { mockContentSearchConfig, mockSearchResult } from '@src/search-manager/data/api.mock';
import { mockContentLibrary, mockGetContainerChildren, mockGetContainerMetadata } from '../data/api.mocks';
import { LibraryProvider } from '../common/context/LibraryContext';
import ContainerInfo from './ContainerInfo';
Expand All @@ -15,17 +17,26 @@ mockGetContainerMetadata.applyMock();
mockContentLibrary.applyMock();
mockGetContainerMetadata.applyMock();
mockGetContainerChildren.applyMock();
mockContentSearchConfig.applyMock();

// TODO Remove this to un-skip section/subsection tests, when implemented
const testIf = (condition) => (condition ? it : it.skip);

const { libraryId } = mockContentLibrary;
const { unitId, subsectionId, sectionId } = mockGetContainerMetadata;

const render = (containerId, showOnlyPublished: boolean = false) => {
const render = (
containerId,
containerType: string = '', // renders container page
showOnlyPublished: boolean = false,
) => {
const params: { libraryId: string, selectedItemId?: string } = { libraryId, selectedItemId: containerId };
const path = containerType
? `/library/:libraryId/${containerType.toLowerCase()}/:selectedItemId?`
: '/library/:libraryId/:selectedItemId?';

return baseRender(<ContainerInfo />, {
path: '/library/:libraryId/:selectedItemId?',
path,
params,
extraWrapper: ({ children }) => (
<LibraryProvider
Expand All @@ -50,22 +61,32 @@ let mockShowToast;
describe('<ContainerInfo />', () => {
beforeEach(() => {
({ axiosMock, mockShowToast } = initializeMocks());
mockSearchResult({
results: [ // @ts-ignore
{
hits: [],
},
],
});
});

[
{
containerType: 'Unit',
containerId: unitId,
childType: 'component',
},
{
containerType: 'Subsection',
containerId: subsectionId,
childType: 'unit',
},
{
containerType: 'Section',
containerId: sectionId,
childType: 'subsection',
},
].forEach(({ containerId, containerType }) => {
].forEach(({ containerId, containerType, childType }) => {
testIf(containerType === 'Unit')(`should delete the ${containerType} using the menu`, async () => {
axiosMock.onDelete(getLibraryContainerApiUrl(containerId)).reply(200);
render(containerId);
Expand All @@ -90,14 +111,74 @@ describe('<ContainerInfo />', () => {
expect(mockShowToast).toHaveBeenCalled();
});

it('can publish the container', async () => {
it(`shows Published if the ${containerType} has no draft changes`, async () => {
axiosMock.onPost(getLibraryContainerPublishApiUrl(containerId)).reply(200);
render(containerId);
mockSearchResult({
results: [ // @ts-ignore
{
hits: [
{
type: 'library_container',
usageKey: containerId,
blockType: containerType.toLowerCase(),
publishStatus: PublishStatus.Published,
},
],
},
],
});
render(containerId, containerType);

// "Published" status should be displayed
const publishedStatus = await screen.findByText('Published');
expect(publishedStatus).toBeInTheDocument();
});

it(`can publish the ${containerType} from the container page`, async () => {
axiosMock.onPost(getLibraryContainerPublishApiUrl(containerId)).reply(200);
mockSearchResult({
results: [ // @ts-ignore
{
hits: [
{
type: 'library_container',
usageKey: containerId,
blockType: containerType.toLowerCase(),
publishStatus: PublishStatus.Modified,
},
],
},
],
});
render(containerId, containerType);

// Click on Publish button
const publishButton = await screen.findByRole('button', { name: 'Publish' });
let publishButton = await screen.findByRole('button', { name: /publish changes/i });
expect(publishButton).toBeInTheDocument();
userEvent.click(publishButton);
expect(publishButton).not.toBeInTheDocument();

// Reveals the confirmation box with warning text
expect(await screen.findByText(
`Are you sure you want to publish this ${containerType.toLowerCase()}?`,
)).toBeInTheDocument();

// Click on the confirm Cancel button
const publishCancel = await screen.findByRole('button', { name: 'Cancel' });
expect(publishCancel).toBeInTheDocument();
userEvent.click(publishCancel);
expect(axiosMock.history.post.length).toBe(0);

// Click on Publish button again
publishButton = await screen.findByRole('button', { name: /publish changes/i });
expect(publishButton).toBeInTheDocument();
userEvent.click(publishButton);
expect(publishButton).not.toBeInTheDocument();

// Click on the confirm Publish button
const publishConfirm = await screen.findByRole('button', { name: 'Publish' });
expect(publishConfirm).toBeInTheDocument();
userEvent.click(publishConfirm);

await waitFor(() => {
expect(axiosMock.history.post.length).toBe(1);
Expand All @@ -107,21 +188,111 @@ describe('<ContainerInfo />', () => {

it(`shows an error if publishing the ${containerType} fails`, async () => {
axiosMock.onPost(getLibraryContainerPublishApiUrl(containerId)).reply(500);
render(containerId);
mockSearchResult({
results: [ // @ts-ignore
{
hits: [
{
type: 'library_container',
usageKey: containerId,
blockType: containerType.toLowerCase(),
publishStatus: PublishStatus.Modified,
},
],
},
],
});
render(containerId, containerType);

// Click on Publish button
const publishButton = await screen.findByRole('button', { name: 'Publish' });
// Click on Publish button to reveal the confirmation box
const publishButton = await screen.findByRole('button', { name: /publish changes/i });
expect(publishButton).toBeInTheDocument();
userEvent.click(publishButton);
expect(publishButton).not.toBeInTheDocument();

// Click on the confirm Publish button
const publishConfirm = await screen.findByRole('button', { name: 'Publish' });
expect(publishConfirm).toBeInTheDocument();
userEvent.click(publishConfirm);

await waitFor(() => {
expect(axiosMock.history.post.length).toBe(1);
});
expect(mockShowToast).toHaveBeenCalledWith('Failed to publish changes');
});

it(`shows single child before publishing the ${containerType}`, async () => {
axiosMock.onPost(getLibraryContainerPublishApiUrl(containerId)).reply(200);
mockSearchResult({
results: [ // @ts-ignore
{
hits: [
{
type: 'library_container',
usageKey: containerId,
blockType: containerType.toLowerCase(),
publishStatus: PublishStatus.Modified,
content: {
childDisplayNames: [
'one',
],
},
},
],
},
],
});
render(containerId, containerType);

// Click on Publish button
const publishButton = await screen.findByRole('button', { name: /publish changes/i });
expect(publishButton).toBeInTheDocument();
userEvent.click(publishButton);
expect(publishButton).not.toBeInTheDocument();

// Check warning text in the confirmation box
expect(await screen.findByText(
`This ${containerType.toLowerCase()} and its 1 ${childType} will all be published.`,
)).toBeInTheDocument();
});

it(`shows child count before publishing the ${containerType}`, async () => {
axiosMock.onPost(getLibraryContainerPublishApiUrl(containerId)).reply(200);
mockSearchResult({
results: [ // @ts-ignore
{
hits: [
{
type: 'library_container',
usageKey: containerId,
blockType: containerType.toLowerCase(),
publishStatus: PublishStatus.Modified,
content: {
childDisplayNames: [
'one', 'two',
],
},
},
],
},
],
});
render(containerId, containerType);

// Click on Publish button
const publishButton = await screen.findByRole('button', { name: /publish changes/i });
expect(publishButton).toBeInTheDocument();
userEvent.click(publishButton);
expect(publishButton).not.toBeInTheDocument();

// Check warning text in the confirmation box
expect(await screen.findByText(
`This ${containerType.toLowerCase()} and its 2 ${childType}s will all be published.`,
)).toBeInTheDocument();
});

testIf(containerType === 'Unit')(`show only published ${containerType} content`, async () => {
render(containerId, true);
render(containerId, '', true);
expect(await screen.findByTestId('container-info-menu-toggle')).toBeInTheDocument();
expect(screen.getByText(/text block published 1/i)).toBeInTheDocument();
});
Expand Down
32 changes: 8 additions & 24 deletions src/library-authoring/containers/ContainerInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import React, { useCallback } from 'react';
import { Link } from 'react-router-dom';
import { MoreVert } from '@openedx/paragon/icons';

import { ContainerType, getBlockType } from '@src/generic/key-utils';
import { useComponentPickerContext } from '../common/context/ComponentPickerContext';
import { useLibraryContext } from '../common/context/LibraryContext';
import {
Expand All @@ -28,9 +29,8 @@ import { LibraryContainerChildren } from '../section-subsections/LibraryContaine
import messages from './messages';
import componentMessages from '../components/messages';
import ContainerDeleter from '../components/ContainerDeleter';
import { useContainer, usePublishContainer } from '../data/apiHooks';
import { ContainerType, getBlockType } from '../../generic/key-utils';
import { ToastContext } from '../../generic/toast-context';
import ContainerPublishStatus from './ContainerPublishStatus';
import { useContainer } from '../data/apiHooks';

type ContainerMenuProps = {
containerId: string,
Expand Down Expand Up @@ -85,9 +85,8 @@ const ContainerPreview = ({ containerId } : ContainerPreviewProps) => {
const ContainerInfo = () => {
const intl = useIntl();

const { libraryId, readOnly } = useLibraryContext();
const { libraryId } = useLibraryContext();
const { componentPickerMode } = useComponentPickerContext();
const { showToast } = React.useContext(ToastContext);
const {
defaultTab,
hiddenTabs,
Expand All @@ -101,7 +100,6 @@ const ContainerInfo = () => {
const containerId = sidebarItemInfo?.id;
const containerType = containerId ? getBlockType(containerId) : undefined;
const { data: container } = useContainer(containerId);
const publishContainer = usePublishContainer(containerId!);

const defaultContainerTab = defaultTab.container;
const tab: ContainerInfoTab = (
Expand Down Expand Up @@ -130,15 +128,6 @@ const ContainerInfo = () => {
);
}, [hiddenTabs, defaultContainerTab, containerId]);

const handlePublish = useCallback(async () => {
try {
await publishContainer.mutateAsync();
showToast(intl.formatMessage(messages.publishContainerSuccess));
} catch (error) {
showToast(intl.formatMessage(messages.publishContainerFailed));
}
}, [publishContainer]);

if (!container || !containerId || !containerType) {
return null;
}
Expand All @@ -156,15 +145,10 @@ const ContainerInfo = () => {
{intl.formatMessage(messages.openButton)}
</Button>
)}
{!componentPickerMode && !readOnly && (
<Button
variant="outline-primary"
className="m-1 text-nowrap flex-grow-1"
disabled={!container.hasUnpublishedChanges || publishContainer.isLoading}
onClick={handlePublish}
>
{intl.formatMessage(messages.publishContainerButton)}
</Button>
{!showOpenButton && !componentPickerMode && (
<ContainerPublishStatus
containerId={containerId}
/>
)}
{showOpenButton && (
<ContainerMenu
Expand Down
Loading